From c077dae79c6068c165b0a667eeb4aea455ea59c7 Mon Sep 17 00:00:00 2001 From: "Mark S. Lewis" Date: Sat, 15 Jun 2024 14:33:30 +0100 Subject: [PATCH] Update TypeScript implementations - Dependency updates - ESLint flat configuration format, replacing deprecated configuration - Minor fixes to compile and lint issues - Consistent TypeScript formatting with .editorconfig Signed-off-by: Mark S. Lewis --- .editorconfig | 17 + .../.eslintrc.json | 45 - .../eslint.config.mjs | 13 + .../package.json | 14 +- .../application-gateway-typescript/src/app.ts | 21 +- .../tsconfig.json | 28 +- .../chaincode-typescript/src/asset.ts | 10 +- .../rest-api-typescript/.editorconfig | 3 + .../rest-api-typescript/.prettierrc.json | 3 - .../rest-api-typescript/jest.config.ts | 288 +-- .../src/__tests__/api.test.ts | 1418 +++++----- .../rest-api-typescript/src/assets.router.ts | 564 ++-- .../rest-api-typescript/src/auth.ts | 82 +- .../rest-api-typescript/src/config.spec.ts | 1134 ++++---- .../rest-api-typescript/src/config.ts | 234 +- .../rest-api-typescript/src/errors.spec.ts | 595 ++--- .../rest-api-typescript/src/errors.ts | 290 ++- .../rest-api-typescript/src/fabric.spec.ts | 536 ++-- .../rest-api-typescript/src/fabric.ts | 246 +- .../rest-api-typescript/src/health.router.ts | 58 +- .../rest-api-typescript/src/index.ts | 118 +- .../rest-api-typescript/src/jobs.router.ts | 50 +- .../rest-api-typescript/src/jobs.spec.ts | 616 ++--- .../rest-api-typescript/src/jobs.ts | 468 ++-- .../rest-api-typescript/src/logger.ts | 2 +- .../rest-api-typescript/src/redis.spec.ts | 39 +- .../rest-api-typescript/src/redis.ts | 60 +- .../rest-api-typescript/src/server.ts | 128 +- .../src/transactions.router.ts | 74 +- .../.eslintrc.json | 46 - .../eslint.config.mjs | 13 + .../package.json | 14 +- .../application-gateway-typescript/src/app.ts | 6 +- .../src/connect.ts | 6 +- .../tsconfig.json | 28 +- .../.eslintrc.json | 45 - .../eslint.config.mjs | 13 + .../package.json | 14 +- .../application-gateway-typescript/src/app.ts | 25 +- .../src/connect.ts | 6 +- .../tsconfig.json | 28 +- .../chaincode-typescript/npm-shrinkwrap.json | 6 +- .../.eslintrc.json | 102 - .../eslint.config.mjs | 13 + .../package.json | 14 +- .../application-gateway-typescript/src/app.ts | 18 +- .../src/connect.ts | 6 +- .../src/contractWrapper.ts | 13 +- .../src/utils.ts | 2 +- .../tsconfig.json | 28 +- .../applications/conga-cards/package.json | 4 +- .../applications/ping-chaincode/package.json | 4 +- .../applications/rest-api/package.json | 4 +- .../trader-typescript/.eslintrc.js | 99 - .../trader-typescript/eslint.config.mjs | 13 + .../trader-typescript/package.json | 20 +- .../applications/trader-typescript/src/app.ts | 6 +- .../trader-typescript/src/commands/create.ts | 8 +- .../src/commands/getAllAssets.ts | 5 +- .../trader-typescript/src/commands/listen.ts | 4 +- .../src/commands/transfer.ts | 8 +- .../trader-typescript/src/utils.ts | 10 +- .../trader-typescript/tsconfig.json | 16 +- .../asset-transfer-typescript/.eslintrc.js | 101 - .../asset-transfer-typescript/.gitignore | 1 - .../asset-transfer-typescript/Dockerfile | 2 +- .../eslint.config.mjs | 13 + .../npm-shrinkwrap.json | 2274 +++++++++++++++++ .../asset-transfer-typescript/package.json | 16 +- .../asset-transfer-typescript/src/asset.ts | 4 - .../src/assetTransfer.ts | 6 +- .../asset-transfer-typescript/tsconfig.json | 14 +- .../application-typescript/.eslintrc.yaml | 29 - .../application-typescript/eslint.config.mjs | 13 + .../application-typescript/package.json | 18 +- .../application-typescript/src/hsm-sample.ts | 6 +- .../application-typescript/tsconfig.json | 11 +- .../application-typescript/.eslintrc.js | 99 - .../application-typescript/eslint.config.mjs | 13 + .../application-typescript/package.json | 20 +- .../application-typescript/src/app.ts | 2 +- .../application-typescript/src/blockParser.ts | 140 +- .../application-typescript/src/connect.ts | 5 +- .../src/getAllAssets.ts | 5 +- .../application-typescript/src/listen.ts | 8 +- .../application-typescript/src/utils.ts | 5 +- .../application-typescript/tsconfig.json | 16 +- 87 files changed, 6281 insertions(+), 4341 deletions(-) create mode 100644 .editorconfig delete mode 100644 asset-transfer-basic/application-gateway-typescript/.eslintrc.json create mode 100644 asset-transfer-basic/application-gateway-typescript/eslint.config.mjs create mode 100644 asset-transfer-basic/rest-api-typescript/.editorconfig delete mode 100644 asset-transfer-basic/rest-api-typescript/.prettierrc.json delete mode 100644 asset-transfer-events/application-gateway-typescript/.eslintrc.json create mode 100644 asset-transfer-events/application-gateway-typescript/eslint.config.mjs mode change 100755 => 100644 asset-transfer-events/application-gateway-typescript/tsconfig.json delete mode 100644 asset-transfer-private-data/application-gateway-typescript/.eslintrc.json create mode 100644 asset-transfer-private-data/application-gateway-typescript/eslint.config.mjs delete mode 100644 asset-transfer-secured-agreement/application-gateway-typescript/.eslintrc.json create mode 100644 asset-transfer-secured-agreement/application-gateway-typescript/eslint.config.mjs delete mode 100644 full-stack-asset-transfer-guide/applications/trader-typescript/.eslintrc.js create mode 100644 full-stack-asset-transfer-guide/applications/trader-typescript/eslint.config.mjs delete mode 100644 full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/.eslintrc.js create mode 100644 full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/eslint.config.mjs create mode 100644 full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/npm-shrinkwrap.json delete mode 100644 hardware-security-module/application-typescript/.eslintrc.yaml create mode 100644 hardware-security-module/application-typescript/eslint.config.mjs delete mode 100644 off_chain_data/application-typescript/.eslintrc.js create mode 100644 off_chain_data/application-typescript/eslint.config.mjs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..5ec8533458 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +indent_size = 4 +quote_type = single + +[*.json] +indent_size = 4 + +[*.md] +max_line_length = off diff --git a/asset-transfer-basic/application-gateway-typescript/.eslintrc.json b/asset-transfer-basic/application-gateway-typescript/.eslintrc.json deleted file mode 100644 index cc7230a80d..0000000000 --- a/asset-transfer-basic/application-gateway-typescript/.eslintrc.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "env": { - "node": true, - "es6": true - }, - "root": true, - "ignorePatterns": [ - "dist/" - ], - "extends": [ - "eslint:recommended" - ], - "rules": { - "indent": [ - "error", - 4 - ], - "quotes": [ - "error", - "single" - ] - }, - "overrides": [ - { - "files": [ - "**/*.ts" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "sourceType": "module", - "ecmaFeatures": { - "impliedStrict": true - } - }, - "plugins": [ - "@typescript-eslint" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ] - } - ] - } \ No newline at end of file diff --git a/asset-transfer-basic/application-gateway-typescript/eslint.config.mjs b/asset-transfer-basic/application-gateway-typescript/eslint.config.mjs new file mode 100644 index 0000000000..9ef6b24340 --- /dev/null +++ b/asset-transfer-basic/application-gateway-typescript/eslint.config.mjs @@ -0,0 +1,13 @@ +import js from '@eslint/js'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config(js.configs.recommended, ...tseslint.configs.strictTypeChecked, { + languageOptions: { + ecmaVersion: 2023, + sourceType: 'module', + parserOptions: { + project: 'tsconfig.json', + tsconfigRootDir: import.meta.dirname, + }, + }, +}); diff --git a/asset-transfer-basic/application-gateway-typescript/package.json b/asset-transfer-basic/application-gateway-typescript/package.json index 353a2a9f7c..60e20219fd 100644 --- a/asset-transfer-basic/application-gateway-typescript/package.json +++ b/asset-transfer-basic/application-gateway-typescript/package.json @@ -10,7 +10,7 @@ "scripts": { "build": "tsc", "build:watch": "tsc -w", - "lint": "eslint . --ext .ts", + "lint": "eslint src", "prepare": "npm run build", "pretest": "npm run lint", "start": "node dist/app.js" @@ -19,15 +19,15 @@ "author": "Hyperledger", "license": "Apache-2.0", "dependencies": { - "@grpc/grpc-js": "^1.9.7", - "@hyperledger/fabric-gateway": "~1.4.0" + "@grpc/grpc-js": "^1.10", + "@hyperledger/fabric-gateway": "^1.5" }, "devDependencies": { + "@eslint/js": "^9.3.0", "@tsconfig/node18": "^18.2.2", "@types/node": "^18.18.6", - "@typescript-eslint/eslint-plugin": "^6.9.0", - "@typescript-eslint/parser": "^6.9.0", - "eslint": "^8.52.0", - "typescript": "~5.2.2" + "eslint": "^8.57.0", + "typescript": "~5.4", + "typescript-eslint": "^7.13.0" } } diff --git a/asset-transfer-basic/application-gateway-typescript/src/app.ts b/asset-transfer-basic/application-gateway-typescript/src/app.ts index e6c345a381..f53573d49d 100644 --- a/asset-transfer-basic/application-gateway-typescript/src/app.ts +++ b/asset-transfer-basic/application-gateway-typescript/src/app.ts @@ -34,11 +34,10 @@ const peerEndpoint = envOrDefault('PEER_ENDPOINT', 'localhost:7051'); const peerHostAlias = envOrDefault('PEER_HOST_ALIAS', 'peer0.org1.example.com'); const utf8Decoder = new TextDecoder(); -const assetId = `asset${Date.now()}`; +const assetId = `asset${String(Date.now())}`; async function main(): Promise { - - await displayInputParameters(); + displayInputParameters(); // The gRPC client connection should be shared by all Gateway connections to this endpoint. const client = await newGrpcConnection(); @@ -92,7 +91,7 @@ async function main(): Promise { } } -main().catch(error => { +main().catch((error: unknown) => { console.error('******** FAILED to run the application:', error); process.exitCode = 1; }); @@ -113,7 +112,11 @@ async function newIdentity(): Promise { async function getFirstDirFileName(dirPath: string): Promise { const files = await fs.readdir(dirPath); - return path.join(dirPath, files[0]); + const file = files[0]; + if (!file) { + throw new Error(`No files in directory: ${dirPath}`); + } + return path.join(dirPath, file); } async function newSigner(): Promise { @@ -144,7 +147,7 @@ async function getAllAssets(contract: Contract): Promise { const resultBytes = await contract.evaluateTransaction('GetAllAssets'); const resultJson = utf8Decoder.decode(resultBytes); - const result = JSON.parse(resultJson); + const result: unknown = JSON.parse(resultJson); console.log('*** Result:', result); } @@ -183,7 +186,7 @@ async function transferAssetAsync(contract: Contract): Promise { const status = await commit.getStatus(); if (!status.successful) { - throw new Error(`Transaction ${status.transactionId} failed to commit with status code ${status.code}`); + throw new Error(`Transaction ${status.transactionId} failed to commit with status code ${String(status.code)}`); } console.log('*** Transaction committed successfully'); @@ -195,7 +198,7 @@ async function readAssetByID(contract: Contract): Promise { const resultBytes = await contract.evaluateTransaction('ReadAsset', assetId); const resultJson = utf8Decoder.decode(resultBytes); - const result = JSON.parse(resultJson); + const result: unknown = JSON.parse(resultJson); console.log('*** Result:', result); } @@ -230,7 +233,7 @@ function envOrDefault(key: string, defaultValue: string): string { /** * displayInputParameters() will print the global scope parameters used by the main driver routine. */ -async function displayInputParameters(): Promise { +function displayInputParameters(): void { console.log(`channelName: ${channelName}`); console.log(`chaincodeName: ${chaincodeName}`); console.log(`mspId: ${mspId}`); diff --git a/asset-transfer-basic/application-gateway-typescript/tsconfig.json b/asset-transfer-basic/application-gateway-typescript/tsconfig.json index 2a3ffbeb7d..4c20df24e6 100644 --- a/asset-transfer-basic/application-gateway-typescript/tsconfig.json +++ b/asset-transfer-basic/application-gateway-typescript/tsconfig.json @@ -1,17 +1,15 @@ { - "extends":"@tsconfig/node18/tsconfig.json", - "compilerOptions": { - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "outDir": "dist", - "declaration": true, - "sourceMap": true, - "noImplicitAny": true - }, - "include": [ - "./src/**/*" - ], - "exclude": [ - "./src/**/*.spec.ts" - ] + "extends": "@tsconfig/node18/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "noUnusedLocals": true, + "noImplicitReturns": true, + "noUncheckedIndexedAccess": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["./src/**/*"], + "exclude": ["./src/**/*.spec.ts"] } diff --git a/asset-transfer-basic/chaincode-typescript/src/asset.ts b/asset-transfer-basic/chaincode-typescript/src/asset.ts index 9fd62d35e3..8845ab03a8 100644 --- a/asset-transfer-basic/chaincode-typescript/src/asset.ts +++ b/asset-transfer-basic/chaincode-typescript/src/asset.ts @@ -10,17 +10,17 @@ export class Asset { public docType?: string; @Property() - public ID: string; + public ID: string = ''; @Property() - public Color: string; + public Color: string = ''; @Property() - public Size: number; + public Size: number = 0; @Property() - public Owner: string; + public Owner: string = ''; @Property() - public AppraisedValue: number; + public AppraisedValue: number = 0; } diff --git a/asset-transfer-basic/rest-api-typescript/.editorconfig b/asset-transfer-basic/rest-api-typescript/.editorconfig new file mode 100644 index 0000000000..409a1d31b4 --- /dev/null +++ b/asset-transfer-basic/rest-api-typescript/.editorconfig @@ -0,0 +1,3 @@ +[*.ts] +indent_size = 4 +quote_type = single diff --git a/asset-transfer-basic/rest-api-typescript/.prettierrc.json b/asset-transfer-basic/rest-api-typescript/.prettierrc.json deleted file mode 100644 index 8db60caace..0000000000 --- a/asset-transfer-basic/rest-api-typescript/.prettierrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "singleQuote": true -} diff --git a/asset-transfer-basic/rest-api-typescript/jest.config.ts b/asset-transfer-basic/rest-api-typescript/jest.config.ts index d11325d5c0..d409b0a1f5 100644 --- a/asset-transfer-basic/rest-api-typescript/jest.config.ts +++ b/asset-transfer-basic/rest-api-typescript/jest.config.ts @@ -4,205 +4,205 @@ */ export default { - // All imported modules in your tests should be mocked automatically - // automock: false, + // All imported modules in your tests should be mocked automatically + // automock: false, - // Stop running tests after `n` failures - // bail: 0, + // Stop running tests after `n` failures + // bail: 0, - // The directory where Jest should store its cached dependency information - // cacheDirectory: "/private/var/folders/04/rqvxpdk52gvf1_qq9l8gt4d40000gn/T/jest_dx", + // The directory where Jest should store its cached dependency information + // cacheDirectory: "/private/var/folders/04/rqvxpdk52gvf1_qq9l8gt4d40000gn/T/jest_dx", - // Automatically clear mock calls and instances between every test - clearMocks: true, + // Automatically clear mock calls and instances between every test + clearMocks: true, - // Indicates whether the coverage information should be collected while executing the test - collectCoverage: true, + // Indicates whether the coverage information should be collected while executing the test + collectCoverage: true, - // An array of glob patterns indicating a set of files for which coverage information should be collected - // collectCoverageFrom: undefined, + // An array of glob patterns indicating a set of files for which coverage information should be collected + // collectCoverageFrom: undefined, - // The directory where Jest should output its coverage files - coverageDirectory: 'coverage', + // The directory where Jest should output its coverage files + coverageDirectory: 'coverage', - // An array of regexp pattern strings used to skip coverage collection - // coveragePathIgnorePatterns: [ - // "/node_modules/" - // ], + // An array of regexp pattern strings used to skip coverage collection + // coveragePathIgnorePatterns: [ + // "/node_modules/" + // ], - // Indicates which provider should be used to instrument code for coverage - coverageProvider: 'v8', + // Indicates which provider should be used to instrument code for coverage + coverageProvider: 'v8', - // A list of reporter names that Jest uses when writing coverage reports - // coverageReporters: [ - // "json", - // "text", - // "lcov", - // "clover" - // ], + // A list of reporter names that Jest uses when writing coverage reports + // coverageReporters: [ + // "json", + // "text", + // "lcov", + // "clover" + // ], - // An object that configures minimum threshold enforcement for coverage results - // coverageThreshold: undefined, + // An object that configures minimum threshold enforcement for coverage results + // coverageThreshold: undefined, - // A path to a custom dependency extractor - // dependencyExtractor: undefined, + // A path to a custom dependency extractor + // dependencyExtractor: undefined, - // Make calling deprecated APIs throw helpful error messages - // errorOnDeprecated: false, + // Make calling deprecated APIs throw helpful error messages + // errorOnDeprecated: false, - // Force coverage collection from ignored files using an array of glob patterns - // forceCoverageMatch: [], + // Force coverage collection from ignored files using an array of glob patterns + // forceCoverageMatch: [], - // A path to a module which exports an async function that is triggered once before all test suites - // globalSetup: undefined, + // A path to a module which exports an async function that is triggered once before all test suites + // globalSetup: undefined, - // A path to a module which exports an async function that is triggered once after all test suites - // globalTeardown: undefined, + // A path to a module which exports an async function that is triggered once after all test suites + // globalTeardown: undefined, - // A set of global variables that need to be available in all test environments - // globals: {}, + // A set of global variables that need to be available in all test environments + // globals: {}, - // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. - // maxWorkers: "50%", + // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. + // maxWorkers: "50%", - // An array of directory names to be searched recursively up from the requiring module's location - // moduleDirectories: [ - // "node_modules" - // ], + // An array of directory names to be searched recursively up from the requiring module's location + // moduleDirectories: [ + // "node_modules" + // ], - // An array of file extensions your modules use - // moduleFileExtensions: [ - // "js", - // "jsx", - // "ts", - // "tsx", - // "json", - // "node" - // ], + // An array of file extensions your modules use + // moduleFileExtensions: [ + // "js", + // "jsx", + // "ts", + // "tsx", + // "json", + // "node" + // ], - // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module - // moduleNameMapper: {}, + // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module + // moduleNameMapper: {}, - // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader - // modulePathIgnorePatterns: [], + // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader + // modulePathIgnorePatterns: [], - // Activates notifications for test results - // notify: false, + // Activates notifications for test results + // notify: false, - // An enum that specifies notification mode. Requires { notify: true } - // notifyMode: "failure-change", + // An enum that specifies notification mode. Requires { notify: true } + // notifyMode: "failure-change", - // A preset that is used as a base for Jest's configuration - preset: 'ts-jest', + // A preset that is used as a base for Jest's configuration + preset: 'ts-jest', - // Run tests from one or more projects - // projects: undefined, + // Run tests from one or more projects + // projects: undefined, - // Use this configuration option to add custom reporters to Jest - // reporters: undefined, + // Use this configuration option to add custom reporters to Jest + // reporters: undefined, - // Automatically reset mock state between every test - // resetMocks: false, + // Automatically reset mock state between every test + // resetMocks: false, - // Reset the module registry before running each individual test - // resetModules: false, + // Reset the module registry before running each individual test + // resetModules: false, - // A path to a custom resolver - // resolver: undefined, + // A path to a custom resolver + // resolver: undefined, - // Automatically restore mock state between every test - // restoreMocks: false, + // Automatically restore mock state between every test + // restoreMocks: false, - // The root directory that Jest should scan for tests and modules within - // rootDir: undefined, + // The root directory that Jest should scan for tests and modules within + // rootDir: undefined, - // A list of paths to directories that Jest should use to search for files in - roots: ['/src'], + // A list of paths to directories that Jest should use to search for files in + roots: ['/src'], - // Allows you to use a custom runner instead of Jest's default test runner - // runner: "jest-runner", + // Allows you to use a custom runner instead of Jest's default test runner + // runner: "jest-runner", - // The paths to modules that run some code to configure or set up the testing environment before each test - // setupFiles: [], + // The paths to modules that run some code to configure or set up the testing environment before each test + // setupFiles: [], - // A list of paths to modules that run some code to configure or set up the testing framework before each test - // setupFilesAfterEnv: [], + // A list of paths to modules that run some code to configure or set up the testing framework before each test + // setupFilesAfterEnv: [], - // The number of seconds after which a test is considered as slow and reported as such in the results. - // slowTestThreshold: 5, + // The number of seconds after which a test is considered as slow and reported as such in the results. + // slowTestThreshold: 5, - // A list of paths to snapshot serializer modules Jest should use for snapshot testing - // snapshotSerializers: [], + // A list of paths to snapshot serializer modules Jest should use for snapshot testing + // snapshotSerializers: [], - // The test environment that will be used for testing - // testEnvironment: "jest-environment-node", + // The test environment that will be used for testing + // testEnvironment: "jest-environment-node", - // Options that will be passed to the testEnvironment - // testEnvironmentOptions: {}, + // Options that will be passed to the testEnvironment + // testEnvironmentOptions: {}, - // Adds a location field to test results - // testLocationInResults: false, + // Adds a location field to test results + // testLocationInResults: false, - // The glob patterns Jest uses to detect test files - testMatch: [ - // "**/__tests__/**/*.[jt]s?(x)", - '**/?(*.)+(spec|test).[tj]s?(x)', - ], + // The glob patterns Jest uses to detect test files + testMatch: [ + // "**/__tests__/**/*.[jt]s?(x)", + '**/?(*.)+(spec|test).[tj]s?(x)', + ], - // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped - // testPathIgnorePatterns: [ - // "/node_modules/" - // ], + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + // testPathIgnorePatterns: [ + // "/node_modules/" + // ], - // The regexp pattern or array of patterns that Jest uses to detect test files - // testRegex: [], + // The regexp pattern or array of patterns that Jest uses to detect test files + // testRegex: [], - // This option allows the use of a custom results processor - // testResultsProcessor: undefined, + // This option allows the use of a custom results processor + // testResultsProcessor: undefined, - // This option allows use of a custom test runner - // testRunner: "jest-circus/runner", + // This option allows use of a custom test runner + // testRunner: "jest-circus/runner", - // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href - // testURL: "http://localhost", + // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href + // testURL: "http://localhost", - // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" - // timers: "real", + // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" + // timers: "real", - // A map from regular expressions to paths to transformers - // transform: undefined, + // A map from regular expressions to paths to transformers + // transform: undefined, - // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation - // transformIgnorePatterns: [ - // "/node_modules/", - // "\\.pnp\\.[^\\/]+$" - // ], + // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation + // transformIgnorePatterns: [ + // "/node_modules/", + // "\\.pnp\\.[^\\/]+$" + // ], - // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them - // unmockedModulePathPatterns: undefined, + // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them + // unmockedModulePathPatterns: undefined, - // Indicates whether each individual test should be reported during the run - // verbose: undefined, + // Indicates whether each individual test should be reported during the run + // verbose: undefined, - // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode - // watchPathIgnorePatterns: [], + // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode + // watchPathIgnorePatterns: [], - // Whether to use watchman for file crawling - // watchman: true, + // Whether to use watchman for file crawling + // watchman: true, }; // Required environment variable values for the config.ts file process.env = Object.assign(process.env, { - HLF_CONNECTION_PROFILE_ORG1: '{"name":"mock-profile-org1"}', - HLF_CERTIFICATE_ORG1: - '"-----BEGIN CERTIFICATE-----\\nMOCK\\n-----END CERTIFICATE-----\\n"', - HLF_PRIVATE_KEY_ORG1: - '"-----BEGIN PRIVATE KEY-----\\nMOCK\\n-----END PRIVATE KEY-----\\n"', - HLF_CONNECTION_PROFILE_ORG2: '{"name":"mock-profile-org2"}', - HLF_CERTIFICATE_ORG2: - '"-----BEGIN CERTIFICATE-----\\nMOCK\\n-----END CERTIFICATE-----\\n"', - HLF_PRIVATE_KEY_ORG2: - '"-----BEGIN PRIVATE KEY-----\\nMOCK\\n-----END PRIVATE KEY-----\\n"', - ORG1_APIKEY: 'ORG1MOCKAPIKEY', - ORG2_APIKEY: 'ORG2MOCKAPIKEY', + HLF_CONNECTION_PROFILE_ORG1: '{"name":"mock-profile-org1"}', + HLF_CERTIFICATE_ORG1: + '"-----BEGIN CERTIFICATE-----\\nMOCK\\n-----END CERTIFICATE-----\\n"', + HLF_PRIVATE_KEY_ORG1: + '"-----BEGIN PRIVATE KEY-----\\nMOCK\\n-----END PRIVATE KEY-----\\n"', + HLF_CONNECTION_PROFILE_ORG2: '{"name":"mock-profile-org2"}', + HLF_CERTIFICATE_ORG2: + '"-----BEGIN CERTIFICATE-----\\nMOCK\\n-----END CERTIFICATE-----\\n"', + HLF_PRIVATE_KEY_ORG2: + '"-----BEGIN PRIVATE KEY-----\\nMOCK\\n-----END PRIVATE KEY-----\\n"', + ORG1_APIKEY: 'ORG1MOCKAPIKEY', + ORG2_APIKEY: 'ORG2MOCKAPIKEY', }); diff --git a/asset-transfer-basic/rest-api-typescript/src/__tests__/api.test.ts b/asset-transfer-basic/rest-api-typescript/src/__tests__/api.test.ts index c20cbcf30e..913f6d951b 100644 --- a/asset-transfer-basic/rest-api-typescript/src/__tests__/api.test.ts +++ b/asset-transfer-basic/rest-api-typescript/src/__tests__/api.test.ts @@ -16,723 +16,727 @@ jest.mock('../config'); jest.mock('bullmq'); const mockAsset1 = { - ID: 'asset1', - Color: 'blue', - Size: 5, - Owner: 'Tomoko', - AppraisedValue: 300, + ID: 'asset1', + Color: 'blue', + Size: 5, + Owner: 'Tomoko', + AppraisedValue: 300, }; const mockAsset1Buffer = Buffer.from(JSON.stringify(mockAsset1)); const mockAsset2 = { - ID: 'asset2', - Color: 'red', - Size: 5, - Owner: 'Brad', - AppraisedValue: 400, + ID: 'asset2', + Color: 'red', + Size: 5, + Owner: 'Brad', + AppraisedValue: 400, }; const mockAllAssetsBuffer = Buffer.from( - JSON.stringify([mockAsset1, mockAsset2]) + JSON.stringify([mockAsset1, mockAsset2]) ); // TODO add tests for server errors describe('Asset Transfer Besic REST API', () => { - let app: Application; - let mockJobQueue: MockProxy; - - beforeEach(async () => { - app = await createServer(); - - const mockJob = mock(); - mockJob.id = '1'; - mockJobQueue = mock(); - mockJobQueue.add.mockResolvedValue(mockJob); - app.locals.jobq = mockJobQueue; - }); - - describe('/ready', () => { - it('GET should respond with 200 OK json', async () => { - const response = await request(app).get('/ready'); - expect(response.statusCode).toEqual(200); - expect(response.header).toHaveProperty( - 'content-type', - 'application/json; charset=utf-8' - ); - expect(response.body).toEqual({ - status: 'OK', - timestamp: expect.any(String), - }); + let app: Application; + let mockJobQueue: MockProxy; + + beforeEach(async () => { + app = await createServer(); + + const mockJob = mock(); + mockJob.id = '1'; + mockJobQueue = mock(); + mockJobQueue.add.mockResolvedValue(mockJob); + app.locals.jobq = mockJobQueue; + }); + + describe('/ready', () => { + it('GET should respond with 200 OK json', async () => { + const response = await request(app).get('/ready'); + expect(response.statusCode).toEqual(200); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + status: 'OK', + timestamp: expect.any(String), + }); + }); + }); + + describe('/live', () => { + it('GET should respond with 200 OK json', async () => { + const mockBlockchainInfoProto = + fabricProtos.common.BlockchainInfo.create(); + mockBlockchainInfoProto.height = 42; + const mockBlockchainInfoBuffer = Buffer.from( + fabricProtos.common.BlockchainInfo.encode( + mockBlockchainInfoProto + ).finish() + ); + + const mockOrg1QsccContract = mock(); + mockOrg1QsccContract.evaluateTransaction + .calledWith('GetChainInfo') + .mockResolvedValue(mockBlockchainInfoBuffer); + app.locals[config.mspIdOrg1] = { + qsccContract: mockOrg1QsccContract, + }; + + const mockOrg2QsccContract = mock(); + mockOrg2QsccContract.evaluateTransaction + .calledWith('GetChainInfo') + .mockResolvedValue(mockBlockchainInfoBuffer); + app.locals[config.mspIdOrg2] = { + qsccContract: mockOrg2QsccContract, + }; + + const response = await request(app).get('/live'); + expect(response.statusCode).toEqual(200); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + status: 'OK', + timestamp: expect.any(String), + }); + }); + }); + + describe('/api/assets', () => { + let mockGetAllAssetsTransaction: MockProxy; + + beforeEach(() => { + mockGetAllAssetsTransaction = mock(); + const mockBasicContract = mock(); + mockBasicContract.createTransaction + .calledWith('GetAllAssets') + .mockReturnValue(mockGetAllAssetsTransaction); + app.locals[config.mspIdOrg1] = { + assetContract: mockBasicContract, + }; + }); + + it('GET should respond with 401 unauthorized json when an invalid API key is specified', async () => { + const response = await request(app) + .get('/api/assets') + .set('X-Api-Key', 'NOTTHERIGHTAPIKEY'); + expect(response.statusCode).toEqual(401); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + reason: 'NO_VALID_APIKEY', + status: 'Unauthorized', + timestamp: expect.any(String), + }); + }); + + it('GET should respond with an empty json array when there are no assets', async () => { + mockGetAllAssetsTransaction.evaluate.mockResolvedValue( + Buffer.from('') + ); + + const response = await request(app) + .get('/api/assets') + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(200); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual([]); + }); + + it('GET should respond with json array of assets', async () => { + mockGetAllAssetsTransaction.evaluate.mockResolvedValue( + mockAllAssetsBuffer + ); + + const response = await request(app) + .get('/api/assets') + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(200); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual([ + { + ID: 'asset1', + Color: 'blue', + Size: 5, + Owner: 'Tomoko', + AppraisedValue: 300, + }, + { + ID: 'asset2', + Color: 'red', + Size: 5, + Owner: 'Brad', + AppraisedValue: 400, + }, + ]); + }); + + it('POST should respond with 401 unauthorized json when an invalid API key is specified', async () => { + const response = await request(app) + .post('/api/assets') + .send({ + ID: 'asset6', + Color: 'white', + Size: 15, + Owner: 'Michel', + AppraisedValue: 800, + }) + .set('X-Api-Key', 'NOTTHERIGHTAPIKEY'); + expect(response.statusCode).toEqual(401); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + reason: 'NO_VALID_APIKEY', + status: 'Unauthorized', + timestamp: expect.any(String), + }); + }); + + it('POST should respond with 400 bad request json for invalid asset json', async () => { + const response = await request(app) + .post('/api/assets') + .send({ + wrongidfield: 'asset3', + Color: 'red', + Size: 5, + Owner: 'Brad', + AppraisedValue: 400, + }) + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(400); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + status: 'Bad Request', + reason: 'VALIDATION_ERROR', + errors: [ + { + location: 'body', + msg: 'must be a string', + param: 'ID', + }, + ], + message: 'Invalid request body', + timestamp: expect.any(String), + }); + }); + + it('POST should respond with 202 accepted json', async () => { + const response = await request(app) + .post('/api/assets') + .send({ + ID: 'asset3', + Color: 'red', + Size: 5, + Owner: 'Brad', + AppraisedValue: 400, + }) + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(202); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + status: 'Accepted', + jobId: '1', + timestamp: expect.any(String), + }); + }); + }); + + describe('/api/assets/:id', () => { + let mockAssetExistsTransaction: MockProxy; + let mockReadAssetTransaction: MockProxy; + + beforeEach(() => { + const mockBasicContract = mock(); + + mockAssetExistsTransaction = mock(); + mockBasicContract.createTransaction + .calledWith('AssetExists') + .mockReturnValue(mockAssetExistsTransaction); + + mockReadAssetTransaction = mock(); + mockBasicContract.createTransaction + .calledWith('ReadAsset') + .mockReturnValue(mockReadAssetTransaction); + + app.locals[config.mspIdOrg1] = { + assetContract: mockBasicContract, + }; + }); + + it('OPTIONS should respond with 401 unauthorized json when an invalid API key is specified', async () => { + const response = await request(app) + .options('/api/assets/asset1') + .set('X-Api-Key', 'NOTTHERIGHTAPIKEY'); + expect(response.statusCode).toEqual(401); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + reason: 'NO_VALID_APIKEY', + status: 'Unauthorized', + timestamp: expect.any(String), + }); + }); + + it('OPTIONS should respond with 404 not found json without the allow header when there is no asset with the specified ID', async () => { + mockAssetExistsTransaction.evaluate + .calledWith('asset3') + .mockResolvedValue(Buffer.from('false')); + + const response = await request(app) + .options('/api/assets/asset3') + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(404); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.header).not.toHaveProperty('allow'); + expect(response.body).toEqual({ + status: 'Not Found', + timestamp: expect.any(String), + }); + }); + + it('OPTIONS should respond with 200 OK json with the allow header', async () => { + mockAssetExistsTransaction.evaluate + .calledWith('asset1') + .mockResolvedValue(Buffer.from('true')); + + const response = await request(app) + .options('/api/assets/asset1') + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(200); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.header).toHaveProperty( + 'allow', + 'DELETE,GET,OPTIONS,PATCH,PUT' + ); + expect(response.body).toEqual({ + status: 'OK', + timestamp: expect.any(String), + }); + }); + + it('GET should respond with 401 unauthorized json when an invalid API key is specified', async () => { + const response = await request(app) + .get('/api/assets/asset1') + .set('X-Api-Key', 'NOTTHERIGHTAPIKEY'); + expect(response.statusCode).toEqual(401); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + reason: 'NO_VALID_APIKEY', + status: 'Unauthorized', + timestamp: expect.any(String), + }); + }); + + it('GET should respond with 404 not found json when there is no asset with the specified ID', async () => { + mockReadAssetTransaction.evaluate + .calledWith('asset3') + .mockRejectedValue( + new Error('the asset asset3 does not exist') + ); + + const response = await request(app) + .get('/api/assets/asset3') + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(404); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + status: 'Not Found', + timestamp: expect.any(String), + }); + }); + + it('GET should respond with the asset json when the asset exists', async () => { + mockReadAssetTransaction.evaluate + .calledWith('asset1') + .mockResolvedValue(mockAsset1Buffer); + + const response = await request(app) + .get('/api/assets/asset1') + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(200); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + ID: 'asset1', + Color: 'blue', + Size: 5, + Owner: 'Tomoko', + AppraisedValue: 300, + }); + }); + + it('PUT should respond with 401 unauthorized json when an invalid API key is specified', async () => { + const response = await request(app) + .put('/api/assets/asset1') + .send({ + ID: 'asset3', + Color: 'red', + Size: 5, + Owner: 'Brad', + AppraisedValue: 400, + }) + .set('X-Api-Key', 'NOTTHERIGHTAPIKEY'); + expect(response.statusCode).toEqual(401); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + reason: 'NO_VALID_APIKEY', + status: 'Unauthorized', + timestamp: expect.any(String), + }); + }); + + it('PUT should respond with 400 bad request json when IDs do not match', async () => { + const response = await request(app) + .put('/api/assets/asset1') + .send({ + ID: 'asset2', + Color: 'red', + Size: 5, + Owner: 'Brad', + AppraisedValue: 400, + }) + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(400); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + status: 'Bad Request', + reason: 'ASSET_ID_MISMATCH', + message: 'Asset IDs must match', + timestamp: expect.any(String), + }); + }); + + it('PUT should respond with 400 bad request json for invalid asset json', async () => { + const response = await request(app) + .put('/api/assets/asset1') + .send({ + wrongID: 'asset1', + Color: 'red', + Size: 5, + Owner: 'Brad', + AppraisedValue: 400, + }) + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(400); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + status: 'Bad Request', + reason: 'VALIDATION_ERROR', + errors: [ + { + location: 'body', + msg: 'must be a string', + param: 'ID', + }, + ], + message: 'Invalid request body', + timestamp: expect.any(String), + }); + }); + + it('PUT should respond with 202 accepted json', async () => { + const response = await request(app) + .put('/api/assets/asset1') + .send({ + ID: 'asset1', + Color: 'red', + Size: 5, + Owner: 'Brad', + AppraisedValue: 400, + }) + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(202); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + status: 'Accepted', + jobId: '1', + timestamp: expect.any(String), + }); + }); + + it('PATCH should respond with 401 unauthorized json when an invalid API key is specified', async () => { + const response = await request(app) + .patch('/api/assets/asset1') + .send([{ op: 'replace', path: '/Owner', value: 'Ashleigh' }]) + .set('X-Api-Key', 'NOTTHERIGHTAPIKEY'); + expect(response.statusCode).toEqual(401); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + reason: 'NO_VALID_APIKEY', + status: 'Unauthorized', + timestamp: expect.any(String), + }); + }); + + it('PATCH should respond with 400 bad request json for invalid patch op/path', async () => { + const response = await request(app) + .patch('/api/assets/asset1') + .send([{ op: 'replace', path: '/color', value: 'orange' }]) + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(400); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + status: 'Bad Request', + reason: 'VALIDATION_ERROR', + errors: [ + { + location: 'body', + msg: "path must be '/Owner'", + param: '[0].path', + value: '/color', + }, + ], + message: 'Invalid request body', + timestamp: expect.any(String), + }); + }); + + it('PATCH should respond with 202 accepted json', async () => { + const response = await request(app) + .patch('/api/assets/asset1') + .send([{ op: 'replace', path: '/Owner', value: 'Ashleigh' }]) + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(202); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + status: 'Accepted', + jobId: '1', + timestamp: expect.any(String), + }); + }); + + it('DELETE should respond with 401 unauthorized json when an invalid API key is specified', async () => { + const response = await request(app) + .delete('/api/assets/asset1') + .set('X-Api-Key', 'NOTTHERIGHTAPIKEY'); + expect(response.statusCode).toEqual(401); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + reason: 'NO_VALID_APIKEY', + status: 'Unauthorized', + timestamp: expect.any(String), + }); + }); + + it('DELETE should respond with 202 accepted json', async () => { + const response = await request(app) + .delete('/api/assets/asset1') + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(202); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + status: 'Accepted', + jobId: '1', + timestamp: expect.any(String), + }); + }); + }); + + describe('/api/jobs/:id', () => { + it('GET should respond with 401 unauthorized json when an invalid API key is specified', async () => { + const response = await request(app) + .get('/api/jobs/1') + .set('X-Api-Key', 'NOTTHERIGHTAPIKEY'); + expect(response.statusCode).toEqual(401); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + reason: 'NO_VALID_APIKEY', + status: 'Unauthorized', + timestamp: expect.any(String), + }); + }); + + it('GET should respond with 404 not found json when there is no job with the specified ID', async () => { + jest.mocked(Job.fromId).mockResolvedValue(undefined); + + const response = await request(app) + .get('/api/jobs/3') + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(404); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + status: 'Not Found', + timestamp: expect.any(String), + }); + }); + + it('GET should respond with json details for the specified job ID', async () => { + const mockJob = mock(); + mockJob.id = '2'; + mockJob.data = { + transactionIds: ['txn1', 'txn2'], + }; + mockJob.returnvalue = { + transactionError: 'Mock error', + transactionPayload: Buffer.from('Mock payload'), + }; + mockJobQueue.getJob.calledWith('2').mockResolvedValue(mockJob); + + const response = await request(app) + .get('/api/jobs/2') + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(200); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + jobId: '2', + transactionIds: ['txn1', 'txn2'], + transactionError: 'Mock error', + transactionPayload: 'Mock payload', + }); + }); + }); + + describe('/api/transactions/:id', () => { + let mockGetTransactionByIDTransaction: MockProxy; + + beforeEach(() => { + mockGetTransactionByIDTransaction = mock(); + const mockQsccContract = mock(); + mockQsccContract.createTransaction + .calledWith('GetTransactionByID') + .mockReturnValue(mockGetTransactionByIDTransaction); + app.locals[config.mspIdOrg1] = { + qsccContract: mockQsccContract, + }; + }); + + it('GET should respond with 401 unauthorized json when an invalid API key is specified', async () => { + const response = await request(app) + .get('/api/transactions/txn1') + .set('X-Api-Key', 'NOTTHERIGHTAPIKEY'); + expect(response.statusCode).toEqual(401); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + reason: 'NO_VALID_APIKEY', + status: 'Unauthorized', + timestamp: expect.any(String), + }); + }); + + it('GET should respond with 404 not found json when there is no transaction with the specified ID', async () => { + mockGetTransactionByIDTransaction.evaluate + .calledWith('mychannel', 'txn3') + .mockRejectedValue( + new Error( + 'Failed to get transaction with id txn3, error Entry not found in index' + ) + ); + + const response = await request(app) + .get('/api/transactions/txn3') + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(404); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + status: 'Not Found', + timestamp: expect.any(String), + }); + }); + + it('GET should respond with json details for the specified transaction ID', async () => { + const processedTransactionProto = + fabricProtos.protos.ProcessedTransaction.create(); + processedTransactionProto.validationCode = + fabricProtos.protos.TxValidationCode.VALID; + const processedTransactionBuffer = Buffer.from( + fabricProtos.protos.ProcessedTransaction.encode( + processedTransactionProto + ).finish() + ); + mockGetTransactionByIDTransaction.evaluate + .calledWith('mychannel', 'txn2') + .mockResolvedValue(processedTransactionBuffer); + + const response = await request(app) + .get('/api/transactions/txn2') + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(200); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + transactionId: 'txn2', + validationCode: 'VALID', + }); + }); }); - }); - - describe('/live', () => { - it('GET should respond with 200 OK json', async () => { - const mockBlockchainInfoProto = - fabricProtos.common.BlockchainInfo.create(); - mockBlockchainInfoProto.height = 42; - const mockBlockchainInfoBuffer = Buffer.from( - fabricProtos.common.BlockchainInfo.encode( - mockBlockchainInfoProto - ).finish() - ); - - const mockOrg1QsccContract = mock(); - mockOrg1QsccContract.evaluateTransaction - .calledWith('GetChainInfo') - .mockResolvedValue(mockBlockchainInfoBuffer); - app.locals[config.mspIdOrg1] = { - qsccContract: mockOrg1QsccContract, - }; - - const mockOrg2QsccContract = mock(); - mockOrg2QsccContract.evaluateTransaction - .calledWith('GetChainInfo') - .mockResolvedValue(mockBlockchainInfoBuffer); - app.locals[config.mspIdOrg2] = { - qsccContract: mockOrg2QsccContract, - }; - - const response = await request(app).get('/live'); - expect(response.statusCode).toEqual(200); - expect(response.header).toHaveProperty( - 'content-type', - 'application/json; charset=utf-8' - ); - expect(response.body).toEqual({ - status: 'OK', - timestamp: expect.any(String), - }); - }); - }); - - describe('/api/assets', () => { - let mockGetAllAssetsTransaction: MockProxy; - - beforeEach(() => { - mockGetAllAssetsTransaction = mock(); - const mockBasicContract = mock(); - mockBasicContract.createTransaction - .calledWith('GetAllAssets') - .mockReturnValue(mockGetAllAssetsTransaction); - app.locals[config.mspIdOrg1] = { - assetContract: mockBasicContract, - }; - }); - - it('GET should respond with 401 unauthorized json when an invalid API key is specified', async () => { - const response = await request(app) - .get('/api/assets') - .set('X-Api-Key', 'NOTTHERIGHTAPIKEY'); - expect(response.statusCode).toEqual(401); - expect(response.header).toHaveProperty( - 'content-type', - 'application/json; charset=utf-8' - ); - expect(response.body).toEqual({ - reason: 'NO_VALID_APIKEY', - status: 'Unauthorized', - timestamp: expect.any(String), - }); - }); - - it('GET should respond with an empty json array when there are no assets', async () => { - mockGetAllAssetsTransaction.evaluate.mockResolvedValue(Buffer.from('')); - - const response = await request(app) - .get('/api/assets') - .set('X-Api-Key', 'ORG1MOCKAPIKEY'); - expect(response.statusCode).toEqual(200); - expect(response.header).toHaveProperty( - 'content-type', - 'application/json; charset=utf-8' - ); - expect(response.body).toEqual([]); - }); - - it('GET should respond with json array of assets', async () => { - mockGetAllAssetsTransaction.evaluate.mockResolvedValue( - mockAllAssetsBuffer - ); - - const response = await request(app) - .get('/api/assets') - .set('X-Api-Key', 'ORG1MOCKAPIKEY'); - expect(response.statusCode).toEqual(200); - expect(response.header).toHaveProperty( - 'content-type', - 'application/json; charset=utf-8' - ); - expect(response.body).toEqual([ - { - ID: 'asset1', - Color: 'blue', - Size: 5, - Owner: 'Tomoko', - AppraisedValue: 300, - }, - { - ID: 'asset2', - Color: 'red', - Size: 5, - Owner: 'Brad', - AppraisedValue: 400, - }, - ]); - }); - - it('POST should respond with 401 unauthorized json when an invalid API key is specified', async () => { - const response = await request(app) - .post('/api/assets') - .send({ - ID: 'asset6', - Color: 'white', - Size: 15, - Owner: 'Michel', - AppraisedValue: 800, - }) - .set('X-Api-Key', 'NOTTHERIGHTAPIKEY'); - expect(response.statusCode).toEqual(401); - expect(response.header).toHaveProperty( - 'content-type', - 'application/json; charset=utf-8' - ); - expect(response.body).toEqual({ - reason: 'NO_VALID_APIKEY', - status: 'Unauthorized', - timestamp: expect.any(String), - }); - }); - - it('POST should respond with 400 bad request json for invalid asset json', async () => { - const response = await request(app) - .post('/api/assets') - .send({ - wrongidfield: 'asset3', - Color: 'red', - Size: 5, - Owner: 'Brad', - AppraisedValue: 400, - }) - .set('X-Api-Key', 'ORG1MOCKAPIKEY'); - expect(response.statusCode).toEqual(400); - expect(response.header).toHaveProperty( - 'content-type', - 'application/json; charset=utf-8' - ); - expect(response.body).toEqual({ - status: 'Bad Request', - reason: 'VALIDATION_ERROR', - errors: [ - { - location: 'body', - msg: 'must be a string', - param: 'ID', - }, - ], - message: 'Invalid request body', - timestamp: expect.any(String), - }); - }); - - it('POST should respond with 202 accepted json', async () => { - const response = await request(app) - .post('/api/assets') - .send({ - ID: 'asset3', - Color: 'red', - Size: 5, - Owner: 'Brad', - AppraisedValue: 400, - }) - .set('X-Api-Key', 'ORG1MOCKAPIKEY'); - expect(response.statusCode).toEqual(202); - expect(response.header).toHaveProperty( - 'content-type', - 'application/json; charset=utf-8' - ); - expect(response.body).toEqual({ - status: 'Accepted', - jobId: '1', - timestamp: expect.any(String), - }); - }); - }); - - describe('/api/assets/:id', () => { - let mockAssetExistsTransaction: MockProxy; - let mockReadAssetTransaction: MockProxy; - - beforeEach(() => { - const mockBasicContract = mock(); - - mockAssetExistsTransaction = mock(); - mockBasicContract.createTransaction - .calledWith('AssetExists') - .mockReturnValue(mockAssetExistsTransaction); - - mockReadAssetTransaction = mock(); - mockBasicContract.createTransaction - .calledWith('ReadAsset') - .mockReturnValue(mockReadAssetTransaction); - - app.locals[config.mspIdOrg1] = { - assetContract: mockBasicContract, - }; - }); - - it('OPTIONS should respond with 401 unauthorized json when an invalid API key is specified', async () => { - const response = await request(app) - .options('/api/assets/asset1') - .set('X-Api-Key', 'NOTTHERIGHTAPIKEY'); - expect(response.statusCode).toEqual(401); - expect(response.header).toHaveProperty( - 'content-type', - 'application/json; charset=utf-8' - ); - expect(response.body).toEqual({ - reason: 'NO_VALID_APIKEY', - status: 'Unauthorized', - timestamp: expect.any(String), - }); - }); - - it('OPTIONS should respond with 404 not found json without the allow header when there is no asset with the specified ID', async () => { - mockAssetExistsTransaction.evaluate - .calledWith('asset3') - .mockResolvedValue(Buffer.from('false')); - - const response = await request(app) - .options('/api/assets/asset3') - .set('X-Api-Key', 'ORG1MOCKAPIKEY'); - expect(response.statusCode).toEqual(404); - expect(response.header).toHaveProperty( - 'content-type', - 'application/json; charset=utf-8' - ); - expect(response.header).not.toHaveProperty('allow'); - expect(response.body).toEqual({ - status: 'Not Found', - timestamp: expect.any(String), - }); - }); - - it('OPTIONS should respond with 200 OK json with the allow header', async () => { - mockAssetExistsTransaction.evaluate - .calledWith('asset1') - .mockResolvedValue(Buffer.from('true')); - - const response = await request(app) - .options('/api/assets/asset1') - .set('X-Api-Key', 'ORG1MOCKAPIKEY'); - expect(response.statusCode).toEqual(200); - expect(response.header).toHaveProperty( - 'content-type', - 'application/json; charset=utf-8' - ); - expect(response.header).toHaveProperty( - 'allow', - 'DELETE,GET,OPTIONS,PATCH,PUT' - ); - expect(response.body).toEqual({ - status: 'OK', - timestamp: expect.any(String), - }); - }); - - it('GET should respond with 401 unauthorized json when an invalid API key is specified', async () => { - const response = await request(app) - .get('/api/assets/asset1') - .set('X-Api-Key', 'NOTTHERIGHTAPIKEY'); - expect(response.statusCode).toEqual(401); - expect(response.header).toHaveProperty( - 'content-type', - 'application/json; charset=utf-8' - ); - expect(response.body).toEqual({ - reason: 'NO_VALID_APIKEY', - status: 'Unauthorized', - timestamp: expect.any(String), - }); - }); - - it('GET should respond with 404 not found json when there is no asset with the specified ID', async () => { - mockReadAssetTransaction.evaluate - .calledWith('asset3') - .mockRejectedValue(new Error('the asset asset3 does not exist')); - - const response = await request(app) - .get('/api/assets/asset3') - .set('X-Api-Key', 'ORG1MOCKAPIKEY'); - expect(response.statusCode).toEqual(404); - expect(response.header).toHaveProperty( - 'content-type', - 'application/json; charset=utf-8' - ); - expect(response.body).toEqual({ - status: 'Not Found', - timestamp: expect.any(String), - }); - }); - - it('GET should respond with the asset json when the asset exists', async () => { - mockReadAssetTransaction.evaluate - .calledWith('asset1') - .mockResolvedValue(mockAsset1Buffer); - - const response = await request(app) - .get('/api/assets/asset1') - .set('X-Api-Key', 'ORG1MOCKAPIKEY'); - expect(response.statusCode).toEqual(200); - expect(response.header).toHaveProperty( - 'content-type', - 'application/json; charset=utf-8' - ); - expect(response.body).toEqual({ - ID: 'asset1', - Color: 'blue', - Size: 5, - Owner: 'Tomoko', - AppraisedValue: 300, - }); - }); - - it('PUT should respond with 401 unauthorized json when an invalid API key is specified', async () => { - const response = await request(app) - .put('/api/assets/asset1') - .send({ - ID: 'asset3', - Color: 'red', - Size: 5, - Owner: 'Brad', - AppraisedValue: 400, - }) - .set('X-Api-Key', 'NOTTHERIGHTAPIKEY'); - expect(response.statusCode).toEqual(401); - expect(response.header).toHaveProperty( - 'content-type', - 'application/json; charset=utf-8' - ); - expect(response.body).toEqual({ - reason: 'NO_VALID_APIKEY', - status: 'Unauthorized', - timestamp: expect.any(String), - }); - }); - - it('PUT should respond with 400 bad request json when IDs do not match', async () => { - const response = await request(app) - .put('/api/assets/asset1') - .send({ - ID: 'asset2', - Color: 'red', - Size: 5, - Owner: 'Brad', - AppraisedValue: 400, - }) - .set('X-Api-Key', 'ORG1MOCKAPIKEY'); - expect(response.statusCode).toEqual(400); - expect(response.header).toHaveProperty( - 'content-type', - 'application/json; charset=utf-8' - ); - expect(response.body).toEqual({ - status: 'Bad Request', - reason: 'ASSET_ID_MISMATCH', - message: 'Asset IDs must match', - timestamp: expect.any(String), - }); - }); - - it('PUT should respond with 400 bad request json for invalid asset json', async () => { - const response = await request(app) - .put('/api/assets/asset1') - .send({ - wrongID: 'asset1', - Color: 'red', - Size: 5, - Owner: 'Brad', - AppraisedValue: 400, - }) - .set('X-Api-Key', 'ORG1MOCKAPIKEY'); - expect(response.statusCode).toEqual(400); - expect(response.header).toHaveProperty( - 'content-type', - 'application/json; charset=utf-8' - ); - expect(response.body).toEqual({ - status: 'Bad Request', - reason: 'VALIDATION_ERROR', - errors: [ - { - location: 'body', - msg: 'must be a string', - param: 'ID', - }, - ], - message: 'Invalid request body', - timestamp: expect.any(String), - }); - }); - - it('PUT should respond with 202 accepted json', async () => { - const response = await request(app) - .put('/api/assets/asset1') - .send({ - ID: 'asset1', - Color: 'red', - Size: 5, - Owner: 'Brad', - AppraisedValue: 400, - }) - .set('X-Api-Key', 'ORG1MOCKAPIKEY'); - expect(response.statusCode).toEqual(202); - expect(response.header).toHaveProperty( - 'content-type', - 'application/json; charset=utf-8' - ); - expect(response.body).toEqual({ - status: 'Accepted', - jobId: '1', - timestamp: expect.any(String), - }); - }); - - it('PATCH should respond with 401 unauthorized json when an invalid API key is specified', async () => { - const response = await request(app) - .patch('/api/assets/asset1') - .send([{ op: 'replace', path: '/Owner', value: 'Ashleigh' }]) - .set('X-Api-Key', 'NOTTHERIGHTAPIKEY'); - expect(response.statusCode).toEqual(401); - expect(response.header).toHaveProperty( - 'content-type', - 'application/json; charset=utf-8' - ); - expect(response.body).toEqual({ - reason: 'NO_VALID_APIKEY', - status: 'Unauthorized', - timestamp: expect.any(String), - }); - }); - - it('PATCH should respond with 400 bad request json for invalid patch op/path', async () => { - const response = await request(app) - .patch('/api/assets/asset1') - .send([{ op: 'replace', path: '/color', value: 'orange' }]) - .set('X-Api-Key', 'ORG1MOCKAPIKEY'); - expect(response.statusCode).toEqual(400); - expect(response.header).toHaveProperty( - 'content-type', - 'application/json; charset=utf-8' - ); - expect(response.body).toEqual({ - status: 'Bad Request', - reason: 'VALIDATION_ERROR', - errors: [ - { - location: 'body', - msg: "path must be '/Owner'", - param: '[0].path', - value: '/color', - }, - ], - message: 'Invalid request body', - timestamp: expect.any(String), - }); - }); - - it('PATCH should respond with 202 accepted json', async () => { - const response = await request(app) - .patch('/api/assets/asset1') - .send([{ op: 'replace', path: '/Owner', value: 'Ashleigh' }]) - .set('X-Api-Key', 'ORG1MOCKAPIKEY'); - expect(response.statusCode).toEqual(202); - expect(response.header).toHaveProperty( - 'content-type', - 'application/json; charset=utf-8' - ); - expect(response.body).toEqual({ - status: 'Accepted', - jobId: '1', - timestamp: expect.any(String), - }); - }); - - it('DELETE should respond with 401 unauthorized json when an invalid API key is specified', async () => { - const response = await request(app) - .delete('/api/assets/asset1') - .set('X-Api-Key', 'NOTTHERIGHTAPIKEY'); - expect(response.statusCode).toEqual(401); - expect(response.header).toHaveProperty( - 'content-type', - 'application/json; charset=utf-8' - ); - expect(response.body).toEqual({ - reason: 'NO_VALID_APIKEY', - status: 'Unauthorized', - timestamp: expect.any(String), - }); - }); - - it('DELETE should respond with 202 accepted json', async () => { - const response = await request(app) - .delete('/api/assets/asset1') - .set('X-Api-Key', 'ORG1MOCKAPIKEY'); - expect(response.statusCode).toEqual(202); - expect(response.header).toHaveProperty( - 'content-type', - 'application/json; charset=utf-8' - ); - expect(response.body).toEqual({ - status: 'Accepted', - jobId: '1', - timestamp: expect.any(String), - }); - }); - }); - - describe('/api/jobs/:id', () => { - it('GET should respond with 401 unauthorized json when an invalid API key is specified', async () => { - const response = await request(app) - .get('/api/jobs/1') - .set('X-Api-Key', 'NOTTHERIGHTAPIKEY'); - expect(response.statusCode).toEqual(401); - expect(response.header).toHaveProperty( - 'content-type', - 'application/json; charset=utf-8' - ); - expect(response.body).toEqual({ - reason: 'NO_VALID_APIKEY', - status: 'Unauthorized', - timestamp: expect.any(String), - }); - }); - - it('GET should respond with 404 not found json when there is no job with the specified ID', async () => { - jest.mocked(Job.fromId).mockResolvedValue(undefined); - - const response = await request(app) - .get('/api/jobs/3') - .set('X-Api-Key', 'ORG1MOCKAPIKEY'); - expect(response.statusCode).toEqual(404); - expect(response.header).toHaveProperty( - 'content-type', - 'application/json; charset=utf-8' - ); - expect(response.body).toEqual({ - status: 'Not Found', - timestamp: expect.any(String), - }); - }); - - it('GET should respond with json details for the specified job ID', async () => { - const mockJob = mock(); - mockJob.id = '2'; - mockJob.data = { - transactionIds: ['txn1', 'txn2'], - }; - mockJob.returnvalue = { - transactionError: 'Mock error', - transactionPayload: Buffer.from('Mock payload'), - }; - mockJobQueue.getJob.calledWith('2').mockResolvedValue(mockJob); - - const response = await request(app) - .get('/api/jobs/2') - .set('X-Api-Key', 'ORG1MOCKAPIKEY'); - expect(response.statusCode).toEqual(200); - expect(response.header).toHaveProperty( - 'content-type', - 'application/json; charset=utf-8' - ); - expect(response.body).toEqual({ - jobId: '2', - transactionIds: ['txn1', 'txn2'], - transactionError: 'Mock error', - transactionPayload: 'Mock payload', - }); - }); - }); - - describe('/api/transactions/:id', () => { - let mockGetTransactionByIDTransaction: MockProxy; - - beforeEach(() => { - mockGetTransactionByIDTransaction = mock(); - const mockQsccContract = mock(); - mockQsccContract.createTransaction - .calledWith('GetTransactionByID') - .mockReturnValue(mockGetTransactionByIDTransaction); - app.locals[config.mspIdOrg1] = { - qsccContract: mockQsccContract, - }; - }); - - it('GET should respond with 401 unauthorized json when an invalid API key is specified', async () => { - const response = await request(app) - .get('/api/transactions/txn1') - .set('X-Api-Key', 'NOTTHERIGHTAPIKEY'); - expect(response.statusCode).toEqual(401); - expect(response.header).toHaveProperty( - 'content-type', - 'application/json; charset=utf-8' - ); - expect(response.body).toEqual({ - reason: 'NO_VALID_APIKEY', - status: 'Unauthorized', - timestamp: expect.any(String), - }); - }); - - it('GET should respond with 404 not found json when there is no transaction with the specified ID', async () => { - mockGetTransactionByIDTransaction.evaluate - .calledWith('mychannel', 'txn3') - .mockRejectedValue( - new Error( - 'Failed to get transaction with id txn3, error Entry not found in index' - ) - ); - - const response = await request(app) - .get('/api/transactions/txn3') - .set('X-Api-Key', 'ORG1MOCKAPIKEY'); - expect(response.statusCode).toEqual(404); - expect(response.header).toHaveProperty( - 'content-type', - 'application/json; charset=utf-8' - ); - expect(response.body).toEqual({ - status: 'Not Found', - timestamp: expect.any(String), - }); - }); - - it('GET should respond with json details for the specified transaction ID', async () => { - const processedTransactionProto = - fabricProtos.protos.ProcessedTransaction.create(); - processedTransactionProto.validationCode = - fabricProtos.protos.TxValidationCode.VALID; - const processedTransactionBuffer = Buffer.from( - fabricProtos.protos.ProcessedTransaction.encode( - processedTransactionProto - ).finish() - ); - mockGetTransactionByIDTransaction.evaluate - .calledWith('mychannel', 'txn2') - .mockResolvedValue(processedTransactionBuffer); - - const response = await request(app) - .get('/api/transactions/txn2') - .set('X-Api-Key', 'ORG1MOCKAPIKEY'); - expect(response.statusCode).toEqual(200); - expect(response.header).toHaveProperty( - 'content-type', - 'application/json; charset=utf-8' - ); - expect(response.body).toEqual({ - transactionId: 'txn2', - validationCode: 'VALID', - }); - }); - }); }); diff --git a/asset-transfer-basic/rest-api-typescript/src/assets.router.ts b/asset-transfer-basic/rest-api-typescript/src/assets.router.ts index f76deb4255..39cf66dd97 100644 --- a/asset-transfer-basic/rest-api-typescript/src/assets.router.ts +++ b/asset-transfer-basic/rest-api-typescript/src/assets.router.ts @@ -29,319 +29,325 @@ import { addSubmitTransactionJob } from './jobs'; import { logger } from './logger'; const { ACCEPTED, BAD_REQUEST, INTERNAL_SERVER_ERROR, NOT_FOUND, OK } = - StatusCodes; + StatusCodes; export const assetsRouter = express.Router(); assetsRouter.get('/', async (req: Request, res: Response) => { - logger.debug('Get all assets request received'); - try { - const mspId = req.user as string; - const contract = req.app.locals[mspId]?.assetContract as Contract; + logger.debug('Get all assets request received'); + try { + const mspId = req.user as string; + const contract = req.app.locals[mspId]?.assetContract as Contract; - const data = await evatuateTransaction(contract, 'GetAllAssets'); - let assets = []; - if (data.length > 0) { - assets = JSON.parse(data.toString()); - } + const data = await evatuateTransaction(contract, 'GetAllAssets'); + let assets = []; + if (data.length > 0) { + assets = JSON.parse(data.toString()); + } - return res.status(OK).json(assets); - } catch (err) { - logger.error({ err }, 'Error processing get all assets request'); - return res.status(INTERNAL_SERVER_ERROR).json({ - status: getReasonPhrase(INTERNAL_SERVER_ERROR), - timestamp: new Date().toISOString(), - }); - } + return res.status(OK).json(assets); + } catch (err) { + logger.error({ err }, 'Error processing get all assets request'); + return res.status(INTERNAL_SERVER_ERROR).json({ + status: getReasonPhrase(INTERNAL_SERVER_ERROR), + timestamp: new Date().toISOString(), + }); + } }); assetsRouter.post( - '/', - body().isObject().withMessage('body must contain an asset object'), - body('ID', 'must be a string').notEmpty(), - body('Color', 'must be a string').notEmpty(), - body('Size', 'must be a number').isNumeric(), - body('Owner', 'must be a string').notEmpty(), - body('AppraisedValue', 'must be a number').isNumeric(), - async (req: Request, res: Response) => { - logger.debug(req.body, 'Create asset request received'); - - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(BAD_REQUEST).json({ - status: getReasonPhrase(BAD_REQUEST), - reason: 'VALIDATION_ERROR', - message: 'Invalid request body', - timestamp: new Date().toISOString(), - errors: errors.array(), - }); - } - - const mspId = req.user as string; - const assetId = req.body.ID; - - try { - const submitQueue = req.app.locals.jobq as Queue; - const jobId = await addSubmitTransactionJob( - submitQueue, - mspId, - 'CreateAsset', - assetId, - req.body.Color, - req.body.Size, - req.body.Owner, - req.body.AppraisedValue - ); - - return res.status(ACCEPTED).json({ - status: getReasonPhrase(ACCEPTED), - jobId: jobId, - timestamp: new Date().toISOString(), - }); - } catch (err) { - logger.error( - { err }, - 'Error processing create asset request for asset ID %s', - assetId - ); - - return res.status(INTERNAL_SERVER_ERROR).json({ - status: getReasonPhrase(INTERNAL_SERVER_ERROR), - timestamp: new Date().toISOString(), - }); + '/', + body().isObject().withMessage('body must contain an asset object'), + body('ID', 'must be a string').notEmpty(), + body('Color', 'must be a string').notEmpty(), + body('Size', 'must be a number').isNumeric(), + body('Owner', 'must be a string').notEmpty(), + body('AppraisedValue', 'must be a number').isNumeric(), + async (req: Request, res: Response) => { + logger.debug(req.body, 'Create asset request received'); + + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(BAD_REQUEST).json({ + status: getReasonPhrase(BAD_REQUEST), + reason: 'VALIDATION_ERROR', + message: 'Invalid request body', + timestamp: new Date().toISOString(), + errors: errors.array(), + }); + } + + const mspId = req.user as string; + const assetId = req.body.ID; + + try { + const submitQueue = req.app.locals.jobq as Queue; + const jobId = await addSubmitTransactionJob( + submitQueue, + mspId, + 'CreateAsset', + assetId, + req.body.Color, + req.body.Size, + req.body.Owner, + req.body.AppraisedValue + ); + + return res.status(ACCEPTED).json({ + status: getReasonPhrase(ACCEPTED), + jobId: jobId, + timestamp: new Date().toISOString(), + }); + } catch (err) { + logger.error( + { err }, + 'Error processing create asset request for asset ID %s', + assetId + ); + + return res.status(INTERNAL_SERVER_ERROR).json({ + status: getReasonPhrase(INTERNAL_SERVER_ERROR), + timestamp: new Date().toISOString(), + }); + } } - } ); assetsRouter.options('/:assetId', async (req: Request, res: Response) => { - const assetId = req.params.assetId; - logger.debug('Asset options request received for asset ID %s', assetId); - - try { - const mspId = req.user as string; - const contract = req.app.locals[mspId]?.assetContract as Contract; - - const data = await evatuateTransaction(contract, 'AssetExists', assetId); - const exists = data.toString() === 'true'; + const assetId = req.params.assetId; + logger.debug('Asset options request received for asset ID %s', assetId); - if (exists) { - return res - .status(OK) - .set({ - Allow: 'DELETE,GET,OPTIONS,PATCH,PUT', - }) - .json({ - status: getReasonPhrase(OK), - timestamp: new Date().toISOString(), + try { + const mspId = req.user as string; + const contract = req.app.locals[mspId]?.assetContract as Contract; + + const data = await evatuateTransaction( + contract, + 'AssetExists', + assetId + ); + const exists = data.toString() === 'true'; + + if (exists) { + return res + .status(OK) + .set({ + Allow: 'DELETE,GET,OPTIONS,PATCH,PUT', + }) + .json({ + status: getReasonPhrase(OK), + timestamp: new Date().toISOString(), + }); + } else { + return res.status(NOT_FOUND).json({ + status: getReasonPhrase(NOT_FOUND), + timestamp: new Date().toISOString(), + }); + } + } catch (err) { + logger.error( + { err }, + 'Error processing asset options request for asset ID %s', + assetId + ); + return res.status(INTERNAL_SERVER_ERROR).json({ + status: getReasonPhrase(INTERNAL_SERVER_ERROR), + timestamp: new Date().toISOString(), }); - } else { - return res.status(NOT_FOUND).json({ - status: getReasonPhrase(NOT_FOUND), - timestamp: new Date().toISOString(), - }); } - } catch (err) { - logger.error( - { err }, - 'Error processing asset options request for asset ID %s', - assetId - ); - return res.status(INTERNAL_SERVER_ERROR).json({ - status: getReasonPhrase(INTERNAL_SERVER_ERROR), - timestamp: new Date().toISOString(), - }); - } }); assetsRouter.get('/:assetId', async (req: Request, res: Response) => { - const assetId = req.params.assetId; - logger.debug('Read asset request received for asset ID %s', assetId); - - try { - const mspId = req.user as string; - const contract = req.app.locals[mspId]?.assetContract as Contract; - - const data = await evatuateTransaction(contract, 'ReadAsset', assetId); - const asset = JSON.parse(data.toString()); - - return res.status(OK).json(asset); - } catch (err) { - logger.error( - { err }, - 'Error processing read asset request for asset ID %s', - assetId - ); - - if (err instanceof AssetNotFoundError) { - return res.status(NOT_FOUND).json({ - status: getReasonPhrase(NOT_FOUND), - timestamp: new Date().toISOString(), - }); - } + const assetId = req.params.assetId; + logger.debug('Read asset request received for asset ID %s', assetId); - return res.status(INTERNAL_SERVER_ERROR).json({ - status: getReasonPhrase(INTERNAL_SERVER_ERROR), - timestamp: new Date().toISOString(), - }); - } -}); + try { + const mspId = req.user as string; + const contract = req.app.locals[mspId]?.assetContract as Contract; -assetsRouter.put( - '/:assetId', - body().isObject().withMessage('body must contain an asset object'), - body('ID', 'must be a string').notEmpty(), - body('Color', 'must be a string').notEmpty(), - body('Size', 'must be a number').isNumeric(), - body('Owner', 'must be a string').notEmpty(), - body('AppraisedValue', 'must be a number').isNumeric(), - async (req: Request, res: Response) => { - logger.debug(req.body, 'Update asset request received'); - - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(BAD_REQUEST).json({ - status: getReasonPhrase(BAD_REQUEST), - reason: 'VALIDATION_ERROR', - message: 'Invalid request body', - timestamp: new Date().toISOString(), - errors: errors.array(), - }); - } + const data = await evatuateTransaction(contract, 'ReadAsset', assetId); + const asset = JSON.parse(data.toString()); - if (req.params.assetId != req.body.ID) { - return res.status(BAD_REQUEST).json({ - status: getReasonPhrase(BAD_REQUEST), - reason: 'ASSET_ID_MISMATCH', - message: 'Asset IDs must match', - timestamp: new Date().toISOString(), - }); + return res.status(OK).json(asset); + } catch (err) { + logger.error( + { err }, + 'Error processing read asset request for asset ID %s', + assetId + ); + + if (err instanceof AssetNotFoundError) { + return res.status(NOT_FOUND).json({ + status: getReasonPhrase(NOT_FOUND), + timestamp: new Date().toISOString(), + }); + } + + return res.status(INTERNAL_SERVER_ERROR).json({ + status: getReasonPhrase(INTERNAL_SERVER_ERROR), + timestamp: new Date().toISOString(), + }); } +}); - const mspId = req.user as string; - const assetId = req.params.assetId; - - try { - const submitQueue = req.app.locals.jobq as Queue; - const jobId = await addSubmitTransactionJob( - submitQueue, - mspId, - 'UpdateAsset', - assetId, - req.body.color, - req.body.size, - req.body.owner, - req.body.appraisedValue - ); - - return res.status(ACCEPTED).json({ - status: getReasonPhrase(ACCEPTED), - jobId: jobId, - timestamp: new Date().toISOString(), - }); - } catch (err) { - logger.error( - { err }, - 'Error processing update asset request for asset ID %s', - assetId - ); - - return res.status(INTERNAL_SERVER_ERROR).json({ - status: getReasonPhrase(INTERNAL_SERVER_ERROR), - timestamp: new Date().toISOString(), - }); +assetsRouter.put( + '/:assetId', + body().isObject().withMessage('body must contain an asset object'), + body('ID', 'must be a string').notEmpty(), + body('Color', 'must be a string').notEmpty(), + body('Size', 'must be a number').isNumeric(), + body('Owner', 'must be a string').notEmpty(), + body('AppraisedValue', 'must be a number').isNumeric(), + async (req: Request, res: Response) => { + logger.debug(req.body, 'Update asset request received'); + + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(BAD_REQUEST).json({ + status: getReasonPhrase(BAD_REQUEST), + reason: 'VALIDATION_ERROR', + message: 'Invalid request body', + timestamp: new Date().toISOString(), + errors: errors.array(), + }); + } + + if (req.params.assetId != req.body.ID) { + return res.status(BAD_REQUEST).json({ + status: getReasonPhrase(BAD_REQUEST), + reason: 'ASSET_ID_MISMATCH', + message: 'Asset IDs must match', + timestamp: new Date().toISOString(), + }); + } + + const mspId = req.user as string; + const assetId = req.params.assetId; + + try { + const submitQueue = req.app.locals.jobq as Queue; + const jobId = await addSubmitTransactionJob( + submitQueue, + mspId, + 'UpdateAsset', + assetId, + req.body.color, + req.body.size, + req.body.owner, + req.body.appraisedValue + ); + + return res.status(ACCEPTED).json({ + status: getReasonPhrase(ACCEPTED), + jobId: jobId, + timestamp: new Date().toISOString(), + }); + } catch (err) { + logger.error( + { err }, + 'Error processing update asset request for asset ID %s', + assetId + ); + + return res.status(INTERNAL_SERVER_ERROR).json({ + status: getReasonPhrase(INTERNAL_SERVER_ERROR), + timestamp: new Date().toISOString(), + }); + } } - } ); assetsRouter.patch( - '/:assetId', - body() - .isArray({ - min: 1, - max: 1, - }) - .withMessage('body must contain an array with a single patch operation'), - body('*.op', "operation must be 'replace'").equals('replace'), - body('*.path', "path must be '/Owner'").equals('/Owner'), - body('*.value', 'must be a string').isString(), - async (req: Request, res: Response) => { - logger.debug(req.body, 'Transfer asset request received'); - - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(BAD_REQUEST).json({ - status: getReasonPhrase(BAD_REQUEST), - reason: 'VALIDATION_ERROR', - message: 'Invalid request body', - timestamp: new Date().toISOString(), - errors: errors.array(), - }); + '/:assetId', + body() + .isArray({ + min: 1, + max: 1, + }) + .withMessage( + 'body must contain an array with a single patch operation' + ), + body('*.op', "operation must be 'replace'").equals('replace'), + body('*.path', "path must be '/Owner'").equals('/Owner'), + body('*.value', 'must be a string').isString(), + async (req: Request, res: Response) => { + logger.debug(req.body, 'Transfer asset request received'); + + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(BAD_REQUEST).json({ + status: getReasonPhrase(BAD_REQUEST), + reason: 'VALIDATION_ERROR', + message: 'Invalid request body', + timestamp: new Date().toISOString(), + errors: errors.array(), + }); + } + + const mspId = req.user as string; + const assetId = req.params.assetId; + const newOwner = req.body[0].value; + + try { + const submitQueue = req.app.locals.jobq as Queue; + const jobId = await addSubmitTransactionJob( + submitQueue, + mspId, + 'TransferAsset', + assetId, + newOwner + ); + + return res.status(ACCEPTED).json({ + status: getReasonPhrase(ACCEPTED), + jobId: jobId, + timestamp: new Date().toISOString(), + }); + } catch (err) { + logger.error( + { err }, + 'Error processing update asset request for asset ID %s', + req.params.assetId + ); + + return res.status(INTERNAL_SERVER_ERROR).json({ + status: getReasonPhrase(INTERNAL_SERVER_ERROR), + timestamp: new Date().toISOString(), + }); + } } +); + +assetsRouter.delete('/:assetId', async (req: Request, res: Response) => { + logger.debug(req.body, 'Delete asset request received'); const mspId = req.user as string; const assetId = req.params.assetId; - const newOwner = req.body[0].value; try { - const submitQueue = req.app.locals.jobq as Queue; - const jobId = await addSubmitTransactionJob( - submitQueue, - mspId, - 'TransferAsset', - assetId, - newOwner - ); - - return res.status(ACCEPTED).json({ - status: getReasonPhrase(ACCEPTED), - jobId: jobId, - timestamp: new Date().toISOString(), - }); + const submitQueue = req.app.locals.jobq as Queue; + const jobId = await addSubmitTransactionJob( + submitQueue, + mspId, + 'DeleteAsset', + assetId + ); + + return res.status(ACCEPTED).json({ + status: getReasonPhrase(ACCEPTED), + jobId: jobId, + timestamp: new Date().toISOString(), + }); } catch (err) { - logger.error( - { err }, - 'Error processing update asset request for asset ID %s', - req.params.assetId - ); - - return res.status(INTERNAL_SERVER_ERROR).json({ - status: getReasonPhrase(INTERNAL_SERVER_ERROR), - timestamp: new Date().toISOString(), - }); + logger.error( + { err }, + 'Error processing delete asset request for asset ID %s', + assetId + ); + + return res.status(INTERNAL_SERVER_ERROR).json({ + status: getReasonPhrase(INTERNAL_SERVER_ERROR), + timestamp: new Date().toISOString(), + }); } - } -); - -assetsRouter.delete('/:assetId', async (req: Request, res: Response) => { - logger.debug(req.body, 'Delete asset request received'); - - const mspId = req.user as string; - const assetId = req.params.assetId; - - try { - const submitQueue = req.app.locals.jobq as Queue; - const jobId = await addSubmitTransactionJob( - submitQueue, - mspId, - 'DeleteAsset', - assetId - ); - - return res.status(ACCEPTED).json({ - status: getReasonPhrase(ACCEPTED), - jobId: jobId, - timestamp: new Date().toISOString(), - }); - } catch (err) { - logger.error( - { err }, - 'Error processing delete asset request for asset ID %s', - assetId - ); - - return res.status(INTERNAL_SERVER_ERROR).json({ - status: getReasonPhrase(INTERNAL_SERVER_ERROR), - timestamp: new Date().toISOString(), - }); - } }); diff --git a/asset-transfer-basic/rest-api-typescript/src/auth.ts b/asset-transfer-basic/rest-api-typescript/src/auth.ts index b89fed1cd2..96b28ae126 100644 --- a/asset-transfer-basic/rest-api-typescript/src/auth.ts +++ b/asset-transfer-basic/rest-api-typescript/src/auth.ts @@ -12,50 +12,50 @@ import * as config from './config'; const { UNAUTHORIZED } = StatusCodes; export const fabricAPIKeyStrategy: HeaderAPIKeyStrategy = - new HeaderAPIKeyStrategy( - { header: 'X-API-Key', prefix: '' }, - false, - function (apikey, done) { - logger.debug({ apikey }, 'Checking X-API-Key'); - if (apikey === config.org1ApiKey) { - const user = config.mspIdOrg1; - logger.debug('User set to %s', user); - done(null, user); - } else if (apikey === config.org2ApiKey) { - const user = config.mspIdOrg2; - logger.debug('User set to %s', user); - done(null, user); - } else { - logger.debug({ apikey }, 'No valid X-API-Key'); - return done(null, false); - } - } - ); + new HeaderAPIKeyStrategy( + { header: 'X-API-Key', prefix: '' }, + false, + function (apikey, done) { + logger.debug({ apikey }, 'Checking X-API-Key'); + if (apikey === config.org1ApiKey) { + const user = config.mspIdOrg1; + logger.debug('User set to %s', user); + done(null, user); + } else if (apikey === config.org2ApiKey) { + const user = config.mspIdOrg2; + logger.debug('User set to %s', user); + done(null, user); + } else { + logger.debug({ apikey }, 'No valid X-API-Key'); + return done(null, false); + } + } + ); export const authenticateApiKey = ( - req: Request, - res: Response, - next: NextFunction + req: Request, + res: Response, + next: NextFunction ): void => { - passport.authenticate( - 'headerapikey', - { session: false }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (err: any, user: Express.User, _info: any) => { - if (err) return next(err); - if (!user) - return res.status(UNAUTHORIZED).json({ - status: getReasonPhrase(UNAUTHORIZED), - reason: 'NO_VALID_APIKEY', - timestamp: new Date().toISOString(), - }); + passport.authenticate( + 'headerapikey', + { session: false }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (err: any, user: Express.User, _info: any) => { + if (err) return next(err); + if (!user) + return res.status(UNAUTHORIZED).json({ + status: getReasonPhrase(UNAUTHORIZED), + reason: 'NO_VALID_APIKEY', + timestamp: new Date().toISOString(), + }); - req.logIn(user, { session: false }, async (err) => { - if (err) { - return next(err); + req.logIn(user, { session: false }, async (err) => { + if (err) { + return next(err); + } + return next(); + }); } - return next(); - }); - } - )(req, res, next); + )(req, res, next); }; diff --git a/asset-transfer-basic/rest-api-typescript/src/config.spec.ts b/asset-transfer-basic/rest-api-typescript/src/config.spec.ts index 96ffaad5cd..460c404f28 100644 --- a/asset-transfer-basic/rest-api-typescript/src/config.spec.ts +++ b/asset-transfer-basic/rest-api-typescript/src/config.spec.ts @@ -5,571 +5,573 @@ /* eslint-disable @typescript-eslint/no-var-requires */ describe('Config values', () => { - const ORIGINAL_ENV = process.env; - - beforeEach(async () => { - jest.resetModules(); - process.env = { ...ORIGINAL_ENV }; - }); - - afterAll(() => { - process.env = { ...ORIGINAL_ENV }; - }); - - describe('logLevel', () => { - it('defaults to "info"', () => { - const config = require('./config'); - expect(config.logLevel).toBe('info'); - }); - - it('can be configured using the "LOG_LEVEL" environment variable', () => { - process.env.LOG_LEVEL = 'debug'; - const config = require('./config'); - expect(config.logLevel).toBe('debug'); - }); - - it('throws an error when the "LOG_LEVEL" environment variable has an invalid log level', () => { - process.env.LOG_LEVEL = 'ludicrous'; - expect(() => { - require('./config'); - }).toThrow( - 'env-var: "LOG_LEVEL" should be one of [fatal, error, warn, info, debug, trace, silent]' - ); - }); - }); - - describe('port', () => { - it('defaults to "3000"', () => { - const config = require('./config'); - expect(config.port).toBe(3000); - }); - - it('can be configured using the "PORT" environment variable', () => { - process.env.PORT = '8000'; - const config = require('./config'); - expect(config.port).toBe(8000); - }); - - it('throws an error when the "PORT" environment variable has an invalid port number', () => { - process.env.PORT = '65536'; - expect(() => { - require('./config'); - }).toThrow( - 'env-var: "PORT" cannot assign a port number greater than 65535. An example of a valid value would be: 3000' - ); - }); - }); - - describe('submitJobBackoffType', () => { - it('defaults to "fixed"', () => { - const config = require('./config'); - expect(config.submitJobBackoffType).toBe('fixed'); - }); - - it('can be configured using the "SUBMIT_JOB_BACKOFF_TYPE" environment variable', () => { - process.env.SUBMIT_JOB_BACKOFF_TYPE = 'exponential'; - const config = require('./config'); - expect(config.submitJobBackoffType).toBe('exponential'); - }); - - it('throws an error when the "LOG_LEVEL" environment variable has an invalid log level', () => { - process.env.SUBMIT_JOB_BACKOFF_TYPE = 'jitter'; - expect(() => { - require('./config'); - }).toThrow( - 'env-var: "SUBMIT_JOB_BACKOFF_TYPE" should be one of [fixed, exponential]' - ); - }); - }); - - describe('submitJobBackoffDelay', () => { - it('defaults to "3000"', () => { - const config = require('./config'); - expect(config.submitJobBackoffDelay).toBe(3000); - }); - - it('can be configured using the "SUBMIT_JOB_BACKOFF_DELAY" environment variable', () => { - process.env.SUBMIT_JOB_BACKOFF_DELAY = '9999'; - const config = require('./config'); - expect(config.submitJobBackoffDelay).toBe(9999); - }); - - it('throws an error when the "SUBMIT_JOB_BACKOFF_DELAY" environment variable has an invalid number', () => { - process.env.SUBMIT_JOB_BACKOFF_DELAY = 'short'; - expect(() => { - require('./config'); - }).toThrow( - 'env-var: "SUBMIT_JOB_BACKOFF_DELAY" should be a valid integer. An example of a valid value would be: 3000' - ); - }); - }); - - describe('submitJobAttempts', () => { - it('defaults to "5"', () => { - const config = require('./config'); - expect(config.submitJobAttempts).toBe(5); - }); - - it('can be configured using the "SUBMIT_JOB_ATTEMPTS" environment variable', () => { - process.env.SUBMIT_JOB_ATTEMPTS = '9999'; - const config = require('./config'); - expect(config.submitJobAttempts).toBe(9999); - }); - - it('throws an error when the "SUBMIT_JOB_ATTEMPTS" environment variable has an invalid number', () => { - process.env.SUBMIT_JOB_ATTEMPTS = 'lots'; - expect(() => { - require('./config'); - }).toThrow( - 'env-var: "SUBMIT_JOB_ATTEMPTS" should be a valid integer. An example of a valid value would be: 5' - ); - }); - }); - - describe('submitJobConcurrency', () => { - it('defaults to "5"', () => { - const config = require('./config'); - expect(config.submitJobConcurrency).toBe(5); - }); - - it('can be configured using the "SUBMIT_JOB_CONCURRENCY" environment variable', () => { - process.env.SUBMIT_JOB_CONCURRENCY = '9999'; - const config = require('./config'); - expect(config.submitJobConcurrency).toBe(9999); - }); - - it('throws an error when the "SUBMIT_JOB_CONCURRENCY" environment variable has an invalid number', () => { - process.env.SUBMIT_JOB_CONCURRENCY = 'lots'; - expect(() => { - require('./config'); - }).toThrow( - 'env-var: "SUBMIT_JOB_CONCURRENCY" should be a valid integer. An example of a valid value would be: 5' - ); - }); - }); - - describe('maxCompletedSubmitJobs', () => { - it('defaults to "1000"', () => { - const config = require('./config'); - expect(config.maxCompletedSubmitJobs).toBe(1000); - }); - - it('can be configured using the "MAX_COMPLETED_SUBMIT_JOBS" environment variable', () => { - process.env.MAX_COMPLETED_SUBMIT_JOBS = '9999'; - const config = require('./config'); - expect(config.maxCompletedSubmitJobs).toBe(9999); - }); - - it('throws an error when the "MAX_COMPLETED_SUBMIT_JOBS" environment variable has an invalid number', () => { - process.env.MAX_COMPLETED_SUBMIT_JOBS = 'lots'; - expect(() => { - require('./config'); - }).toThrow( - 'env-var: "MAX_COMPLETED_SUBMIT_JOBS" should be a valid integer. An example of a valid value would be: 1000' - ); - }); - }); - - describe('maxFailedSubmitJobs', () => { - it('defaults to "1000"', () => { - const config = require('./config'); - expect(config.maxFailedSubmitJobs).toBe(1000); - }); - - it('can be configured using the "MAX_FAILED_SUBMIT_JOBS" environment variable', () => { - process.env.MAX_FAILED_SUBMIT_JOBS = '9999'; - const config = require('./config'); - expect(config.maxFailedSubmitJobs).toBe(9999); - }); - - it('throws an error when the "MAX_FAILED_SUBMIT_JOBS" environment variable has an invalid number', () => { - process.env.MAX_FAILED_SUBMIT_JOBS = 'lots'; - expect(() => { - require('./config'); - }).toThrow( - 'env-var: "MAX_FAILED_SUBMIT_JOBS" should be a valid integer. An example of a valid value would be: 1000' - ); - }); - }); - - describe('submitJobQueueScheduler', () => { - it('defaults to "true"', () => { - const config = require('./config'); - expect(config.submitJobQueueScheduler).toBe(true); - }); - - it('can be configured using the "SUBMIT_JOB_QUEUE_SCHEDULER" environment variable', () => { - process.env.SUBMIT_JOB_QUEUE_SCHEDULER = 'false'; - const config = require('./config'); - expect(config.submitJobQueueScheduler).toBe(false); - }); - - it('throws an error when the "SUBMIT_JOB_QUEUE_SCHEDULER" environment variable has an invalid boolean value', () => { - process.env.SUBMIT_JOB_QUEUE_SCHEDULER = '11'; - expect(() => { - require('./config'); - }).toThrow( - 'env-var: "SUBMIT_JOB_QUEUE_SCHEDULER" should be either "true", "false", "TRUE", or "FALSE". An example of a valid value would be: true' - ); - }); - }); - - describe('asLocalhost', () => { - it('defaults to "true"', () => { - const config = require('./config'); - expect(config.asLocalhost).toBe(true); - }); - - it('can be configured using the "AS_LOCAL_HOST" environment variable', () => { - process.env.AS_LOCAL_HOST = 'false'; - const config = require('./config'); - expect(config.asLocalhost).toBe(false); - }); - - it('throws an error when the "AS_LOCAL_HOST" environment variable has an invalid boolean value', () => { - process.env.AS_LOCAL_HOST = '11'; - expect(() => { - require('./config'); - }).toThrow( - 'env-var: "AS_LOCAL_HOST" should be either "true", "false", "TRUE", or "FALSE". An example of a valid value would be: true' - ); - }); - }); - - describe('mspIdOrg1', () => { - it('defaults to "Org1MSP"', () => { - const config = require('./config'); - expect(config.mspIdOrg1).toBe('Org1MSP'); - }); - - it('can be configured using the "HLF_MSP_ID_ORG1" environment variable', () => { - process.env.HLF_MSP_ID_ORG1 = 'Test1MSP'; - const config = require('./config'); - expect(config.mspIdOrg1).toBe('Test1MSP'); - }); - }); - - describe('mspIdOrg2', () => { - it('defaults to "Org2MSP"', () => { - const config = require('./config'); - expect(config.mspIdOrg2).toBe('Org2MSP'); - }); - - it('can be configured using the "HLF_MSP_ID_ORG2" environment variable', () => { - process.env.HLF_MSP_ID_ORG2 = 'Test2MSP'; - const config = require('./config'); - expect(config.mspIdOrg2).toBe('Test2MSP'); - }); - }); - - describe('channelName', () => { - it('defaults to "mychannel"', () => { - const config = require('./config'); - expect(config.channelName).toBe('mychannel'); - }); - - it('can be configured using the "HLF_CHANNEL_NAME" environment variable', () => { - process.env.HLF_CHANNEL_NAME = 'testchannel'; - const config = require('./config'); - expect(config.channelName).toBe('testchannel'); - }); - }); - - describe('chaincodeName', () => { - it('defaults to "basic"', () => { - const config = require('./config'); - expect(config.chaincodeName).toBe('basic'); - }); - - it('can be configured using the "HLF_CHAINCODE_NAME" environment variable', () => { - process.env.HLF_CHAINCODE_NAME = 'testcc'; - const config = require('./config'); - expect(config.chaincodeName).toBe('testcc'); - }); - }); - - describe('commitTimeout', () => { - it('defaults to "300"', () => { - const config = require('./config'); - expect(config.commitTimeout).toBe(300); - }); - - it('can be configured using the "HLF_COMMIT_TIMEOUT" environment variable', () => { - process.env.HLF_COMMIT_TIMEOUT = '9999'; - const config = require('./config'); - expect(config.commitTimeout).toBe(9999); - }); - - it('throws an error when the "HLF_COMMIT_TIMEOUT" environment variable has an invalid number', () => { - process.env.HLF_COMMIT_TIMEOUT = 'short'; - expect(() => { - require('./config'); - }).toThrow( - 'env-var: "HLF_COMMIT_TIMEOUT" should be a valid integer. An example of a valid value would be: 300' - ); - }); - }); - - describe('endorseTimeout', () => { - it('defaults to "30"', () => { - const config = require('./config'); - expect(config.endorseTimeout).toBe(30); - }); - - it('can be configured using the "HLF_ENDORSE_TIMEOUT" environment variable', () => { - process.env.HLF_ENDORSE_TIMEOUT = '9999'; - const config = require('./config'); - expect(config.endorseTimeout).toBe(9999); - }); - - it('throws an error when the "HLF_ENDORSE_TIMEOUT" environment variable has an invalid number', () => { - process.env.HLF_ENDORSE_TIMEOUT = 'short'; - expect(() => { - require('./config'); - }).toThrow( - 'env-var: "HLF_ENDORSE_TIMEOUT" should be a valid integer. An example of a valid value would be: 30' - ); - }); - }); - - describe('queryTimeout', () => { - it('defaults to "3"', () => { - const config = require('./config'); - expect(config.queryTimeout).toBe(3); - }); - - it('can be configured using the "HLF_QUERY_TIMEOUT" environment variable', () => { - process.env.HLF_QUERY_TIMEOUT = '9999'; - const config = require('./config'); - expect(config.queryTimeout).toBe(9999); - }); - - it('throws an error when the "HLF_QUERY_TIMEOUT" environment variable has an invalid number', () => { - process.env.HLF_QUERY_TIMEOUT = 'long'; - expect(() => { - require('./config'); - }).toThrow( - 'env-var: "HLF_QUERY_TIMEOUT" should be a valid integer. An example of a valid value would be: 3' - ); - }); - }); - - describe('connectionProfileOrg1', () => { - it('throws an error when the "HLF_CONNECTION_PROFILE_ORG1" environment variable is not set', () => { - delete process.env.HLF_CONNECTION_PROFILE_ORG1; - expect(() => { - require('./config'); - }).toThrow( - 'env-var: "HLF_CONNECTION_PROFILE_ORG1" is a required variable, but it was not set. An example of a valid value would be: {"name":"test-network-org1","version":"1.0.0","client":{"organization":"Org1" ... }' - ); - }); - - it('can be configured using the "HLF_CONNECTION_PROFILE_ORG1" environment variable', () => { - process.env.HLF_CONNECTION_PROFILE_ORG1 = '{"name":"test-network-org1"}'; - const config = require('./config'); - expect(config.connectionProfileOrg1).toStrictEqual({ - name: 'test-network-org1', - }); - }); - - it('throws an error when the "HLF_CONNECTION_PROFILE_ORG1" environment variable is set to invalid json', () => { - process.env.HLF_CONNECTION_PROFILE_ORG1 = 'testing'; - expect(() => { - require('./config'); - }).toThrow( - 'env-var: "HLF_CONNECTION_PROFILE_ORG1" should be valid (parseable) JSON. An example of a valid value would be: {"name":"test-network-org1","version":"1.0.0","client":{"organization":"Org1" ... }' - ); - }); - }); - - describe('certificateOrg1', () => { - it('throws an error when the "HLF_CERTIFICATE_ORG1" environment variable is not set', () => { - delete process.env.HLF_CERTIFICATE_ORG1; - expect(() => { - require('./config'); - }).toThrow( - 'env-var: "HLF_CERTIFICATE_ORG1" is a required variable, but it was not set. An example of a valid value would be: "-----BEGIN CERTIFICATE-----\\n...\\n-----END CERTIFICATE-----\\n"' - ); - }); - - it('can be configured using the "HLF_CERTIFICATE_ORG1" environment variable', () => { - process.env.HLF_CERTIFICATE_ORG1 = 'ORG1CERT'; - const config = require('./config'); - expect(config.certificateOrg1).toBe('ORG1CERT'); - }); - }); - - describe('privateKeyOrg1', () => { - it('throws an error when the "HLF_PRIVATE_KEY_ORG1" environment variable is not set', () => { - delete process.env.HLF_PRIVATE_KEY_ORG1; - expect(() => { - require('./config'); - }).toThrow( - 'env-var: "HLF_PRIVATE_KEY_ORG1" is a required variable, but it was not set. An example of a valid value would be: "-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n"' - ); - }); - - it('can be configured using the "HLF_PRIVATE_KEY_ORG1" environment variable', () => { - process.env.HLF_PRIVATE_KEY_ORG1 = 'ORG1PK'; - const config = require('./config'); - expect(config.privateKeyOrg1).toBe('ORG1PK'); - }); - }); - - describe('connectionProfileOrg2', () => { - it('throws an error when the "HLF_CONNECTION_PROFILE_ORG2" environment variable is not set', () => { - delete process.env.HLF_CONNECTION_PROFILE_ORG2; - expect(() => { - require('./config'); - }).toThrow( - 'env-var: "HLF_CONNECTION_PROFILE_ORG2" is a required variable, but it was not set. An example of a valid value would be: {"name":"test-network-org2","version":"1.0.0","client":{"organization":"Org2" ... }' - ); - }); - - it('can be configured using the "HLF_CONNECTION_PROFILE_ORG2" environment variable', () => { - process.env.HLF_CONNECTION_PROFILE_ORG2 = '{"name":"test-network-org2"}'; - const config = require('./config'); - expect(config.connectionProfileOrg2).toStrictEqual({ - name: 'test-network-org2', - }); - }); - - it('throws an error when the "HLF_CONNECTION_PROFILE_ORG2" environment variable is set to invalid json', () => { - process.env.HLF_CONNECTION_PROFILE_ORG2 = 'testing'; - expect(() => { - require('./config'); - }).toThrow( - 'env-var: "HLF_CONNECTION_PROFILE_ORG2" should be valid (parseable) JSON. An example of a valid value would be: {"name":"test-network-org2","version":"1.0.0","client":{"organization":"Org2" ... }' - ); - }); - }); - - describe('certificateOrg2', () => { - it('throws an error when the "HLF_CERTIFICATE_ORG2" environment variable is not set', () => { - delete process.env.HLF_CERTIFICATE_ORG2; - expect(() => { - require('./config'); - }).toThrow( - 'env-var: "HLF_CERTIFICATE_ORG2" is a required variable, but it was not set. An example of a valid value would be: "-----BEGIN CERTIFICATE-----\\n...\\n-----END CERTIFICATE-----\\n"' - ); - }); - - it('can be configured using the "HLF_CERTIFICATE_ORG2" environment variable', () => { - process.env.HLF_CERTIFICATE_ORG2 = 'ORG2CERT'; - const config = require('./config'); - expect(config.certificateOrg2).toBe('ORG2CERT'); - }); - }); - - describe('privateKeyOrg2', () => { - it('throws an error when the "HLF_PRIVATE_KEY_ORG2" environment variable is not set', () => { - delete process.env.HLF_PRIVATE_KEY_ORG2; - expect(() => { - require('./config'); - }).toThrow( - 'env-var: "HLF_PRIVATE_KEY_ORG2" is a required variable, but it was not set. An example of a valid value would be: "-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n"' - ); - }); - - it('can be configured using the "HLF_PRIVATE_KEY_ORG2" environment variable', () => { - process.env.HLF_PRIVATE_KEY_ORG2 = 'ORG2PK'; - const config = require('./config'); - expect(config.privateKeyOrg2).toBe('ORG2PK'); - }); - }); - - describe('redisHost', () => { - it('defaults to "localhost"', () => { - const config = require('./config'); - expect(config.redisHost).toBe('localhost'); - }); - - it('can be configured using the "REDIS_HOST" environment variable', () => { - process.env.REDIS_HOST = 'redis.example.org'; - const config = require('./config'); - expect(config.redisHost).toBe('redis.example.org'); - }); - }); - - describe('redisPort', () => { - it('defaults to "6379"', () => { - const config = require('./config'); - expect(config.redisPort).toBe(6379); - }); - - it('can be configured with a valid port number using the "REDIS_PORT" environment variable', () => { - process.env.REDIS_PORT = '9736'; - const config = require('./config'); - expect(config.redisPort).toBe(9736); - }); - - it('throws an error when the "REDIS_PORT" environment variable has an invalid port number', () => { - process.env.REDIS_PORT = '65536'; - expect(() => { - require('./config'); - }).toThrow( - 'env-var: "REDIS_PORT" cannot assign a port number greater than 65535. An example of a valid value would be: 6379' - ); - }); - }); - - describe('redisUsername', () => { - it('has no default value', () => { - const config = require('./config'); - expect(config.redisUsername).toBeUndefined(); - }); - - it('can be configured using the "REDIS_USERNAME" environment variable', () => { - process.env.REDIS_USERNAME = 'test'; - const config = require('./config'); - expect(config.redisUsername).toBe('test'); - }); - }); - - describe('redisPassword', () => { - it('has no default value', () => { - const config = require('./config'); - expect(config.redisPassword).toBeUndefined(); - }); - - it('can be configured using the "REDIS_PASSWORD" environment variable', () => { - process.env.REDIS_PASSWORD = 'testpw'; - const config = require('./config'); - expect(config.redisPassword).toBe('testpw'); - }); - }); - - describe('org1ApiKey', () => { - it('throws an error when the "ORG1_APIKEY" environment variable is not set', () => { - delete process.env.ORG1_APIKEY; - expect(() => { - require('./config'); - }).toThrow( - 'env-var: "ORG1_APIKEY" is a required variable, but it was not set. An example of a valid value would be: 123' - ); - }); - - it('can be configured using the "ORG1_APIKEY" environment variable', () => { - process.env.ORG1_APIKEY = 'org1ApiKey'; - const config = require('./config'); - expect(config.org1ApiKey).toBe('org1ApiKey'); - }); - }); - - describe('org2ApiKey', () => { - it('throws an error when the "ORG1_APIKEY" environment variable is not set', () => { - delete process.env.ORG2_APIKEY; - expect(() => { - require('./config'); - }).toThrow( - 'env-var: "ORG2_APIKEY" is a required variable, but it was not set. An example of a valid value would be: 456' - ); - }); - - it('can be configured using the "ORG1_APIKEY" environment variable', () => { - process.env.ORG2_APIKEY = 'org2ApiKey'; - const config = require('./config'); - expect(config.org2ApiKey).toBe('org2ApiKey'); + const ORIGINAL_ENV = process.env; + + beforeEach(async () => { + jest.resetModules(); + process.env = { ...ORIGINAL_ENV }; + }); + + afterAll(() => { + process.env = { ...ORIGINAL_ENV }; + }); + + describe('logLevel', () => { + it('defaults to "info"', () => { + const config = require('./config'); + expect(config.logLevel).toBe('info'); + }); + + it('can be configured using the "LOG_LEVEL" environment variable', () => { + process.env.LOG_LEVEL = 'debug'; + const config = require('./config'); + expect(config.logLevel).toBe('debug'); + }); + + it('throws an error when the "LOG_LEVEL" environment variable has an invalid log level', () => { + process.env.LOG_LEVEL = 'ludicrous'; + expect(() => { + require('./config'); + }).toThrow( + 'env-var: "LOG_LEVEL" should be one of [fatal, error, warn, info, debug, trace, silent]' + ); + }); + }); + + describe('port', () => { + it('defaults to "3000"', () => { + const config = require('./config'); + expect(config.port).toBe(3000); + }); + + it('can be configured using the "PORT" environment variable', () => { + process.env.PORT = '8000'; + const config = require('./config'); + expect(config.port).toBe(8000); + }); + + it('throws an error when the "PORT" environment variable has an invalid port number', () => { + process.env.PORT = '65536'; + expect(() => { + require('./config'); + }).toThrow( + 'env-var: "PORT" cannot assign a port number greater than 65535. An example of a valid value would be: 3000' + ); + }); + }); + + describe('submitJobBackoffType', () => { + it('defaults to "fixed"', () => { + const config = require('./config'); + expect(config.submitJobBackoffType).toBe('fixed'); + }); + + it('can be configured using the "SUBMIT_JOB_BACKOFF_TYPE" environment variable', () => { + process.env.SUBMIT_JOB_BACKOFF_TYPE = 'exponential'; + const config = require('./config'); + expect(config.submitJobBackoffType).toBe('exponential'); + }); + + it('throws an error when the "LOG_LEVEL" environment variable has an invalid log level', () => { + process.env.SUBMIT_JOB_BACKOFF_TYPE = 'jitter'; + expect(() => { + require('./config'); + }).toThrow( + 'env-var: "SUBMIT_JOB_BACKOFF_TYPE" should be one of [fixed, exponential]' + ); + }); + }); + + describe('submitJobBackoffDelay', () => { + it('defaults to "3000"', () => { + const config = require('./config'); + expect(config.submitJobBackoffDelay).toBe(3000); + }); + + it('can be configured using the "SUBMIT_JOB_BACKOFF_DELAY" environment variable', () => { + process.env.SUBMIT_JOB_BACKOFF_DELAY = '9999'; + const config = require('./config'); + expect(config.submitJobBackoffDelay).toBe(9999); + }); + + it('throws an error when the "SUBMIT_JOB_BACKOFF_DELAY" environment variable has an invalid number', () => { + process.env.SUBMIT_JOB_BACKOFF_DELAY = 'short'; + expect(() => { + require('./config'); + }).toThrow( + 'env-var: "SUBMIT_JOB_BACKOFF_DELAY" should be a valid integer. An example of a valid value would be: 3000' + ); + }); + }); + + describe('submitJobAttempts', () => { + it('defaults to "5"', () => { + const config = require('./config'); + expect(config.submitJobAttempts).toBe(5); + }); + + it('can be configured using the "SUBMIT_JOB_ATTEMPTS" environment variable', () => { + process.env.SUBMIT_JOB_ATTEMPTS = '9999'; + const config = require('./config'); + expect(config.submitJobAttempts).toBe(9999); + }); + + it('throws an error when the "SUBMIT_JOB_ATTEMPTS" environment variable has an invalid number', () => { + process.env.SUBMIT_JOB_ATTEMPTS = 'lots'; + expect(() => { + require('./config'); + }).toThrow( + 'env-var: "SUBMIT_JOB_ATTEMPTS" should be a valid integer. An example of a valid value would be: 5' + ); + }); + }); + + describe('submitJobConcurrency', () => { + it('defaults to "5"', () => { + const config = require('./config'); + expect(config.submitJobConcurrency).toBe(5); + }); + + it('can be configured using the "SUBMIT_JOB_CONCURRENCY" environment variable', () => { + process.env.SUBMIT_JOB_CONCURRENCY = '9999'; + const config = require('./config'); + expect(config.submitJobConcurrency).toBe(9999); + }); + + it('throws an error when the "SUBMIT_JOB_CONCURRENCY" environment variable has an invalid number', () => { + process.env.SUBMIT_JOB_CONCURRENCY = 'lots'; + expect(() => { + require('./config'); + }).toThrow( + 'env-var: "SUBMIT_JOB_CONCURRENCY" should be a valid integer. An example of a valid value would be: 5' + ); + }); + }); + + describe('maxCompletedSubmitJobs', () => { + it('defaults to "1000"', () => { + const config = require('./config'); + expect(config.maxCompletedSubmitJobs).toBe(1000); + }); + + it('can be configured using the "MAX_COMPLETED_SUBMIT_JOBS" environment variable', () => { + process.env.MAX_COMPLETED_SUBMIT_JOBS = '9999'; + const config = require('./config'); + expect(config.maxCompletedSubmitJobs).toBe(9999); + }); + + it('throws an error when the "MAX_COMPLETED_SUBMIT_JOBS" environment variable has an invalid number', () => { + process.env.MAX_COMPLETED_SUBMIT_JOBS = 'lots'; + expect(() => { + require('./config'); + }).toThrow( + 'env-var: "MAX_COMPLETED_SUBMIT_JOBS" should be a valid integer. An example of a valid value would be: 1000' + ); + }); + }); + + describe('maxFailedSubmitJobs', () => { + it('defaults to "1000"', () => { + const config = require('./config'); + expect(config.maxFailedSubmitJobs).toBe(1000); + }); + + it('can be configured using the "MAX_FAILED_SUBMIT_JOBS" environment variable', () => { + process.env.MAX_FAILED_SUBMIT_JOBS = '9999'; + const config = require('./config'); + expect(config.maxFailedSubmitJobs).toBe(9999); + }); + + it('throws an error when the "MAX_FAILED_SUBMIT_JOBS" environment variable has an invalid number', () => { + process.env.MAX_FAILED_SUBMIT_JOBS = 'lots'; + expect(() => { + require('./config'); + }).toThrow( + 'env-var: "MAX_FAILED_SUBMIT_JOBS" should be a valid integer. An example of a valid value would be: 1000' + ); + }); + }); + + describe('submitJobQueueScheduler', () => { + it('defaults to "true"', () => { + const config = require('./config'); + expect(config.submitJobQueueScheduler).toBe(true); + }); + + it('can be configured using the "SUBMIT_JOB_QUEUE_SCHEDULER" environment variable', () => { + process.env.SUBMIT_JOB_QUEUE_SCHEDULER = 'false'; + const config = require('./config'); + expect(config.submitJobQueueScheduler).toBe(false); + }); + + it('throws an error when the "SUBMIT_JOB_QUEUE_SCHEDULER" environment variable has an invalid boolean value', () => { + process.env.SUBMIT_JOB_QUEUE_SCHEDULER = '11'; + expect(() => { + require('./config'); + }).toThrow( + 'env-var: "SUBMIT_JOB_QUEUE_SCHEDULER" should be either "true", "false", "TRUE", or "FALSE". An example of a valid value would be: true' + ); + }); + }); + + describe('asLocalhost', () => { + it('defaults to "true"', () => { + const config = require('./config'); + expect(config.asLocalhost).toBe(true); + }); + + it('can be configured using the "AS_LOCAL_HOST" environment variable', () => { + process.env.AS_LOCAL_HOST = 'false'; + const config = require('./config'); + expect(config.asLocalhost).toBe(false); + }); + + it('throws an error when the "AS_LOCAL_HOST" environment variable has an invalid boolean value', () => { + process.env.AS_LOCAL_HOST = '11'; + expect(() => { + require('./config'); + }).toThrow( + 'env-var: "AS_LOCAL_HOST" should be either "true", "false", "TRUE", or "FALSE". An example of a valid value would be: true' + ); + }); + }); + + describe('mspIdOrg1', () => { + it('defaults to "Org1MSP"', () => { + const config = require('./config'); + expect(config.mspIdOrg1).toBe('Org1MSP'); + }); + + it('can be configured using the "HLF_MSP_ID_ORG1" environment variable', () => { + process.env.HLF_MSP_ID_ORG1 = 'Test1MSP'; + const config = require('./config'); + expect(config.mspIdOrg1).toBe('Test1MSP'); + }); + }); + + describe('mspIdOrg2', () => { + it('defaults to "Org2MSP"', () => { + const config = require('./config'); + expect(config.mspIdOrg2).toBe('Org2MSP'); + }); + + it('can be configured using the "HLF_MSP_ID_ORG2" environment variable', () => { + process.env.HLF_MSP_ID_ORG2 = 'Test2MSP'; + const config = require('./config'); + expect(config.mspIdOrg2).toBe('Test2MSP'); + }); + }); + + describe('channelName', () => { + it('defaults to "mychannel"', () => { + const config = require('./config'); + expect(config.channelName).toBe('mychannel'); + }); + + it('can be configured using the "HLF_CHANNEL_NAME" environment variable', () => { + process.env.HLF_CHANNEL_NAME = 'testchannel'; + const config = require('./config'); + expect(config.channelName).toBe('testchannel'); + }); + }); + + describe('chaincodeName', () => { + it('defaults to "basic"', () => { + const config = require('./config'); + expect(config.chaincodeName).toBe('basic'); + }); + + it('can be configured using the "HLF_CHAINCODE_NAME" environment variable', () => { + process.env.HLF_CHAINCODE_NAME = 'testcc'; + const config = require('./config'); + expect(config.chaincodeName).toBe('testcc'); + }); + }); + + describe('commitTimeout', () => { + it('defaults to "300"', () => { + const config = require('./config'); + expect(config.commitTimeout).toBe(300); + }); + + it('can be configured using the "HLF_COMMIT_TIMEOUT" environment variable', () => { + process.env.HLF_COMMIT_TIMEOUT = '9999'; + const config = require('./config'); + expect(config.commitTimeout).toBe(9999); + }); + + it('throws an error when the "HLF_COMMIT_TIMEOUT" environment variable has an invalid number', () => { + process.env.HLF_COMMIT_TIMEOUT = 'short'; + expect(() => { + require('./config'); + }).toThrow( + 'env-var: "HLF_COMMIT_TIMEOUT" should be a valid integer. An example of a valid value would be: 300' + ); + }); + }); + + describe('endorseTimeout', () => { + it('defaults to "30"', () => { + const config = require('./config'); + expect(config.endorseTimeout).toBe(30); + }); + + it('can be configured using the "HLF_ENDORSE_TIMEOUT" environment variable', () => { + process.env.HLF_ENDORSE_TIMEOUT = '9999'; + const config = require('./config'); + expect(config.endorseTimeout).toBe(9999); + }); + + it('throws an error when the "HLF_ENDORSE_TIMEOUT" environment variable has an invalid number', () => { + process.env.HLF_ENDORSE_TIMEOUT = 'short'; + expect(() => { + require('./config'); + }).toThrow( + 'env-var: "HLF_ENDORSE_TIMEOUT" should be a valid integer. An example of a valid value would be: 30' + ); + }); + }); + + describe('queryTimeout', () => { + it('defaults to "3"', () => { + const config = require('./config'); + expect(config.queryTimeout).toBe(3); + }); + + it('can be configured using the "HLF_QUERY_TIMEOUT" environment variable', () => { + process.env.HLF_QUERY_TIMEOUT = '9999'; + const config = require('./config'); + expect(config.queryTimeout).toBe(9999); + }); + + it('throws an error when the "HLF_QUERY_TIMEOUT" environment variable has an invalid number', () => { + process.env.HLF_QUERY_TIMEOUT = 'long'; + expect(() => { + require('./config'); + }).toThrow( + 'env-var: "HLF_QUERY_TIMEOUT" should be a valid integer. An example of a valid value would be: 3' + ); + }); + }); + + describe('connectionProfileOrg1', () => { + it('throws an error when the "HLF_CONNECTION_PROFILE_ORG1" environment variable is not set', () => { + delete process.env.HLF_CONNECTION_PROFILE_ORG1; + expect(() => { + require('./config'); + }).toThrow( + 'env-var: "HLF_CONNECTION_PROFILE_ORG1" is a required variable, but it was not set. An example of a valid value would be: {"name":"test-network-org1","version":"1.0.0","client":{"organization":"Org1" ... }' + ); + }); + + it('can be configured using the "HLF_CONNECTION_PROFILE_ORG1" environment variable', () => { + process.env.HLF_CONNECTION_PROFILE_ORG1 = + '{"name":"test-network-org1"}'; + const config = require('./config'); + expect(config.connectionProfileOrg1).toStrictEqual({ + name: 'test-network-org1', + }); + }); + + it('throws an error when the "HLF_CONNECTION_PROFILE_ORG1" environment variable is set to invalid json', () => { + process.env.HLF_CONNECTION_PROFILE_ORG1 = 'testing'; + expect(() => { + require('./config'); + }).toThrow( + 'env-var: "HLF_CONNECTION_PROFILE_ORG1" should be valid (parseable) JSON. An example of a valid value would be: {"name":"test-network-org1","version":"1.0.0","client":{"organization":"Org1" ... }' + ); + }); + }); + + describe('certificateOrg1', () => { + it('throws an error when the "HLF_CERTIFICATE_ORG1" environment variable is not set', () => { + delete process.env.HLF_CERTIFICATE_ORG1; + expect(() => { + require('./config'); + }).toThrow( + 'env-var: "HLF_CERTIFICATE_ORG1" is a required variable, but it was not set. An example of a valid value would be: "-----BEGIN CERTIFICATE-----\\n...\\n-----END CERTIFICATE-----\\n"' + ); + }); + + it('can be configured using the "HLF_CERTIFICATE_ORG1" environment variable', () => { + process.env.HLF_CERTIFICATE_ORG1 = 'ORG1CERT'; + const config = require('./config'); + expect(config.certificateOrg1).toBe('ORG1CERT'); + }); + }); + + describe('privateKeyOrg1', () => { + it('throws an error when the "HLF_PRIVATE_KEY_ORG1" environment variable is not set', () => { + delete process.env.HLF_PRIVATE_KEY_ORG1; + expect(() => { + require('./config'); + }).toThrow( + 'env-var: "HLF_PRIVATE_KEY_ORG1" is a required variable, but it was not set. An example of a valid value would be: "-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n"' + ); + }); + + it('can be configured using the "HLF_PRIVATE_KEY_ORG1" environment variable', () => { + process.env.HLF_PRIVATE_KEY_ORG1 = 'ORG1PK'; + const config = require('./config'); + expect(config.privateKeyOrg1).toBe('ORG1PK'); + }); + }); + + describe('connectionProfileOrg2', () => { + it('throws an error when the "HLF_CONNECTION_PROFILE_ORG2" environment variable is not set', () => { + delete process.env.HLF_CONNECTION_PROFILE_ORG2; + expect(() => { + require('./config'); + }).toThrow( + 'env-var: "HLF_CONNECTION_PROFILE_ORG2" is a required variable, but it was not set. An example of a valid value would be: {"name":"test-network-org2","version":"1.0.0","client":{"organization":"Org2" ... }' + ); + }); + + it('can be configured using the "HLF_CONNECTION_PROFILE_ORG2" environment variable', () => { + process.env.HLF_CONNECTION_PROFILE_ORG2 = + '{"name":"test-network-org2"}'; + const config = require('./config'); + expect(config.connectionProfileOrg2).toStrictEqual({ + name: 'test-network-org2', + }); + }); + + it('throws an error when the "HLF_CONNECTION_PROFILE_ORG2" environment variable is set to invalid json', () => { + process.env.HLF_CONNECTION_PROFILE_ORG2 = 'testing'; + expect(() => { + require('./config'); + }).toThrow( + 'env-var: "HLF_CONNECTION_PROFILE_ORG2" should be valid (parseable) JSON. An example of a valid value would be: {"name":"test-network-org2","version":"1.0.0","client":{"organization":"Org2" ... }' + ); + }); + }); + + describe('certificateOrg2', () => { + it('throws an error when the "HLF_CERTIFICATE_ORG2" environment variable is not set', () => { + delete process.env.HLF_CERTIFICATE_ORG2; + expect(() => { + require('./config'); + }).toThrow( + 'env-var: "HLF_CERTIFICATE_ORG2" is a required variable, but it was not set. An example of a valid value would be: "-----BEGIN CERTIFICATE-----\\n...\\n-----END CERTIFICATE-----\\n"' + ); + }); + + it('can be configured using the "HLF_CERTIFICATE_ORG2" environment variable', () => { + process.env.HLF_CERTIFICATE_ORG2 = 'ORG2CERT'; + const config = require('./config'); + expect(config.certificateOrg2).toBe('ORG2CERT'); + }); + }); + + describe('privateKeyOrg2', () => { + it('throws an error when the "HLF_PRIVATE_KEY_ORG2" environment variable is not set', () => { + delete process.env.HLF_PRIVATE_KEY_ORG2; + expect(() => { + require('./config'); + }).toThrow( + 'env-var: "HLF_PRIVATE_KEY_ORG2" is a required variable, but it was not set. An example of a valid value would be: "-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n"' + ); + }); + + it('can be configured using the "HLF_PRIVATE_KEY_ORG2" environment variable', () => { + process.env.HLF_PRIVATE_KEY_ORG2 = 'ORG2PK'; + const config = require('./config'); + expect(config.privateKeyOrg2).toBe('ORG2PK'); + }); + }); + + describe('redisHost', () => { + it('defaults to "localhost"', () => { + const config = require('./config'); + expect(config.redisHost).toBe('localhost'); + }); + + it('can be configured using the "REDIS_HOST" environment variable', () => { + process.env.REDIS_HOST = 'redis.example.org'; + const config = require('./config'); + expect(config.redisHost).toBe('redis.example.org'); + }); + }); + + describe('redisPort', () => { + it('defaults to "6379"', () => { + const config = require('./config'); + expect(config.redisPort).toBe(6379); + }); + + it('can be configured with a valid port number using the "REDIS_PORT" environment variable', () => { + process.env.REDIS_PORT = '9736'; + const config = require('./config'); + expect(config.redisPort).toBe(9736); + }); + + it('throws an error when the "REDIS_PORT" environment variable has an invalid port number', () => { + process.env.REDIS_PORT = '65536'; + expect(() => { + require('./config'); + }).toThrow( + 'env-var: "REDIS_PORT" cannot assign a port number greater than 65535. An example of a valid value would be: 6379' + ); + }); + }); + + describe('redisUsername', () => { + it('has no default value', () => { + const config = require('./config'); + expect(config.redisUsername).toBeUndefined(); + }); + + it('can be configured using the "REDIS_USERNAME" environment variable', () => { + process.env.REDIS_USERNAME = 'test'; + const config = require('./config'); + expect(config.redisUsername).toBe('test'); + }); + }); + + describe('redisPassword', () => { + it('has no default value', () => { + const config = require('./config'); + expect(config.redisPassword).toBeUndefined(); + }); + + it('can be configured using the "REDIS_PASSWORD" environment variable', () => { + process.env.REDIS_PASSWORD = 'testpw'; + const config = require('./config'); + expect(config.redisPassword).toBe('testpw'); + }); + }); + + describe('org1ApiKey', () => { + it('throws an error when the "ORG1_APIKEY" environment variable is not set', () => { + delete process.env.ORG1_APIKEY; + expect(() => { + require('./config'); + }).toThrow( + 'env-var: "ORG1_APIKEY" is a required variable, but it was not set. An example of a valid value would be: 123' + ); + }); + + it('can be configured using the "ORG1_APIKEY" environment variable', () => { + process.env.ORG1_APIKEY = 'org1ApiKey'; + const config = require('./config'); + expect(config.org1ApiKey).toBe('org1ApiKey'); + }); + }); + + describe('org2ApiKey', () => { + it('throws an error when the "ORG1_APIKEY" environment variable is not set', () => { + delete process.env.ORG2_APIKEY; + expect(() => { + require('./config'); + }).toThrow( + 'env-var: "ORG2_APIKEY" is a required variable, but it was not set. An example of a valid value would be: 456' + ); + }); + + it('can be configured using the "ORG1_APIKEY" environment variable', () => { + process.env.ORG2_APIKEY = 'org2ApiKey'; + const config = require('./config'); + expect(config.org2ApiKey).toBe('org2ApiKey'); + }); }); - }); }); diff --git a/asset-transfer-basic/rest-api-typescript/src/config.ts b/asset-transfer-basic/rest-api-typescript/src/config.ts index c70d70f8c6..b709415f4e 100644 --- a/asset-transfer-basic/rest-api-typescript/src/config.ts +++ b/asset-transfer-basic/rest-api-typescript/src/config.ts @@ -24,71 +24,71 @@ export const JOB_QUEUE_NAME = 'submit'; * Log level for the REST server */ export const logLevel = env - .get('LOG_LEVEL') - .default('info') - .asEnum(['fatal', 'error', 'warn', 'info', 'debug', 'trace', 'silent']); + .get('LOG_LEVEL') + .default('info') + .asEnum(['fatal', 'error', 'warn', 'info', 'debug', 'trace', 'silent']); /** * The port to start the REST server on */ export const port = env - .get('PORT') - .default('3000') - .example('3000') - .asPortNumber(); + .get('PORT') + .default('3000') + .example('3000') + .asPortNumber(); /** * The type of backoff to use for retrying failed submit jobs */ export const submitJobBackoffType = env - .get('SUBMIT_JOB_BACKOFF_TYPE') - .default('fixed') - .asEnum(['fixed', 'exponential']); + .get('SUBMIT_JOB_BACKOFF_TYPE') + .default('fixed') + .asEnum(['fixed', 'exponential']); /** * Backoff delay for retrying failed submit jobs in milliseconds */ export const submitJobBackoffDelay = env - .get('SUBMIT_JOB_BACKOFF_DELAY') - .default('3000') - .example('3000') - .asIntPositive(); + .get('SUBMIT_JOB_BACKOFF_DELAY') + .default('3000') + .example('3000') + .asIntPositive(); /** * The total number of attempts to try a submit job until it completes */ export const submitJobAttempts = env - .get('SUBMIT_JOB_ATTEMPTS') - .default('5') - .example('5') - .asIntPositive(); + .get('SUBMIT_JOB_ATTEMPTS') + .default('5') + .example('5') + .asIntPositive(); /** * The maximum number of submit jobs that can be processed in parallel */ export const submitJobConcurrency = env - .get('SUBMIT_JOB_CONCURRENCY') - .default('5') - .example('5') - .asIntPositive(); + .get('SUBMIT_JOB_CONCURRENCY') + .default('5') + .example('5') + .asIntPositive(); /** * The number of completed submit jobs to keep */ export const maxCompletedSubmitJobs = env - .get('MAX_COMPLETED_SUBMIT_JOBS') - .default('1000') - .example('1000') - .asIntPositive(); + .get('MAX_COMPLETED_SUBMIT_JOBS') + .default('1000') + .example('1000') + .asIntPositive(); /** * The number of failed submit jobs to keep */ export const maxFailedSubmitJobs = env - .get('MAX_FAILED_SUBMIT_JOBS') - .default('1000') - .example('1000') - .asIntPositive(); + .get('MAX_FAILED_SUBMIT_JOBS') + .default('1000') + .example('1000') + .asIntPositive(); /** * Whether to initialise a scheduler for the submit job queue @@ -96,10 +96,10 @@ export const maxFailedSubmitJobs = env * more than one for redundancy */ export const submitJobQueueScheduler = env - .get('SUBMIT_JOB_QUEUE_SCHEDULER') - .default('true') - .example('true') - .asBoolStrict(); + .get('SUBMIT_JOB_QUEUE_SCHEDULER') + .default('true') + .example('true') + .asBoolStrict(); /** * Whether to convert discovered host addresses to be 'localhost' @@ -107,157 +107,165 @@ export const submitJobQueueScheduler = env * local system, e.g. using the test network; otherwise should it should be 'false' */ export const asLocalhost = env - .get('AS_LOCAL_HOST') - .default('true') - .example('true') - .asBoolStrict(); + .get('AS_LOCAL_HOST') + .default('true') + .example('true') + .asBoolStrict(); /** * The Org1 MSP ID */ export const mspIdOrg1 = env - .get('HLF_MSP_ID_ORG1') - .default(`${ORG1}MSP`) - .example(`${ORG1}MSP`) - .asString(); + .get('HLF_MSP_ID_ORG1') + .default(`${ORG1}MSP`) + .example(`${ORG1}MSP`) + .asString(); /** * The Org2 MSP ID */ export const mspIdOrg2 = env - .get('HLF_MSP_ID_ORG2') - .default(`${ORG2}MSP`) - .example(`${ORG2}MSP`) - .asString(); + .get('HLF_MSP_ID_ORG2') + .default(`${ORG2}MSP`) + .example(`${ORG2}MSP`) + .asString(); /** * Name of the channel which the basic asset sample chaincode has been installed on */ export const channelName = env - .get('HLF_CHANNEL_NAME') - .default('mychannel') - .example('mychannel') - .asString(); + .get('HLF_CHANNEL_NAME') + .default('mychannel') + .example('mychannel') + .asString(); /** * Name used to install the basic asset sample */ export const chaincodeName = env - .get('HLF_CHAINCODE_NAME') - .default('basic') - .example('basic') - .asString(); + .get('HLF_CHAINCODE_NAME') + .default('basic') + .example('basic') + .asString(); /** * The transaction submit timeout in seconds for commit notification to complete */ export const commitTimeout = env - .get('HLF_COMMIT_TIMEOUT') - .default('300') - .example('300') - .asIntPositive(); + .get('HLF_COMMIT_TIMEOUT') + .default('300') + .example('300') + .asIntPositive(); /** * The transaction submit timeout in seconds for the endorsement to complete */ export const endorseTimeout = env - .get('HLF_ENDORSE_TIMEOUT') - .default('30') - .example('30') - .asIntPositive(); + .get('HLF_ENDORSE_TIMEOUT') + .default('30') + .example('30') + .asIntPositive(); /** * The transaction query timeout in seconds */ export const queryTimeout = env - .get('HLF_QUERY_TIMEOUT') - .default('3') - .example('3') - .asIntPositive(); + .get('HLF_QUERY_TIMEOUT') + .default('3') + .example('3') + .asIntPositive(); /** * The Org1 connection profile JSON */ export const connectionProfileOrg1 = env - .get('HLF_CONNECTION_PROFILE_ORG1') - .required() - .example( - '{"name":"test-network-org1","version":"1.0.0","client":{"organization":"Org1" ... }' - ) - .asJsonObject() as Record; + .get('HLF_CONNECTION_PROFILE_ORG1') + .required() + .example( + '{"name":"test-network-org1","version":"1.0.0","client":{"organization":"Org1" ... }' + ) + .asJsonObject() as Record; /** * Certificate for an Org1 identity to evaluate and submit transactions */ export const certificateOrg1 = env - .get('HLF_CERTIFICATE_ORG1') - .required() - .example('"-----BEGIN CERTIFICATE-----\\n...\\n-----END CERTIFICATE-----\\n"') - .asString(); + .get('HLF_CERTIFICATE_ORG1') + .required() + .example( + '"-----BEGIN CERTIFICATE-----\\n...\\n-----END CERTIFICATE-----\\n"' + ) + .asString(); /** * Private key for an Org1 identity to evaluate and submit transactions */ export const privateKeyOrg1 = env - .get('HLF_PRIVATE_KEY_ORG1') - .required() - .example('"-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n"') - .asString(); + .get('HLF_PRIVATE_KEY_ORG1') + .required() + .example( + '"-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n"' + ) + .asString(); /** * The Org2 connection profile JSON */ export const connectionProfileOrg2 = env - .get('HLF_CONNECTION_PROFILE_ORG2') - .required() - .example( - '{"name":"test-network-org2","version":"1.0.0","client":{"organization":"Org2" ... }' - ) - .asJsonObject() as Record; + .get('HLF_CONNECTION_PROFILE_ORG2') + .required() + .example( + '{"name":"test-network-org2","version":"1.0.0","client":{"organization":"Org2" ... }' + ) + .asJsonObject() as Record; /** * Certificate for an Org2 identity to evaluate and submit transactions */ export const certificateOrg2 = env - .get('HLF_CERTIFICATE_ORG2') - .required() - .example('"-----BEGIN CERTIFICATE-----\\n...\\n-----END CERTIFICATE-----\\n"') - .asString(); + .get('HLF_CERTIFICATE_ORG2') + .required() + .example( + '"-----BEGIN CERTIFICATE-----\\n...\\n-----END CERTIFICATE-----\\n"' + ) + .asString(); /** * Private key for an Org2 identity to evaluate and submit transactions */ export const privateKeyOrg2 = env - .get('HLF_PRIVATE_KEY_ORG2') - .required() - .example('"-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n"') - .asString(); + .get('HLF_PRIVATE_KEY_ORG2') + .required() + .example( + '"-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n"' + ) + .asString(); /** * The host the Redis server is running on */ export const redisHost = env - .get('REDIS_HOST') - .default('localhost') - .example('localhost') - .asString(); + .get('REDIS_HOST') + .default('localhost') + .example('localhost') + .asString(); /** * The port the Redis server is running on */ export const redisPort = env - .get('REDIS_PORT') - .default('6379') - .example('6379') - .asPortNumber(); + .get('REDIS_PORT') + .default('6379') + .example('6379') + .asPortNumber(); /** * Username for the Redis server */ export const redisUsername = env - .get('REDIS_USERNAME') - .example('fabric') - .asString(); + .get('REDIS_USERNAME') + .example('fabric') + .asString(); /** * Password for the Redis server @@ -269,17 +277,17 @@ export const redisPassword = env.get('REDIS_PASSWORD').asString(); * Specify this API key with the X-Api-Key header to use the Org1 connection profile and credentials */ export const org1ApiKey = env - .get('ORG1_APIKEY') - .required() - .example('123') - .asString(); + .get('ORG1_APIKEY') + .required() + .example('123') + .asString(); /** * API key for Org2 * Specify this API key with the X-Api-Key header to use the Org2 connection profile and credentials */ export const org2ApiKey = env - .get('ORG2_APIKEY') - .required() - .example('456') - .asString(); + .get('ORG2_APIKEY') + .required() + .example('456') + .asString(); diff --git a/asset-transfer-basic/rest-api-typescript/src/errors.spec.ts b/asset-transfer-basic/rest-api-typescript/src/errors.spec.ts index ddf4c9f059..d4d93cc0e8 100644 --- a/asset-transfer-basic/rest-api-typescript/src/errors.spec.ts +++ b/asset-transfer-basic/rest-api-typescript/src/errors.spec.ts @@ -4,311 +4,326 @@ import { TimeoutError, TransactionError } from 'fabric-network'; import { - AssetExistsError, - AssetNotFoundError, - TransactionNotFoundError, - getRetryAction, - handleError, - isDuplicateTransactionError, - isErrorLike, - RetryAction, + AssetExistsError, + AssetNotFoundError, + TransactionNotFoundError, + getRetryAction, + handleError, + isDuplicateTransactionError, + isErrorLike, + RetryAction, } from './errors'; import { mock } from 'jest-mock-extended'; describe('Errors', () => { - describe('isErrorLike', () => { - it('returns false for null', () => { - expect(isErrorLike(null)).toBe(false); + describe('isErrorLike', () => { + it('returns false for null', () => { + expect(isErrorLike(null)).toBe(false); + }); + + it('returns false for undefined', () => { + expect(isErrorLike(undefined)).toBe(false); + }); + + it('returns false for empty object', () => { + expect(isErrorLike({})).toBe(false); + }); + + it('returns false for string', () => { + expect(isErrorLike('true')).toBe(false); + }); + + it('returns false for non-error object', () => { + expect(isErrorLike({ size: 42 })).toBe(false); + }); + + it('returns false for invalid error object', () => { + expect(isErrorLike({ name: 'MockError', message: 42 })).toBe(false); + }); + + it('returns false for error like object with invalid stack', () => { + expect( + isErrorLike({ + name: 'MockError', + message: 'Fail', + stack: false, + }) + ).toBe(false); + }); + + it('returns true for error like object', () => { + expect(isErrorLike({ name: 'MockError', message: 'Fail' })).toBe( + true + ); + }); + + it('returns true for new Error', () => { + expect(isErrorLike(new Error('Error'))).toBe(true); + }); }); - it('returns false for undefined', () => { - expect(isErrorLike(undefined)).toBe(false); + describe('isDuplicateTransactionError', () => { + it('returns true for a TransactionError with a transaction code of DUPLICATE_TXID', () => { + const mockDuplicateTransactionError = mock(); + mockDuplicateTransactionError.transactionCode = 'DUPLICATE_TXID'; + + expect( + isDuplicateTransactionError(mockDuplicateTransactionError) + ).toBe(true); + }); + + it('returns false for a TransactionError without a transaction code of MVCC_READ_CONFLICT', () => { + const mockDuplicateTransactionError = mock(); + mockDuplicateTransactionError.transactionCode = + 'MVCC_READ_CONFLICT'; + + expect( + isDuplicateTransactionError(mockDuplicateTransactionError) + ).toBe(false); + }); + + it('returns true for an error when all endorsement details are duplicate transaction found', () => { + const mockDuplicateTransactionError = { + errors: [ + { + endorsements: [ + { + details: 'duplicate transaction found', + }, + { + details: 'duplicate transaction found', + }, + { + details: 'duplicate transaction found', + }, + ], + }, + ], + }; + + expect( + isDuplicateTransactionError(mockDuplicateTransactionError) + ).toBe(true); + }); + + it('returns true for an error when at least one endorsement details are duplicate transaction found', () => { + const mockDuplicateTransactionError = { + errors: [ + { + endorsements: [ + { + details: 'duplicate transaction found', + }, + { + details: 'mock endorsement details', + }, + { + details: 'mock endorsement details', + }, + ], + }, + ], + }; + + expect( + isDuplicateTransactionError(mockDuplicateTransactionError) + ).toBe(true); + }); + + it('returns false for an error without duplicate transaction endorsement details', () => { + const mockDuplicateTransactionError = { + errors: [ + { + endorsements: [ + { + details: 'mock endorsement details', + }, + { + details: 'mock endorsement details', + }, + ], + }, + ], + }; + + expect( + isDuplicateTransactionError(mockDuplicateTransactionError) + ).toBe(false); + }); + + it('returns false for an error without endorsement details', () => { + const mockDuplicateTransactionError = { + errors: [ + { + rejections: [ + { + details: 'duplicate transaction found', + }, + ], + }, + ], + }; + + expect( + isDuplicateTransactionError(mockDuplicateTransactionError) + ).toBe(false); + }); + + it('returns false for a basic Error object without endorsement details', () => { + expect( + isDuplicateTransactionError( + new Error('duplicate transaction found') + ) + ).toBe(false); + }); + + it('returns false for an undefined error', () => { + expect(isDuplicateTransactionError(undefined)).toBe(false); + }); + + it('returns false for a null error', () => { + expect(isDuplicateTransactionError(null)).toBe(false); + }); }); - it('returns false for empty object', () => { - expect(isErrorLike({})).toBe(false); + describe('getRetryAction', () => { + it('returns RetryAction.None for duplicate transaction errors', () => { + const mockDuplicateTransactionError = { + errors: [ + { + endorsements: [ + { + details: 'duplicate transaction found', + }, + { + details: 'duplicate transaction found', + }, + { + details: 'duplicate transaction found', + }, + ], + }, + ], + }; + + expect(getRetryAction(mockDuplicateTransactionError)).toBe( + RetryAction.None + ); + }); + + it('returns RetryAction.None for a TransactionNotFoundError', () => { + const mockTransactionNotFoundError = new TransactionNotFoundError( + 'Failed to get transaction with id txn, error Entry not found in index', + 'txn1' + ); + + expect(getRetryAction(mockTransactionNotFoundError)).toBe( + RetryAction.None + ); + }); + + it('returns RetryAction.None for an AssetExistsError', () => { + const mockAssetExistsError = new AssetExistsError( + 'The asset MOCK_ASSET already exists', + 'txn1' + ); + + expect(getRetryAction(mockAssetExistsError)).toBe(RetryAction.None); + }); + + it('returns RetryAction.None for an AssetNotFoundError', () => { + const mockAssetNotFoundError = new AssetNotFoundError( + 'the asset MOCK_ASSET does not exist', + 'txn1' + ); + + expect(getRetryAction(mockAssetNotFoundError)).toBe( + RetryAction.None + ); + }); + + it('returns RetryAction.WithExistingTransactionId for a TimeoutError', () => { + const mockTimeoutError = new TimeoutError('MOCK TIMEOUT ERROR'); + + expect(getRetryAction(mockTimeoutError)).toBe( + RetryAction.WithExistingTransactionId + ); + }); + + it('returns RetryAction.WithNewTransactionId for an MVCC_READ_CONFLICT TransactionError', () => { + const mockTransactionError = mock(); + mockTransactionError.transactionCode = 'MVCC_READ_CONFLICT'; + + expect(getRetryAction(mockTransactionError)).toBe( + RetryAction.WithNewTransactionId + ); + }); + + it('returns RetryAction.WithNewTransactionId for an Error', () => { + const mockError = new Error('MOCK ERROR'); + + expect(getRetryAction(mockError)).toBe( + RetryAction.WithNewTransactionId + ); + }); + + it('returns RetryAction.WithNewTransactionId for a string error', () => { + const mockError = 'MOCK ERROR'; + + expect(getRetryAction(mockError)).toBe( + RetryAction.WithNewTransactionId + ); + }); }); - it('returns false for string', () => { - expect(isErrorLike('true')).toBe(false); - }); - - it('returns false for non-error object', () => { - expect(isErrorLike({ size: 42 })).toBe(false); - }); - - it('returns false for invalid error object', () => { - expect(isErrorLike({ name: 'MockError', message: 42 })).toBe(false); - }); - - it('returns false for error like object with invalid stack', () => { - expect( - isErrorLike({ name: 'MockError', message: 'Fail', stack: false }) - ).toBe(false); - }); - - it('returns true for error like object', () => { - expect(isErrorLike({ name: 'MockError', message: 'Fail' })).toBe(true); - }); - - it('returns true for new Error', () => { - expect(isErrorLike(new Error('Error'))).toBe(true); - }); - }); - - describe('isDuplicateTransactionError', () => { - it('returns true for a TransactionError with a transaction code of DUPLICATE_TXID', () => { - const mockDuplicateTransactionError = mock(); - mockDuplicateTransactionError.transactionCode = 'DUPLICATE_TXID'; - - expect(isDuplicateTransactionError(mockDuplicateTransactionError)).toBe( - true - ); - }); - - it('returns false for a TransactionError without a transaction code of MVCC_READ_CONFLICT', () => { - const mockDuplicateTransactionError = mock(); - mockDuplicateTransactionError.transactionCode = 'MVCC_READ_CONFLICT'; - - expect(isDuplicateTransactionError(mockDuplicateTransactionError)).toBe( - false - ); - }); - - it('returns true for an error when all endorsement details are duplicate transaction found', () => { - const mockDuplicateTransactionError = { - errors: [ - { - endorsements: [ - { - details: 'duplicate transaction found', - }, - { - details: 'duplicate transaction found', - }, - { - details: 'duplicate transaction found', - }, - ], - }, - ], - }; - - expect(isDuplicateTransactionError(mockDuplicateTransactionError)).toBe( - true - ); - }); - - it('returns true for an error when at least one endorsement details are duplicate transaction found', () => { - const mockDuplicateTransactionError = { - errors: [ - { - endorsements: [ - { - details: 'duplicate transaction found', - }, - { - details: 'mock endorsement details', - }, - { - details: 'mock endorsement details', - }, - ], - }, - ], - }; - - expect(isDuplicateTransactionError(mockDuplicateTransactionError)).toBe( - true - ); - }); - - it('returns false for an error without duplicate transaction endorsement details', () => { - const mockDuplicateTransactionError = { - errors: [ - { - endorsements: [ - { - details: 'mock endorsement details', - }, - { - details: 'mock endorsement details', - }, - ], - }, - ], - }; - - expect(isDuplicateTransactionError(mockDuplicateTransactionError)).toBe( - false - ); - }); - - it('returns false for an error without endorsement details', () => { - const mockDuplicateTransactionError = { - errors: [ - { - rejections: [ - { - details: 'duplicate transaction found', - }, - ], - }, - ], - }; - - expect(isDuplicateTransactionError(mockDuplicateTransactionError)).toBe( - false - ); - }); - - it('returns false for a basic Error object without endorsement details', () => { - expect( - isDuplicateTransactionError(new Error('duplicate transaction found')) - ).toBe(false); - }); - - it('returns false for an undefined error', () => { - expect(isDuplicateTransactionError(undefined)).toBe(false); - }); - - it('returns false for a null error', () => { - expect(isDuplicateTransactionError(null)).toBe(false); - }); - }); - - describe('getRetryAction', () => { - it('returns RetryAction.None for duplicate transaction errors', () => { - const mockDuplicateTransactionError = { - errors: [ - { - endorsements: [ - { - details: 'duplicate transaction found', - }, - { - details: 'duplicate transaction found', - }, - { - details: 'duplicate transaction found', - }, - ], - }, - ], - }; - - expect(getRetryAction(mockDuplicateTransactionError)).toBe( - RetryAction.None - ); - }); - - it('returns RetryAction.None for a TransactionNotFoundError', () => { - const mockTransactionNotFoundError = new TransactionNotFoundError( - 'Failed to get transaction with id txn, error Entry not found in index', - 'txn1' - ); - - expect(getRetryAction(mockTransactionNotFoundError)).toBe( - RetryAction.None - ); - }); - - it('returns RetryAction.None for an AssetExistsError', () => { - const mockAssetExistsError = new AssetExistsError( - 'The asset MOCK_ASSET already exists', - 'txn1' - ); - - expect(getRetryAction(mockAssetExistsError)).toBe(RetryAction.None); - }); - - it('returns RetryAction.None for an AssetNotFoundError', () => { - const mockAssetNotFoundError = new AssetNotFoundError( - 'the asset MOCK_ASSET does not exist', - 'txn1' - ); - - expect(getRetryAction(mockAssetNotFoundError)).toBe(RetryAction.None); - }); - - it('returns RetryAction.WithExistingTransactionId for a TimeoutError', () => { - const mockTimeoutError = new TimeoutError('MOCK TIMEOUT ERROR'); - - expect(getRetryAction(mockTimeoutError)).toBe( - RetryAction.WithExistingTransactionId - ); - }); - - it('returns RetryAction.WithNewTransactionId for an MVCC_READ_CONFLICT TransactionError', () => { - const mockTransactionError = mock(); - mockTransactionError.transactionCode = 'MVCC_READ_CONFLICT'; - - expect(getRetryAction(mockTransactionError)).toBe( - RetryAction.WithNewTransactionId - ); - }); - - it('returns RetryAction.WithNewTransactionId for an Error', () => { - const mockError = new Error('MOCK ERROR'); - - expect(getRetryAction(mockError)).toBe(RetryAction.WithNewTransactionId); - }); - - it('returns RetryAction.WithNewTransactionId for a string error', () => { - const mockError = 'MOCK ERROR'; - - expect(getRetryAction(mockError)).toBe(RetryAction.WithNewTransactionId); - }); - }); - - describe('handleError', () => { - it.each([ - 'the asset GOCHAINCODE already exists', - 'Asset JAVACHAINCODE already exists', - 'The asset JSCHAINCODE already exists', - ])( - 'returns a AssetExistsError for errors with an asset already exists message: %s', - (msg) => { - expect(handleError('txn1', new Error(msg))).toStrictEqual( - new AssetExistsError(msg, 'txn1') + describe('handleError', () => { + it.each([ + 'the asset GOCHAINCODE already exists', + 'Asset JAVACHAINCODE already exists', + 'The asset JSCHAINCODE already exists', + ])( + 'returns a AssetExistsError for errors with an asset already exists message: %s', + (msg) => { + expect(handleError('txn1', new Error(msg))).toStrictEqual( + new AssetExistsError(msg, 'txn1') + ); + } ); - } - ); - - it.each([ - 'the asset GOCHAINCODE does not exist', - 'Asset JAVACHAINCODE does not exist', - 'The asset JSCHAINCODE does not exist', - ])( - 'returns a AssetNotFoundError for errors with an asset does not exist message: %s', - (msg) => { - expect(handleError('txn1', new Error(msg))).toStrictEqual( - new AssetNotFoundError(msg, 'txn1') + + it.each([ + 'the asset GOCHAINCODE does not exist', + 'Asset JAVACHAINCODE does not exist', + 'The asset JSCHAINCODE does not exist', + ])( + 'returns a AssetNotFoundError for errors with an asset does not exist message: %s', + (msg) => { + expect(handleError('txn1', new Error(msg))).toStrictEqual( + new AssetNotFoundError(msg, 'txn1') + ); + } ); - } - ); - - it.each([ - 'Failed to get transaction with id txn, error Entry not found in index', - 'Failed to get transaction with id txn, error no such transaction ID [txn] in index', - ])( - 'returns a TransactionNotFoundError for errors with a transaction not found message: %s', - (msg) => { - expect(handleError('txn1', new Error(msg))).toStrictEqual( - new TransactionNotFoundError(msg, 'txn1') + + it.each([ + 'Failed to get transaction with id txn, error Entry not found in index', + 'Failed to get transaction with id txn, error no such transaction ID [txn] in index', + ])( + 'returns a TransactionNotFoundError for errors with a transaction not found message: %s', + (msg) => { + expect(handleError('txn1', new Error(msg))).toStrictEqual( + new TransactionNotFoundError(msg, 'txn1') + ); + } ); - } - ); - it('returns the original error for errors with other messages', () => { - expect(handleError('txn1', new Error('MOCK ERROR'))).toStrictEqual( - new Error('MOCK ERROR') - ); - }); + it('returns the original error for errors with other messages', () => { + expect(handleError('txn1', new Error('MOCK ERROR'))).toStrictEqual( + new Error('MOCK ERROR') + ); + }); - it('returns the original error for errors of other types', () => { - expect(handleError('txn1', 42)).toEqual(42); + it('returns the original error for errors of other types', () => { + expect(handleError('txn1', 42)).toEqual(42); + }); }); - }); }); diff --git a/asset-transfer-basic/rest-api-typescript/src/errors.ts b/asset-transfer-basic/rest-api-typescript/src/errors.ts index cb451f9bf7..9be1dbe08f 100644 --- a/asset-transfer-basic/rest-api-typescript/src/errors.ts +++ b/asset-transfer-basic/rest-api-typescript/src/errors.ts @@ -14,15 +14,15 @@ import { logger } from './logger'; * These errors will not be retried. */ export class ContractError extends Error { - transactionId: string; + transactionId: string; - constructor(message: string, transactionId: string) { - super(message); - Object.setPrototypeOf(this, ContractError.prototype); + constructor(message: string, transactionId: string) { + super(message); + Object.setPrototypeOf(this, ContractError.prototype); - this.name = 'TransactionError'; - this.transactionId = transactionId; - } + this.name = 'TransactionError'; + this.transactionId = transactionId; + } } /** @@ -30,12 +30,12 @@ export class ContractError extends Error { * evaluated is not implemented in a smart contract. */ export class TransactionNotFoundError extends ContractError { - constructor(message: string, transactionId: string) { - super(message, transactionId); - Object.setPrototypeOf(this, TransactionNotFoundError.prototype); + constructor(message: string, transactionId: string) { + super(message, transactionId); + Object.setPrototypeOf(this, TransactionNotFoundError.prototype); - this.name = 'TransactionNotFoundError'; - } + this.name = 'TransactionNotFoundError'; + } } /** @@ -43,12 +43,12 @@ export class TransactionNotFoundError extends ContractError { * implementation when an asset already exists. */ export class AssetExistsError extends ContractError { - constructor(message: string, transactionId: string) { - super(message, transactionId); - Object.setPrototypeOf(this, AssetExistsError.prototype); + constructor(message: string, transactionId: string) { + super(message, transactionId); + Object.setPrototypeOf(this, AssetExistsError.prototype); - this.name = 'AssetExistsError'; - } + this.name = 'AssetExistsError'; + } } /** @@ -56,35 +56,35 @@ export class AssetExistsError extends ContractError { * implementation when an asset does not exist. */ export class AssetNotFoundError extends ContractError { - constructor(message: string, transactionId: string) { - super(message, transactionId); - Object.setPrototypeOf(this, AssetNotFoundError.prototype); + constructor(message: string, transactionId: string) { + super(message, transactionId); + Object.setPrototypeOf(this, AssetNotFoundError.prototype); - this.name = 'AssetNotFoundError'; - } + this.name = 'AssetNotFoundError'; + } } /** * Enumeration of possible retry actions. */ export enum RetryAction { - /** - * Transactions should be retried using the same transaction ID to protect - * against duplicate transactions being committed if a timeout error occurs - */ - WithExistingTransactionId, - - /** - * Transactions which could not be committed due to other errors require a - * new transaction ID when retrying - */ - WithNewTransactionId, - - /** - * Transactions that failed due to a duplicate transaction error, or errors - * from the smart contract, should not be retried - */ - None, + /** + * Transactions should be retried using the same transaction ID to protect + * against duplicate transactions being committed if a timeout error occurs + */ + WithExistingTransactionId, + + /** + * Transactions which could not be committed due to other errors require a + * new transaction ID when retrying + */ + WithNewTransactionId, + + /** + * Transactions that failed due to a duplicate transaction error, or errors + * from the smart contract, should not be retried + */ + None, } /** @@ -103,27 +103,27 @@ export enum RetryAction { * - EXPIRED_CHAINCODE */ export const getRetryAction = (err: unknown): RetryAction => { - if (isDuplicateTransactionError(err) || err instanceof ContractError) { - return RetryAction.None; - } else if (err instanceof TimeoutError) { - return RetryAction.WithExistingTransactionId; - } + if (isDuplicateTransactionError(err) || err instanceof ContractError) { + return RetryAction.None; + } else if (err instanceof TimeoutError) { + return RetryAction.WithExistingTransactionId; + } - return RetryAction.WithNewTransactionId; + return RetryAction.WithNewTransactionId; }; /** * Type guard to make catching unknown errors easier */ export const isErrorLike = (err: unknown): err is Error => { - return ( - err != undefined && - err != null && - typeof (err as Error).name === 'string' && - typeof (err as Error).message === 'string' && - ((err as Error).stack === undefined || - typeof (err as Error).stack === 'string') - ); + return ( + err != undefined && + err != null && + typeof (err as Error).name === 'string' && + typeof (err as Error).message === 'string' && + ((err as Error).stack === undefined || + typeof (err as Error).stack === 'string') + ); }; /** @@ -132,31 +132,31 @@ export const isErrorLike = (err: unknown): err is Error => { * This is ...painful. */ export const isDuplicateTransactionError = (err: unknown): boolean => { - logger.debug({ err }, 'Checking for duplicate transaction error'); - - if (err === undefined || err === null) return false; - - let isDuplicate; - if (typeof (err as TransactionError).transactionCode === 'string') { - // Checking whether a commit failure is caused by a duplicate transaction - // is straightforward because the transaction code should be available - isDuplicate = - (err as TransactionError).transactionCode === 'DUPLICATE_TXID'; - } else { - // Checking whether an endorsement failure is caused by a duplicate - // transaction is only possible by processing error strings, which is not ideal. - const endorsementError = err as { - errors: { endorsements: { details: string }[] }[]; - }; - - isDuplicate = endorsementError?.errors?.some((err) => - err?.endorsements?.some((endorsement) => - endorsement?.details?.startsWith('duplicate transaction found') - ) - ); - } + logger.debug({ err }, 'Checking for duplicate transaction error'); + + if (err === undefined || err === null) return false; + + let isDuplicate; + if (typeof (err as TransactionError).transactionCode === 'string') { + // Checking whether a commit failure is caused by a duplicate transaction + // is straightforward because the transaction code should be available + isDuplicate = + (err as TransactionError).transactionCode === 'DUPLICATE_TXID'; + } else { + // Checking whether an endorsement failure is caused by a duplicate + // transaction is only possible by processing error strings, which is not ideal. + const endorsementError = err as { + errors: { endorsements: { details: string }[] }[]; + }; + + isDuplicate = endorsementError?.errors?.some((err) => + err?.endorsements?.some((endorsement) => + endorsement?.details?.startsWith('duplicate transaction found') + ) + ); + } - return isDuplicate === true; + return isDuplicate === true; }; /** @@ -168,18 +168,18 @@ export const isDuplicateTransactionError = (err: unknown): boolean => { * - "Asset %s already exists" */ const matchAssetAlreadyExistsMessage = (message: string): string | null => { - const assetAlreadyExistsRegex = /([tT]he )?[aA]sset \w* already exists/g; - const assetAlreadyExistsMatch = message.match(assetAlreadyExistsRegex); - logger.debug( - { message: message, result: assetAlreadyExistsMatch }, - 'Checking for asset already exists message' - ); - - if (assetAlreadyExistsMatch !== null) { - return assetAlreadyExistsMatch[0]; - } - - return null; + const assetAlreadyExistsRegex = /([tT]he )?[aA]sset \w* already exists/g; + const assetAlreadyExistsMatch = message.match(assetAlreadyExistsRegex); + logger.debug( + { message: message, result: assetAlreadyExistsMatch }, + 'Checking for asset already exists message' + ); + + if (assetAlreadyExistsMatch !== null) { + return assetAlreadyExistsMatch[0]; + } + + return null; }; /** @@ -191,18 +191,18 @@ const matchAssetAlreadyExistsMessage = (message: string): string | null => { * - "Asset %s does not exist" */ const matchAssetDoesNotExistMessage = (message: string): string | null => { - const assetDoesNotExistRegex = /([tT]he )?[aA]sset \w* does not exist/g; - const assetDoesNotExistMatch = message.match(assetDoesNotExistRegex); - logger.debug( - { message: message, result: assetDoesNotExistMatch }, - 'Checking for asset does not exist message' - ); - - if (assetDoesNotExistMatch !== null) { - return assetDoesNotExistMatch[0]; - } - - return null; + const assetDoesNotExistRegex = /([tT]he )?[aA]sset \w* does not exist/g; + const assetDoesNotExistMatch = message.match(assetDoesNotExistRegex); + logger.debug( + { message: message, result: assetDoesNotExistMatch }, + 'Checking for asset does not exist message' + ); + + if (assetDoesNotExistMatch !== null) { + return assetDoesNotExistMatch[0]; + } + + return null; }; /** @@ -213,23 +213,23 @@ const matchAssetDoesNotExistMessage = (message: string): string | null => { * - "Failed to get transaction with id %s, error no such transaction ID [%s] in index" */ const matchTransactionDoesNotExistMessage = ( - message: string + message: string ): string | null => { - const transactionDoesNotExistRegex = - /Failed to get transaction with id [^,]*, error (?:(?:Entry not found)|(?:no such transaction ID \[[^\]]*\])) in index/g; - const transactionDoesNotExistMatch = message.match( - transactionDoesNotExistRegex - ); - logger.debug( - { message: message, result: transactionDoesNotExistMatch }, - 'Checking for transaction does not exist message' - ); - - if (transactionDoesNotExistMatch !== null) { - return transactionDoesNotExistMatch[0]; - } - - return null; + const transactionDoesNotExistRegex = + /Failed to get transaction with id [^,]*, error (?:(?:Entry not found)|(?:no such transaction ID \[[^\]]*\])) in index/g; + const transactionDoesNotExistMatch = message.match( + transactionDoesNotExistRegex + ); + logger.debug( + { message: message, result: transactionDoesNotExistMatch }, + 'Checking for transaction does not exist message' + ); + + if (transactionDoesNotExistMatch !== null) { + return transactionDoesNotExistMatch[0]; + } + + return null; }; /** @@ -242,32 +242,38 @@ const matchTransactionDoesNotExistMessage = ( * Javascript implementations of the chaincode! */ export const handleError = ( - transactionId: string, - err: unknown + transactionId: string, + err: unknown ): Error | unknown => { - logger.debug({ transactionId: transactionId, err }, 'Processing error'); - - if (isErrorLike(err)) { - const assetAlreadyExistsMatch = matchAssetAlreadyExistsMessage(err.message); - if (assetAlreadyExistsMatch !== null) { - return new AssetExistsError(assetAlreadyExistsMatch, transactionId); - } - - const assetDoesNotExistMatch = matchAssetDoesNotExistMessage(err.message); - if (assetDoesNotExistMatch !== null) { - return new AssetNotFoundError(assetDoesNotExistMatch, transactionId); - } - - const transactionDoesNotExistMatch = matchTransactionDoesNotExistMessage( - err.message - ); - if (transactionDoesNotExistMatch !== null) { - return new TransactionNotFoundError( - transactionDoesNotExistMatch, - transactionId - ); + logger.debug({ transactionId: transactionId, err }, 'Processing error'); + + if (isErrorLike(err)) { + const assetAlreadyExistsMatch = matchAssetAlreadyExistsMessage( + err.message + ); + if (assetAlreadyExistsMatch !== null) { + return new AssetExistsError(assetAlreadyExistsMatch, transactionId); + } + + const assetDoesNotExistMatch = matchAssetDoesNotExistMessage( + err.message + ); + if (assetDoesNotExistMatch !== null) { + return new AssetNotFoundError( + assetDoesNotExistMatch, + transactionId + ); + } + + const transactionDoesNotExistMatch = + matchTransactionDoesNotExistMessage(err.message); + if (transactionDoesNotExistMatch !== null) { + return new TransactionNotFoundError( + transactionDoesNotExistMatch, + transactionId + ); + } } - } - return err; + return err; }; diff --git a/asset-transfer-basic/rest-api-typescript/src/fabric.spec.ts b/asset-transfer-basic/rest-api-typescript/src/fabric.spec.ts index b7ba2805e0..3a382f0e1e 100644 --- a/asset-transfer-basic/rest-api-typescript/src/fabric.spec.ts +++ b/asset-transfer-basic/rest-api-typescript/src/fabric.spec.ts @@ -3,30 +3,30 @@ */ import { - createGateway, - createWallet, - getContracts, - getNetwork, - evatuateTransaction, - submitTransaction, - getBlockHeight, - getTransactionValidationCode, + createGateway, + createWallet, + getContracts, + getNetwork, + evatuateTransaction, + submitTransaction, + getBlockHeight, + getTransactionValidationCode, } from './fabric'; import * as config from './config'; import { - AssetExistsError, - AssetNotFoundError, - TransactionNotFoundError, + AssetExistsError, + AssetNotFoundError, + TransactionNotFoundError, } from './errors'; import { - Contract, - Gateway, - GatewayOptions, - Network, - Transaction, - Wallet, + Contract, + Gateway, + GatewayOptions, + Network, + Transaction, + Wallet, } from 'fabric-network'; import * as fabricProtos from 'fabric-protos'; @@ -36,275 +36,279 @@ import Long from 'long'; jest.mock('./config'); jest.mock('fabric-network', () => { - type FabricNetworkModule = jest.Mocked; - const originalModule: FabricNetworkModule = - jest.requireActual('fabric-network'); - const mockModule: FabricNetworkModule = - jest.createMockFromModule('fabric-network'); - - return { - __esModule: true, - ...mockModule, - Wallets: originalModule.Wallets, - }; + type FabricNetworkModule = jest.Mocked; + const originalModule: FabricNetworkModule = + jest.requireActual('fabric-network'); + const mockModule: FabricNetworkModule = + jest.createMockFromModule('fabric-network'); + + return { + __esModule: true, + ...mockModule, + Wallets: originalModule.Wallets, + }; }); jest.mock('ioredis', () => require('ioredis-mock/jest')); describe('Fabric', () => { - describe('createWallet', () => { - it('creates a wallet containing identities for both orgs', async () => { - const wallet = await createWallet(); + describe('createWallet', () => { + it('creates a wallet containing identities for both orgs', async () => { + const wallet = await createWallet(); - expect(await wallet.list()).toStrictEqual(['Org1MSP', 'Org2MSP']); + expect(await wallet.list()).toStrictEqual(['Org1MSP', 'Org2MSP']); + }); }); - }); - - describe('createGateway', () => { - it('creates a Gateway and connects using the provided arguments', async () => { - const connectionProfile = config.connectionProfileOrg1; - const identity = config.mspIdOrg1; - const mockWallet = mock(); - - const gateway = await createGateway( - connectionProfile, - identity, - mockWallet - ); - - expect(gateway.connect).toBeCalledWith( - connectionProfile, - expect.objectContaining({ - wallet: mockWallet, - identity, - discovery: expect.any(Object), - eventHandlerOptions: expect.any(Object), - queryHandlerOptions: expect.any(Object), - }) - ); - }); - }); - - describe('getNetwork', () => { - it('gets a Network instance for the required channel from the Gateway', async () => { - const mockGateway = mock(); - - await getNetwork(mockGateway); - expect(mockGateway.getNetwork).toHaveBeenCalledWith(config.channelName); - }); - }); - - describe('getContracts', () => { - it('gets the asset and qscc contracts from the network', async () => { - const mockBasicContract = mock(); - const mockSystemContract = mock(); - const mockNetwork = mock(); - mockNetwork.getContract - .calledWith(config.chaincodeName) - .mockReturnValue(mockBasicContract); - mockNetwork.getContract - .calledWith('qscc') - .mockReturnValue(mockSystemContract); - - const contracts = await getContracts(mockNetwork); - - expect(contracts).toStrictEqual({ - assetContract: mockBasicContract, - qsccContract: mockSystemContract, - }); - }); - }); - - describe('evatuateTransaction', () => { - const mockPayload = Buffer.from('MOCK PAYLOAD'); - let mockTransaction: MockProxy; - let mockContract: MockProxy; - - beforeEach(() => { - mockTransaction = mock(); - mockTransaction.evaluate.mockResolvedValue(mockPayload); - mockContract = mock(); - mockContract.createTransaction - .calledWith('txn') - .mockReturnValue(mockTransaction); + describe('createGateway', () => { + it('creates a Gateway and connects using the provided arguments', async () => { + const connectionProfile = config.connectionProfileOrg1; + const identity = config.mspIdOrg1; + const mockWallet = mock(); + + const gateway = await createGateway( + connectionProfile, + identity, + mockWallet + ); + + expect(gateway.connect).toBeCalledWith( + connectionProfile, + expect.objectContaining({ + wallet: mockWallet, + identity, + discovery: expect.any(Object), + eventHandlerOptions: expect.any(Object), + queryHandlerOptions: expect.any(Object), + }) + ); + }); }); - it('gets the result of evaluating a transaction', async () => { - const result = await evatuateTransaction( - mockContract, - 'txn', - 'arga', - 'argb' - ); - expect(result.toString()).toBe(mockPayload.toString()); - }); + describe('getNetwork', () => { + it('gets a Network instance for the required channel from the Gateway', async () => { + const mockGateway = mock(); - it('throws an AssetExistsError an asset already exists error occurs', async () => { - mockTransaction.evaluate.mockRejectedValue( - new Error('The asset JSCHAINCODE already exists') - ); + await getNetwork(mockGateway); - await expect(async () => { - await evatuateTransaction(mockContract, 'txn', 'arga', 'argb'); - }).rejects.toThrow(AssetExistsError); + expect(mockGateway.getNetwork).toHaveBeenCalledWith( + config.channelName + ); + }); }); - it('throws an AssetNotFoundError if an asset does not exist error occurs', async () => { - mockTransaction.evaluate.mockRejectedValue( - new Error('The asset JSCHAINCODE does not exist') - ); - - await expect(async () => { - await evatuateTransaction(mockContract, 'txn', 'arga', 'argb'); - }).rejects.toThrow(AssetNotFoundError); + describe('getContracts', () => { + it('gets the asset and qscc contracts from the network', async () => { + const mockBasicContract = mock(); + const mockSystemContract = mock(); + const mockNetwork = mock(); + mockNetwork.getContract + .calledWith(config.chaincodeName) + .mockReturnValue(mockBasicContract); + mockNetwork.getContract + .calledWith('qscc') + .mockReturnValue(mockSystemContract); + + const contracts = await getContracts(mockNetwork); + + expect(contracts).toStrictEqual({ + assetContract: mockBasicContract, + qsccContract: mockSystemContract, + }); + }); }); - it('throws a TransactionNotFoundError if a transaction not found error occurs', async () => { - mockTransaction.evaluate.mockRejectedValue( - new Error( - 'Failed to get transaction with id txn, error Entry not found in index' - ) - ); - - await expect(async () => { - await evatuateTransaction(mockContract, 'txn', 'arga', 'argb'); - }).rejects.toThrow(TransactionNotFoundError); + describe('evatuateTransaction', () => { + const mockPayload = Buffer.from('MOCK PAYLOAD'); + let mockTransaction: MockProxy; + let mockContract: MockProxy; + + beforeEach(() => { + mockTransaction = mock(); + mockTransaction.evaluate.mockResolvedValue(mockPayload); + mockContract = mock(); + mockContract.createTransaction + .calledWith('txn') + .mockReturnValue(mockTransaction); + }); + + it('gets the result of evaluating a transaction', async () => { + const result = await evatuateTransaction( + mockContract, + 'txn', + 'arga', + 'argb' + ); + expect(result.toString()).toBe(mockPayload.toString()); + }); + + it('throws an AssetExistsError an asset already exists error occurs', async () => { + mockTransaction.evaluate.mockRejectedValue( + new Error('The asset JSCHAINCODE already exists') + ); + + await expect(async () => { + await evatuateTransaction(mockContract, 'txn', 'arga', 'argb'); + }).rejects.toThrow(AssetExistsError); + }); + + it('throws an AssetNotFoundError if an asset does not exist error occurs', async () => { + mockTransaction.evaluate.mockRejectedValue( + new Error('The asset JSCHAINCODE does not exist') + ); + + await expect(async () => { + await evatuateTransaction(mockContract, 'txn', 'arga', 'argb'); + }).rejects.toThrow(AssetNotFoundError); + }); + + it('throws a TransactionNotFoundError if a transaction not found error occurs', async () => { + mockTransaction.evaluate.mockRejectedValue( + new Error( + 'Failed to get transaction with id txn, error Entry not found in index' + ) + ); + + await expect(async () => { + await evatuateTransaction(mockContract, 'txn', 'arga', 'argb'); + }).rejects.toThrow(TransactionNotFoundError); + }); + + it('throws an Error for other errors', async () => { + mockTransaction.evaluate.mockRejectedValue(new Error('MOCK ERROR')); + await expect(async () => { + await evatuateTransaction(mockContract, 'txn', 'arga', 'argb'); + }).rejects.toThrow(Error); + }); }); - it('throws an Error for other errors', async () => { - mockTransaction.evaluate.mockRejectedValue(new Error('MOCK ERROR')); - await expect(async () => { - await evatuateTransaction(mockContract, 'txn', 'arga', 'argb'); - }).rejects.toThrow(Error); + describe('submitTransaction', () => { + let mockTransaction: MockProxy; + + beforeEach(() => { + mockTransaction = mock(); + }); + + it('gets the result of submitting a transaction', async () => { + const mockPayload = Buffer.from('MOCK PAYLOAD'); + mockTransaction.submit.mockResolvedValue(mockPayload); + + const result = await submitTransaction( + mockTransaction, + 'txn', + 'arga', + 'argb' + ); + expect(result.toString()).toBe(mockPayload.toString()); + }); + + it('throws an AssetExistsError an asset already exists error occurs', async () => { + mockTransaction.submit.mockRejectedValue( + new Error('The asset JSCHAINCODE already exists') + ); + + await expect(async () => { + await submitTransaction( + mockTransaction, + 'mspid', + 'txn', + 'arga', + 'argb' + ); + }).rejects.toThrow(AssetExistsError); + }); + + it('throws an AssetNotFoundError if an asset does not exist error occurs', async () => { + mockTransaction.submit.mockRejectedValue( + new Error('The asset JSCHAINCODE does not exist') + ); + + await expect(async () => { + await submitTransaction( + mockTransaction, + 'mspid', + 'txn', + 'arga', + 'argb' + ); + }).rejects.toThrow(AssetNotFoundError); + }); + + it('throws a TransactionNotFoundError if a transaction not found error occurs', async () => { + mockTransaction.submit.mockRejectedValue( + new Error( + 'Failed to get transaction with id txn, error Entry not found in index' + ) + ); + + await expect(async () => { + await submitTransaction( + mockTransaction, + 'mspid', + 'txn', + 'arga', + 'argb' + ); + }).rejects.toThrow(TransactionNotFoundError); + }); + + it('throws an Error for other errors', async () => { + mockTransaction.submit.mockRejectedValue(new Error('MOCK ERROR')); + + await expect(async () => { + await submitTransaction( + mockTransaction, + 'mspid', + 'txn', + 'arga', + 'argb' + ); + }).rejects.toThrow(Error); + }); }); - }); - describe('submitTransaction', () => { - let mockTransaction: MockProxy; - - beforeEach(() => { - mockTransaction = mock(); + describe('getTransactionValidationCode', () => { + it('gets the validation code from a processed transaction', async () => { + const processedTransactionProto = + fabricProtos.protos.ProcessedTransaction.create(); + processedTransactionProto.validationCode = + fabricProtos.protos.TxValidationCode.VALID; + const processedTransactionBuffer = Buffer.from( + fabricProtos.protos.ProcessedTransaction.encode( + processedTransactionProto + ).finish() + ); + + const mockTransaction = mock(); + mockTransaction.evaluate.mockResolvedValue( + processedTransactionBuffer + ); + const mockContract = mock(); + mockContract.createTransaction + .calledWith('GetTransactionByID') + .mockReturnValue(mockTransaction); + expect( + await getTransactionValidationCode(mockContract, 'txn1') + ).toBe('VALID'); + }); }); - it('gets the result of submitting a transaction', async () => { - const mockPayload = Buffer.from('MOCK PAYLOAD'); - mockTransaction.submit.mockResolvedValue(mockPayload); - - const result = await submitTransaction( - mockTransaction, - 'txn', - 'arga', - 'argb' - ); - expect(result.toString()).toBe(mockPayload.toString()); - }); - - it('throws an AssetExistsError an asset already exists error occurs', async () => { - mockTransaction.submit.mockRejectedValue( - new Error('The asset JSCHAINCODE already exists') - ); - - await expect(async () => { - await submitTransaction( - mockTransaction, - 'mspid', - 'txn', - 'arga', - 'argb' - ); - }).rejects.toThrow(AssetExistsError); - }); - - it('throws an AssetNotFoundError if an asset does not exist error occurs', async () => { - mockTransaction.submit.mockRejectedValue( - new Error('The asset JSCHAINCODE does not exist') - ); - - await expect(async () => { - await submitTransaction( - mockTransaction, - 'mspid', - 'txn', - 'arga', - 'argb' - ); - }).rejects.toThrow(AssetNotFoundError); - }); - - it('throws a TransactionNotFoundError if a transaction not found error occurs', async () => { - mockTransaction.submit.mockRejectedValue( - new Error( - 'Failed to get transaction with id txn, error Entry not found in index' - ) - ); - - await expect(async () => { - await submitTransaction( - mockTransaction, - 'mspid', - 'txn', - 'arga', - 'argb' - ); - }).rejects.toThrow(TransactionNotFoundError); - }); - - it('throws an Error for other errors', async () => { - mockTransaction.submit.mockRejectedValue(new Error('MOCK ERROR')); - - await expect(async () => { - await submitTransaction( - mockTransaction, - 'mspid', - 'txn', - 'arga', - 'argb' - ); - }).rejects.toThrow(Error); - }); - }); - - describe('getTransactionValidationCode', () => { - it('gets the validation code from a processed transaction', async () => { - const processedTransactionProto = - fabricProtos.protos.ProcessedTransaction.create(); - processedTransactionProto.validationCode = - fabricProtos.protos.TxValidationCode.VALID; - const processedTransactionBuffer = Buffer.from( - fabricProtos.protos.ProcessedTransaction.encode( - processedTransactionProto - ).finish() - ); - - const mockTransaction = mock(); - mockTransaction.evaluate.mockResolvedValue(processedTransactionBuffer); - const mockContract = mock(); - mockContract.createTransaction - .calledWith('GetTransactionByID') - .mockReturnValue(mockTransaction); - expect(await getTransactionValidationCode(mockContract, 'txn1')).toBe( - 'VALID' - ); - }); - }); - - describe('getBlockHeight', () => { - it('gets the current block height', async () => { - const mockBlockchainInfoProto = - fabricProtos.common.BlockchainInfo.create(); - mockBlockchainInfoProto.height = 42; - const mockBlockchainInfoBuffer = Buffer.from( - fabricProtos.common.BlockchainInfo.encode( - mockBlockchainInfoProto - ).finish() - ); - const mockContract = mock(); - mockContract.evaluateTransaction - .calledWith('GetChainInfo', 'mychannel') - .mockResolvedValue(mockBlockchainInfoBuffer); - - const result = (await getBlockHeight(mockContract)) as Long; - expect(result.toInt()).toStrictEqual(42); + describe('getBlockHeight', () => { + it('gets the current block height', async () => { + const mockBlockchainInfoProto = + fabricProtos.common.BlockchainInfo.create(); + mockBlockchainInfoProto.height = 42; + const mockBlockchainInfoBuffer = Buffer.from( + fabricProtos.common.BlockchainInfo.encode( + mockBlockchainInfoProto + ).finish() + ); + const mockContract = mock(); + mockContract.evaluateTransaction + .calledWith('GetChainInfo', 'mychannel') + .mockResolvedValue(mockBlockchainInfoBuffer); + + const result = (await getBlockHeight(mockContract)) as Long; + expect(result.toInt()).toStrictEqual(42); + }); }); - }); }); diff --git a/asset-transfer-basic/rest-api-typescript/src/fabric.ts b/asset-transfer-basic/rest-api-typescript/src/fabric.ts index cebfbfe899..b6e2a9c972 100644 --- a/asset-transfer-basic/rest-api-typescript/src/fabric.ts +++ b/asset-transfer-basic/rest-api-typescript/src/fabric.ts @@ -3,15 +3,15 @@ */ import { - Contract, - DefaultEventHandlerStrategies, - DefaultQueryHandlerStrategies, - Gateway, - GatewayOptions, - Network, - Transaction, - Wallet, - Wallets, + Contract, + DefaultEventHandlerStrategies, + DefaultQueryHandlerStrategies, + Gateway, + GatewayOptions, + Network, + Transaction, + Wallet, + Wallets, } from 'fabric-network'; import * as protos from 'fabric-protos'; import Long from 'long'; @@ -29,31 +29,31 @@ import { logger } from './logger'; * or it could use credentials supplied in the REST requests */ export const createWallet = async (): Promise => { - const wallet = await Wallets.newInMemoryWallet(); - - const org1Identity = { - credentials: { - certificate: config.certificateOrg1, - privateKey: config.privateKeyOrg1, - }, - mspId: config.mspIdOrg1, - type: 'X.509', - }; - - await wallet.put(config.mspIdOrg1, org1Identity); - - const org2Identity = { - credentials: { - certificate: config.certificateOrg2, - privateKey: config.privateKeyOrg2, - }, - mspId: config.mspIdOrg2, - type: 'X.509', - }; - - await wallet.put(config.mspIdOrg2, org2Identity); - - return wallet; + const wallet = await Wallets.newInMemoryWallet(); + + const org1Identity = { + credentials: { + certificate: config.certificateOrg1, + privateKey: config.privateKeyOrg1, + }, + mspId: config.mspIdOrg1, + type: 'X.509', + }; + + await wallet.put(config.mspIdOrg1, org1Identity); + + const org2Identity = { + credentials: { + certificate: config.certificateOrg2, + privateKey: config.privateKeyOrg2, + }, + mspId: config.mspIdOrg2, + type: 'X.509', + }; + + await wallet.put(config.mspIdOrg2, org2Identity); + + return wallet; }; /** @@ -62,32 +62,33 @@ export const createWallet = async (): Promise => { * Gateway instances can and should be reused rather than connecting to submit every transaction */ export const createGateway = async ( - connectionProfile: Record, - identity: string, - wallet: Wallet + connectionProfile: Record, + identity: string, + wallet: Wallet ): Promise => { - logger.debug({ connectionProfile, identity }, 'Configuring gateway'); - - const gateway = new Gateway(); - - const options: GatewayOptions = { - wallet, - identity, - discovery: { enabled: true, asLocalhost: config.asLocalhost }, - eventHandlerOptions: { - commitTimeout: config.commitTimeout, - endorseTimeout: config.endorseTimeout, - strategy: DefaultEventHandlerStrategies.PREFER_MSPID_SCOPE_ANYFORTX, - }, - queryHandlerOptions: { - timeout: config.queryTimeout, - strategy: DefaultQueryHandlerStrategies.PREFER_MSPID_SCOPE_ROUND_ROBIN, - }, - }; - - await gateway.connect(connectionProfile, options); - - return gateway; + logger.debug({ connectionProfile, identity }, 'Configuring gateway'); + + const gateway = new Gateway(); + + const options: GatewayOptions = { + wallet, + identity, + discovery: { enabled: true, asLocalhost: config.asLocalhost }, + eventHandlerOptions: { + commitTimeout: config.commitTimeout, + endorseTimeout: config.endorseTimeout, + strategy: DefaultEventHandlerStrategies.PREFER_MSPID_SCOPE_ANYFORTX, + }, + queryHandlerOptions: { + timeout: config.queryTimeout, + strategy: + DefaultQueryHandlerStrategies.PREFER_MSPID_SCOPE_ROUND_ROBIN, + }, + }; + + await gateway.connect(connectionProfile, options); + + return gateway; }; /** @@ -97,8 +98,8 @@ export const createGateway = async ( * start a block event listener */ export const getNetwork = async (gateway: Gateway): Promise => { - const network = await gateway.getNetwork(config.channelName); - return network; + const network = await gateway.getNetwork(config.channelName); + return network; }; /** @@ -107,79 +108,80 @@ export const getNetwork = async (gateway: Gateway): Promise => { * The system contract is used for the liveness REST endpoint */ export const getContracts = async ( - network: Network + network: Network ): Promise<{ assetContract: Contract; qsccContract: Contract }> => { - const assetContract = network.getContract(config.chaincodeName); - const qsccContract = network.getContract('qscc'); - return { assetContract, qsccContract }; + const assetContract = network.getContract(config.chaincodeName); + const qsccContract = network.getContract('qscc'); + return { assetContract, qsccContract }; }; /** * Evaluate a transaction and handle any errors */ export const evatuateTransaction = async ( - contract: Contract, - transactionName: string, - ...transactionArgs: string[] + contract: Contract, + transactionName: string, + ...transactionArgs: string[] ): Promise => { - const transaction = contract.createTransaction(transactionName); - const transactionId = transaction.getTransactionId(); - logger.trace({ transaction }, 'Evaluating transaction'); - - try { - const payload = await transaction.evaluate(...transactionArgs); - logger.trace( - { transactionId: transactionId, payload: payload.toString() }, - 'Evaluate transaction response received' - ); - return payload; - } catch (err) { - throw handleError(transactionId, err); - } + const transaction = contract.createTransaction(transactionName); + const transactionId = transaction.getTransactionId(); + logger.trace({ transaction }, 'Evaluating transaction'); + + try { + const payload = await transaction.evaluate(...transactionArgs); + logger.trace( + { transactionId: transactionId, payload: payload.toString() }, + 'Evaluate transaction response received' + ); + return payload; + } catch (err) { + throw handleError(transactionId, err); + } }; /** * Submit a transaction and handle any errors */ export const submitTransaction = async ( - transaction: Transaction, - ...transactionArgs: string[] + transaction: Transaction, + ...transactionArgs: string[] ): Promise => { - logger.trace({ transaction }, 'Submitting transaction'); - const txnId = transaction.getTransactionId(); - - try { - const payload = await transaction.submit(...transactionArgs); - logger.trace( - { transactionId: txnId, payload: payload.toString() }, - 'Submit transaction response received' - ); - return payload; - } catch (err) { - throw handleError(txnId, err); - } + logger.trace({ transaction }, 'Submitting transaction'); + const txnId = transaction.getTransactionId(); + + try { + const payload = await transaction.submit(...transactionArgs); + logger.trace( + { transactionId: txnId, payload: payload.toString() }, + 'Submit transaction response received' + ); + return payload; + } catch (err) { + throw handleError(txnId, err); + } }; /** * Get the validation code of the specified transaction */ export const getTransactionValidationCode = async ( - qsccContract: Contract, - transactionId: string + qsccContract: Contract, + transactionId: string ): Promise => { - const data = await evatuateTransaction( - qsccContract, - 'GetTransactionByID', - config.channelName, - transactionId - ); - - const processedTransaction = protos.protos.ProcessedTransaction.decode(data); - const validationCode = - protos.protos.TxValidationCode[processedTransaction.validationCode]; - - logger.debug({ transactionId }, 'Validation code: %s', validationCode); - return validationCode; + const data = await evatuateTransaction( + qsccContract, + 'GetTransactionByID', + config.channelName, + transactionId + ); + + const processedTransaction = + protos.protos.ProcessedTransaction.decode(data); + const validationCode = + protos.protos.TxValidationCode[processedTransaction.validationCode]; + + logger.debug({ transactionId }, 'Validation code: %s', validationCode); + return validationCode; }; /** @@ -189,15 +191,15 @@ export const getTransactionValidationCode = async ( * endpoint */ export const getBlockHeight = async ( - qscc: Contract + qscc: Contract ): Promise => { - const data = await qscc.evaluateTransaction( - 'GetChainInfo', - config.channelName - ); - const info = protos.common.BlockchainInfo.decode(data); - const blockHeight = info.height; - - logger.debug('Current block height: %d', blockHeight); - return blockHeight; + const data = await qscc.evaluateTransaction( + 'GetChainInfo', + config.channelName + ); + const info = protos.common.BlockchainInfo.decode(data); + const blockHeight = info.height; + + logger.debug('Current block height: %d', blockHeight); + return blockHeight; }; diff --git a/asset-transfer-basic/rest-api-typescript/src/health.router.ts b/asset-transfer-basic/rest-api-typescript/src/health.router.ts index e0e3258866..86f7155940 100644 --- a/asset-transfer-basic/rest-api-typescript/src/health.router.ts +++ b/asset-transfer-basic/rest-api-typescript/src/health.router.ts @@ -20,36 +20,38 @@ export const healthRouter = express.Router(); */ healthRouter.get('/ready', (_req, res: Response) => - res.status(OK).json({ - status: getReasonPhrase(OK), - timestamp: new Date().toISOString(), - }) + res.status(OK).json({ + status: getReasonPhrase(OK), + timestamp: new Date().toISOString(), + }) ); healthRouter.get('/live', async (req: Request, res: Response) => { - logger.debug(req.body, 'Liveness request received'); - - try { - const submitQueue = req.app.locals.jobq as Queue; - const qsccOrg1 = req.app.locals[config.mspIdOrg1]?.qsccContract as Contract; - const qsccOrg2 = req.app.locals[config.mspIdOrg2]?.qsccContract as Contract; - - await Promise.all([ - getBlockHeight(qsccOrg1), - getBlockHeight(qsccOrg2), - getJobCounts(submitQueue), - ]); - } catch (err) { - logger.error({ err }, 'Error processing liveness request'); - - return res.status(SERVICE_UNAVAILABLE).json({ - status: getReasonPhrase(SERVICE_UNAVAILABLE), - timestamp: new Date().toISOString(), + logger.debug(req.body, 'Liveness request received'); + + try { + const submitQueue = req.app.locals.jobq as Queue; + const qsccOrg1 = req.app.locals[config.mspIdOrg1] + ?.qsccContract as Contract; + const qsccOrg2 = req.app.locals[config.mspIdOrg2] + ?.qsccContract as Contract; + + await Promise.all([ + getBlockHeight(qsccOrg1), + getBlockHeight(qsccOrg2), + getJobCounts(submitQueue), + ]); + } catch (err) { + logger.error({ err }, 'Error processing liveness request'); + + return res.status(SERVICE_UNAVAILABLE).json({ + status: getReasonPhrase(SERVICE_UNAVAILABLE), + timestamp: new Date().toISOString(), + }); + } + + return res.status(OK).json({ + status: getReasonPhrase(OK), + timestamp: new Date().toISOString(), }); - } - - return res.status(OK).json({ - status: getReasonPhrase(OK), - timestamp: new Date().toISOString(), - }); }); diff --git a/asset-transfer-basic/rest-api-typescript/src/index.ts b/asset-transfer-basic/rest-api-typescript/src/index.ts index 75c0d66ccf..bbaf4ab835 100644 --- a/asset-transfer-basic/rest-api-typescript/src/index.ts +++ b/asset-transfer-basic/rest-api-typescript/src/index.ts @@ -8,15 +8,15 @@ import * as config from './config'; import { - createGateway, - createWallet, - getContracts, - getNetwork, + createGateway, + createWallet, + getContracts, + getNetwork, } from './fabric'; import { - initJobQueue, - initJobQueueScheduler, - initJobQueueWorker, + initJobQueue, + initJobQueueScheduler, + initJobQueueWorker, } from './jobs'; import { logger } from './logger'; import { createServer } from './server'; @@ -28,70 +28,70 @@ let jobQueueWorker: Worker | undefined; let jobQueueScheduler: QueueScheduler | undefined; async function main() { - logger.info('Checking Redis config'); - if (!(await isMaxmemoryPolicyNoeviction())) { - throw new Error( - 'Invalid redis configuration: redis instance must have the setting maxmemory-policy=noeviction' - ); - } + logger.info('Checking Redis config'); + if (!(await isMaxmemoryPolicyNoeviction())) { + throw new Error( + 'Invalid redis configuration: redis instance must have the setting maxmemory-policy=noeviction' + ); + } - logger.info('Creating REST server'); - const app = await createServer(); + logger.info('Creating REST server'); + const app = await createServer(); - logger.info('Connecting to Fabric network with org1 mspid'); - const wallet = await createWallet(); + logger.info('Connecting to Fabric network with org1 mspid'); + const wallet = await createWallet(); - const gatewayOrg1 = await createGateway( - config.connectionProfileOrg1, - config.mspIdOrg1, - wallet - ); - const networkOrg1 = await getNetwork(gatewayOrg1); - const contractsOrg1 = await getContracts(networkOrg1); + const gatewayOrg1 = await createGateway( + config.connectionProfileOrg1, + config.mspIdOrg1, + wallet + ); + const networkOrg1 = await getNetwork(gatewayOrg1); + const contractsOrg1 = await getContracts(networkOrg1); - app.locals[config.mspIdOrg1] = contractsOrg1; + app.locals[config.mspIdOrg1] = contractsOrg1; - logger.info('Connecting to Fabric network with org2 mspid'); - const gatewayOrg2 = await createGateway( - config.connectionProfileOrg2, - config.mspIdOrg2, - wallet - ); - const networkOrg2 = await getNetwork(gatewayOrg2); - const contractsOrg2 = await getContracts(networkOrg2); + logger.info('Connecting to Fabric network with org2 mspid'); + const gatewayOrg2 = await createGateway( + config.connectionProfileOrg2, + config.mspIdOrg2, + wallet + ); + const networkOrg2 = await getNetwork(gatewayOrg2); + const contractsOrg2 = await getContracts(networkOrg2); - app.locals[config.mspIdOrg2] = contractsOrg2; + app.locals[config.mspIdOrg2] = contractsOrg2; - logger.info('Initialising submit job queue'); - jobQueue = initJobQueue(); - jobQueueWorker = initJobQueueWorker(app); - if (config.submitJobQueueScheduler === true) { - logger.info('Initialising submit job queue scheduler'); - jobQueueScheduler = initJobQueueScheduler(); - } - app.locals.jobq = jobQueue; + logger.info('Initialising submit job queue'); + jobQueue = initJobQueue(); + jobQueueWorker = initJobQueueWorker(app); + if (config.submitJobQueueScheduler === true) { + logger.info('Initialising submit job queue scheduler'); + jobQueueScheduler = initJobQueueScheduler(); + } + app.locals.jobq = jobQueue; - logger.info('Starting REST server'); - app.listen(config.port, () => { - logger.info('REST server started on port: %d', config.port); - }); + logger.info('Starting REST server'); + app.listen(config.port, () => { + logger.info('REST server started on port: %d', config.port); + }); } main().catch(async (err) => { - logger.error({ err }, 'Unxepected error'); + logger.error({ err }, 'Unxepected error'); - if (jobQueueScheduler != undefined) { - logger.debug('Closing job queue scheduler'); - await jobQueueScheduler.close(); - } + if (jobQueueScheduler != undefined) { + logger.debug('Closing job queue scheduler'); + await jobQueueScheduler.close(); + } - if (jobQueueWorker != undefined) { - logger.debug('Closing job queue worker'); - await jobQueueWorker.close(); - } + if (jobQueueWorker != undefined) { + logger.debug('Closing job queue worker'); + await jobQueueWorker.close(); + } - if (jobQueue != undefined) { - logger.debug('Closing job queue'); - await jobQueue.close(); - } + if (jobQueue != undefined) { + logger.debug('Closing job queue'); + await jobQueue.close(); + } }); diff --git a/asset-transfer-basic/rest-api-typescript/src/jobs.router.ts b/asset-transfer-basic/rest-api-typescript/src/jobs.router.ts index 2c021a2978..2eecefc473 100644 --- a/asset-transfer-basic/rest-api-typescript/src/jobs.router.ts +++ b/asset-transfer-basic/rest-api-typescript/src/jobs.router.ts @@ -13,28 +13,32 @@ const { INTERNAL_SERVER_ERROR, NOT_FOUND, OK } = StatusCodes; export const jobsRouter = express.Router(); jobsRouter.get('/:jobId', async (req: Request, res: Response) => { - const jobId = req.params.jobId; - logger.debug('Read request received for job ID %s', jobId); - - try { - const submitQueue = req.app.locals.jobq as Queue; - - const jobSummary = await getJobSummary(submitQueue, jobId); - - return res.status(OK).json(jobSummary); - } catch (err) { - logger.error({ err }, 'Error processing read request for job ID %s', jobId); - - if (err instanceof JobNotFoundError) { - return res.status(NOT_FOUND).json({ - status: getReasonPhrase(NOT_FOUND), - timestamp: new Date().toISOString(), - }); + const jobId = req.params.jobId; + logger.debug('Read request received for job ID %s', jobId); + + try { + const submitQueue = req.app.locals.jobq as Queue; + + const jobSummary = await getJobSummary(submitQueue, jobId); + + return res.status(OK).json(jobSummary); + } catch (err) { + logger.error( + { err }, + 'Error processing read request for job ID %s', + jobId + ); + + if (err instanceof JobNotFoundError) { + return res.status(NOT_FOUND).json({ + status: getReasonPhrase(NOT_FOUND), + timestamp: new Date().toISOString(), + }); + } + + return res.status(INTERNAL_SERVER_ERROR).json({ + status: getReasonPhrase(INTERNAL_SERVER_ERROR), + timestamp: new Date().toISOString(), + }); } - - return res.status(INTERNAL_SERVER_ERROR).json({ - status: getReasonPhrase(INTERNAL_SERVER_ERROR), - timestamp: new Date().toISOString(), - }); - } }); diff --git a/asset-transfer-basic/rest-api-typescript/src/jobs.spec.ts b/asset-transfer-basic/rest-api-typescript/src/jobs.spec.ts index 1d8b7fe29b..7f327c377b 100644 --- a/asset-transfer-basic/rest-api-typescript/src/jobs.spec.ts +++ b/asset-transfer-basic/rest-api-typescript/src/jobs.spec.ts @@ -4,346 +4,348 @@ import { Job, Queue } from 'bullmq'; import { - addSubmitTransactionJob, - getJobCounts, - getJobSummary, - processSubmitTransactionJob, - JobNotFoundError, - updateJobData, + addSubmitTransactionJob, + getJobCounts, + getJobSummary, + processSubmitTransactionJob, + JobNotFoundError, + updateJobData, } from './jobs'; import { Contract, Transaction } from 'fabric-network'; import { mock, MockProxy } from 'jest-mock-extended'; import { Application } from 'express'; describe('addSubmitTransactionJob', () => { - let mockJob: MockProxy; - let mockQueue: MockProxy; - - beforeEach(() => { - mockJob = mock(); - mockQueue = mock(); - mockQueue.add.mockResolvedValue(mockJob); - }); - - it('returns the new job ID', async () => { - mockJob.id = 'mockJobId'; - - const jobid = await addSubmitTransactionJob( - mockQueue, - 'mockMspId', - 'txn', - 'arg1', - 'arg2' - ); - - expect(jobid).toBe('mockJobId'); - }); - - it('throws an error if there is no job ID', async () => { - mockJob.id = undefined; - - await expect(async () => { - await addSubmitTransactionJob( - mockQueue, - 'mockMspId', - 'txn', - 'arg1', - 'arg2' - ); - }).rejects.toThrowError('Submit transaction job ID not available'); - }); + let mockJob: MockProxy; + let mockQueue: MockProxy; + + beforeEach(() => { + mockJob = mock(); + mockQueue = mock(); + mockQueue.add.mockResolvedValue(mockJob); + }); + + it('returns the new job ID', async () => { + mockJob.id = 'mockJobId'; + + const jobid = await addSubmitTransactionJob( + mockQueue, + 'mockMspId', + 'txn', + 'arg1', + 'arg2' + ); + + expect(jobid).toBe('mockJobId'); + }); + + it('throws an error if there is no job ID', async () => { + mockJob.id = undefined; + + await expect(async () => { + await addSubmitTransactionJob( + mockQueue, + 'mockMspId', + 'txn', + 'arg1', + 'arg2' + ); + }).rejects.toThrowError('Submit transaction job ID not available'); + }); }); describe('getJobSummary', () => { - let mockQueue: MockProxy; - let mockJob: MockProxy; - - beforeEach(() => { - mockQueue = mock(); - mockJob = mock(); - }); - - it('throws a JobNotFoundError if the Job is undefined', async () => { - mockQueue.getJob.calledWith('1').mockResolvedValue(undefined); - - await expect(async () => { - await getJobSummary(mockQueue, '1'); - }).rejects.toThrow(JobNotFoundError); - }); - - it('gets a job summary with transaction payload data', async () => { - mockQueue.getJob.calledWith('1').mockResolvedValue(mockJob); - mockJob.id = '1'; - mockJob.data = { - transactionIds: ['txn1'], - }; - mockJob.returnvalue = { - transactionPayload: Buffer.from('MOCK PAYLOAD'), - }; - - expect(await getJobSummary(mockQueue, '1')).toStrictEqual({ - jobId: '1', - transactionIds: ['txn1'], - transactionError: undefined, - transactionPayload: 'MOCK PAYLOAD', - }); - }); - - it('gets a job summary with empty transaction payload data', async () => { - mockQueue.getJob.calledWith('1').mockResolvedValue(mockJob); - mockJob.id = '1'; - mockJob.data = { - transactionIds: ['txn1'], - }; - mockJob.returnvalue = { - transactionPayload: Buffer.from(''), - }; - - expect(await getJobSummary(mockQueue, '1')).toStrictEqual({ - jobId: '1', - transactionIds: ['txn1'], - transactionError: undefined, - transactionPayload: '', + let mockQueue: MockProxy; + let mockJob: MockProxy; + + beforeEach(() => { + mockQueue = mock(); + mockJob = mock(); }); - }); - - it('gets a job summary with a transaction error', async () => { - mockQueue.getJob.calledWith('1').mockResolvedValue(mockJob); - mockJob.id = '1'; - mockJob.data = { - transactionIds: ['txn1'], - }; - mockJob.returnvalue = { - transactionError: 'MOCK ERROR', - }; - - expect(await getJobSummary(mockQueue, '1')).toStrictEqual({ - jobId: '1', - transactionIds: ['txn1'], - transactionError: 'MOCK ERROR', - transactionPayload: '', + + it('throws a JobNotFoundError if the Job is undefined', async () => { + mockQueue.getJob.calledWith('1').mockResolvedValue(undefined); + + await expect(async () => { + await getJobSummary(mockQueue, '1'); + }).rejects.toThrow(JobNotFoundError); }); - }); - - it('gets a job summary when there is no return value', async () => { - mockQueue.getJob.calledWith('1').mockResolvedValue(mockJob); - mockJob.id = '1'; - mockJob.returnvalue = undefined; - mockJob.data = { - transactionIds: ['txn1'], - }; - - expect(await getJobSummary(mockQueue, '1')).toStrictEqual({ - jobId: '1', - transactionIds: ['txn1'], - transactionError: undefined, - transactionPayload: undefined, + + it('gets a job summary with transaction payload data', async () => { + mockQueue.getJob.calledWith('1').mockResolvedValue(mockJob); + mockJob.id = '1'; + mockJob.data = { + transactionIds: ['txn1'], + }; + mockJob.returnvalue = { + transactionPayload: Buffer.from('MOCK PAYLOAD'), + }; + + expect(await getJobSummary(mockQueue, '1')).toStrictEqual({ + jobId: '1', + transactionIds: ['txn1'], + transactionError: undefined, + transactionPayload: 'MOCK PAYLOAD', + }); }); - }); - - it('gets a job summary when there is no job data', async () => { - mockQueue.getJob.calledWith('1').mockResolvedValue(mockJob); - mockJob.id = '1'; - mockJob.data = undefined; - mockJob.returnvalue = { - transactionPayload: Buffer.from('MOCK PAYLOAD'), - }; - - expect(await getJobSummary(mockQueue, '1')).toStrictEqual({ - jobId: '1', - transactionIds: [], - transactionError: undefined, - transactionPayload: 'MOCK PAYLOAD', + + it('gets a job summary with empty transaction payload data', async () => { + mockQueue.getJob.calledWith('1').mockResolvedValue(mockJob); + mockJob.id = '1'; + mockJob.data = { + transactionIds: ['txn1'], + }; + mockJob.returnvalue = { + transactionPayload: Buffer.from(''), + }; + + expect(await getJobSummary(mockQueue, '1')).toStrictEqual({ + jobId: '1', + transactionIds: ['txn1'], + transactionError: undefined, + transactionPayload: '', + }); }); - }); -}); -describe('updateJobData', () => { - let mockJob: MockProxy; - - beforeEach(() => { - mockJob = mock(); - mockJob.data = { - transactionIds: ['txn1'], - }; - }); - - it('stores the serialized state in the job data if a transaction is specified', async () => { - const mockSavedState = Buffer.from('MOCK SAVED STATE'); - const mockTransaction = mock(); - mockTransaction.getTransactionId.mockReturnValue('txn2'); - mockTransaction.serialize.mockReturnValue(mockSavedState); - - await updateJobData(mockJob, mockTransaction); - - expect(mockJob.update).toBeCalledTimes(1); - expect(mockJob.update).toBeCalledWith({ - transactionIds: ['txn1', 'txn2'], - transactionState: mockSavedState, + it('gets a job summary with a transaction error', async () => { + mockQueue.getJob.calledWith('1').mockResolvedValue(mockJob); + mockJob.id = '1'; + mockJob.data = { + transactionIds: ['txn1'], + }; + mockJob.returnvalue = { + transactionError: 'MOCK ERROR', + }; + + expect(await getJobSummary(mockQueue, '1')).toStrictEqual({ + jobId: '1', + transactionIds: ['txn1'], + transactionError: 'MOCK ERROR', + transactionPayload: '', + }); }); - }); - it('removes the serialized state from the job data if a transaction is not specified', async () => { - await updateJobData(mockJob, undefined); + it('gets a job summary when there is no return value', async () => { + mockQueue.getJob.calledWith('1').mockResolvedValue(mockJob); + mockJob.id = '1'; + mockJob.returnvalue = undefined; + mockJob.data = { + transactionIds: ['txn1'], + }; + + expect(await getJobSummary(mockQueue, '1')).toStrictEqual({ + jobId: '1', + transactionIds: ['txn1'], + transactionError: undefined, + transactionPayload: undefined, + }); + }); - expect(mockJob.update).toBeCalledTimes(1); - expect(mockJob.update).toBeCalledWith({ - transactionIds: ['txn1'], - transactionState: undefined, + it('gets a job summary when there is no job data', async () => { + mockQueue.getJob.calledWith('1').mockResolvedValue(mockJob); + mockJob.id = '1'; + mockJob.data = undefined; + mockJob.returnvalue = { + transactionPayload: Buffer.from('MOCK PAYLOAD'), + }; + + expect(await getJobSummary(mockQueue, '1')).toStrictEqual({ + jobId: '1', + transactionIds: [], + transactionError: undefined, + transactionPayload: 'MOCK PAYLOAD', + }); }); - }); }); -describe('getJobCounts', () => { - it('gets job counts from the specified queue', async () => { - const mockQueue = mock(); - mockQueue.getJobCounts - .calledWith('active', 'completed', 'delayed', 'failed', 'waiting') - .mockResolvedValue({ - active: 1, - completed: 2, - delayed: 3, - failed: 4, - waiting: 5, - }); - - expect(await getJobCounts(mockQueue)).toStrictEqual({ - active: 1, - completed: 2, - delayed: 3, - failed: 4, - waiting: 5, - }); - }); - - describe('processSubmitTransactionJob', () => { - const mockContracts = new Map(); - const mockPayload = Buffer.from('MOCK PAYLOAD'); - const mockSavedState = Buffer.from('MOCK SAVED STATE'); - let mockTransaction: MockProxy; - let mockContract: MockProxy; - let mockApplication: MockProxy; +describe('updateJobData', () => { let mockJob: MockProxy; beforeEach(() => { - mockTransaction = mock(); - mockTransaction.getTransactionId.mockReturnValue('mockTransactionId'); - - mockContract = mock(); - mockContract.createTransaction - .calledWith('txn') - .mockReturnValue(mockTransaction); - mockContract.deserializeTransaction - .calledWith(mockSavedState) - .mockReturnValue(mockTransaction); - mockContracts.set('mockMspid', mockContract); - - mockApplication = mock(); - mockApplication.locals.mockMspid = { assetContract: mockContract }; - - mockJob = mock(); + mockJob = mock(); + mockJob.data = { + transactionIds: ['txn1'], + }; }); - it('gets job result with no error or payload if no contract is available for the required mspid', async () => { - mockJob.data = { - mspid: 'missingMspid', - }; + it('stores the serialized state in the job data if a transaction is specified', async () => { + const mockSavedState = Buffer.from('MOCK SAVED STATE'); + const mockTransaction = mock(); + mockTransaction.getTransactionId.mockReturnValue('txn2'); + mockTransaction.serialize.mockReturnValue(mockSavedState); - const jobResult = await processSubmitTransactionJob( - mockApplication, - mockJob - ); + await updateJobData(mockJob, mockTransaction); - expect(jobResult).toStrictEqual({ - transactionError: undefined, - transactionPayload: undefined, - }); + expect(mockJob.update).toBeCalledTimes(1); + expect(mockJob.update).toBeCalledWith({ + transactionIds: ['txn1', 'txn2'], + transactionState: mockSavedState, + }); }); - it('gets a job result containing a payload if the transaction was successful first time', async () => { - mockJob.data = { - mspid: 'mockMspid', - transactionName: 'txn', - transactionArgs: ['arg1', 'arg2'], - }; - mockTransaction.submit - .calledWith('arg1', 'arg2') - .mockResolvedValue(mockPayload); - - const jobResult = await processSubmitTransactionJob( - mockApplication, - mockJob - ); - - expect(jobResult).toStrictEqual({ - transactionError: undefined, - transactionPayload: Buffer.from('MOCK PAYLOAD'), - }); - }); + it('removes the serialized state from the job data if a transaction is not specified', async () => { + await updateJobData(mockJob, undefined); - it('gets a job result containing a payload if the transaction was successfully rerun using saved transaction state', async () => { - mockJob.data = { - mspid: 'mockMspid', - transactionName: 'txn', - transactionArgs: ['arg1', 'arg2'], - transactionState: mockSavedState, - }; - mockTransaction.submit - .calledWith('arg1', 'arg2') - .mockResolvedValue(mockPayload); - - const jobResult = await processSubmitTransactionJob( - mockApplication, - mockJob - ); - - expect(jobResult).toStrictEqual({ - transactionError: undefined, - transactionPayload: Buffer.from('MOCK PAYLOAD'), - }); + expect(mockJob.update).toBeCalledTimes(1); + expect(mockJob.update).toBeCalledWith({ + transactionIds: ['txn1'], + transactionState: undefined, + }); }); +}); - it('gets a job result containing an error message if the transaction fails but cannot be retried', async () => { - mockJob.data = { - mspid: 'mockMspid', - transactionName: 'txn', - transactionArgs: ['arg1', 'arg2'], - transactionState: mockSavedState, - }; - mockTransaction.submit - .calledWith('arg1', 'arg2') - .mockRejectedValue( - new Error( - 'Failed to get transaction with id txn, error Entry not found in index' - ) - ); - - const jobResult = await processSubmitTransactionJob( - mockApplication, - mockJob - ); - - expect(jobResult).toStrictEqual({ - transactionError: - 'TransactionNotFoundError: Failed to get transaction with id txn, error Entry not found in index', - transactionPayload: undefined, - }); +describe('getJobCounts', () => { + it('gets job counts from the specified queue', async () => { + const mockQueue = mock(); + mockQueue.getJobCounts + .calledWith('active', 'completed', 'delayed', 'failed', 'waiting') + .mockResolvedValue({ + active: 1, + completed: 2, + delayed: 3, + failed: 4, + waiting: 5, + }); + + expect(await getJobCounts(mockQueue)).toStrictEqual({ + active: 1, + completed: 2, + delayed: 3, + failed: 4, + waiting: 5, + }); }); - it('throws an error if the transaction fails but can be retried', async () => { - mockJob.data = { - mspid: 'mockMspid', - transactionName: 'txn', - transactionArgs: ['arg1', 'arg2'], - transactionState: mockSavedState, - }; - mockTransaction.submit - .calledWith('arg1', 'arg2') - .mockRejectedValue(new Error('MOCK ERROR')); - - await expect(async () => { - await processSubmitTransactionJob(mockApplication, mockJob); - }).rejects.toThrow('MOCK ERROR'); + describe('processSubmitTransactionJob', () => { + const mockContracts = new Map(); + const mockPayload = Buffer.from('MOCK PAYLOAD'); + const mockSavedState = Buffer.from('MOCK SAVED STATE'); + let mockTransaction: MockProxy; + let mockContract: MockProxy; + let mockApplication: MockProxy; + let mockJob: MockProxy; + + beforeEach(() => { + mockTransaction = mock(); + mockTransaction.getTransactionId.mockReturnValue( + 'mockTransactionId' + ); + + mockContract = mock(); + mockContract.createTransaction + .calledWith('txn') + .mockReturnValue(mockTransaction); + mockContract.deserializeTransaction + .calledWith(mockSavedState) + .mockReturnValue(mockTransaction); + mockContracts.set('mockMspid', mockContract); + + mockApplication = mock(); + mockApplication.locals.mockMspid = { assetContract: mockContract }; + + mockJob = mock(); + }); + + it('gets job result with no error or payload if no contract is available for the required mspid', async () => { + mockJob.data = { + mspid: 'missingMspid', + }; + + const jobResult = await processSubmitTransactionJob( + mockApplication, + mockJob + ); + + expect(jobResult).toStrictEqual({ + transactionError: undefined, + transactionPayload: undefined, + }); + }); + + it('gets a job result containing a payload if the transaction was successful first time', async () => { + mockJob.data = { + mspid: 'mockMspid', + transactionName: 'txn', + transactionArgs: ['arg1', 'arg2'], + }; + mockTransaction.submit + .calledWith('arg1', 'arg2') + .mockResolvedValue(mockPayload); + + const jobResult = await processSubmitTransactionJob( + mockApplication, + mockJob + ); + + expect(jobResult).toStrictEqual({ + transactionError: undefined, + transactionPayload: Buffer.from('MOCK PAYLOAD'), + }); + }); + + it('gets a job result containing a payload if the transaction was successfully rerun using saved transaction state', async () => { + mockJob.data = { + mspid: 'mockMspid', + transactionName: 'txn', + transactionArgs: ['arg1', 'arg2'], + transactionState: mockSavedState, + }; + mockTransaction.submit + .calledWith('arg1', 'arg2') + .mockResolvedValue(mockPayload); + + const jobResult = await processSubmitTransactionJob( + mockApplication, + mockJob + ); + + expect(jobResult).toStrictEqual({ + transactionError: undefined, + transactionPayload: Buffer.from('MOCK PAYLOAD'), + }); + }); + + it('gets a job result containing an error message if the transaction fails but cannot be retried', async () => { + mockJob.data = { + mspid: 'mockMspid', + transactionName: 'txn', + transactionArgs: ['arg1', 'arg2'], + transactionState: mockSavedState, + }; + mockTransaction.submit + .calledWith('arg1', 'arg2') + .mockRejectedValue( + new Error( + 'Failed to get transaction with id txn, error Entry not found in index' + ) + ); + + const jobResult = await processSubmitTransactionJob( + mockApplication, + mockJob + ); + + expect(jobResult).toStrictEqual({ + transactionError: + 'TransactionNotFoundError: Failed to get transaction with id txn, error Entry not found in index', + transactionPayload: undefined, + }); + }); + + it('throws an error if the transaction fails but can be retried', async () => { + mockJob.data = { + mspid: 'mockMspid', + transactionName: 'txn', + transactionArgs: ['arg1', 'arg2'], + transactionState: mockSavedState, + }; + mockTransaction.submit + .calledWith('arg1', 'arg2') + .mockRejectedValue(new Error('MOCK ERROR')); + + await expect(async () => { + await processSubmitTransactionJob(mockApplication, mockJob); + }).rejects.toThrow('MOCK ERROR'); + }); }); - }); }); diff --git a/asset-transfer-basic/rest-api-typescript/src/jobs.ts b/asset-transfer-basic/rest-api-typescript/src/jobs.ts index 51b77b932f..0a0890f322 100644 --- a/asset-transfer-basic/rest-api-typescript/src/jobs.ts +++ b/asset-transfer-basic/rest-api-typescript/src/jobs.ts @@ -14,62 +14,62 @@ import { submitTransaction } from './fabric'; import { logger } from './logger'; export type JobData = { - mspid: string; - transactionName: string; - transactionArgs: string[]; - transactionState?: Buffer; - transactionIds: string[]; + mspid: string; + transactionName: string; + transactionArgs: string[]; + transactionState?: Buffer; + transactionIds: string[]; }; export type JobResult = { - transactionPayload?: Buffer; - transactionError?: string; + transactionPayload?: Buffer; + transactionError?: string; }; export type JobSummary = { - jobId: string; - transactionIds: string[]; - transactionPayload?: string; - transactionError?: string; + jobId: string; + transactionIds: string[]; + transactionPayload?: string; + transactionError?: string; }; export class JobNotFoundError extends Error { - jobId: string; + jobId: string; - constructor(message: string, jobId: string) { - super(message); - Object.setPrototypeOf(this, JobNotFoundError.prototype); + constructor(message: string, jobId: string) { + super(message); + Object.setPrototypeOf(this, JobNotFoundError.prototype); - this.name = 'JobNotFoundError'; - this.jobId = jobId; - } + this.name = 'JobNotFoundError'; + this.jobId = jobId; + } } const connection: ConnectionOptions = { - port: config.redisPort, - host: config.redisHost, - username: config.redisUsername, - password: config.redisPassword, + port: config.redisPort, + host: config.redisHost, + username: config.redisUsername, + password: config.redisPassword, }; /** * Set up the queue for submit jobs */ export const initJobQueue = (): Queue => { - const submitQueue = new Queue(config.JOB_QUEUE_NAME, { - connection, - defaultJobOptions: { - attempts: config.submitJobAttempts, - backoff: { - type: config.submitJobBackoffType, - delay: config.submitJobBackoffDelay, - }, - removeOnComplete: config.maxCompletedSubmitJobs, - removeOnFail: config.maxFailedSubmitJobs, - }, - }); - - return submitQueue; + const submitQueue = new Queue(config.JOB_QUEUE_NAME, { + connection, + defaultJobOptions: { + attempts: config.submitJobAttempts, + backoff: { + type: config.submitJobBackoffType, + delay: config.submitJobBackoffDelay, + }, + removeOnComplete: config.maxCompletedSubmitJobs, + removeOnFail: config.maxFailedSubmitJobs, + }, + }); + + return submitQueue; }; /** @@ -77,31 +77,31 @@ export const initJobQueue = (): Queue => { * processSubmitTransactionJob function below */ export const initJobQueueWorker = (app: Application): Worker => { - const worker = new Worker( - config.JOB_QUEUE_NAME, - async (job): Promise => { - return await processSubmitTransactionJob(app, job); - }, - { connection, concurrency: config.submitJobConcurrency } - ); - - worker.on('failed', (job) => { - logger.warn({ job }, 'Job failed'); - }); - - // Important: need to handle this error otherwise worker may stop - // processing jobs - worker.on('error', (err) => { - logger.error({ err }, 'Worker error'); - }); - - if (logger.isLevelEnabled('debug')) { - worker.on('completed', (job) => { - logger.debug({ job }, 'Job completed'); + const worker = new Worker( + config.JOB_QUEUE_NAME, + async (job): Promise => { + return await processSubmitTransactionJob(app, job); + }, + { connection, concurrency: config.submitJobConcurrency } + ); + + worker.on('failed', (job) => { + logger.warn({ job }, 'Job failed'); }); - } - return worker; + // Important: need to handle this error otherwise worker may stop + // processing jobs + worker.on('error', (err) => { + logger.error({ err }, 'Worker error'); + }); + + if (logger.isLevelEnabled('debug')) { + worker.on('completed', (job) => { + logger.debug({ job }, 'Job completed'); + }); + } + + return worker; }; /** @@ -110,103 +110,103 @@ export const initJobQueueWorker = (app: Application): Worker => { * The job will be retried if this function throws an error */ export const processSubmitTransactionJob = async ( - app: Application, - job: Job + app: Application, + job: Job ): Promise => { - logger.debug({ jobId: job.id, jobName: job.name }, 'Processing job'); - - const contract = app.locals[job.data.mspid]?.assetContract as Contract; - if (contract === undefined) { - logger.error( - { jobId: job.id, jobName: job.name }, - 'Contract not found for MSP ID %s', - job.data.mspid - ); - - // Retrying will never work without a contract, so give up with an - // empty job result - return { - transactionError: undefined, - transactionPayload: undefined, - }; - } - - const args = job.data.transactionArgs; - let transaction: Transaction; - - if (job.data.transactionState) { - const savedState = job.data.transactionState; - logger.debug( - { - jobId: job.id, - jobName: job.name, - savedState, - }, - 'Reusing previously saved transaction state' - ); - - transaction = contract.deserializeTransaction(savedState); - } else { - logger.debug( - { - jobId: job.id, - jobName: job.name, - }, - 'Using new transaction' - ); + logger.debug({ jobId: job.id, jobName: job.name }, 'Processing job'); + + const contract = app.locals[job.data.mspid]?.assetContract as Contract; + if (contract === undefined) { + logger.error( + { jobId: job.id, jobName: job.name }, + 'Contract not found for MSP ID %s', + job.data.mspid + ); + + // Retrying will never work without a contract, so give up with an + // empty job result + return { + transactionError: undefined, + transactionPayload: undefined, + }; + } - transaction = contract.createTransaction(job.data.transactionName); - await updateJobData(job, transaction); - } - - logger.debug( - { - jobId: job.id, - jobName: job.name, - transactionId: transaction.getTransactionId(), - }, - 'Submitting transaction' - ); - - try { - const payload = await submitTransaction(transaction, ...args); - - return { - transactionError: undefined, - transactionPayload: payload, - }; - } catch (err) { - const retryAction = getRetryAction(err); - - if (retryAction === RetryAction.None) { - logger.error( - { jobId: job.id, jobName: job.name, err }, - 'Fatal transaction error occurred' - ); - - // Not retriable so return a job result with the error details - return { - transactionError: `${err}`, - transactionPayload: undefined, - }; + const args = job.data.transactionArgs; + let transaction: Transaction; + + if (job.data.transactionState) { + const savedState = job.data.transactionState; + logger.debug( + { + jobId: job.id, + jobName: job.name, + savedState, + }, + 'Reusing previously saved transaction state' + ); + + transaction = contract.deserializeTransaction(savedState); + } else { + logger.debug( + { + jobId: job.id, + jobName: job.name, + }, + 'Using new transaction' + ); + + transaction = contract.createTransaction(job.data.transactionName); + await updateJobData(job, transaction); } - logger.warn( - { jobId: job.id, jobName: job.name, err }, - 'Retryable transaction error occurred' + logger.debug( + { + jobId: job.id, + jobName: job.name, + transactionId: transaction.getTransactionId(), + }, + 'Submitting transaction' ); - if (retryAction === RetryAction.WithNewTransactionId) { - logger.debug( - { jobId: job.id, jobName: job.name }, - 'Clearing saved transaction state' - ); - await updateJobData(job, undefined); + try { + const payload = await submitTransaction(transaction, ...args); + + return { + transactionError: undefined, + transactionPayload: payload, + }; + } catch (err) { + const retryAction = getRetryAction(err); + + if (retryAction === RetryAction.None) { + logger.error( + { jobId: job.id, jobName: job.name, err }, + 'Fatal transaction error occurred' + ); + + // Not retriable so return a job result with the error details + return { + transactionError: `${err}`, + transactionPayload: undefined, + }; + } + + logger.warn( + { jobId: job.id, jobName: job.name, err }, + 'Retryable transaction error occurred' + ); + + if (retryAction === RetryAction.WithNewTransactionId) { + logger.debug( + { jobId: job.id, jobName: job.name }, + 'Clearing saved transaction state' + ); + await updateJobData(job, undefined); + } + + // Rethrow the error to keep retrying + throw err; } - - // Rethrow the error to keep retrying - throw err; - } }; /** @@ -215,63 +215,63 @@ export const processSubmitTransactionJob = async ( * This manages stalled and delayed jobs and is required for retries with backoff */ export const initJobQueueScheduler = (): QueueScheduler => { - const queueScheduler = new QueueScheduler(config.JOB_QUEUE_NAME, { - connection, - }); + const queueScheduler = new QueueScheduler(config.JOB_QUEUE_NAME, { + connection, + }); - queueScheduler.on('failed', (jobId, failedReason) => { - logger.error({ jobId, failedReason }, 'Queue sceduler failure'); - }); + queueScheduler.on('failed', (jobId, failedReason) => { + logger.error({ jobId, failedReason }, 'Queue sceduler failure'); + }); - return queueScheduler; + return queueScheduler; }; /** * Helper to add a new submit transaction job to the queue */ export const addSubmitTransactionJob = async ( - submitQueue: Queue, - mspid: string, - transactionName: string, - ...transactionArgs: string[] + submitQueue: Queue, + mspid: string, + transactionName: string, + ...transactionArgs: string[] ): Promise => { - const jobName = `submit ${transactionName} transaction`; - const job = await submitQueue.add(jobName, { - mspid, - transactionName, - transactionArgs: transactionArgs, - transactionIds: [], - }); - - if (job?.id === undefined) { - throw new Error('Submit transaction job ID not available'); - } - - return job.id; + const jobName = `submit ${transactionName} transaction`; + const job = await submitQueue.add(jobName, { + mspid, + transactionName, + transactionArgs: transactionArgs, + transactionIds: [], + }); + + if (job?.id === undefined) { + throw new Error('Submit transaction job ID not available'); + } + + return job.id; }; /** * Helper to update the data for an existing job */ export const updateJobData = async ( - job: Job, - transaction: Transaction | undefined + job: Job, + transaction: Transaction | undefined ): Promise => { - const newData = { ...job.data }; + const newData = { ...job.data }; - if (transaction != undefined) { - const transationIds = ([] as string[]).concat( - newData.transactionIds, - transaction.getTransactionId() - ); - newData.transactionIds = transationIds; + if (transaction != undefined) { + const transationIds = ([] as string[]).concat( + newData.transactionIds, + transaction.getTransactionId() + ); + newData.transactionIds = transationIds; - newData.transactionState = transaction.serialize(); - } else { - newData.transactionState = undefined; - } + newData.transactionState = transaction.serialize(); + } else { + newData.transactionState = undefined; + } - await job.update(newData); + await job.update(newData); }; /** @@ -280,49 +280,49 @@ export const updateJobData = async ( * This function is used for the jobs REST endpoint */ export const getJobSummary = async ( - queue: Queue, - jobId: string + queue: Queue, + jobId: string ): Promise => { - const job: Job | undefined = await queue.getJob(jobId); - logger.debug({ job }, 'Got job'); - - if (!(job && job.id != undefined)) { - throw new JobNotFoundError(`Job ${jobId} not found`, jobId); - } - - let transactionIds: string[]; - if (job.data && job.data.transactionIds) { - transactionIds = job.data.transactionIds; - } else { - transactionIds = []; - } - - let transactionError; - let transactionPayload; - const returnValue = job.returnvalue; - if (returnValue) { - if (returnValue.transactionError) { - transactionError = returnValue.transactionError; + const job: Job | undefined = await queue.getJob(jobId); + logger.debug({ job }, 'Got job'); + + if (!(job && job.id != undefined)) { + throw new JobNotFoundError(`Job ${jobId} not found`, jobId); } - if ( - returnValue.transactionPayload && - returnValue.transactionPayload.length > 0 - ) { - transactionPayload = returnValue.transactionPayload.toString(); + let transactionIds: string[]; + if (job.data && job.data.transactionIds) { + transactionIds = job.data.transactionIds; } else { - transactionPayload = ''; + transactionIds = []; + } + + let transactionError; + let transactionPayload; + const returnValue = job.returnvalue; + if (returnValue) { + if (returnValue.transactionError) { + transactionError = returnValue.transactionError; + } + + if ( + returnValue.transactionPayload && + returnValue.transactionPayload.length > 0 + ) { + transactionPayload = returnValue.transactionPayload.toString(); + } else { + transactionPayload = ''; + } } - } - const jobSummary: JobSummary = { - jobId: job.id, - transactionIds, - transactionError, - transactionPayload, - }; + const jobSummary: JobSummary = { + jobId: job.id, + transactionIds, + transactionError, + transactionPayload, + }; - return jobSummary; + return jobSummary; }; /** @@ -331,16 +331,16 @@ export const getJobSummary = async ( * This function is used for the liveness REST endpoint */ export const getJobCounts = async ( - queue: Queue + queue: Queue ): Promise<{ [index: string]: number }> => { - const jobCounts = await queue.getJobCounts( - 'active', - 'completed', - 'delayed', - 'failed', - 'waiting' - ); - logger.debug({ jobCounts }, 'Current job counts'); - - return jobCounts; + const jobCounts = await queue.getJobCounts( + 'active', + 'completed', + 'delayed', + 'failed', + 'waiting' + ); + logger.debug({ jobCounts }, 'Current job counts'); + + return jobCounts; }; diff --git a/asset-transfer-basic/rest-api-typescript/src/logger.ts b/asset-transfer-basic/rest-api-typescript/src/logger.ts index 1f1cea83c5..d686269038 100644 --- a/asset-transfer-basic/rest-api-typescript/src/logger.ts +++ b/asset-transfer-basic/rest-api-typescript/src/logger.ts @@ -6,5 +6,5 @@ import pino from 'pino'; import * as config from './config'; export const logger = pino({ - level: config.logLevel, + level: config.logLevel, }); diff --git a/asset-transfer-basic/rest-api-typescript/src/redis.spec.ts b/asset-transfer-basic/rest-api-typescript/src/redis.spec.ts index a73f3b8cbd..bda7e8d604 100644 --- a/asset-transfer-basic/rest-api-typescript/src/redis.spec.ts +++ b/asset-transfer-basic/rest-api-typescript/src/redis.spec.ts @@ -6,29 +6,32 @@ import { isMaxmemoryPolicyNoeviction } from './redis'; const mockRedisConfig = jest.fn(); jest.mock('ioredis', () => { - return jest.fn().mockImplementation(() => { - return { - config: mockRedisConfig, - disconnect: jest.fn(), - }; - }); + return jest.fn().mockImplementation(() => { + return { + config: mockRedisConfig, + disconnect: jest.fn(), + }; + }); }); jest.mock('./config'); describe('Redis', () => { - beforeEach(() => { - mockRedisConfig.mockClear(); - }); - - describe('isMaxmemoryPolicyNoeviction', () => { - it('returns true when the maxmemory-policy is noeviction', async () => { - mockRedisConfig.mockReturnValue(['maxmemory-policy', 'noeviction']); - expect(await isMaxmemoryPolicyNoeviction()).toBe(true); + beforeEach(() => { + mockRedisConfig.mockClear(); }); - it('returns false when the maxmemory-policy is not noeviction', async () => { - mockRedisConfig.mockReturnValue(['maxmemory-policy', 'allkeys-lru']); - expect(await isMaxmemoryPolicyNoeviction()).toBe(false); + describe('isMaxmemoryPolicyNoeviction', () => { + it('returns true when the maxmemory-policy is noeviction', async () => { + mockRedisConfig.mockReturnValue(['maxmemory-policy', 'noeviction']); + expect(await isMaxmemoryPolicyNoeviction()).toBe(true); + }); + + it('returns false when the maxmemory-policy is not noeviction', async () => { + mockRedisConfig.mockReturnValue([ + 'maxmemory-policy', + 'allkeys-lru', + ]); + expect(await isMaxmemoryPolicyNoeviction()).toBe(false); + }); }); - }); }); diff --git a/asset-transfer-basic/rest-api-typescript/src/redis.ts b/asset-transfer-basic/rest-api-typescript/src/redis.ts index 9dfc85bff4..fcf14f008f 100644 --- a/asset-transfer-basic/rest-api-typescript/src/redis.ts +++ b/asset-transfer-basic/rest-api-typescript/src/redis.ts @@ -16,36 +16,36 @@ import { logger } from './logger'; * For details, see: https://docs.bullmq.io/guide/connections */ export const isMaxmemoryPolicyNoeviction = async (): Promise => { - let redis: Redis | undefined; - - const redisOptions: RedisOptions = { - port: config.redisPort, - host: config.redisHost, - username: config.redisUsername, - password: config.redisPassword, - }; - - try { - redis = new IORedis(redisOptions); - - const maxmemoryPolicyConfig = await (redis as Redis).config( - 'GET', - 'maxmemory-policy' - ); - logger.debug({ maxmemoryPolicyConfig }, 'Got maxmemory-policy config'); - - if ( - maxmemoryPolicyConfig.length == 2 && - 'maxmemory-policy' === maxmemoryPolicyConfig[0] && - 'noeviction' === maxmemoryPolicyConfig[1] - ) { - return true; + let redis: Redis | undefined; + + const redisOptions: RedisOptions = { + port: config.redisPort, + host: config.redisHost, + username: config.redisUsername, + password: config.redisPassword, + }; + + try { + redis = new IORedis(redisOptions); + + const maxmemoryPolicyConfig = await (redis as Redis).config( + 'GET', + 'maxmemory-policy' + ); + logger.debug({ maxmemoryPolicyConfig }, 'Got maxmemory-policy config'); + + if ( + maxmemoryPolicyConfig.length == 2 && + 'maxmemory-policy' === maxmemoryPolicyConfig[0] && + 'noeviction' === maxmemoryPolicyConfig[1] + ) { + return true; + } + } finally { + if (redis != undefined) { + redis.disconnect(); + } } - } finally { - if (redis != undefined) { - redis.disconnect(); - } - } - return false; + return false; }; diff --git a/asset-transfer-basic/rest-api-typescript/src/server.ts b/asset-transfer-basic/rest-api-typescript/src/server.ts index d088dbeb26..2d7d31bf3d 100644 --- a/asset-transfer-basic/rest-api-typescript/src/server.ts +++ b/asset-transfer-basic/rest-api-typescript/src/server.ts @@ -18,70 +18,70 @@ import cors from 'cors'; const { BAD_REQUEST, INTERNAL_SERVER_ERROR, NOT_FOUND } = StatusCodes; export const createServer = async (): Promise => { - const app = express(); - - app.use( - pinoMiddleware({ - logger, - customLogLevel: function customLogLevel(res, err) { - if ( - res.statusCode >= BAD_REQUEST && - res.statusCode < INTERNAL_SERVER_ERROR - ) { - return 'warn'; - } - - if (res.statusCode >= INTERNAL_SERVER_ERROR || err) { - return 'error'; - } - - return 'debug'; - }, - }) - ); - - app.use(express.json()); - app.use(express.urlencoded({ extended: true })); - - // define passport startegy - passport.use(fabricAPIKeyStrategy); - - // initialize passport js - app.use(passport.initialize()); - - if (process.env.NODE_ENV === 'development') { - app.use(cors()); - } - - if (process.env.NODE_ENV === 'test') { - // TBC - } - - if (process.env.NODE_ENV === 'production') { - app.use(helmet()); - } - - app.use('/', healthRouter); - app.use('/api/assets', authenticateApiKey, assetsRouter); - app.use('/api/jobs', authenticateApiKey, jobsRouter); - app.use('/api/transactions', authenticateApiKey, transactionsRouter); - - // For everything else - app.use((_req, res) => - res.status(NOT_FOUND).json({ - status: getReasonPhrase(NOT_FOUND), - timestamp: new Date().toISOString(), - }) - ); - - // Print API errors - app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => { - logger.error(err); - return res.status(INTERNAL_SERVER_ERROR).json({ - status: getReasonPhrase(INTERNAL_SERVER_ERROR), - timestamp: new Date().toISOString(), + const app = express(); + + app.use( + pinoMiddleware({ + logger, + customLogLevel: function customLogLevel(res, err) { + if ( + res.statusCode >= BAD_REQUEST && + res.statusCode < INTERNAL_SERVER_ERROR + ) { + return 'warn'; + } + + if (res.statusCode >= INTERNAL_SERVER_ERROR || err) { + return 'error'; + } + + return 'debug'; + }, + }) + ); + + app.use(express.json()); + app.use(express.urlencoded({ extended: true })); + + // define passport startegy + passport.use(fabricAPIKeyStrategy); + + // initialize passport js + app.use(passport.initialize()); + + if (process.env.NODE_ENV === 'development') { + app.use(cors()); + } + + if (process.env.NODE_ENV === 'test') { + // TBC + } + + if (process.env.NODE_ENV === 'production') { + app.use(helmet()); + } + + app.use('/', healthRouter); + app.use('/api/assets', authenticateApiKey, assetsRouter); + app.use('/api/jobs', authenticateApiKey, jobsRouter); + app.use('/api/transactions', authenticateApiKey, transactionsRouter); + + // For everything else + app.use((_req, res) => + res.status(NOT_FOUND).json({ + status: getReasonPhrase(NOT_FOUND), + timestamp: new Date().toISOString(), + }) + ); + + // Print API errors + app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => { + logger.error(err); + return res.status(INTERNAL_SERVER_ERROR).json({ + status: getReasonPhrase(INTERNAL_SERVER_ERROR), + timestamp: new Date().toISOString(), + }); }); - }); - return app; + return app; }; diff --git a/asset-transfer-basic/rest-api-typescript/src/transactions.router.ts b/asset-transfer-basic/rest-api-typescript/src/transactions.router.ts index d092337d45..6868d4691c 100644 --- a/asset-transfer-basic/rest-api-typescript/src/transactions.router.ts +++ b/asset-transfer-basic/rest-api-typescript/src/transactions.router.ts @@ -14,42 +14,46 @@ const { INTERNAL_SERVER_ERROR, NOT_FOUND, OK } = StatusCodes; export const transactionsRouter = express.Router(); transactionsRouter.get( - '/:transactionId', - async (req: Request, res: Response) => { - const mspId = req.user as string; - const transactionId = req.params.transactionId; - logger.debug('Read request received for transaction ID %s', transactionId); - - try { - const qsccContract = req.app.locals[mspId]?.qsccContract as Contract; - - const validationCode = await getTransactionValidationCode( - qsccContract, - transactionId - ); - - return res.status(OK).json({ - transactionId, - validationCode, - }); - } catch (err) { - if (err instanceof TransactionNotFoundError) { - return res.status(NOT_FOUND).json({ - status: getReasonPhrase(NOT_FOUND), - timestamp: new Date().toISOString(), - }); - } else { - logger.error( - { err }, - 'Error processing read request for transaction ID %s', - transactionId + '/:transactionId', + async (req: Request, res: Response) => { + const mspId = req.user as string; + const transactionId = req.params.transactionId; + logger.debug( + 'Read request received for transaction ID %s', + transactionId ); - return res.status(INTERNAL_SERVER_ERROR).json({ - status: getReasonPhrase(INTERNAL_SERVER_ERROR), - timestamp: new Date().toISOString(), - }); - } + try { + const qsccContract = req.app.locals[mspId] + ?.qsccContract as Contract; + + const validationCode = await getTransactionValidationCode( + qsccContract, + transactionId + ); + + return res.status(OK).json({ + transactionId, + validationCode, + }); + } catch (err) { + if (err instanceof TransactionNotFoundError) { + return res.status(NOT_FOUND).json({ + status: getReasonPhrase(NOT_FOUND), + timestamp: new Date().toISOString(), + }); + } else { + logger.error( + { err }, + 'Error processing read request for transaction ID %s', + transactionId + ); + + return res.status(INTERNAL_SERVER_ERROR).json({ + status: getReasonPhrase(INTERNAL_SERVER_ERROR), + timestamp: new Date().toISOString(), + }); + } + } } - } ); diff --git a/asset-transfer-events/application-gateway-typescript/.eslintrc.json b/asset-transfer-events/application-gateway-typescript/.eslintrc.json deleted file mode 100644 index fabcde92b3..0000000000 --- a/asset-transfer-events/application-gateway-typescript/.eslintrc.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "env": { - "node": true, - "es2020": true - }, - "root": true, - "ignorePatterns": [ - "dist/" - ], - "extends": [ - "eslint:recommended" - ], - "rules": { - "indent": [ - "error", - 4 - ], - "quotes": [ - "error", - "single" - ] - }, - "overrides": [ - { - "files": [ - "**/*.ts" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "sourceType": "module", - "ecmaFeatures": { - "impliedStrict": true - }, - "project": "./tsconfig.json" - }, - "plugins": [ - "@typescript-eslint" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:@typescript-eslint/recommended-requiring-type-checking" - ] - } - ] - } \ No newline at end of file diff --git a/asset-transfer-events/application-gateway-typescript/eslint.config.mjs b/asset-transfer-events/application-gateway-typescript/eslint.config.mjs new file mode 100644 index 0000000000..9ef6b24340 --- /dev/null +++ b/asset-transfer-events/application-gateway-typescript/eslint.config.mjs @@ -0,0 +1,13 @@ +import js from '@eslint/js'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config(js.configs.recommended, ...tseslint.configs.strictTypeChecked, { + languageOptions: { + ecmaVersion: 2023, + sourceType: 'module', + parserOptions: { + project: 'tsconfig.json', + tsconfigRootDir: import.meta.dirname, + }, + }, +}); diff --git a/asset-transfer-events/application-gateway-typescript/package.json b/asset-transfer-events/application-gateway-typescript/package.json index 217805e53f..82248261cc 100755 --- a/asset-transfer-events/application-gateway-typescript/package.json +++ b/asset-transfer-events/application-gateway-typescript/package.json @@ -10,7 +10,7 @@ "scripts": { "build": "tsc", "build:watch": "tsc -w", - "lint": "eslint . --ext .ts", + "lint": "eslint src", "prepare": "npm run build", "pretest": "npm run lint", "start": "node dist/app.js" @@ -19,15 +19,15 @@ "author": "Hyperledger", "license": "Apache-2.0", "dependencies": { - "@grpc/grpc-js": "^1.9.7", - "@hyperledger/fabric-gateway": "~1.4.0" + "@grpc/grpc-js": "^1.10", + "@hyperledger/fabric-gateway": "^1.5" }, "devDependencies": { + "@eslint/js": "^9.3.0", "@tsconfig/node18": "^18.2.2", "@types/node": "^18.18.6", - "@typescript-eslint/eslint-plugin": "^6.9.0", - "@typescript-eslint/parser": "^6.9.0", - "eslint": "^8.52.0", - "typescript": "~5.2.2" + "eslint": "^8.57.0", + "typescript": "~5.4", + "typescript-eslint": "^7.13.0" } } diff --git a/asset-transfer-events/application-gateway-typescript/src/app.ts b/asset-transfer-events/application-gateway-typescript/src/app.ts index 551e000345..24ef33390c 100755 --- a/asset-transfer-events/application-gateway-typescript/src/app.ts +++ b/asset-transfer-events/application-gateway-typescript/src/app.ts @@ -14,7 +14,7 @@ const chaincodeName = 'events'; const utf8Decoder = new TextDecoder(); const now = Date.now(); -const assetId = `asset${now}`; +const assetId = `asset${String(now)}`; async function main(): Promise { @@ -60,7 +60,7 @@ async function main(): Promise { } } -main().catch(error => { +main().catch((error: unknown) => { console.error('******** FAILED to run the application:', error); process.exitCode = 1; }); @@ -102,7 +102,7 @@ async function createAsset(contract: Contract): Promise { const status = await result.getStatus(); if (!status.successful) { - throw new Error(`failed to commit transaction ${status.transactionId} with status code ${status.code}`); + throw new Error(`failed to commit transaction ${status.transactionId} with status code ${String(status.code)}`); } console.log('\n*** CreateAsset committed successfully'); diff --git a/asset-transfer-events/application-gateway-typescript/src/connect.ts b/asset-transfer-events/application-gateway-typescript/src/connect.ts index 05ce5fe479..c72f766a73 100644 --- a/asset-transfer-events/application-gateway-typescript/src/connect.ts +++ b/asset-transfer-events/application-gateway-typescript/src/connect.ts @@ -50,5 +50,9 @@ export async function newSigner(): Promise { async function getFirstDirFileName(dirPath: string): Promise { const files = await fs.readdir(dirPath); - return path.join(dirPath, files[0]); + const file = files[0]; + if (!file) { + throw new Error(`No files in directory: ${dirPath}`); + } + return path.join(dirPath, file); } diff --git a/asset-transfer-events/application-gateway-typescript/tsconfig.json b/asset-transfer-events/application-gateway-typescript/tsconfig.json old mode 100755 new mode 100644 index 2a3ffbeb7d..4c20df24e6 --- a/asset-transfer-events/application-gateway-typescript/tsconfig.json +++ b/asset-transfer-events/application-gateway-typescript/tsconfig.json @@ -1,17 +1,15 @@ { - "extends":"@tsconfig/node18/tsconfig.json", - "compilerOptions": { - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "outDir": "dist", - "declaration": true, - "sourceMap": true, - "noImplicitAny": true - }, - "include": [ - "./src/**/*" - ], - "exclude": [ - "./src/**/*.spec.ts" - ] + "extends": "@tsconfig/node18/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "noUnusedLocals": true, + "noImplicitReturns": true, + "noUncheckedIndexedAccess": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["./src/**/*"], + "exclude": ["./src/**/*.spec.ts"] } diff --git a/asset-transfer-private-data/application-gateway-typescript/.eslintrc.json b/asset-transfer-private-data/application-gateway-typescript/.eslintrc.json deleted file mode 100644 index cc7230a80d..0000000000 --- a/asset-transfer-private-data/application-gateway-typescript/.eslintrc.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "env": { - "node": true, - "es6": true - }, - "root": true, - "ignorePatterns": [ - "dist/" - ], - "extends": [ - "eslint:recommended" - ], - "rules": { - "indent": [ - "error", - 4 - ], - "quotes": [ - "error", - "single" - ] - }, - "overrides": [ - { - "files": [ - "**/*.ts" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "sourceType": "module", - "ecmaFeatures": { - "impliedStrict": true - } - }, - "plugins": [ - "@typescript-eslint" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ] - } - ] - } \ No newline at end of file diff --git a/asset-transfer-private-data/application-gateway-typescript/eslint.config.mjs b/asset-transfer-private-data/application-gateway-typescript/eslint.config.mjs new file mode 100644 index 0000000000..9ef6b24340 --- /dev/null +++ b/asset-transfer-private-data/application-gateway-typescript/eslint.config.mjs @@ -0,0 +1,13 @@ +import js from '@eslint/js'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config(js.configs.recommended, ...tseslint.configs.strictTypeChecked, { + languageOptions: { + ecmaVersion: 2023, + sourceType: 'module', + parserOptions: { + project: 'tsconfig.json', + tsconfigRootDir: import.meta.dirname, + }, + }, +}); diff --git a/asset-transfer-private-data/application-gateway-typescript/package.json b/asset-transfer-private-data/application-gateway-typescript/package.json index caad0cae1f..690773b405 100644 --- a/asset-transfer-private-data/application-gateway-typescript/package.json +++ b/asset-transfer-private-data/application-gateway-typescript/package.json @@ -10,7 +10,7 @@ "scripts": { "build": "tsc", "build:watch": "tsc -w", - "lint": "eslint . --ext .ts", + "lint": "eslint src", "prepare": "npm run build", "pretest": "npm run lint", "start": "node dist/app.js" @@ -19,15 +19,15 @@ "author": "Hyperledger", "license": "Apache-2.0", "dependencies": { - "@grpc/grpc-js": "^1.9.7", - "@hyperledger/fabric-gateway": "~1.4.0" + "@grpc/grpc-js": "^1.10", + "@hyperledger/fabric-gateway": "^1.5" }, "devDependencies": { + "@eslint/js": "^9.3.0", "@tsconfig/node18": "^18.2.2", "@types/node": "^18.18.6", - "@typescript-eslint/eslint-plugin": "^6.9.0", - "@typescript-eslint/parser": "^6.9.0", - "eslint": "^8.52.0", - "typescript": "~5.2.2" + "eslint": "^8.57.0", + "typescript": "~5.4", + "typescript-eslint": "^7.13.0" } } diff --git a/asset-transfer-private-data/application-gateway-typescript/src/app.ts b/asset-transfer-private-data/application-gateway-typescript/src/app.ts index 4a3211b83a..0b8a7a783c 100644 --- a/asset-transfer-private-data/application-gateway-typescript/src/app.ts +++ b/asset-transfer-private-data/application-gateway-typescript/src/app.ts @@ -27,8 +27,8 @@ const RESET = '\x1b[0m'; // Use a unique key so that we can run multiple times const now = Date.now(); -const assetID1 = `asset${now}`; -const assetID2 = `asset${now + 1}`; +const assetID1 = `asset${String(now)}`; +const assetID2 = `asset${String(now + 1)}`; async function main(): Promise { const clientOrg1 = await newGrpcConnection( @@ -74,14 +74,13 @@ async function main(): Promise { // Read asset from the Org1's private data collection with ID in the given range. await getAssetsByRange(contractOrg1); - try{ + try { // Attempt to transfer asset without prior aprroval from Org2, transaction expected to fail. console.log('\nAttempt TransferAsset without prior AgreeToTransfer'); await transferAsset(contractOrg1, assetID1); doFail('TransferAsset transaction succeeded when it was expected to fail'); - } - catch(e){ - console.log(`*** Received expected error: ${e}`); + } catch (e) { + console.log('*** Received expected error:', e); } console.log('\n~~~~~~~~~~~~~~~~ As Org2 Client ~~~~~~~~~~~~~~~~'); @@ -122,7 +121,7 @@ async function main(): Promise { await deleteAsset(contractOrg2, assetID2); doFail('DeleteAsset transaction succeeded when it was expected to fail'); } catch (e) { - console.log(`*** Received expected error: ${e}`); + console.log('*** Received expected error:', e); } console.log('\n~~~~~~~~~~~~~~~~ As Org1 Client ~~~~~~~~~~~~~~~~'); @@ -142,7 +141,7 @@ async function main(): Promise { } } -main().catch((error) => { +main().catch((error: unknown) => { console.error('******** FAILED to run the application:', error); process.exitCode = 1; }); @@ -192,14 +191,14 @@ async function getAssetsByRange(contract: Contract): Promise { const resultBytes = await contract.evaluateTransaction( 'GetAssetByRange', assetID1, - `asset${now + 2}` + `asset${String(now + 2)}` ); const resultString = utf8Decoder.decode(resultBytes); if (!resultString) { doFail('Received empty query list for readAssetPrivateDetailsOrg1'); } - const result = JSON.parse(resultString); + const result: unknown = JSON.parse(resultString); console.log('*** Result:', result); } @@ -211,7 +210,7 @@ async function readAssetByID(contract: Contract, assetID: string): Promise if (!resultString) { doFail('Received empty result for ReadAsset'); } - const result = JSON.parse(resultString); + const result: unknown = JSON.parse(resultString); console.log('*** Result:', result); } @@ -241,7 +240,7 @@ async function readTransferAgreement(contract: Contract, assetID: string): Promi if (!resultString) { doFail('Received no result for ReadTransferAgreement'); } - const result = JSON.parse(resultString); + const result: unknown = JSON.parse(resultString); console.log('*** Result:', result); } @@ -290,7 +289,7 @@ async function readAssetPrivateDetails(contract: Contract, assetID: string, coll console.log('*** No result'); return false; } - const result = JSON.parse(resultJson); + const result: unknown = JSON.parse(resultJson); console.log('*** Result:', result); return true; } diff --git a/asset-transfer-private-data/application-gateway-typescript/src/connect.ts b/asset-transfer-private-data/application-gateway-typescript/src/connect.ts index 7f243c4845..0fe7f3a35e 100644 --- a/asset-transfer-private-data/application-gateway-typescript/src/connect.ts +++ b/asset-transfer-private-data/application-gateway-typescript/src/connect.ts @@ -127,5 +127,9 @@ export async function newSigner(keyDirectoryPath: string): Promise { async function getFirstDirFileName(dirPath: string): Promise { const files = await fs.readdir(dirPath); - return path.join(dirPath, files[0]); + const file = files[0]; + if (!file) { + throw new Error(`No files in directory: ${dirPath}`); + } + return path.join(dirPath, file); } diff --git a/asset-transfer-private-data/application-gateway-typescript/tsconfig.json b/asset-transfer-private-data/application-gateway-typescript/tsconfig.json index 2a3ffbeb7d..4c20df24e6 100644 --- a/asset-transfer-private-data/application-gateway-typescript/tsconfig.json +++ b/asset-transfer-private-data/application-gateway-typescript/tsconfig.json @@ -1,17 +1,15 @@ { - "extends":"@tsconfig/node18/tsconfig.json", - "compilerOptions": { - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "outDir": "dist", - "declaration": true, - "sourceMap": true, - "noImplicitAny": true - }, - "include": [ - "./src/**/*" - ], - "exclude": [ - "./src/**/*.spec.ts" - ] + "extends": "@tsconfig/node18/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "noUnusedLocals": true, + "noImplicitReturns": true, + "noUncheckedIndexedAccess": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["./src/**/*"], + "exclude": ["./src/**/*.spec.ts"] } diff --git a/asset-transfer-sbe/chaincode-typescript/npm-shrinkwrap.json b/asset-transfer-sbe/chaincode-typescript/npm-shrinkwrap.json index 44f384dc8b..8ddcd6acae 100644 --- a/asset-transfer-sbe/chaincode-typescript/npm-shrinkwrap.json +++ b/asset-transfer-sbe/chaincode-typescript/npm-shrinkwrap.json @@ -91,9 +91,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.4.0.tgz", - "integrity": "sha512-fdI7VJjP3Rvc70lC4xkFXHB0fiPeojiL1PxVG6t1ZvXQrarj893PweuBTujxDUFk0Fxj4R7PIIAZ/aiiyZPZcg==", + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.5.0.tgz", + "integrity": "sha512-A7+AOT2ICkodvtsWnxZP4Xxk3NbZ3VMHd8oihydLRGrJgqqdEz1qSeEgXYyT/Cu8h1TWWsQRejIx48mtjZ5y1w==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/asset-transfer-secured-agreement/application-gateway-typescript/.eslintrc.json b/asset-transfer-secured-agreement/application-gateway-typescript/.eslintrc.json deleted file mode 100644 index fb2391e0e6..0000000000 --- a/asset-transfer-secured-agreement/application-gateway-typescript/.eslintrc.json +++ /dev/null @@ -1,102 +0,0 @@ -{ - "env": { - "node": true, - "es6": true - }, - "root": true, - "ignorePatterns": [ - "dist/" - ], - "extends": [ - "eslint:recommended" - ], - "rules": { - "indent": [ - "error", - 4 - ], - "quotes": [ - "error", - "single" - ] - }, - "overrides": [ - { - "files": [ - "**/*.ts" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "sourceType": "module", - "ecmaFeatures": { - "impliedStrict": true - }, - "project": [ - "./tsconfig.json" - ] - }, - "plugins": [ - "@typescript-eslint" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ], - "rules": { - "@typescript-eslint/comma-spacing": [ - "error" - ], - "@typescript-eslint/explicit-function-return-type": [ - "error", - { - "allowExpressions": true - } - ], - "@typescript-eslint/func-call-spacing": [ - "error" - ], - "@typescript-eslint/member-delimiter-style": [ - "error" - ], - "@typescript-eslint/indent": [ - "error", - 4, - { - "SwitchCase": 0 - } - ], - "@typescript-eslint/prefer-nullish-coalescing": [ - "error" - ], - "@typescript-eslint/prefer-optional-chain": [ - "error" - ], - "@typescript-eslint/prefer-reduce-type-parameter": [ - "error" - ], - "@typescript-eslint/prefer-return-this-type": [ - "error" - ], - "@typescript-eslint/quotes": [ - "error", - "single" - ], - "@typescript-eslint/type-annotation-spacing": [ - "error" - ], - "@typescript-eslint/semi": [ - "error" - ], - "@typescript-eslint/space-before-function-paren": [ - "error", - { - "anonymous": "never", - "named": "never", - "asyncArrow": "always" - } - ] - } - } - ] - } \ No newline at end of file diff --git a/asset-transfer-secured-agreement/application-gateway-typescript/eslint.config.mjs b/asset-transfer-secured-agreement/application-gateway-typescript/eslint.config.mjs new file mode 100644 index 0000000000..9ef6b24340 --- /dev/null +++ b/asset-transfer-secured-agreement/application-gateway-typescript/eslint.config.mjs @@ -0,0 +1,13 @@ +import js from '@eslint/js'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config(js.configs.recommended, ...tseslint.configs.strictTypeChecked, { + languageOptions: { + ecmaVersion: 2023, + sourceType: 'module', + parserOptions: { + project: 'tsconfig.json', + tsconfigRootDir: import.meta.dirname, + }, + }, +}); diff --git a/asset-transfer-secured-agreement/application-gateway-typescript/package.json b/asset-transfer-secured-agreement/application-gateway-typescript/package.json index 0da9d8bcc2..ae967074e5 100644 --- a/asset-transfer-secured-agreement/application-gateway-typescript/package.json +++ b/asset-transfer-secured-agreement/application-gateway-typescript/package.json @@ -10,7 +10,7 @@ "scripts": { "build": "tsc", "build:watch": "tsc -w", - "lint": "eslint . --ext .ts", + "lint": "eslint src", "prepare": "npm run build", "pretest": "npm run lint", "start": "node dist/app.js" @@ -19,15 +19,15 @@ "author": "Hyperledger", "license": "Apache-2.0", "dependencies": { - "@grpc/grpc-js": "^1.9.7", - "@hyperledger/fabric-gateway": "~1.4.0" + "@grpc/grpc-js": "^1.10", + "@hyperledger/fabric-gateway": "^1.5" }, "devDependencies": { + "@eslint/js": "^9.3.0", "@tsconfig/node18": "^18.2.2", "@types/node": "^18.18.6", - "@typescript-eslint/eslint-plugin": "^6.9.0", - "@typescript-eslint/parser": "^6.9.0", - "eslint": "^8.52.0", - "typescript": "~5.2.2" + "eslint": "^8.57.0", + "typescript": "~5.4", + "typescript-eslint": "^7.13.0" } } diff --git a/asset-transfer-secured-agreement/application-gateway-typescript/src/app.ts b/asset-transfer-secured-agreement/application-gateway-typescript/src/app.ts index f3d142bd9e..74f17c1f68 100644 --- a/asset-transfer-secured-agreement/application-gateway-typescript/src/app.ts +++ b/asset-transfer-secured-agreement/application-gateway-typescript/src/app.ts @@ -72,8 +72,8 @@ async function main(): Promise { // Org2 is not the owner and does not have the private details, read expected to fail. try { await contractWrapperOrg2.getAssetPrivateProperties(assetKey, mspIdOrg1); - } catch(e) { - console.log(`${RED}*** Successfully caught the failure: getAssetPrivateProperties - ${e}${RESET}`); + } catch (e) { + console.log(`${RED}*** Successfully caught the failure: getAssetPrivateProperties - ${String(e)}${RESET}`); } // Org1 updates the assets public description. @@ -94,7 +94,7 @@ async function main(): Promise { ownerOrg: mspIdOrg1, publicDescription: `Asset ${assetKey} owned by ${mspIdOrg2} is NOT for sale`}); } catch(e) { - console.log(`${RED}*** Successfully caught the failure: changePublicDescription - ${e}${RESET}`); + console.log(`${RED}*** Successfully caught the failure: changePublicDescription - ${String(e)}${RESET}`); } // Read the public details by org1. @@ -126,14 +126,14 @@ async function main(): Promise { try{ await contractWrapperOrg2.getAssetSalesPrice(assetKey, mspIdOrg1); } catch(e) { - console.log(`${RED}*** Successfully caught the failure: getAssetSalesPrice - ${e}${RESET}`); + console.log(`${RED}*** Successfully caught the failure: getAssetSalesPrice - ${String(e)}${RESET}`); } // Org1 has not agreed to buy so this should fail. try{ await contractWrapperOrg1.getAssetBidPrice(assetKey, mspIdOrg2); } catch(e) { - console.log(`${RED}*** Successfully caught the failure: getAssetBidPrice - ${e}${RESET}`); + console.log(`${RED}*** Successfully caught the failure: getAssetBidPrice - ${String(e)}${RESET}`); } // Org2 should be able to see the price it has agreed. await contractWrapperOrg2.getAssetBidPrice(assetKey, mspIdOrg2); @@ -143,7 +143,7 @@ async function main(): Promise { try{ await contractWrapperOrg1.transferAsset({ assetId: assetKey, price: 110, tradeId: now}, [ mspIdOrg1, mspIdOrg2 ], mspIdOrg1, mspIdOrg2); } catch(e) { - console.log(`${RED}*** Successfully caught the failure: transferAsset - ${e}${RESET}`); + console.log(`${RED}*** Successfully caught the failure: transferAsset - ${String(e)}${RESET}`); } // Agree to a sell by Org1, the seller will agree to the bid price of Org2. await contractWrapperOrg1.agreeToSell({assetId:assetKey, price:100, tradeId:now}); @@ -168,7 +168,7 @@ async function main(): Promise { try{ await contractWrapperOrg2.transferAsset({ assetId: assetKey, price: 100, tradeId: now}, [ mspIdOrg1, mspIdOrg2 ], mspIdOrg1, mspIdOrg2); } catch(e) { - console.log(`${RED}*** Successfully caught the failure: transferAsset - ${e}${RESET}`); + console.log(`${RED}*** Successfully caught the failure: transferAsset - ${String(e)}${RESET}`); } // Org1 will transfer the asset to Org2. @@ -188,7 +188,7 @@ async function main(): Promise { try{ await contractWrapperOrg1.getAssetPrivateProperties(assetKey, mspIdOrg2); } catch(e) { - console.log(`${RED}*** Successfully caught the failure: getAssetPrivateProperties - ${e}${RESET}`); + console.log(`${RED}*** Successfully caught the failure: getAssetPrivateProperties - ${String(e)}${RESET}`); } // This is an update to the public state and requires only the owner to endorse. @@ -209,7 +209,7 @@ async function main(): Promise { } } -main().catch(error => { +main().catch((error: unknown) => { console.error('******** FAILED to run the application:', error); process.exitCode = 1; }); diff --git a/asset-transfer-secured-agreement/application-gateway-typescript/src/connect.ts b/asset-transfer-secured-agreement/application-gateway-typescript/src/connect.ts index 345f3aaec9..b6afd2e8d7 100644 --- a/asset-transfer-secured-agreement/application-gateway-typescript/src/connect.ts +++ b/asset-transfer-secured-agreement/application-gateway-typescript/src/connect.ts @@ -103,5 +103,9 @@ export async function newSigner(keyDirectoryPath: string): Promise { async function getFirstDirFileName(dirPath: string): Promise { const files = await fs.readdir(dirPath); - return path.join(dirPath, files[0]); + const file = files[0]; + if (!file) { + throw new Error(`No files in directory: ${dirPath}`); + } + return path.join(dirPath, file); } diff --git a/asset-transfer-secured-agreement/application-gateway-typescript/src/contractWrapper.ts b/asset-transfer-secured-agreement/application-gateway-typescript/src/contractWrapper.ts index 82ac77fc1e..44c5f7d28e 100644 --- a/asset-transfer-secured-agreement/application-gateway-typescript/src/contractWrapper.ts +++ b/asset-transfer-secured-agreement/application-gateway-typescript/src/contractWrapper.ts @@ -6,10 +6,10 @@ import { Contract } from '@hyperledger/fabric-gateway'; import { TextDecoder } from 'util'; import { GREEN, parse, RED, RESET } from './utils'; -import crpto from 'crypto'; +import crypto from 'crypto'; import { mspIdOrg2 } from './connect'; -const randomBytes = crpto.randomBytes(256).toString('hex'); +const randomBytes = crypto.randomBytes(256).toString('hex'); interface AssetJSON { objectType: string; @@ -151,7 +151,7 @@ export class ContractWrapper { endorsingOrganizations: this.#endorsingOrgs[assetPrice.assetId] }); - console.log(`*** Result: committed, ${this.#org} has agreed to sell asset ${assetPrice.assetId} for ${assetPrice.price}`); + console.log(`*** Result: committed, ${this.#org} has agreed to sell asset ${assetPrice.assetId} for ${String(assetPrice.price)}`); } public async verifyAssetProperties(assetId: string, assetProperties: AssetProperties): Promise { @@ -169,16 +169,11 @@ export class ContractWrapper { const resultString = this.#utf8Decoder.decode(resultBytes); if (resultString.length !== 0) { const json = parse(resultString); - const result: AssetProperties = { - color: json.color, - size: json.size - }; - if (result) { + if (typeof json === 'object') { console.log(`*** Success VerifyAssetProperties, private information about asset ${assetId} has been verified by ${this.#org}`); } else { console.log(`*** Failed: VerifyAssetProperties, private information about asset ${assetId} has not been verified by ${this.#org}`); } - } else { throw new Error(`Private information about asset ${assetId} has not been verified by ${this.#org}`); } diff --git a/asset-transfer-secured-agreement/application-gateway-typescript/src/utils.ts b/asset-transfer-secured-agreement/application-gateway-typescript/src/utils.ts index 6ff968fb75..0442ff02e1 100644 --- a/asset-transfer-secured-agreement/application-gateway-typescript/src/utils.ts +++ b/asset-transfer-secured-agreement/application-gateway-typescript/src/utils.ts @@ -9,5 +9,5 @@ export const GREEN = '\x1b[32m\n'; export const RESET = '\x1b[0m'; export function parse(data: string): T { - return JSON.parse(data); + return JSON.parse(data) as T; } diff --git a/asset-transfer-secured-agreement/application-gateway-typescript/tsconfig.json b/asset-transfer-secured-agreement/application-gateway-typescript/tsconfig.json index 2a3ffbeb7d..4c20df24e6 100644 --- a/asset-transfer-secured-agreement/application-gateway-typescript/tsconfig.json +++ b/asset-transfer-secured-agreement/application-gateway-typescript/tsconfig.json @@ -1,17 +1,15 @@ { - "extends":"@tsconfig/node18/tsconfig.json", - "compilerOptions": { - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "outDir": "dist", - "declaration": true, - "sourceMap": true, - "noImplicitAny": true - }, - "include": [ - "./src/**/*" - ], - "exclude": [ - "./src/**/*.spec.ts" - ] + "extends": "@tsconfig/node18/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "noUnusedLocals": true, + "noImplicitReturns": true, + "noUncheckedIndexedAccess": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["./src/**/*"], + "exclude": ["./src/**/*.spec.ts"] } diff --git a/full-stack-asset-transfer-guide/applications/conga-cards/package.json b/full-stack-asset-transfer-guide/applications/conga-cards/package.json index c4dd469925..10a9db415b 100644 --- a/full-stack-asset-transfer-guide/applications/conga-cards/package.json +++ b/full-stack-asset-transfer-guide/applications/conga-cards/package.json @@ -19,8 +19,8 @@ "author": "Hyperledger", "license": "Apache-2.0", "dependencies": { - "@grpc/grpc-js": "^1.9.7", - "@hyperledger/fabric-gateway": "~1.4.0", + "@grpc/grpc-js": "^1.10", + "@hyperledger/fabric-gateway": "^1.5", "axios": "^0.27.2", "source-map-support": "^0.5.21" }, diff --git a/full-stack-asset-transfer-guide/applications/ping-chaincode/package.json b/full-stack-asset-transfer-guide/applications/ping-chaincode/package.json index 5630f6bf44..2ca68b0da3 100644 --- a/full-stack-asset-transfer-guide/applications/ping-chaincode/package.json +++ b/full-stack-asset-transfer-guide/applications/ping-chaincode/package.json @@ -18,8 +18,8 @@ "author": "Hyperledger", "license": "Apache-2.0", "dependencies": { - "@grpc/grpc-js": "^1.9.7", - "@hyperledger/fabric-gateway": "~1.4.0", + "@grpc/grpc-js": "^1.10", + "@hyperledger/fabric-gateway": "^1.5", "dotenv": "^16.0.1", "env-var": "^7.1.1", "js-yaml": "^4.1.0" diff --git a/full-stack-asset-transfer-guide/applications/rest-api/package.json b/full-stack-asset-transfer-guide/applications/rest-api/package.json index f540e2d5bf..dfe7db8ac1 100644 --- a/full-stack-asset-transfer-guide/applications/rest-api/package.json +++ b/full-stack-asset-transfer-guide/applications/rest-api/package.json @@ -29,8 +29,8 @@ "typescript": "~5.2.2" }, "dependencies": { - "@grpc/grpc-js": "^1.9.7", - "@hyperledger/fabric-gateway": "~1.4.0", + "@grpc/grpc-js": "^1.10", + "@hyperledger/fabric-gateway": "^1.5", "body-parser": "^1.18.3", "cors": "^2.8.5", "express": "^4.16.3" diff --git a/full-stack-asset-transfer-guide/applications/trader-typescript/.eslintrc.js b/full-stack-asset-transfer-guide/applications/trader-typescript/.eslintrc.js deleted file mode 100644 index 207a02ca82..0000000000 --- a/full-stack-asset-transfer-guide/applications/trader-typescript/.eslintrc.js +++ /dev/null @@ -1,99 +0,0 @@ -module.exports = { - env: { - node: true, - es2021: true, - }, - extends: [ - 'eslint:recommended', - ], - root: true, - ignorePatterns: [ - 'dist/', - ], - rules: { - 'arrow-spacing': ['error'], - 'comma-style': ['error'], - complexity: ['error', 10], - 'eol-last': ['error'], - 'generator-star-spacing': ['error', 'after'], - 'key-spacing': [ - 'error', - { - beforeColon: false, - afterColon: true, - mode: 'minimum', - }, - ], - 'keyword-spacing': ['error'], - 'no-multiple-empty-lines': ['error'], - 'no-trailing-spaces': ['error'], - 'no-whitespace-before-property': ['error'], - 'object-curly-newline': ['error'], - 'padded-blocks': ['error', 'never'], - 'rest-spread-spacing': ['error'], - 'semi-style': ['error'], - 'space-before-blocks': ['error'], - 'space-in-parens': ['error'], - 'space-unary-ops': ['error'], - 'spaced-comment': ['error'], - 'template-curly-spacing': ['error'], - 'yield-star-spacing': ['error', 'after'], - }, - overrides: [ - { - files: [ - '**/*.ts', - ], - parser: '@typescript-eslint/parser', - parserOptions: { - sourceType: 'module', - ecmaFeatures: { - impliedStrict: true, - }, - project: './tsconfig.json', - tsconfigRootDir: process.env.TSCONFIG_ROOT_DIR || __dirname, - }, - plugins: [ - '@typescript-eslint', - ], - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:@typescript-eslint/recommended-requiring-type-checking', - ], - rules: { - '@typescript-eslint/comma-spacing': ['error'], - '@typescript-eslint/explicit-function-return-type': [ - 'error', - { - allowExpressions: true, - }, - ], - '@typescript-eslint/func-call-spacing': ['error'], - '@typescript-eslint/member-delimiter-style': ['error'], - '@typescript-eslint/indent': [ - 'error', - 4, - { - SwitchCase: 0, - }, - ], - '@typescript-eslint/prefer-nullish-coalescing': ['error'], - '@typescript-eslint/prefer-optional-chain': ['error'], - '@typescript-eslint/prefer-reduce-type-parameter': ['error'], - '@typescript-eslint/prefer-return-this-type': ['error'], - '@typescript-eslint/quotes': ['error', 'single'], - '@typescript-eslint/type-annotation-spacing': ['error'], - '@typescript-eslint/semi': ['error'], - '@typescript-eslint/space-before-function-paren': [ - 'error', - { - anonymous: 'never', - named: 'never', - asyncArrow: 'always', - }, - ], - }, - }, - ], -}; diff --git a/full-stack-asset-transfer-guide/applications/trader-typescript/eslint.config.mjs b/full-stack-asset-transfer-guide/applications/trader-typescript/eslint.config.mjs new file mode 100644 index 0000000000..9ef6b24340 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/trader-typescript/eslint.config.mjs @@ -0,0 +1,13 @@ +import js from '@eslint/js'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config(js.configs.recommended, ...tseslint.configs.strictTypeChecked, { + languageOptions: { + ecmaVersion: 2023, + sourceType: 'module', + parserOptions: { + project: 'tsconfig.json', + tsconfigRootDir: import.meta.dirname, + }, + }, +}); diff --git a/full-stack-asset-transfer-guide/applications/trader-typescript/package.json b/full-stack-asset-transfer-guide/applications/trader-typescript/package.json index 8999c7dd58..b1042cbbc6 100644 --- a/full-stack-asset-transfer-guide/applications/trader-typescript/package.json +++ b/full-stack-asset-transfer-guide/applications/trader-typescript/package.json @@ -10,7 +10,7 @@ "scripts": { "build": "tsc", "build:watch": "tsc -w", - "lint": "eslint ./src", + "lint": "eslint src", "prepare": "npm run build", "pretest": "npm run lint", "start": "node ./dist/app", @@ -19,15 +19,15 @@ "author": "Hyperledger", "license": "Apache-2.0", "dependencies": { - "@grpc/grpc-js": "^1.9.7", - "@hyperledger/fabric-gateway": "~1.4.0" + "@grpc/grpc-js": "^1.10", + "@hyperledger/fabric-gateway": "^1.5" }, "devDependencies": { - "@tsconfig/node18": "^18.2.2", - "@types/node": "^18.18.6", - "@typescript-eslint/eslint-plugin": "^6.9.0", - "@typescript-eslint/parser": "^6.9.0", - "eslint": "^8.52.0", - "typescript": "~5.2.2" - } + "@types/node": "^18.19.33", + "@eslint/js": "^9.3.0", + "@tsconfig/node18": "^18.2.4", + "eslint": "^8.57.0", + "typescript": "~5.4.5", + "typescript-eslint": "^7.11.0" + } } diff --git a/full-stack-asset-transfer-guide/applications/trader-typescript/src/app.ts b/full-stack-asset-transfer-guide/applications/trader-typescript/src/app.ts index cd666a2849..3d25d14d08 100644 --- a/full-stack-asset-transfer-guide/applications/trader-typescript/src/app.ts +++ b/full-stack-asset-transfer-guide/applications/trader-typescript/src/app.ts @@ -12,10 +12,10 @@ async function main(): Promise { const commandName = process.argv[2]; const args = process.argv.slice(3); - const command = commands[commandName]; + const command = commandName && commands[commandName]; if (!command) { printUsage(); - throw new Error(`Unknown command: ${commandName}`); + throw new Error(`Unknown command: ${String(commandName)}`); } await runCommand(command, args); @@ -41,7 +41,7 @@ function printUsage(): void { console.log(`\t${Object.keys(commands).sort().join('\n\t')}`); } -main().catch(error => { +main().catch((error: unknown) => { if (error instanceof ExpectedError) { console.log(error); } else { diff --git a/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/create.ts b/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/create.ts index 8ecad3c97c..423a796879 100644 --- a/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/create.ts +++ b/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/create.ts @@ -7,10 +7,14 @@ import { Gateway } from '@hyperledger/fabric-gateway'; import { CHAINCODE_NAME, CHANNEL_NAME } from '../config'; import { AssetTransfer } from '../contract'; -import { assertAllDefined } from '../utils'; +import { assertDefined } from '../utils'; + +const usage = 'Arguments: '; export default async function main(gateway: Gateway, args: string[]): Promise { - const [assetId, owner, color] = assertAllDefined([args[0], args[1], args[2]], 'Arguments: '); + const assetId = assertDefined(args[0], usage); + const owner = assertDefined(args[1], usage); + const color = assertDefined(args[2], usage); const network = gateway.getNetwork(CHANNEL_NAME); const contract = network.getContract(CHAINCODE_NAME); diff --git a/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/getAllAssets.ts b/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/getAllAssets.ts index 34157a2e9c..c849363c14 100644 --- a/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/getAllAssets.ts +++ b/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/getAllAssets.ts @@ -16,5 +16,8 @@ export default async function main(gateway: Gateway): Promise { const assets = await smartContract.getAllAssets(); const assetsJson = JSON.stringify(assets, undefined, 2); - assetsJson.split('\n').forEach(line => console.log(line)); // Write line-by-line to avoid truncation + // Write line-by-line to avoid truncation + assetsJson.split('\n').forEach((line) => { + console.log(line); + }); } diff --git a/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/listen.ts b/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/listen.ts index 5c9fa152f2..488bef9947 100644 --- a/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/listen.ts +++ b/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/listen.ts @@ -19,10 +19,10 @@ export default async function main(gateway: Gateway): Promise { const network = gateway.getNetwork(CHANNEL_NAME); const checkpointer = await checkpointers.file(checkpointFile); - console.log(`Starting event listening from block ${checkpointer.getBlockNumber() ?? startBlock}`); + console.log('Starting event listening from block', checkpointer.getBlockNumber() ?? startBlock); console.log('Last processed transaction ID within block:', checkpointer.getTransactionId()); if (simulatedFailureCount > 0) { - console.log(`Simulating a write failure every ${simulatedFailureCount} transactions`); + console.log('Simulating a write failure every', simulatedFailureCount, 'transactions'); } const events = await network.getChaincodeEvents(CHAINCODE_NAME, { diff --git a/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/transfer.ts b/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/transfer.ts index 3b014c61d6..71039ee570 100644 --- a/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/transfer.ts +++ b/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/transfer.ts @@ -7,10 +7,14 @@ import { Gateway } from '@hyperledger/fabric-gateway'; import { CHAINCODE_NAME, CHANNEL_NAME } from '../config'; import { AssetTransfer } from '../contract'; -import { assertAllDefined } from '../utils'; +import { assertDefined } from '../utils'; + +const usage = 'Arguments: '; export default async function main(gateway: Gateway, args: string[]): Promise { - const [assetId, newOwner, newOwnerOrg] = assertAllDefined([args[0], args[1], args[2]], 'Arguments: '); + const assetId = assertDefined(args[0], usage); + const newOwner = assertDefined(args[1], usage); + const newOwnerOrg = assertDefined(args[2], usage); const network = gateway.getNetwork(CHANNEL_NAME); const contract = network.getContract(CHAINCODE_NAME); diff --git a/full-stack-asset-transfer-guide/applications/trader-typescript/src/utils.ts b/full-stack-asset-transfer-guide/applications/trader-typescript/src/utils.ts index 48a28890f6..b0d3b133eb 100644 --- a/full-stack-asset-transfer-guide/applications/trader-typescript/src/utils.ts +++ b/full-stack-asset-transfer-guide/applications/trader-typescript/src/utils.ts @@ -13,7 +13,8 @@ const utf8Decoder = new TextDecoder(); * @param values Candidate elements. */ export function randomElement(values: T[]): T { - return values[randomInt(values.length)]; + const result = values[randomInt(values.length)]; + return assertDefined(result, `Missing element in {String(values)}`); } /** @@ -47,7 +48,7 @@ export async function allFulfilled(promises: Promise[]): Promise if (failures.length > 0) { const failMessages = '- ' + failures.join('\n- '); - throw new Error(`${failures.length} failures:\n${failMessages}\n`); + throw new Error(`${String(failures.length)} failures:\n${failMessages}\n`); } } @@ -61,11 +62,6 @@ export function printable(event: T): PrintView { ) as PrintView; } -export function assertAllDefined(values: (T | undefined)[], message: string | (() => string)): T[] { - values.forEach(value => assertDefined(value, message)); - return values as T[]; -} - export function assertDefined(value: T | undefined, message: string | (() => string)): T { if (value == undefined) { throw new Error(typeof message === 'string' ? message : message()); diff --git a/full-stack-asset-transfer-guide/applications/trader-typescript/tsconfig.json b/full-stack-asset-transfer-guide/applications/trader-typescript/tsconfig.json index 904acb415b..4c20df24e6 100644 --- a/full-stack-asset-transfer-guide/applications/trader-typescript/tsconfig.json +++ b/full-stack-asset-transfer-guide/applications/trader-typescript/tsconfig.json @@ -1,19 +1,15 @@ { - "$schema": "https://json.schemastore.org/tsconfig", "extends": "@tsconfig/node18/tsconfig.json", "compilerOptions": { + "outDir": "dist", "declaration": true, "declarationMap": true, "sourceMap": true, - "outDir": "dist", - "rootDir": "src", "noUnusedLocals": true, - "noImplicitReturns": true + "noImplicitReturns": true, + "noUncheckedIndexedAccess": true, + "forceConsistentCasingInFileNames": true }, - "include": [ - "src/" - ], - "exclude": [ - "src/**/*.spec.ts" - ] + "include": ["./src/**/*"], + "exclude": ["./src/**/*.spec.ts"] } diff --git a/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/.eslintrc.js b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/.eslintrc.js deleted file mode 100644 index b6c93bfb70..0000000000 --- a/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/.eslintrc.js +++ /dev/null @@ -1,101 +0,0 @@ -module.exports = { - env: { - node: true, - es2021: true, - }, - extends: [ - 'eslint:recommended', - ], - root: true, - ignorePatterns: [ - 'dist/', - ], - rules: { - 'arrow-spacing': ['error'], - 'comma-style': ['error'], - complexity: ['error', 10], - 'eol-last': ['error'], - 'generator-star-spacing': ['error', 'after'], - 'key-spacing': [ - 'error', - { - beforeColon: false, - afterColon: true, - mode: 'minimum', - }, - ], - 'keyword-spacing': ['error'], - 'no-multiple-empty-lines': ['error'], - 'no-trailing-spaces': ['error'], - 'no-whitespace-before-property': ['error'], - 'object-curly-newline': ['error'], - 'padded-blocks': ['error', 'never'], - 'rest-spread-spacing': ['error'], - 'semi-style': ['error'], - 'space-before-blocks': ['error'], - 'space-in-parens': ['error'], - 'space-unary-ops': ['error'], - 'spaced-comment': ['error'], - 'template-curly-spacing': ['error'], - 'yield-star-spacing': ['error', 'after'], - }, - overrides: [ - { - files: [ - '**/*.ts', - ], - parser: '@typescript-eslint/parser', - parserOptions: { - sourceType: 'module', - ecmaFeatures: { - impliedStrict: true, - }, - project: './tsconfig.json', - tsconfigRootDir: process.env.TSCONFIG_ROOT_DIR || __dirname, - }, - plugins: [ - '@typescript-eslint', - ], - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:@typescript-eslint/recommended-requiring-type-checking', - ], - rules: { - '@typescript-eslint/comma-spacing': ['error'], - '@typescript-eslint/explicit-function-return-type': [ - 'error', - { - allowExpressions: true, - }, - ], - '@typescript-eslint/func-call-spacing': ['error'], - '@typescript-eslint/member-delimiter-style': ['error'], - '@typescript-eslint/indent': [ - 'error', - 4, - { - SwitchCase: 0, - ignoredNodes: ["PropertyDefinition"] - }, - ], - - '@typescript-eslint/prefer-nullish-coalescing': ['error'], - '@typescript-eslint/prefer-optional-chain': ['error'], - '@typescript-eslint/prefer-reduce-type-parameter': ['error'], - '@typescript-eslint/prefer-return-this-type': ['error'], - '@typescript-eslint/quotes': ['error', 'single'], - '@typescript-eslint/type-annotation-spacing': ['error'], - '@typescript-eslint/semi': ['error'], - '@typescript-eslint/space-before-function-paren': [ - 'error', - { - anonymous: 'never', - named: 'never', - asyncArrow: 'always', - }, - ], - }, - }, - ], -}; diff --git a/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/.gitignore b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/.gitignore index 90f52b2e16..9d9c13e917 100644 --- a/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/.gitignore +++ b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/.gitignore @@ -10,7 +10,6 @@ coverage node_modules/ jspm_packages/ package-lock.json -npm-shrinkwrap.json # Compiled TypeScript files dist diff --git a/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/Dockerfile b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/Dockerfile index 7b6384da43..184bc27ae1 100644 --- a/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/Dockerfile +++ b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/Dockerfile @@ -1,7 +1,7 @@ # # SPDX-License-Identifier: Apache-2.0 # -FROM node:18.16 AS builder +FROM node:18 AS builder WORKDIR /usr/src/app diff --git a/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/eslint.config.mjs b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/eslint.config.mjs new file mode 100644 index 0000000000..9ef6b24340 --- /dev/null +++ b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/eslint.config.mjs @@ -0,0 +1,13 @@ +import js from '@eslint/js'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config(js.configs.recommended, ...tseslint.configs.strictTypeChecked, { + languageOptions: { + ecmaVersion: 2023, + sourceType: 'module', + parserOptions: { + project: 'tsconfig.json', + tsconfigRootDir: import.meta.dirname, + }, + }, +}); diff --git a/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/npm-shrinkwrap.json b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/npm-shrinkwrap.json new file mode 100644 index 0000000000..acd6b6b3e4 --- /dev/null +++ b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/npm-shrinkwrap.json @@ -0,0 +1,2274 @@ +{ + "name": "asset-transfer", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "asset-transfer", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "fabric-contract-api": "~2.5", + "fabric-shim": "~2.5", + "json-stringify-deterministic": "^1.0.7", + "sort-keys-recursive": "^2.1.7" + }, + "devDependencies": { + "@eslint/js": "^9.3.0", + "@tsconfig/node18": "^18.2.4", + "@types/node": "^18.19.33", + "eslint": "^8.57.0", + "typescript": "~5.4.5", + "typescript-eslint": "^7.11.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz", + "integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.5.0.tgz", + "integrity": "sha512-A7+AOT2ICkodvtsWnxZP4Xxk3NbZ3VMHd8oihydLRGrJgqqdEz1qSeEgXYyT/Cu8h1TWWsQRejIx48mtjZ5y1w==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@fidm/asn1": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@fidm/asn1/-/asn1-1.0.4.tgz", + "integrity": "sha512-esd1jyNvRb2HVaQGq2Gg8Z0kbQPXzV9Tq5Z14KNIov6KfFD6PTaRIO8UpcsYiTNzOqJpmyzWgVTrUwFV3UF4TQ==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@fidm/x509": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@fidm/x509/-/x509-1.2.1.tgz", + "integrity": "sha512-nwc2iesjyc9hkuzcrMCBXQRn653XuAUKorfWM8PZyJawiy1QzLj4vahwzaI25+pfpwOLvMzbJ0uKpWLDNmo16w==", + "dependencies": { + "@fidm/asn1": "^1.0.4", + "tweetnacl": "^1.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.10.9", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.10.9.tgz", + "integrity": "sha512-5tcgUctCG0qoNyfChZifz2tJqbRbXVO9J7X6duFcOjY3HUNCxg5D0ZCK7EP9vIcZ0zRpLU9bWkyCqVCLZ46IbQ==", + "dependencies": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true + }, + "node_modules/@hyperledger/fabric-protos": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@hyperledger/fabric-protos/-/fabric-protos-0.2.1.tgz", + "integrity": "sha512-qjm0vIQIfCall804tWDeA8p/mUfu14sl5Sj+PbOn2yDKJq+7ThoIhNsLAqf+BCxUfqsoqQq6AojhqQeTFyOOqg==", + "dependencies": { + "@grpc/grpc-js": "^1.9.0", + "google-protobuf": "^3.21.0" + }, + "engines": { + "node": ">=14.15.0" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, + "node_modules/@tsconfig/node18": { + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node18/-/node18-18.2.4.tgz", + "integrity": "sha512-5xxU8vVs9/FNcvm3gE07fPbn9tl6tqGGWA9tSlwsUEkBxtRnTsNmwrV8gasZ9F/EobaSv9+nu8AxUKccw77JpQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "18.19.34", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.34.tgz", + "integrity": "sha512-eXF4pfBNV5DAMKGbI02NnDtWrQ40hAN558/2vvS4gMpMIxaf6JmD7YjnZbq0Q9TDSSkKBamime8ewRoomHdt4g==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/class-transformer": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.4.0.tgz", + "integrity": "sha512-ETWD/H2TbWbKEi7m9N4Km5+cw1hNcqJSxlSYhsLsNjQzWWiZIYA1zafxpK9PwVfaZ6AqR5rrjPVUBGESm5tQUA==" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fabric-contract-api": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/fabric-contract-api/-/fabric-contract-api-2.5.6.tgz", + "integrity": "sha512-AosGb8tA+Jgt+pqMEgYNB3/J/P5QuWOC7yhXbhDmAAwUzn4Sc7pdWDICH1YyrFGZNFxMGQmqJmLVWUX8BKHy0w==", + "dependencies": { + "class-transformer": "^0.4.0", + "fabric-shim-api": "2.5.6", + "fast-safe-stringify": "^2.1.1", + "get-params": "^0.1.2", + "reflect-metadata": "^0.1.13", + "winston": "^3.7.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/fabric-shim": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/fabric-shim/-/fabric-shim-2.5.6.tgz", + "integrity": "sha512-4Y8WNFhYuQ9QYSEgPXWdlXnrXjwOlM10sQQzE4kJ7cDh8a4LX0rn44FxtxTCB18lnzrSLMZ8/8Cr5m0c9NeXWA==", + "dependencies": { + "@fidm/x509": "^1.2.1", + "@grpc/grpc-js": "~1.10.9", + "@hyperledger/fabric-protos": "~0.2.1", + "@types/node": "^16.11.1", + "ajv": "^6.12.2", + "fabric-contract-api": "2.5.6", + "fabric-shim-api": "2.5.6", + "fast-safe-stringify": "^2.1.1", + "long": "^5.2.3", + "reflect-metadata": "^0.1.13", + "winston": "^3.7.2", + "yargs": "^17.4.0", + "yargs-parser": "^21.0.1" + }, + "bin": { + "fabric-chaincode-node": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/fabric-shim-api": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/fabric-shim-api/-/fabric-shim-api-2.5.6.tgz", + "integrity": "sha512-1L0nO7CJ31/gEOWKWHEeCqgB5HkqPVfRbpcS7L9eTscT7tffjg2OkZISvC+a7RiqihL0iyrXNBgBg5MwlSSN9g==", + "engines": { + "eslint": "^6.6.0", + "node": ">=18" + } + }, + "node_modules/fabric-shim/node_modules/@types/node": { + "version": "16.18.98", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.98.tgz", + "integrity": "sha512-fpiC20NvLpTLAzo3oVBKIqBGR6Fx/8oAK/SSf7G+fydnXMY1x4x9RZ6sBXhqKlCU21g2QapUsbLlhv3+a7wS+Q==" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-params": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/get-params/-/get-params-0.1.2.tgz", + "integrity": "sha512-41eOxtlGgHQRbFyA8KTH+w+32Em3cRdfBud7j67ulzmIfmaHX9doq47s0fa4P5o9H64BZX9nrYI6sJvk46Op+Q==" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/google-protobuf": { + "version": "3.21.2", + "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.2.tgz", + "integrity": "sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA==" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json-stringify-deterministic": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/json-stringify-deterministic/-/json-stringify-deterministic-1.0.12.tgz", + "integrity": "sha512-q3PN0lbUdv0pmurkBNdJH3pfFvOTL/Zp0lquqpvcjfKzt6Y0j49EPHAmVHCAS4Ceq/Y+PejWTzyiVpoY71+D6g==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/logform": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.0.tgz", + "integrity": "sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/protobufjs": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.2.tgz", + "integrity": "sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/reflect-metadata": { + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", + "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sort-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-4.2.0.tgz", + "integrity": "sha512-aUYIEU/UviqPgc8mHR6IW1EGxkAXpeRETYcrzg8cLAvUPZcpAlleSXHV2mY7G12GphSH6Gzv+4MMVSSkbdteHg==", + "dependencies": { + "is-plain-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sort-keys-recursive": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/sort-keys-recursive/-/sort-keys-recursive-2.1.10.tgz", + "integrity": "sha512-yRLJbEER/PjU7hSRwXvP+NyXiORufu8rbSbp+3wFRuJZXoi/AhuKczbjuipqn7Le0SsTXK4VUeri2+Ni6WS8Hg==", + "dependencies": { + "kind-of": "~6.0.2", + "sort-keys": "~4.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "engines": { + "node": "*" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-7.13.0.tgz", + "integrity": "sha512-upO0AXxyBwJ4BbiC6CRgAJKtGYha2zw4m1g7TIVPSonwYEuf7vCicw3syjS1OxdDMTz96sZIXl3Jx3vWJLLKFw==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "7.13.0", + "@typescript-eslint/parser": "7.13.0", + "@typescript-eslint/utils": "7.13.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.13.0.tgz", + "integrity": "sha512-FX1X6AF0w8MdVFLSdqwqN/me2hyhuQg4ykN6ZpVhh1ij/80pTvDKclX1sZB9iqex8SjQfVhwMKs3JtnnMLzG9w==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.13.0", + "@typescript-eslint/type-utils": "7.13.0", + "@typescript-eslint/utils": "7.13.0", + "@typescript-eslint/visitor-keys": "7.13.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/parser": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.13.0.tgz", + "integrity": "sha512-EjMfl69KOS9awXXe83iRN7oIEXy9yYdqWfqdrFAYAAr6syP8eLEFI7ZE4939antx2mNgPRW/o1ybm2SFYkbTVA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.13.0", + "@typescript-eslint/types": "7.13.0", + "@typescript-eslint/typescript-estree": "7.13.0", + "@typescript-eslint/visitor-keys": "7.13.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/scope-manager": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.13.0.tgz", + "integrity": "sha512-ZrMCe1R6a01T94ilV13egvcnvVJ1pxShkE0+NDjDzH4nvG1wXpwsVI5bZCvE7AEDH1mXEx5tJSVR68bLgG7Dng==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.13.0", + "@typescript-eslint/visitor-keys": "7.13.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/type-utils": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.13.0.tgz", + "integrity": "sha512-xMEtMzxq9eRkZy48XuxlBFzpVMDurUAfDu5Rz16GouAtXm0TaAoTFzqWUFPPuQYXI/CDaH/Bgx/fk/84t/Bc9A==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "7.13.0", + "@typescript-eslint/utils": "7.13.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/types": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.13.0.tgz", + "integrity": "sha512-QWuwm9wcGMAuTsxP+qz6LBBd3Uq8I5Nv8xb0mk54jmNoCyDspnMvVsOxI6IsMmway5d1S9Su2+sCKv1st2l6eA==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.13.0.tgz", + "integrity": "sha512-cAvBvUoobaoIcoqox1YatXOnSl3gx92rCZoMRPzMNisDiM12siGilSM4+dJAekuuHTibI2hVC2fYK79iSFvWjw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.13.0", + "@typescript-eslint/visitor-keys": "7.13.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/utils": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.13.0.tgz", + "integrity": "sha512-jceD8RgdKORVnB4Y6BqasfIkFhl4pajB1wVxrF4akxD2QPM8GNYjgGwEzYS+437ewlqqrg7Dw+6dhdpjMpeBFQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.13.0", + "@typescript-eslint/types": "7.13.0", + "@typescript-eslint/typescript-estree": "7.13.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/visitor-keys": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.13.0.tgz", + "integrity": "sha512-nxn+dozQx+MK61nn/JP+M4eCkHDSxSLDpgE3WcQo0+fkjEolnaB5jswvIKC4K56By8MMgIho7f1PVxERHEo8rw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.13.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/typescript-eslint/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typescript-eslint/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/winston": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.13.0.tgz", + "integrity": "sha512-rwidmA1w3SE4j0E5MuIufFhyJPBDG7Nu71RkZor1p2+qHvJSZ9GYDA81AyleQcZbh/+V6HjeBdfnTZJm9rSeQQ==", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.4.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.7.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.7.0.tgz", + "integrity": "sha512-ajBj65K5I7denzer2IYW6+2bNIVqLGDHqDw3Ow8Ohh+vdW+rv4MZ6eiDvHoKhfJFZ2auyN8byXieDDJ96ViONg==", + "dependencies": { + "logform": "^2.3.2", + "readable-stream": "^3.6.0", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/package.json b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/package.json index 4b1012f34d..3565c3601d 100644 --- a/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/package.json +++ b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/package.json @@ -5,10 +5,10 @@ "main": "dist/index.js", "typings": "dist/index.d.ts", "engines": { - "node": ">=18.12.0" + "node": ">=18" }, "scripts": { - "lint": "eslint ./src --ext .ts", + "lint": "eslint src", "pretest": "npm run lint", "test": "", "start": "set -x && fabric-chaincode-node start", @@ -32,12 +32,12 @@ "sort-keys-recursive": "^2.1.7" }, "devDependencies": { - "@tsconfig/node18": "^2.0.0", - "@types/node": "^18.16.1", - "@typescript-eslint/eslint-plugin": "^5.30.7", - "@typescript-eslint/parser": "^5.30.7", - "eslint": "^8.20.0", - "typescript": "~5.0.4" + "@types/node": "^18.19.33", + "@eslint/js": "^9.3.0", + "@tsconfig/node18": "^18.2.4", + "eslint": "^8.57.0", + "typescript": "~5.4.5", + "typescript-eslint": "^7.11.0" }, "nyc": { "extension": [ diff --git a/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/src/asset.ts b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/src/asset.ts index c3a4db3a98..53538a432d 100644 --- a/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/src/asset.ts +++ b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/src/asset.ts @@ -21,10 +21,6 @@ export class Asset { @Property('Size', 'number') Size = 0; - constructor() { - // Nothing to do - } - static newInstance(state: Partial = {}): Asset { return { ID: assertHasValue(state.ID, 'Missing ID'), diff --git a/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/src/assetTransfer.ts b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/src/assetTransfer.ts index 0fd27cb7f7..b3bd1b2e68 100644 --- a/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/src/assetTransfer.ts +++ b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/src/assetTransfer.ts @@ -50,7 +50,7 @@ export class AssetTransferContract extends Contract { async #readAsset(ctx: Context, id: string): Promise { const assetBytes = await ctx.stub.getState(id); // get the asset from chaincode state - if (!assetBytes || assetBytes.length === 0) { + if (assetBytes.length === 0) { throw new Error(`Sorry, asset ${id} has not been created`); } @@ -64,7 +64,7 @@ export class AssetTransferContract extends Contract { @Transaction() @Param('assetObj', 'Asset', 'Part formed JSON of Asset') async UpdateAsset(ctx: Context, assetUpdate: Asset): Promise { - if (assetUpdate.ID === undefined) { + if (!assetUpdate.ID) { throw new Error('No asset ID specified'); } @@ -113,7 +113,7 @@ export class AssetTransferContract extends Contract { @Returns('boolean') async AssetExists(ctx: Context, id: string): Promise { const assetJson = await ctx.stub.getState(id); - return assetJson?.length > 0; + return assetJson.length > 0; } /** diff --git a/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/tsconfig.json b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/tsconfig.json index 418fa200cf..031d7de798 100644 --- a/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/tsconfig.json +++ b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/tsconfig.json @@ -4,16 +4,14 @@ "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true, - "outDir": "dist", "declaration": true, + "declarationMap": true, "sourceMap": true, + "outDir": "dist", + "strict": true, "noUnusedLocals": true, - "noImplicitReturns": true + "noImplicitReturns": true, + "forceConsistentCasingInFileNames": true }, - "include": [ - "./src/**/*" - ], - "exclude": [ - "./src/**/*.spec.ts" - ] + "include": ["src/"] } diff --git a/hardware-security-module/application-typescript/.eslintrc.yaml b/hardware-security-module/application-typescript/.eslintrc.yaml deleted file mode 100644 index 7961410540..0000000000 --- a/hardware-security-module/application-typescript/.eslintrc.yaml +++ /dev/null @@ -1,29 +0,0 @@ -env: - node: true - es2020: true -root: true -ignorePatterns: - - dist/ -extends: - - eslint:recommended -rules: - indent: - - error - - 4 - quotes: - - error - - single -overrides: - - files: - - "**/*.ts" - parser: "@typescript-eslint/parser" - parserOptions: - sourceType: module - ecmaFeatures: - impliedStrict: true - plugins: - - "@typescript-eslint" - extends: - - eslint:recommended - - plugin:@typescript-eslint/eslint-recommended - - plugin:@typescript-eslint/recommended diff --git a/hardware-security-module/application-typescript/eslint.config.mjs b/hardware-security-module/application-typescript/eslint.config.mjs new file mode 100644 index 0000000000..9ef6b24340 --- /dev/null +++ b/hardware-security-module/application-typescript/eslint.config.mjs @@ -0,0 +1,13 @@ +import js from '@eslint/js'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config(js.configs.recommended, ...tseslint.configs.strictTypeChecked, { + languageOptions: { + ecmaVersion: 2023, + sourceType: 'module', + parserOptions: { + project: 'tsconfig.json', + tsconfigRootDir: import.meta.dirname, + }, + }, +}); diff --git a/hardware-security-module/application-typescript/package.json b/hardware-security-module/application-typescript/package.json index e57d185303..8cd732cb41 100644 --- a/hardware-security-module/application-typescript/package.json +++ b/hardware-security-module/application-typescript/package.json @@ -10,24 +10,24 @@ "build": "tsc", "prepare": "npm run build", "clean": "rimraf dist", - "lint": "eslint . --ext .ts", + "lint": "eslint src", "start": "SOFTHSM2_CONF=${HOME}/softhsm2.conf node dist/hsm-sample.js", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "Apache-2.0", "dependencies": { - "@grpc/grpc-js": "^1.9.7", - "@hyperledger/fabric-gateway": "~1.4.0" + "@grpc/grpc-js": "^1.10", + "@hyperledger/fabric-gateway": "^1.5" }, "devDependencies": { - "@tsconfig/node18": "^18.2.2", - "@types/node": "^18.18.6", - "@typescript-eslint/eslint-plugin": "^6.9.0", - "@typescript-eslint/parser": "^6.9.0", - "eslint": "^8.52.0", + "@types/node": "^18.19.33", + "@eslint/js": "^9.3.0", + "@tsconfig/node18": "^18.2.4", + "eslint": "^8.57.0", "npm-run-all": "^4.1.5", "rimraf": "^5.0.1", - "typescript": "~5.2.2" + "typescript": "~5.4.5", + "typescript-eslint": "^7.11.0" } } diff --git a/hardware-security-module/application-typescript/src/hsm-sample.ts b/hardware-security-module/application-typescript/src/hsm-sample.ts index b99d970789..0bfde855d4 100644 --- a/hardware-security-module/application-typescript/src/hsm-sample.ts +++ b/hardware-security-module/application-typescript/src/hsm-sample.ts @@ -13,7 +13,7 @@ import { TextDecoder } from 'util'; const mspId = 'Org1MSP'; const user = 'HSMUser'; -const assetId = `asset${Date.now()}`; +const assetId = `asset${String(Date.now())}`; const utf8Decoder = new TextDecoder(); // Sample uses fabric-ca-client generated HSM identities, certificate is located in the signcerts directory @@ -85,7 +85,7 @@ async function exampleTransaction(gateway: Gateway):Promise { const resultBytes = await contract.evaluateTransaction('ReadAsset', assetId); const resultJson = utf8Decoder.decode(resultBytes); - const result = JSON.parse(resultJson); + const result: unknown = JSON.parse(resultJson); console.log('*** Result:', result); } @@ -167,7 +167,7 @@ function envOrDefault(key: string, defaultValue: string): string { return process.env[key] || defaultValue; } -main().catch(error => { +main().catch((error: unknown) => { console.error('******** FAILED to run the application:', error); process.exitCode = 1; }); diff --git a/hardware-security-module/application-typescript/tsconfig.json b/hardware-security-module/application-typescript/tsconfig.json index 6773460426..4c20df24e6 100644 --- a/hardware-security-module/application-typescript/tsconfig.json +++ b/hardware-security-module/application-typescript/tsconfig.json @@ -1,18 +1,15 @@ { - "$schema": "https://json.schemastore.org/tsconfig", "extends": "@tsconfig/node18/tsconfig.json", "compilerOptions": { + "outDir": "dist", "declaration": true, "declarationMap": true, "sourceMap": true, - "outDir": "dist", - "rootDir": "src", - "strict": true, "noUnusedLocals": true, "noImplicitReturns": true, + "noUncheckedIndexedAccess": true, "forceConsistentCasingInFileNames": true }, - "include": [ - "src/" - ] + "include": ["./src/**/*"], + "exclude": ["./src/**/*.spec.ts"] } diff --git a/off_chain_data/application-typescript/.eslintrc.js b/off_chain_data/application-typescript/.eslintrc.js deleted file mode 100644 index 351c2d0c5a..0000000000 --- a/off_chain_data/application-typescript/.eslintrc.js +++ /dev/null @@ -1,99 +0,0 @@ -module.exports = { - env: { - node: true, - es2020: true, - }, - extends: [ - 'eslint:recommended', - ], - root: true, - ignorePatterns: [ - 'dist/', - ], - rules: { - 'arrow-spacing': ['error'], - 'comma-style': ['error'], - complexity: ['error', 10], - 'eol-last': ['error'], - 'generator-star-spacing': ['error', 'after'], - 'key-spacing': [ - 'error', - { - beforeColon: false, - afterColon: true, - mode: 'minimum', - }, - ], - 'keyword-spacing': ['error'], - 'no-multiple-empty-lines': ['error'], - 'no-trailing-spaces': ['error'], - 'no-whitespace-before-property': ['error'], - 'object-curly-newline': ['error'], - 'padded-blocks': ['error', 'never'], - 'rest-spread-spacing': ['error'], - 'semi-style': ['error'], - 'space-before-blocks': ['error'], - 'space-in-parens': ['error'], - 'space-unary-ops': ['error'], - 'spaced-comment': ['error'], - 'template-curly-spacing': ['error'], - 'yield-star-spacing': ['error', 'after'], - }, - overrides: [ - { - files: [ - '**/*.ts', - ], - parser: '@typescript-eslint/parser', - parserOptions: { - sourceType: 'module', - ecmaFeatures: { - impliedStrict: true, - }, - project: './tsconfig.json', - tsconfigRootDir: process.env.TSCONFIG_ROOT_DIR || __dirname, - }, - plugins: [ - '@typescript-eslint', - ], - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:@typescript-eslint/recommended-requiring-type-checking', - ], - rules: { - '@typescript-eslint/comma-spacing': ['error'], - '@typescript-eslint/explicit-function-return-type': [ - 'error', - { - allowExpressions: true, - }, - ], - '@typescript-eslint/func-call-spacing': ['error'], - '@typescript-eslint/member-delimiter-style': ['error'], - '@typescript-eslint/indent': [ - 'error', - 4, - { - SwitchCase: 0, - }, - ], - '@typescript-eslint/prefer-nullish-coalescing': ['error'], - '@typescript-eslint/prefer-optional-chain': ['error'], - '@typescript-eslint/prefer-reduce-type-parameter': ['error'], - '@typescript-eslint/prefer-return-this-type': ['error'], - '@typescript-eslint/quotes': ['error', 'single'], - '@typescript-eslint/type-annotation-spacing': ['error'], - '@typescript-eslint/semi': ['error'], - '@typescript-eslint/space-before-function-paren': [ - 'error', - { - anonymous: 'never', - named: 'never', - asyncArrow: 'always', - }, - ], - }, - }, - ], -}; diff --git a/off_chain_data/application-typescript/eslint.config.mjs b/off_chain_data/application-typescript/eslint.config.mjs new file mode 100644 index 0000000000..9ef6b24340 --- /dev/null +++ b/off_chain_data/application-typescript/eslint.config.mjs @@ -0,0 +1,13 @@ +import js from '@eslint/js'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config(js.configs.recommended, ...tseslint.configs.strictTypeChecked, { + languageOptions: { + ecmaVersion: 2023, + sourceType: 'module', + parserOptions: { + project: 'tsconfig.json', + tsconfigRootDir: import.meta.dirname, + }, + }, +}); diff --git a/off_chain_data/application-typescript/package.json b/off_chain_data/application-typescript/package.json index 5aefc1f17f..0d48f88ef9 100644 --- a/off_chain_data/application-typescript/package.json +++ b/off_chain_data/application-typescript/package.json @@ -10,7 +10,7 @@ "scripts": { "build": "tsc", "build:watch": "tsc -w", - "lint": "eslint ./src --ext .ts", + "lint": "eslint src", "prepare": "npm run build", "pretest": "npm run lint", "start": "node ./dist/app" @@ -18,16 +18,16 @@ "author": "Hyperledger", "license": "Apache-2.0", "dependencies": { - "@grpc/grpc-js": "^1.9.7", - "@hyperledger/fabric-gateway": "~1.4.0", + "@grpc/grpc-js": "^1.10", + "@hyperledger/fabric-gateway": "^1.5", "@hyperledger/fabric-protos": "^0.2.1" }, "devDependencies": { - "@tsconfig/node18": "^18.2.2", - "@types/node": "^18.18.6", - "@typescript-eslint/eslint-plugin": "^6.9.0", - "@typescript-eslint/parser": "^6.9.0", - "eslint": "^8.52.0", - "typescript": "~5.2.2" - } + "@types/node": "^18.19.33", + "@eslint/js": "^9.3.0", + "@tsconfig/node18": "^18.2.4", + "eslint": "^8.57.0", + "typescript": "~5.4.5", + "typescript-eslint": "^7.11.0" + } } diff --git a/off_chain_data/application-typescript/src/app.ts b/off_chain_data/application-typescript/src/app.ts index eb9b3dc265..17f123185e 100644 --- a/off_chain_data/application-typescript/src/app.ts +++ b/off_chain_data/application-typescript/src/app.ts @@ -47,7 +47,7 @@ function printUsage(): void { console.log('Available commands:', Object.keys(allCommands).join(', ')); } -main().catch(error => { +main().catch((error: unknown) => { if (error instanceof ExpectedError) { console.log(error); } else { diff --git a/off_chain_data/application-typescript/src/blockParser.ts b/off_chain_data/application-typescript/src/blockParser.ts index 8bde481c46..547d032317 100644 --- a/off_chain_data/application-typescript/src/blockParser.ts +++ b/off_chain_data/application-typescript/src/blockParser.ts @@ -35,10 +35,18 @@ export function parseBlock(block: common.Block): Block { return { getNumber: () => BigInt(header.getNumber()), - getTransactions: cache( - () => getPayloads(block) - .map((payload, i) => parsePayload(payload, validationCodes[i])) - .filter(payload => payload.isEndorserTransaction()) + getTransactions: cache(() => + getPayloads(block) + .map((payload, i) => + parsePayload( + payload, + assertDefined( + validationCodes[i], + `Missing validation code index {String(i)}` + ) + ) + ) + .filter((payload) => payload.isEndorserTransaction()) .map(newTransaction) ), toProto: () => block, @@ -67,15 +75,23 @@ interface ReadWriteSet { function parsePayload(payload: common.Payload, statusCode: number): Payload { const cachedChannelHeader = cache(() => getChannelHeader(payload)); - const isEndorserTransaction = (): boolean => cachedChannelHeader().getType() === common.HeaderType.ENDORSER_TRANSACTION; + const isEndorserTransaction = (): boolean => + cachedChannelHeader().getType() === + common.HeaderType.ENDORSER_TRANSACTION; return { getChannelHeader: cachedChannelHeader, getEndorserTransaction: () => { if (!isEndorserTransaction()) { - throw new Error(`Unexpected payload type: ${cachedChannelHeader().getType()}`); + throw new Error( + `Unexpected payload type: ${String( + cachedChannelHeader().getType() + )}` + ); } - const transaction = peer.Transaction.deserializeBinary(payload.getData_asU8()); + const transaction = peer.Transaction.deserializeBinary( + payload.getData_asU8() + ); return parseEndorserTransaction(transaction); }, getSignatureHeader: cache(() => getSignatureHeader(payload)), @@ -86,16 +102,33 @@ function parsePayload(payload: common.Payload, statusCode: number): Payload { }; } -function parseEndorserTransaction(transaction: peer.Transaction): EndorserTransaction { +function parseEndorserTransaction( + transaction: peer.Transaction +): EndorserTransaction { return { - getReadWriteSets: cache( - () => getChaincodeActionPayloads(transaction) - .map(payload => assertDefined(payload.getAction(), 'Missing chaincode endorsed action')) - .map(endorsedAction => endorsedAction.getProposalResponsePayload_asU8()) - .map(bytes => peer.ProposalResponsePayload.deserializeBinary(bytes)) - .map(responsePayload => peer.ChaincodeAction.deserializeBinary(responsePayload.getExtension_asU8())) - .map(chaincodeAction => chaincodeAction.getResults_asU8()) - .map(bytes => ledger.rwset.TxReadWriteSet.deserializeBinary(bytes)) + getReadWriteSets: cache(() => + getChaincodeActionPayloads(transaction) + .map((payload) => + assertDefined( + payload.getAction(), + 'Missing chaincode endorsed action' + ) + ) + .map((endorsedAction) => + endorsedAction.getProposalResponsePayload_asU8() + ) + .map((bytes) => + peer.ProposalResponsePayload.deserializeBinary(bytes) + ) + .map((responsePayload) => + peer.ChaincodeAction.deserializeBinary( + responsePayload.getExtension_asU8() + ) + ) + .map((chaincodeAction) => chaincodeAction.getResults_asU8()) + .map((bytes) => + ledger.rwset.TxReadWriteSet.deserializeBinary(bytes) + ) .map(parseReadWriteSet) ), toProto: () => transaction, @@ -109,67 +142,88 @@ function newTransaction(payload: Payload): Transaction { getChannelHeader: () => payload.getChannelHeader(), getCreator: () => { const creatorBytes = payload.getSignatureHeader().getCreator_asU8(); - const creator = msp.SerializedIdentity.deserializeBinary(creatorBytes); + const creator = + msp.SerializedIdentity.deserializeBinary(creatorBytes); return { mspId: creator.getMspid(), credentials: creator.getIdBytes_asU8(), }; }, - getNamespaceReadWriteSets: () => transaction.getReadWriteSets() - .flatMap(readWriteSet => readWriteSet.getNamespaceReadWriteSets()), + getNamespaceReadWriteSets: () => + transaction + .getReadWriteSets() + .flatMap((readWriteSet) => + readWriteSet.getNamespaceReadWriteSets() + ), getValidationCode: () => payload.getTransactionValidationCode(), isValid: () => payload.isValid(), toProto: () => payload.toProto(), }; } -function parseReadWriteSet(readWriteSet: ledger.rwset.TxReadWriteSet): ReadWriteSet { +function parseReadWriteSet( + readWriteSet: ledger.rwset.TxReadWriteSet +): ReadWriteSet { return { - getNamespaceReadWriteSets: () => { - if (readWriteSet.getDataModel() !== ledger.rwset.TxReadWriteSet.DataModel.KV) { - throw new Error(`Unexpected read/write set data model: ${readWriteSet.getDataModel()}`); - } - - return readWriteSet.getNsRwsetList().map(parseNamespaceReadWriteSet); - }, + getNamespaceReadWriteSets: () => + readWriteSet.getNsRwsetList().map(parseNamespaceReadWriteSet), toProto: () => readWriteSet, }; } -function parseNamespaceReadWriteSet(nsReadWriteSet: ledger.rwset.NsReadWriteSet): NamespaceReadWriteSet { +function parseNamespaceReadWriteSet( + nsReadWriteSet: ledger.rwset.NsReadWriteSet +): NamespaceReadWriteSet { return { getNamespace: () => nsReadWriteSet.getNamespace(), - getReadWriteSet: cache( - () => ledger.rwset.kvrwset.KVRWSet.deserializeBinary(nsReadWriteSet.getRwset_asU8()) + getReadWriteSet: cache(() => + ledger.rwset.kvrwset.KVRWSet.deserializeBinary( + nsReadWriteSet.getRwset_asU8() + ) ), toProto: () => nsReadWriteSet, }; } function getTransactionValidationCodes(block: common.Block): Uint8Array { - const metadata = assertDefined(block.getMetadata(), 'Missing block metadata'); - return metadata.getMetadataList_asU8()[common.BlockMetadataIndex.TRANSACTIONS_FILTER]; + const metadata = assertDefined( + block.getMetadata(), + 'Missing block metadata' + ); + return assertDefined( + metadata.getMetadataList_asU8()[ + common.BlockMetadataIndex.TRANSACTIONS_FILTER + ], + 'Missing transaction validation code' + ); } function getPayloads(block: common.Block): common.Payload[] { return (block.getData()?.getDataList_asU8() ?? []) - .map(bytes => common.Envelope.deserializeBinary(bytes)) - .map(envelope => envelope.getPayload_asU8()) - .map(bytes => common.Payload.deserializeBinary(bytes)); + .map((bytes) => common.Envelope.deserializeBinary(bytes)) + .map((envelope) => envelope.getPayload_asU8()) + .map((bytes) => common.Payload.deserializeBinary(bytes)); } function getChannelHeader(payload: common.Payload): common.ChannelHeader { const header = assertDefined(payload.getHeader(), 'Missing payload header'); - return common.ChannelHeader.deserializeBinary(header.getChannelHeader_asU8()); + return common.ChannelHeader.deserializeBinary( + header.getChannelHeader_asU8() + ); } function getSignatureHeader(payload: common.Payload): common.SignatureHeader { const header = assertDefined(payload.getHeader(), 'Missing payload header'); - return common.SignatureHeader.deserializeBinary(header.getSignatureHeader_asU8()); -} - -function getChaincodeActionPayloads(transaction: peer.Transaction): peer.ChaincodeActionPayload[] { - return transaction.getActionsList() - .map(transactionAction => transactionAction.getPayload_asU8()) - .map(bytes => peer.ChaincodeActionPayload.deserializeBinary(bytes)); + return common.SignatureHeader.deserializeBinary( + header.getSignatureHeader_asU8() + ); +} + +function getChaincodeActionPayloads( + transaction: peer.Transaction +): peer.ChaincodeActionPayload[] { + return transaction + .getActionsList() + .map((transactionAction) => transactionAction.getPayload_asU8()) + .map((bytes) => peer.ChaincodeActionPayload.deserializeBinary(bytes)); } diff --git a/off_chain_data/application-typescript/src/connect.ts b/off_chain_data/application-typescript/src/connect.ts index b64fb04908..62fb3f0113 100644 --- a/off_chain_data/application-typescript/src/connect.ts +++ b/off_chain_data/application-typescript/src/connect.ts @@ -70,10 +70,11 @@ async function newIdentity(): Promise { async function newSigner(): Promise { const keyFiles = await fs.readdir(keyDirectoryPath); - if (keyFiles.length === 0) { + const keyFile = keyFiles[0]; + if (!keyFile) { throw new Error(`No private key files found in directory ${keyDirectoryPath}`); } - const keyPath = path.resolve(keyDirectoryPath, keyFiles[0]); + const keyPath = path.resolve(keyDirectoryPath, keyFile); const privateKeyPem = await fs.readFile(keyPath); const privateKey = crypto.createPrivateKey(privateKeyPem); return signers.newPrivateKeySigner(privateKey); diff --git a/off_chain_data/application-typescript/src/getAllAssets.ts b/off_chain_data/application-typescript/src/getAllAssets.ts index 78f2d917ec..94461a90d1 100644 --- a/off_chain_data/application-typescript/src/getAllAssets.ts +++ b/off_chain_data/application-typescript/src/getAllAssets.ts @@ -20,7 +20,10 @@ export async function main(client: Client): Promise { const smartContract = new AssetTransferBasic(contract); const assets = await smartContract.getAllAssets(); const assetsJson = JSON.stringify(assets, undefined, 2); - assetsJson.split('\n').forEach(line => console.log(line)); // Write line-by-line to avoid truncation + // Write line-by-line to avoid truncation + assetsJson.split('\n').forEach((line) => { + console.log(line); + }); } finally { gateway.close(); } diff --git a/off_chain_data/application-typescript/src/listen.ts b/off_chain_data/application-typescript/src/listen.ts index 79e49f658a..95c542e1be 100644 --- a/off_chain_data/application-typescript/src/listen.ts +++ b/off_chain_data/application-typescript/src/listen.ts @@ -87,10 +87,10 @@ export async function main(client: Client): Promise { const network = gateway.getNetwork(channelName); const checkpointer = await checkpointers.file(checkpointFile); - console.log(`Starting event listening from block ${checkpointer.getBlockNumber() ?? startBlock}`); + console.log('Starting event listening from block', checkpointer.getBlockNumber() ?? startBlock); console.log('Last processed transaction ID within block:', checkpointer.getTransactionId()); if (simulatedFailureCount > 0) { - console.log(`Simulating a write failure every ${simulatedFailureCount} transactions`); + console.log('Simulating a write failure every', simulatedFailureCount, 'transactions'); } const blocks = await network.getBlockEvents({ @@ -135,7 +135,7 @@ class BlockProcessor { async process(): Promise { const blockNumber = this.#block.getNumber(); - console.log(`\nReceived block ${blockNumber}`); + console.log(`\nReceived block ${String(blockNumber)}`); const validTransactions = this.#getNewTransactions() .filter(transaction => transaction.isValid()); @@ -168,7 +168,7 @@ class BlockProcessor { const blockTransactionIds = transactions.map(transaction => transaction.getChannelHeader().getTxId()); const lastProcessedIndex = blockTransactionIds.indexOf(lastTransactionId); if (lastProcessedIndex < 0) { - throw new Error(`Checkpoint transaction ID ${lastTransactionId} not found in block ${this.#block.getNumber()} containing transactions: ${blockTransactionIds.join(', ')}`); + throw new Error(`Checkpoint transaction ID ${lastTransactionId} not found in block ${String(this.#block.getNumber())} containing transactions: ${blockTransactionIds.join(', ')}`); } return transactions.slice(lastProcessedIndex + 1); diff --git a/off_chain_data/application-typescript/src/utils.ts b/off_chain_data/application-typescript/src/utils.ts index b58ca371eb..e798a20123 100644 --- a/off_chain_data/application-typescript/src/utils.ts +++ b/off_chain_data/application-typescript/src/utils.ts @@ -9,7 +9,8 @@ * @param values Candidate elements. */ export function randomElement(values: T[]): T { - return values[randomInt(values.length)]; + const result = values[randomInt(values.length)]; + return assertDefined(result, `Missing element in {String(values)}`); } /** @@ -42,7 +43,7 @@ export async function allFulfilled(promises: Promise[]): Promise if (failures.length > 0) { const failMessages = ' - ' + failures.join('\n - '); - throw new Error(`${failures.length} failures:\n${failMessages}\n`); + throw new Error(`${String(failures.length)} failures:\n${failMessages}\n`); } } diff --git a/off_chain_data/application-typescript/tsconfig.json b/off_chain_data/application-typescript/tsconfig.json index 904acb415b..4c20df24e6 100644 --- a/off_chain_data/application-typescript/tsconfig.json +++ b/off_chain_data/application-typescript/tsconfig.json @@ -1,19 +1,15 @@ { - "$schema": "https://json.schemastore.org/tsconfig", "extends": "@tsconfig/node18/tsconfig.json", "compilerOptions": { + "outDir": "dist", "declaration": true, "declarationMap": true, "sourceMap": true, - "outDir": "dist", - "rootDir": "src", "noUnusedLocals": true, - "noImplicitReturns": true + "noImplicitReturns": true, + "noUncheckedIndexedAccess": true, + "forceConsistentCasingInFileNames": true }, - "include": [ - "src/" - ], - "exclude": [ - "src/**/*.spec.ts" - ] + "include": ["./src/**/*"], + "exclude": ["./src/**/*.spec.ts"] }