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 <Mark.S.Lewis@outlook.com>
This commit is contained in:
Mark S. Lewis 2024-06-15 14:33:30 +01:00 committed by Dave Enyeart
parent a4f0a2c5b2
commit c077dae79c
87 changed files with 6194 additions and 4254 deletions

17
.editorconfig Normal file
View file

@ -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

View file

@ -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"
]
}
]
}

View file

@ -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,
},
},
});

View file

@ -10,7 +10,7 @@
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"build:watch": "tsc -w", "build:watch": "tsc -w",
"lint": "eslint . --ext .ts", "lint": "eslint src",
"prepare": "npm run build", "prepare": "npm run build",
"pretest": "npm run lint", "pretest": "npm run lint",
"start": "node dist/app.js" "start": "node dist/app.js"
@ -19,15 +19,15 @@
"author": "Hyperledger", "author": "Hyperledger",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@grpc/grpc-js": "^1.9.7", "@grpc/grpc-js": "^1.10",
"@hyperledger/fabric-gateway": "~1.4.0" "@hyperledger/fabric-gateway": "^1.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.3.0",
"@tsconfig/node18": "^18.2.2", "@tsconfig/node18": "^18.2.2",
"@types/node": "^18.18.6", "@types/node": "^18.18.6",
"@typescript-eslint/eslint-plugin": "^6.9.0", "eslint": "^8.57.0",
"@typescript-eslint/parser": "^6.9.0", "typescript": "~5.4",
"eslint": "^8.52.0", "typescript-eslint": "^7.13.0"
"typescript": "~5.2.2"
} }
} }

View file

@ -34,11 +34,10 @@ const peerEndpoint = envOrDefault('PEER_ENDPOINT', 'localhost:7051');
const peerHostAlias = envOrDefault('PEER_HOST_ALIAS', 'peer0.org1.example.com'); const peerHostAlias = envOrDefault('PEER_HOST_ALIAS', 'peer0.org1.example.com');
const utf8Decoder = new TextDecoder(); const utf8Decoder = new TextDecoder();
const assetId = `asset${Date.now()}`; const assetId = `asset${String(Date.now())}`;
async function main(): Promise<void> { async function main(): Promise<void> {
displayInputParameters();
await displayInputParameters();
// The gRPC client connection should be shared by all Gateway connections to this endpoint. // The gRPC client connection should be shared by all Gateway connections to this endpoint.
const client = await newGrpcConnection(); const client = await newGrpcConnection();
@ -92,7 +91,7 @@ async function main(): Promise<void> {
} }
} }
main().catch(error => { main().catch((error: unknown) => {
console.error('******** FAILED to run the application:', error); console.error('******** FAILED to run the application:', error);
process.exitCode = 1; process.exitCode = 1;
}); });
@ -113,7 +112,11 @@ async function newIdentity(): Promise<Identity> {
async function getFirstDirFileName(dirPath: string): Promise<string> { async function getFirstDirFileName(dirPath: string): Promise<string> {
const files = await fs.readdir(dirPath); 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<Signer> { async function newSigner(): Promise<Signer> {
@ -144,7 +147,7 @@ async function getAllAssets(contract: Contract): Promise<void> {
const resultBytes = await contract.evaluateTransaction('GetAllAssets'); const resultBytes = await contract.evaluateTransaction('GetAllAssets');
const resultJson = utf8Decoder.decode(resultBytes); const resultJson = utf8Decoder.decode(resultBytes);
const result = JSON.parse(resultJson); const result: unknown = JSON.parse(resultJson);
console.log('*** Result:', result); console.log('*** Result:', result);
} }
@ -183,7 +186,7 @@ async function transferAssetAsync(contract: Contract): Promise<void> {
const status = await commit.getStatus(); const status = await commit.getStatus();
if (!status.successful) { 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'); console.log('*** Transaction committed successfully');
@ -195,7 +198,7 @@ async function readAssetByID(contract: Contract): Promise<void> {
const resultBytes = await contract.evaluateTransaction('ReadAsset', assetId); const resultBytes = await contract.evaluateTransaction('ReadAsset', assetId);
const resultJson = utf8Decoder.decode(resultBytes); const resultJson = utf8Decoder.decode(resultBytes);
const result = JSON.parse(resultJson); const result: unknown = JSON.parse(resultJson);
console.log('*** Result:', result); 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. * displayInputParameters() will print the global scope parameters used by the main driver routine.
*/ */
async function displayInputParameters(): Promise<void> { function displayInputParameters(): void {
console.log(`channelName: ${channelName}`); console.log(`channelName: ${channelName}`);
console.log(`chaincodeName: ${chaincodeName}`); console.log(`chaincodeName: ${chaincodeName}`);
console.log(`mspId: ${mspId}`); console.log(`mspId: ${mspId}`);

View file

@ -1,17 +1,15 @@
{ {
"extends":"@tsconfig/node18/tsconfig.json", "extends": "@tsconfig/node18/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"outDir": "dist", "outDir": "dist",
"declaration": true, "declaration": true,
"declarationMap": true,
"sourceMap": true, "sourceMap": true,
"noImplicitAny": true "noUnusedLocals": true,
"noImplicitReturns": true,
"noUncheckedIndexedAccess": true,
"forceConsistentCasingInFileNames": true
}, },
"include": [ "include": ["./src/**/*"],
"./src/**/*" "exclude": ["./src/**/*.spec.ts"]
],
"exclude": [
"./src/**/*.spec.ts"
]
} }

View file

@ -10,17 +10,17 @@ export class Asset {
public docType?: string; public docType?: string;
@Property() @Property()
public ID: string; public ID: string = '';
@Property() @Property()
public Color: string; public Color: string = '';
@Property() @Property()
public Size: number; public Size: number = 0;
@Property() @Property()
public Owner: string; public Owner: string = '';
@Property() @Property()
public AppraisedValue: number; public AppraisedValue: number = 0;
} }

View file

@ -0,0 +1,3 @@
[*.ts]
indent_size = 4
quote_type = single

View file

@ -1,3 +0,0 @@
{
"singleQuote": true
}

View file

@ -137,7 +137,9 @@ describe('Asset Transfer Besic REST API', () => {
}); });
it('GET should respond with an empty json array when there are no assets', async () => { it('GET should respond with an empty json array when there are no assets', async () => {
mockGetAllAssetsTransaction.evaluate.mockResolvedValue(Buffer.from('')); mockGetAllAssetsTransaction.evaluate.mockResolvedValue(
Buffer.from('')
);
const response = await request(app) const response = await request(app)
.get('/api/assets') .get('/api/assets')
@ -359,7 +361,9 @@ describe('Asset Transfer Besic REST API', () => {
it('GET should respond with 404 not found json when there is no asset with the specified ID', async () => { it('GET should respond with 404 not found json when there is no asset with the specified ID', async () => {
mockReadAssetTransaction.evaluate mockReadAssetTransaction.evaluate
.calledWith('asset3') .calledWith('asset3')
.mockRejectedValue(new Error('the asset asset3 does not exist')); .mockRejectedValue(
new Error('the asset asset3 does not exist')
);
const response = await request(app) const response = await request(app)
.get('/api/assets/asset3') .get('/api/assets/asset3')

View file

@ -121,7 +121,11 @@ assetsRouter.options('/:assetId', async (req: Request, res: Response) => {
const mspId = req.user as string; const mspId = req.user as string;
const contract = req.app.locals[mspId]?.assetContract as Contract; const contract = req.app.locals[mspId]?.assetContract as Contract;
const data = await evatuateTransaction(contract, 'AssetExists', assetId); const data = await evatuateTransaction(
contract,
'AssetExists',
assetId
);
const exists = data.toString() === 'true'; const exists = data.toString() === 'true';
if (exists) { if (exists) {
@ -260,7 +264,9 @@ assetsRouter.patch(
min: 1, min: 1,
max: 1, max: 1,
}) })
.withMessage('body must contain an array with a single patch operation'), .withMessage(
'body must contain an array with a single patch operation'
),
body('*.op', "operation must be 'replace'").equals('replace'), body('*.op', "operation must be 'replace'").equals('replace'),
body('*.path', "path must be '/Owner'").equals('/Owner'), body('*.path', "path must be '/Owner'").equals('/Owner'),
body('*.value', 'must be a string').isString(), body('*.value', 'must be a string').isString(),

View file

@ -365,7 +365,8 @@ describe('Config values', () => {
}); });
it('can be configured using the "HLF_CONNECTION_PROFILE_ORG1" environment variable', () => { it('can be configured using the "HLF_CONNECTION_PROFILE_ORG1" environment variable', () => {
process.env.HLF_CONNECTION_PROFILE_ORG1 = '{"name":"test-network-org1"}'; process.env.HLF_CONNECTION_PROFILE_ORG1 =
'{"name":"test-network-org1"}';
const config = require('./config'); const config = require('./config');
expect(config.connectionProfileOrg1).toStrictEqual({ expect(config.connectionProfileOrg1).toStrictEqual({
name: 'test-network-org1', name: 'test-network-org1',
@ -427,7 +428,8 @@ describe('Config values', () => {
}); });
it('can be configured using the "HLF_CONNECTION_PROFILE_ORG2" environment variable', () => { it('can be configured using the "HLF_CONNECTION_PROFILE_ORG2" environment variable', () => {
process.env.HLF_CONNECTION_PROFILE_ORG2 = '{"name":"test-network-org2"}'; process.env.HLF_CONNECTION_PROFILE_ORG2 =
'{"name":"test-network-org2"}';
const config = require('./config'); const config = require('./config');
expect(config.connectionProfileOrg2).toStrictEqual({ expect(config.connectionProfileOrg2).toStrictEqual({
name: 'test-network-org2', name: 'test-network-org2',

View file

@ -192,7 +192,9 @@ export const connectionProfileOrg1 = env
export const certificateOrg1 = env export const certificateOrg1 = env
.get('HLF_CERTIFICATE_ORG1') .get('HLF_CERTIFICATE_ORG1')
.required() .required()
.example('"-----BEGIN CERTIFICATE-----\\n...\\n-----END CERTIFICATE-----\\n"') .example(
'"-----BEGIN CERTIFICATE-----\\n...\\n-----END CERTIFICATE-----\\n"'
)
.asString(); .asString();
/** /**
@ -201,7 +203,9 @@ export const certificateOrg1 = env
export const privateKeyOrg1 = env export const privateKeyOrg1 = env
.get('HLF_PRIVATE_KEY_ORG1') .get('HLF_PRIVATE_KEY_ORG1')
.required() .required()
.example('"-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n"') .example(
'"-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n"'
)
.asString(); .asString();
/** /**
@ -221,7 +225,9 @@ export const connectionProfileOrg2 = env
export const certificateOrg2 = env export const certificateOrg2 = env
.get('HLF_CERTIFICATE_ORG2') .get('HLF_CERTIFICATE_ORG2')
.required() .required()
.example('"-----BEGIN CERTIFICATE-----\\n...\\n-----END CERTIFICATE-----\\n"') .example(
'"-----BEGIN CERTIFICATE-----\\n...\\n-----END CERTIFICATE-----\\n"'
)
.asString(); .asString();
/** /**
@ -230,7 +236,9 @@ export const certificateOrg2 = env
export const privateKeyOrg2 = env export const privateKeyOrg2 = env
.get('HLF_PRIVATE_KEY_ORG2') .get('HLF_PRIVATE_KEY_ORG2')
.required() .required()
.example('"-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n"') .example(
'"-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n"'
)
.asString(); .asString();
/** /**

View file

@ -44,12 +44,18 @@ describe('Errors', () => {
it('returns false for error like object with invalid stack', () => { it('returns false for error like object with invalid stack', () => {
expect( expect(
isErrorLike({ name: 'MockError', message: 'Fail', stack: false }) isErrorLike({
name: 'MockError',
message: 'Fail',
stack: false,
})
).toBe(false); ).toBe(false);
}); });
it('returns true for error like object', () => { it('returns true for error like object', () => {
expect(isErrorLike({ name: 'MockError', message: 'Fail' })).toBe(true); expect(isErrorLike({ name: 'MockError', message: 'Fail' })).toBe(
true
);
}); });
it('returns true for new Error', () => { it('returns true for new Error', () => {
@ -62,18 +68,19 @@ describe('Errors', () => {
const mockDuplicateTransactionError = mock<TransactionError>(); const mockDuplicateTransactionError = mock<TransactionError>();
mockDuplicateTransactionError.transactionCode = 'DUPLICATE_TXID'; mockDuplicateTransactionError.transactionCode = 'DUPLICATE_TXID';
expect(isDuplicateTransactionError(mockDuplicateTransactionError)).toBe( expect(
true isDuplicateTransactionError(mockDuplicateTransactionError)
); ).toBe(true);
}); });
it('returns false for a TransactionError without a transaction code of MVCC_READ_CONFLICT', () => { it('returns false for a TransactionError without a transaction code of MVCC_READ_CONFLICT', () => {
const mockDuplicateTransactionError = mock<TransactionError>(); const mockDuplicateTransactionError = mock<TransactionError>();
mockDuplicateTransactionError.transactionCode = 'MVCC_READ_CONFLICT'; mockDuplicateTransactionError.transactionCode =
'MVCC_READ_CONFLICT';
expect(isDuplicateTransactionError(mockDuplicateTransactionError)).toBe( expect(
false isDuplicateTransactionError(mockDuplicateTransactionError)
); ).toBe(false);
}); });
it('returns true for an error when all endorsement details are duplicate transaction found', () => { it('returns true for an error when all endorsement details are duplicate transaction found', () => {
@ -95,9 +102,9 @@ describe('Errors', () => {
], ],
}; };
expect(isDuplicateTransactionError(mockDuplicateTransactionError)).toBe( expect(
true isDuplicateTransactionError(mockDuplicateTransactionError)
); ).toBe(true);
}); });
it('returns true for an error when at least one endorsement details are duplicate transaction found', () => { it('returns true for an error when at least one endorsement details are duplicate transaction found', () => {
@ -119,9 +126,9 @@ describe('Errors', () => {
], ],
}; };
expect(isDuplicateTransactionError(mockDuplicateTransactionError)).toBe( expect(
true isDuplicateTransactionError(mockDuplicateTransactionError)
); ).toBe(true);
}); });
it('returns false for an error without duplicate transaction endorsement details', () => { it('returns false for an error without duplicate transaction endorsement details', () => {
@ -140,9 +147,9 @@ describe('Errors', () => {
], ],
}; };
expect(isDuplicateTransactionError(mockDuplicateTransactionError)).toBe( expect(
false isDuplicateTransactionError(mockDuplicateTransactionError)
); ).toBe(false);
}); });
it('returns false for an error without endorsement details', () => { it('returns false for an error without endorsement details', () => {
@ -158,14 +165,16 @@ describe('Errors', () => {
], ],
}; };
expect(isDuplicateTransactionError(mockDuplicateTransactionError)).toBe( expect(
false isDuplicateTransactionError(mockDuplicateTransactionError)
); ).toBe(false);
}); });
it('returns false for a basic Error object without endorsement details', () => { it('returns false for a basic Error object without endorsement details', () => {
expect( expect(
isDuplicateTransactionError(new Error('duplicate transaction found')) isDuplicateTransactionError(
new Error('duplicate transaction found')
)
).toBe(false); ).toBe(false);
}); });
@ -229,7 +238,9 @@ describe('Errors', () => {
'txn1' 'txn1'
); );
expect(getRetryAction(mockAssetNotFoundError)).toBe(RetryAction.None); expect(getRetryAction(mockAssetNotFoundError)).toBe(
RetryAction.None
);
}); });
it('returns RetryAction.WithExistingTransactionId for a TimeoutError', () => { it('returns RetryAction.WithExistingTransactionId for a TimeoutError', () => {
@ -252,13 +263,17 @@ describe('Errors', () => {
it('returns RetryAction.WithNewTransactionId for an Error', () => { it('returns RetryAction.WithNewTransactionId for an Error', () => {
const mockError = new Error('MOCK ERROR'); const mockError = new Error('MOCK ERROR');
expect(getRetryAction(mockError)).toBe(RetryAction.WithNewTransactionId); expect(getRetryAction(mockError)).toBe(
RetryAction.WithNewTransactionId
);
}); });
it('returns RetryAction.WithNewTransactionId for a string error', () => { it('returns RetryAction.WithNewTransactionId for a string error', () => {
const mockError = 'MOCK ERROR'; const mockError = 'MOCK ERROR';
expect(getRetryAction(mockError)).toBe(RetryAction.WithNewTransactionId); expect(getRetryAction(mockError)).toBe(
RetryAction.WithNewTransactionId
);
}); });
}); });

View file

@ -248,19 +248,25 @@ export const handleError = (
logger.debug({ transactionId: transactionId, err }, 'Processing error'); logger.debug({ transactionId: transactionId, err }, 'Processing error');
if (isErrorLike(err)) { if (isErrorLike(err)) {
const assetAlreadyExistsMatch = matchAssetAlreadyExistsMessage(err.message); const assetAlreadyExistsMatch = matchAssetAlreadyExistsMessage(
err.message
);
if (assetAlreadyExistsMatch !== null) { if (assetAlreadyExistsMatch !== null) {
return new AssetExistsError(assetAlreadyExistsMatch, transactionId); return new AssetExistsError(assetAlreadyExistsMatch, transactionId);
} }
const assetDoesNotExistMatch = matchAssetDoesNotExistMessage(err.message); const assetDoesNotExistMatch = matchAssetDoesNotExistMessage(
if (assetDoesNotExistMatch !== null) {
return new AssetNotFoundError(assetDoesNotExistMatch, transactionId);
}
const transactionDoesNotExistMatch = matchTransactionDoesNotExistMessage(
err.message err.message
); );
if (assetDoesNotExistMatch !== null) {
return new AssetNotFoundError(
assetDoesNotExistMatch,
transactionId
);
}
const transactionDoesNotExistMatch =
matchTransactionDoesNotExistMessage(err.message);
if (transactionDoesNotExistMatch !== null) { if (transactionDoesNotExistMatch !== null) {
return new TransactionNotFoundError( return new TransactionNotFoundError(
transactionDoesNotExistMatch, transactionDoesNotExistMatch,

View file

@ -90,7 +90,9 @@ describe('Fabric', () => {
await getNetwork(mockGateway); await getNetwork(mockGateway);
expect(mockGateway.getNetwork).toHaveBeenCalledWith(config.channelName); expect(mockGateway.getNetwork).toHaveBeenCalledWith(
config.channelName
);
}); });
}); });
@ -277,14 +279,16 @@ describe('Fabric', () => {
); );
const mockTransaction = mock<Transaction>(); const mockTransaction = mock<Transaction>();
mockTransaction.evaluate.mockResolvedValue(processedTransactionBuffer); mockTransaction.evaluate.mockResolvedValue(
processedTransactionBuffer
);
const mockContract = mock<Contract>(); const mockContract = mock<Contract>();
mockContract.createTransaction mockContract.createTransaction
.calledWith('GetTransactionByID') .calledWith('GetTransactionByID')
.mockReturnValue(mockTransaction); .mockReturnValue(mockTransaction);
expect(await getTransactionValidationCode(mockContract, 'txn1')).toBe( expect(
'VALID' await getTransactionValidationCode(mockContract, 'txn1')
); ).toBe('VALID');
}); });
}); });

View file

@ -81,7 +81,8 @@ export const createGateway = async (
}, },
queryHandlerOptions: { queryHandlerOptions: {
timeout: config.queryTimeout, timeout: config.queryTimeout,
strategy: DefaultQueryHandlerStrategies.PREFER_MSPID_SCOPE_ROUND_ROBIN, strategy:
DefaultQueryHandlerStrategies.PREFER_MSPID_SCOPE_ROUND_ROBIN,
}, },
}; };
@ -174,7 +175,8 @@ export const getTransactionValidationCode = async (
transactionId transactionId
); );
const processedTransaction = protos.protos.ProcessedTransaction.decode(data); const processedTransaction =
protos.protos.ProcessedTransaction.decode(data);
const validationCode = const validationCode =
protos.protos.TxValidationCode[processedTransaction.validationCode]; protos.protos.TxValidationCode[processedTransaction.validationCode];

View file

@ -31,8 +31,10 @@ healthRouter.get('/live', async (req: Request, res: Response) => {
try { try {
const submitQueue = req.app.locals.jobq as Queue; const submitQueue = req.app.locals.jobq as Queue;
const qsccOrg1 = req.app.locals[config.mspIdOrg1]?.qsccContract as Contract; const qsccOrg1 = req.app.locals[config.mspIdOrg1]
const qsccOrg2 = req.app.locals[config.mspIdOrg2]?.qsccContract as Contract; ?.qsccContract as Contract;
const qsccOrg2 = req.app.locals[config.mspIdOrg2]
?.qsccContract as Contract;
await Promise.all([ await Promise.all([
getBlockHeight(qsccOrg1), getBlockHeight(qsccOrg1),

View file

@ -23,7 +23,11 @@ jobsRouter.get('/:jobId', async (req: Request, res: Response) => {
return res.status(OK).json(jobSummary); return res.status(OK).json(jobSummary);
} catch (err) { } catch (err) {
logger.error({ err }, 'Error processing read request for job ID %s', jobId); logger.error(
{ err },
'Error processing read request for job ID %s',
jobId
);
if (err instanceof JobNotFoundError) { if (err instanceof JobNotFoundError) {
return res.status(NOT_FOUND).json({ return res.status(NOT_FOUND).json({

View file

@ -227,7 +227,9 @@ describe('getJobCounts', () => {
beforeEach(() => { beforeEach(() => {
mockTransaction = mock<Transaction>(); mockTransaction = mock<Transaction>();
mockTransaction.getTransactionId.mockReturnValue('mockTransactionId'); mockTransaction.getTransactionId.mockReturnValue(
'mockTransactionId'
);
mockContract = mock<Contract>(); mockContract = mock<Contract>();
mockContract.createTransaction mockContract.createTransaction

View file

@ -27,7 +27,10 @@ describe('Redis', () => {
}); });
it('returns false when the maxmemory-policy is not noeviction', async () => { it('returns false when the maxmemory-policy is not noeviction', async () => {
mockRedisConfig.mockReturnValue(['maxmemory-policy', 'allkeys-lru']); mockRedisConfig.mockReturnValue([
'maxmemory-policy',
'allkeys-lru',
]);
expect(await isMaxmemoryPolicyNoeviction()).toBe(false); expect(await isMaxmemoryPolicyNoeviction()).toBe(false);
}); });
}); });

View file

@ -18,10 +18,14 @@ transactionsRouter.get(
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const mspId = req.user as string; const mspId = req.user as string;
const transactionId = req.params.transactionId; const transactionId = req.params.transactionId;
logger.debug('Read request received for transaction ID %s', transactionId); logger.debug(
'Read request received for transaction ID %s',
transactionId
);
try { try {
const qsccContract = req.app.locals[mspId]?.qsccContract as Contract; const qsccContract = req.app.locals[mspId]
?.qsccContract as Contract;
const validationCode = await getTransactionValidationCode( const validationCode = await getTransactionValidationCode(
qsccContract, qsccContract,

View file

@ -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"
]
}
]
}

View file

@ -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,
},
},
});

View file

@ -10,7 +10,7 @@
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"build:watch": "tsc -w", "build:watch": "tsc -w",
"lint": "eslint . --ext .ts", "lint": "eslint src",
"prepare": "npm run build", "prepare": "npm run build",
"pretest": "npm run lint", "pretest": "npm run lint",
"start": "node dist/app.js" "start": "node dist/app.js"
@ -19,15 +19,15 @@
"author": "Hyperledger", "author": "Hyperledger",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@grpc/grpc-js": "^1.9.7", "@grpc/grpc-js": "^1.10",
"@hyperledger/fabric-gateway": "~1.4.0" "@hyperledger/fabric-gateway": "^1.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.3.0",
"@tsconfig/node18": "^18.2.2", "@tsconfig/node18": "^18.2.2",
"@types/node": "^18.18.6", "@types/node": "^18.18.6",
"@typescript-eslint/eslint-plugin": "^6.9.0", "eslint": "^8.57.0",
"@typescript-eslint/parser": "^6.9.0", "typescript": "~5.4",
"eslint": "^8.52.0", "typescript-eslint": "^7.13.0"
"typescript": "~5.2.2"
} }
} }

View file

@ -14,7 +14,7 @@ const chaincodeName = 'events';
const utf8Decoder = new TextDecoder(); const utf8Decoder = new TextDecoder();
const now = Date.now(); const now = Date.now();
const assetId = `asset${now}`; const assetId = `asset${String(now)}`;
async function main(): Promise<void> { async function main(): Promise<void> {
@ -60,7 +60,7 @@ async function main(): Promise<void> {
} }
} }
main().catch(error => { main().catch((error: unknown) => {
console.error('******** FAILED to run the application:', error); console.error('******** FAILED to run the application:', error);
process.exitCode = 1; process.exitCode = 1;
}); });
@ -102,7 +102,7 @@ async function createAsset(contract: Contract): Promise<bigint> {
const status = await result.getStatus(); const status = await result.getStatus();
if (!status.successful) { 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'); console.log('\n*** CreateAsset committed successfully');

View file

@ -50,5 +50,9 @@ export async function newSigner(): Promise<Signer> {
async function getFirstDirFileName(dirPath: string): Promise<string> { async function getFirstDirFileName(dirPath: string): Promise<string> {
const files = await fs.readdir(dirPath); 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);
} }

View file

@ -1,17 +1,15 @@
{ {
"extends":"@tsconfig/node18/tsconfig.json", "extends": "@tsconfig/node18/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"outDir": "dist", "outDir": "dist",
"declaration": true, "declaration": true,
"declarationMap": true,
"sourceMap": true, "sourceMap": true,
"noImplicitAny": true "noUnusedLocals": true,
"noImplicitReturns": true,
"noUncheckedIndexedAccess": true,
"forceConsistentCasingInFileNames": true
}, },
"include": [ "include": ["./src/**/*"],
"./src/**/*" "exclude": ["./src/**/*.spec.ts"]
],
"exclude": [
"./src/**/*.spec.ts"
]
} }

View file

@ -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"
]
}
]
}

View file

@ -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,
},
},
});

View file

@ -10,7 +10,7 @@
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"build:watch": "tsc -w", "build:watch": "tsc -w",
"lint": "eslint . --ext .ts", "lint": "eslint src",
"prepare": "npm run build", "prepare": "npm run build",
"pretest": "npm run lint", "pretest": "npm run lint",
"start": "node dist/app.js" "start": "node dist/app.js"
@ -19,15 +19,15 @@
"author": "Hyperledger", "author": "Hyperledger",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@grpc/grpc-js": "^1.9.7", "@grpc/grpc-js": "^1.10",
"@hyperledger/fabric-gateway": "~1.4.0" "@hyperledger/fabric-gateway": "^1.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.3.0",
"@tsconfig/node18": "^18.2.2", "@tsconfig/node18": "^18.2.2",
"@types/node": "^18.18.6", "@types/node": "^18.18.6",
"@typescript-eslint/eslint-plugin": "^6.9.0", "eslint": "^8.57.0",
"@typescript-eslint/parser": "^6.9.0", "typescript": "~5.4",
"eslint": "^8.52.0", "typescript-eslint": "^7.13.0"
"typescript": "~5.2.2"
} }
} }

View file

@ -27,8 +27,8 @@ const RESET = '\x1b[0m';
// Use a unique key so that we can run multiple times // Use a unique key so that we can run multiple times
const now = Date.now(); const now = Date.now();
const assetID1 = `asset${now}`; const assetID1 = `asset${String(now)}`;
const assetID2 = `asset${now + 1}`; const assetID2 = `asset${String(now + 1)}`;
async function main(): Promise<void> { async function main(): Promise<void> {
const clientOrg1 = await newGrpcConnection( const clientOrg1 = await newGrpcConnection(
@ -74,14 +74,13 @@ async function main(): Promise<void> {
// Read asset from the Org1's private data collection with ID in the given range. // Read asset from the Org1's private data collection with ID in the given range.
await getAssetsByRange(contractOrg1); await getAssetsByRange(contractOrg1);
try{ try {
// Attempt to transfer asset without prior aprroval from Org2, transaction expected to fail. // Attempt to transfer asset without prior aprroval from Org2, transaction expected to fail.
console.log('\nAttempt TransferAsset without prior AgreeToTransfer'); console.log('\nAttempt TransferAsset without prior AgreeToTransfer');
await transferAsset(contractOrg1, assetID1); await transferAsset(contractOrg1, assetID1);
doFail('TransferAsset transaction succeeded when it was expected to fail'); doFail('TransferAsset transaction succeeded when it was expected to fail');
} } catch (e) {
catch(e){ console.log('*** Received expected error:', e);
console.log(`*** Received expected error: ${e}`);
} }
console.log('\n~~~~~~~~~~~~~~~~ As Org2 Client ~~~~~~~~~~~~~~~~'); console.log('\n~~~~~~~~~~~~~~~~ As Org2 Client ~~~~~~~~~~~~~~~~');
@ -122,7 +121,7 @@ async function main(): Promise<void> {
await deleteAsset(contractOrg2, assetID2); await deleteAsset(contractOrg2, assetID2);
doFail('DeleteAsset transaction succeeded when it was expected to fail'); doFail('DeleteAsset transaction succeeded when it was expected to fail');
} catch (e) { } catch (e) {
console.log(`*** Received expected error: ${e}`); console.log('*** Received expected error:', e);
} }
console.log('\n~~~~~~~~~~~~~~~~ As Org1 Client ~~~~~~~~~~~~~~~~'); console.log('\n~~~~~~~~~~~~~~~~ As Org1 Client ~~~~~~~~~~~~~~~~');
@ -142,7 +141,7 @@ async function main(): Promise<void> {
} }
} }
main().catch((error) => { main().catch((error: unknown) => {
console.error('******** FAILED to run the application:', error); console.error('******** FAILED to run the application:', error);
process.exitCode = 1; process.exitCode = 1;
}); });
@ -192,14 +191,14 @@ async function getAssetsByRange(contract: Contract): Promise<void> {
const resultBytes = await contract.evaluateTransaction( const resultBytes = await contract.evaluateTransaction(
'GetAssetByRange', 'GetAssetByRange',
assetID1, assetID1,
`asset${now + 2}` `asset${String(now + 2)}`
); );
const resultString = utf8Decoder.decode(resultBytes); const resultString = utf8Decoder.decode(resultBytes);
if (!resultString) { if (!resultString) {
doFail('Received empty query list for readAssetPrivateDetailsOrg1'); doFail('Received empty query list for readAssetPrivateDetailsOrg1');
} }
const result = JSON.parse(resultString); const result: unknown = JSON.parse(resultString);
console.log('*** Result:', result); console.log('*** Result:', result);
} }
@ -211,7 +210,7 @@ async function readAssetByID(contract: Contract, assetID: string): Promise<void>
if (!resultString) { if (!resultString) {
doFail('Received empty result for ReadAsset'); doFail('Received empty result for ReadAsset');
} }
const result = JSON.parse(resultString); const result: unknown = JSON.parse(resultString);
console.log('*** Result:', result); console.log('*** Result:', result);
} }
@ -241,7 +240,7 @@ async function readTransferAgreement(contract: Contract, assetID: string): Promi
if (!resultString) { if (!resultString) {
doFail('Received no result for ReadTransferAgreement'); doFail('Received no result for ReadTransferAgreement');
} }
const result = JSON.parse(resultString); const result: unknown = JSON.parse(resultString);
console.log('*** Result:', result); console.log('*** Result:', result);
} }
@ -290,7 +289,7 @@ async function readAssetPrivateDetails(contract: Contract, assetID: string, coll
console.log('*** No result'); console.log('*** No result');
return false; return false;
} }
const result = JSON.parse(resultJson); const result: unknown = JSON.parse(resultJson);
console.log('*** Result:', result); console.log('*** Result:', result);
return true; return true;
} }

View file

@ -127,5 +127,9 @@ export async function newSigner(keyDirectoryPath: string): Promise<Signer> {
async function getFirstDirFileName(dirPath: string): Promise<string> { async function getFirstDirFileName(dirPath: string): Promise<string> {
const files = await fs.readdir(dirPath); 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);
} }

View file

@ -1,17 +1,15 @@
{ {
"extends":"@tsconfig/node18/tsconfig.json", "extends": "@tsconfig/node18/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"outDir": "dist", "outDir": "dist",
"declaration": true, "declaration": true,
"declarationMap": true,
"sourceMap": true, "sourceMap": true,
"noImplicitAny": true "noUnusedLocals": true,
"noImplicitReturns": true,
"noUncheckedIndexedAccess": true,
"forceConsistentCasingInFileNames": true
}, },
"include": [ "include": ["./src/**/*"],
"./src/**/*" "exclude": ["./src/**/*.spec.ts"]
],
"exclude": [
"./src/**/*.spec.ts"
]
} }

View file

@ -91,9 +91,9 @@
} }
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "9.4.0", "version": "9.5.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.4.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.5.0.tgz",
"integrity": "sha512-fdI7VJjP3Rvc70lC4xkFXHB0fiPeojiL1PxVG6t1ZvXQrarj893PweuBTujxDUFk0Fxj4R7PIIAZ/aiiyZPZcg==", "integrity": "sha512-A7+AOT2ICkodvtsWnxZP4Xxk3NbZ3VMHd8oihydLRGrJgqqdEz1qSeEgXYyT/Cu8h1TWWsQRejIx48mtjZ5y1w==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"

View file

@ -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"
}
]
}
}
]
}

View file

@ -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,
},
},
});

View file

@ -10,7 +10,7 @@
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"build:watch": "tsc -w", "build:watch": "tsc -w",
"lint": "eslint . --ext .ts", "lint": "eslint src",
"prepare": "npm run build", "prepare": "npm run build",
"pretest": "npm run lint", "pretest": "npm run lint",
"start": "node dist/app.js" "start": "node dist/app.js"
@ -19,15 +19,15 @@
"author": "Hyperledger", "author": "Hyperledger",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@grpc/grpc-js": "^1.9.7", "@grpc/grpc-js": "^1.10",
"@hyperledger/fabric-gateway": "~1.4.0" "@hyperledger/fabric-gateway": "^1.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.3.0",
"@tsconfig/node18": "^18.2.2", "@tsconfig/node18": "^18.2.2",
"@types/node": "^18.18.6", "@types/node": "^18.18.6",
"@typescript-eslint/eslint-plugin": "^6.9.0", "eslint": "^8.57.0",
"@typescript-eslint/parser": "^6.9.0", "typescript": "~5.4",
"eslint": "^8.52.0", "typescript-eslint": "^7.13.0"
"typescript": "~5.2.2"
} }
} }

View file

@ -72,8 +72,8 @@ async function main(): Promise<void> {
// Org2 is not the owner and does not have the private details, read expected to fail. // Org2 is not the owner and does not have the private details, read expected to fail.
try { try {
await contractWrapperOrg2.getAssetPrivateProperties(assetKey, mspIdOrg1); await contractWrapperOrg2.getAssetPrivateProperties(assetKey, mspIdOrg1);
} catch(e) { } catch (e) {
console.log(`${RED}*** Successfully caught the failure: getAssetPrivateProperties - ${e}${RESET}`); console.log(`${RED}*** Successfully caught the failure: getAssetPrivateProperties - ${String(e)}${RESET}`);
} }
// Org1 updates the assets public description. // Org1 updates the assets public description.
@ -94,7 +94,7 @@ async function main(): Promise<void> {
ownerOrg: mspIdOrg1, ownerOrg: mspIdOrg1,
publicDescription: `Asset ${assetKey} owned by ${mspIdOrg2} is NOT for sale`}); publicDescription: `Asset ${assetKey} owned by ${mspIdOrg2} is NOT for sale`});
} catch(e) { } 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. // Read the public details by org1.
@ -126,14 +126,14 @@ async function main(): Promise<void> {
try{ try{
await contractWrapperOrg2.getAssetSalesPrice(assetKey, mspIdOrg1); await contractWrapperOrg2.getAssetSalesPrice(assetKey, mspIdOrg1);
} catch(e) { } 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. // Org1 has not agreed to buy so this should fail.
try{ try{
await contractWrapperOrg1.getAssetBidPrice(assetKey, mspIdOrg2); await contractWrapperOrg1.getAssetBidPrice(assetKey, mspIdOrg2);
} catch(e) { } 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. // Org2 should be able to see the price it has agreed.
await contractWrapperOrg2.getAssetBidPrice(assetKey, mspIdOrg2); await contractWrapperOrg2.getAssetBidPrice(assetKey, mspIdOrg2);
@ -143,7 +143,7 @@ async function main(): Promise<void> {
try{ try{
await contractWrapperOrg1.transferAsset({ assetId: assetKey, price: 110, tradeId: now}, [ mspIdOrg1, mspIdOrg2 ], mspIdOrg1, mspIdOrg2); await contractWrapperOrg1.transferAsset({ assetId: assetKey, price: 110, tradeId: now}, [ mspIdOrg1, mspIdOrg2 ], mspIdOrg1, mspIdOrg2);
} catch(e) { } 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. // 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}); await contractWrapperOrg1.agreeToSell({assetId:assetKey, price:100, tradeId:now});
@ -168,7 +168,7 @@ async function main(): Promise<void> {
try{ try{
await contractWrapperOrg2.transferAsset({ assetId: assetKey, price: 100, tradeId: now}, [ mspIdOrg1, mspIdOrg2 ], mspIdOrg1, mspIdOrg2); await contractWrapperOrg2.transferAsset({ assetId: assetKey, price: 100, tradeId: now}, [ mspIdOrg1, mspIdOrg2 ], mspIdOrg1, mspIdOrg2);
} catch(e) { } 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. // Org1 will transfer the asset to Org2.
@ -188,7 +188,7 @@ async function main(): Promise<void> {
try{ try{
await contractWrapperOrg1.getAssetPrivateProperties(assetKey, mspIdOrg2); await contractWrapperOrg1.getAssetPrivateProperties(assetKey, mspIdOrg2);
} catch(e) { } 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. // This is an update to the public state and requires only the owner to endorse.
@ -209,7 +209,7 @@ async function main(): Promise<void> {
} }
} }
main().catch(error => { main().catch((error: unknown) => {
console.error('******** FAILED to run the application:', error); console.error('******** FAILED to run the application:', error);
process.exitCode = 1; process.exitCode = 1;
}); });

View file

@ -103,5 +103,9 @@ export async function newSigner(keyDirectoryPath: string): Promise<Signer> {
async function getFirstDirFileName(dirPath: string): Promise<string> { async function getFirstDirFileName(dirPath: string): Promise<string> {
const files = await fs.readdir(dirPath); 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);
} }

View file

@ -6,10 +6,10 @@
import { Contract } from '@hyperledger/fabric-gateway'; import { Contract } from '@hyperledger/fabric-gateway';
import { TextDecoder } from 'util'; import { TextDecoder } from 'util';
import { GREEN, parse, RED, RESET } from './utils'; import { GREEN, parse, RED, RESET } from './utils';
import crpto from 'crypto'; import crypto from 'crypto';
import { mspIdOrg2 } from './connect'; import { mspIdOrg2 } from './connect';
const randomBytes = crpto.randomBytes(256).toString('hex'); const randomBytes = crypto.randomBytes(256).toString('hex');
interface AssetJSON { interface AssetJSON {
objectType: string; objectType: string;
@ -151,7 +151,7 @@ export class ContractWrapper {
endorsingOrganizations: this.#endorsingOrgs[assetPrice.assetId] 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<void> { public async verifyAssetProperties(assetId: string, assetProperties: AssetProperties): Promise<void> {
@ -169,16 +169,11 @@ export class ContractWrapper {
const resultString = this.#utf8Decoder.decode(resultBytes); const resultString = this.#utf8Decoder.decode(resultBytes);
if (resultString.length !== 0) { if (resultString.length !== 0) {
const json = parse<AssetPropertiesJSON>(resultString); const json = parse<AssetPropertiesJSON>(resultString);
const result: AssetProperties = { if (typeof json === 'object') {
color: json.color,
size: json.size
};
if (result) {
console.log(`*** Success VerifyAssetProperties, private information about asset ${assetId} has been verified by ${this.#org}`); console.log(`*** Success VerifyAssetProperties, private information about asset ${assetId} has been verified by ${this.#org}`);
} else { } else {
console.log(`*** Failed: VerifyAssetProperties, private information about asset ${assetId} has not been verified by ${this.#org}`); console.log(`*** Failed: VerifyAssetProperties, private information about asset ${assetId} has not been verified by ${this.#org}`);
} }
} else { } else {
throw new Error(`Private information about asset ${assetId} has not been verified by ${this.#org}`); throw new Error(`Private information about asset ${assetId} has not been verified by ${this.#org}`);
} }

View file

@ -9,5 +9,5 @@ export const GREEN = '\x1b[32m\n';
export const RESET = '\x1b[0m'; export const RESET = '\x1b[0m';
export function parse<T>(data: string): T { export function parse<T>(data: string): T {
return JSON.parse(data); return JSON.parse(data) as T;
} }

View file

@ -1,17 +1,15 @@
{ {
"extends":"@tsconfig/node18/tsconfig.json", "extends": "@tsconfig/node18/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"outDir": "dist", "outDir": "dist",
"declaration": true, "declaration": true,
"declarationMap": true,
"sourceMap": true, "sourceMap": true,
"noImplicitAny": true "noUnusedLocals": true,
"noImplicitReturns": true,
"noUncheckedIndexedAccess": true,
"forceConsistentCasingInFileNames": true
}, },
"include": [ "include": ["./src/**/*"],
"./src/**/*" "exclude": ["./src/**/*.spec.ts"]
],
"exclude": [
"./src/**/*.spec.ts"
]
} }

View file

@ -19,8 +19,8 @@
"author": "Hyperledger", "author": "Hyperledger",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@grpc/grpc-js": "^1.9.7", "@grpc/grpc-js": "^1.10",
"@hyperledger/fabric-gateway": "~1.4.0", "@hyperledger/fabric-gateway": "^1.5",
"axios": "^0.27.2", "axios": "^0.27.2",
"source-map-support": "^0.5.21" "source-map-support": "^0.5.21"
}, },

View file

@ -18,8 +18,8 @@
"author": "Hyperledger", "author": "Hyperledger",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@grpc/grpc-js": "^1.9.7", "@grpc/grpc-js": "^1.10",
"@hyperledger/fabric-gateway": "~1.4.0", "@hyperledger/fabric-gateway": "^1.5",
"dotenv": "^16.0.1", "dotenv": "^16.0.1",
"env-var": "^7.1.1", "env-var": "^7.1.1",
"js-yaml": "^4.1.0" "js-yaml": "^4.1.0"

View file

@ -29,8 +29,8 @@
"typescript": "~5.2.2" "typescript": "~5.2.2"
}, },
"dependencies": { "dependencies": {
"@grpc/grpc-js": "^1.9.7", "@grpc/grpc-js": "^1.10",
"@hyperledger/fabric-gateway": "~1.4.0", "@hyperledger/fabric-gateway": "^1.5",
"body-parser": "^1.18.3", "body-parser": "^1.18.3",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.16.3" "express": "^4.16.3"

View file

@ -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',
},
],
},
},
],
};

View file

@ -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,
},
},
});

View file

@ -10,7 +10,7 @@
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"build:watch": "tsc -w", "build:watch": "tsc -w",
"lint": "eslint ./src", "lint": "eslint src",
"prepare": "npm run build", "prepare": "npm run build",
"pretest": "npm run lint", "pretest": "npm run lint",
"start": "node ./dist/app", "start": "node ./dist/app",
@ -19,15 +19,15 @@
"author": "Hyperledger", "author": "Hyperledger",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@grpc/grpc-js": "^1.9.7", "@grpc/grpc-js": "^1.10",
"@hyperledger/fabric-gateway": "~1.4.0" "@hyperledger/fabric-gateway": "^1.5"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node18": "^18.2.2", "@types/node": "^18.19.33",
"@types/node": "^18.18.6", "@eslint/js": "^9.3.0",
"@typescript-eslint/eslint-plugin": "^6.9.0", "@tsconfig/node18": "^18.2.4",
"@typescript-eslint/parser": "^6.9.0", "eslint": "^8.57.0",
"eslint": "^8.52.0", "typescript": "~5.4.5",
"typescript": "~5.2.2" "typescript-eslint": "^7.11.0"
} }
} }

View file

@ -12,10 +12,10 @@ async function main(): Promise<void> {
const commandName = process.argv[2]; const commandName = process.argv[2];
const args = process.argv.slice(3); const args = process.argv.slice(3);
const command = commands[commandName]; const command = commandName && commands[commandName];
if (!command) { if (!command) {
printUsage(); printUsage();
throw new Error(`Unknown command: ${commandName}`); throw new Error(`Unknown command: ${String(commandName)}`);
} }
await runCommand(command, args); await runCommand(command, args);
@ -41,7 +41,7 @@ function printUsage(): void {
console.log(`\t${Object.keys(commands).sort().join('\n\t')}`); console.log(`\t${Object.keys(commands).sort().join('\n\t')}`);
} }
main().catch(error => { main().catch((error: unknown) => {
if (error instanceof ExpectedError) { if (error instanceof ExpectedError) {
console.log(error); console.log(error);
} else { } else {

View file

@ -7,10 +7,14 @@
import { Gateway } from '@hyperledger/fabric-gateway'; import { Gateway } from '@hyperledger/fabric-gateway';
import { CHAINCODE_NAME, CHANNEL_NAME } from '../config'; import { CHAINCODE_NAME, CHANNEL_NAME } from '../config';
import { AssetTransfer } from '../contract'; import { AssetTransfer } from '../contract';
import { assertAllDefined } from '../utils'; import { assertDefined } from '../utils';
const usage = 'Arguments: <assetId> <ownerName> <color>';
export default async function main(gateway: Gateway, args: string[]): Promise<void> { export default async function main(gateway: Gateway, args: string[]): Promise<void> {
const [assetId, owner, color] = assertAllDefined([args[0], args[1], args[2]], 'Arguments: <assetId> <ownerName> <color>'); 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 network = gateway.getNetwork(CHANNEL_NAME);
const contract = network.getContract(CHAINCODE_NAME); const contract = network.getContract(CHAINCODE_NAME);

View file

@ -16,5 +16,8 @@ export default async function main(gateway: Gateway): Promise<void> {
const assets = await smartContract.getAllAssets(); const assets = await smartContract.getAllAssets();
const assetsJson = JSON.stringify(assets, undefined, 2); 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);
});
} }

View file

@ -19,10 +19,10 @@ export default async function main(gateway: Gateway): Promise<void> {
const network = gateway.getNetwork(CHANNEL_NAME); const network = gateway.getNetwork(CHANNEL_NAME);
const checkpointer = await checkpointers.file(checkpointFile); 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()); console.log('Last processed transaction ID within block:', checkpointer.getTransactionId());
if (simulatedFailureCount > 0) { 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, { const events = await network.getChaincodeEvents(CHAINCODE_NAME, {

View file

@ -7,10 +7,14 @@
import { Gateway } from '@hyperledger/fabric-gateway'; import { Gateway } from '@hyperledger/fabric-gateway';
import { CHAINCODE_NAME, CHANNEL_NAME } from '../config'; import { CHAINCODE_NAME, CHANNEL_NAME } from '../config';
import { AssetTransfer } from '../contract'; import { AssetTransfer } from '../contract';
import { assertAllDefined } from '../utils'; import { assertDefined } from '../utils';
const usage = 'Arguments: <assetId> <ownerName> <ownerMspId>';
export default async function main(gateway: Gateway, args: string[]): Promise<void> { export default async function main(gateway: Gateway, args: string[]): Promise<void> {
const [assetId, newOwner, newOwnerOrg] = assertAllDefined([args[0], args[1], args[2]], 'Arguments: <assetId> <ownerName> <ownerMspId>'); 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 network = gateway.getNetwork(CHANNEL_NAME);
const contract = network.getContract(CHAINCODE_NAME); const contract = network.getContract(CHAINCODE_NAME);

View file

@ -13,7 +13,8 @@ const utf8Decoder = new TextDecoder();
* @param values Candidate elements. * @param values Candidate elements.
*/ */
export function randomElement<T>(values: T[]): T { export function randomElement<T>(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<unknown>[]): Promise<void>
if (failures.length > 0) { if (failures.length > 0) {
const failMessages = '- ' + failures.join('\n- '); 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<T extends object>(event: T): PrintView<T> {
) as PrintView<T>; ) as PrintView<T>;
} }
export function assertAllDefined<T>(values: (T | undefined)[], message: string | (() => string)): T[] {
values.forEach(value => assertDefined(value, message));
return values as T[];
}
export function assertDefined<T>(value: T | undefined, message: string | (() => string)): T { export function assertDefined<T>(value: T | undefined, message: string | (() => string)): T {
if (value == undefined) { if (value == undefined) {
throw new Error(typeof message === 'string' ? message : message()); throw new Error(typeof message === 'string' ? message : message());

View file

@ -1,19 +1,15 @@
{ {
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@tsconfig/node18/tsconfig.json", "extends": "@tsconfig/node18/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "dist",
"declaration": true, "declaration": true,
"declarationMap": true, "declarationMap": true,
"sourceMap": true, "sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"noUnusedLocals": true, "noUnusedLocals": true,
"noImplicitReturns": true "noImplicitReturns": true,
"noUncheckedIndexedAccess": true,
"forceConsistentCasingInFileNames": true
}, },
"include": [ "include": ["./src/**/*"],
"src/" "exclude": ["./src/**/*.spec.ts"]
],
"exclude": [
"src/**/*.spec.ts"
]
} }

View file

@ -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',
},
],
},
},
],
};

View file

@ -10,7 +10,6 @@ coverage
node_modules/ node_modules/
jspm_packages/ jspm_packages/
package-lock.json package-lock.json
npm-shrinkwrap.json
# Compiled TypeScript files # Compiled TypeScript files
dist dist

View file

@ -1,7 +1,7 @@
# #
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
# #
FROM node:18.16 AS builder FROM node:18 AS builder
WORKDIR /usr/src/app WORKDIR /usr/src/app

View file

@ -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,
},
},
});

File diff suppressed because it is too large Load diff

View file

@ -5,10 +5,10 @@
"main": "dist/index.js", "main": "dist/index.js",
"typings": "dist/index.d.ts", "typings": "dist/index.d.ts",
"engines": { "engines": {
"node": ">=18.12.0" "node": ">=18"
}, },
"scripts": { "scripts": {
"lint": "eslint ./src --ext .ts", "lint": "eslint src",
"pretest": "npm run lint", "pretest": "npm run lint",
"test": "", "test": "",
"start": "set -x && fabric-chaincode-node start", "start": "set -x && fabric-chaincode-node start",
@ -32,12 +32,12 @@
"sort-keys-recursive": "^2.1.7" "sort-keys-recursive": "^2.1.7"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node18": "^2.0.0", "@types/node": "^18.19.33",
"@types/node": "^18.16.1", "@eslint/js": "^9.3.0",
"@typescript-eslint/eslint-plugin": "^5.30.7", "@tsconfig/node18": "^18.2.4",
"@typescript-eslint/parser": "^5.30.7", "eslint": "^8.57.0",
"eslint": "^8.20.0", "typescript": "~5.4.5",
"typescript": "~5.0.4" "typescript-eslint": "^7.11.0"
}, },
"nyc": { "nyc": {
"extension": [ "extension": [

View file

@ -21,10 +21,6 @@ export class Asset {
@Property('Size', 'number') @Property('Size', 'number')
Size = 0; Size = 0;
constructor() {
// Nothing to do
}
static newInstance(state: Partial<Asset> = {}): Asset { static newInstance(state: Partial<Asset> = {}): Asset {
return { return {
ID: assertHasValue(state.ID, 'Missing ID'), ID: assertHasValue(state.ID, 'Missing ID'),

View file

@ -50,7 +50,7 @@ export class AssetTransferContract extends Contract {
async #readAsset(ctx: Context, id: string): Promise<Uint8Array> { async #readAsset(ctx: Context, id: string): Promise<Uint8Array> {
const assetBytes = await ctx.stub.getState(id); // get the asset from chaincode state 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`); throw new Error(`Sorry, asset ${id} has not been created`);
} }
@ -64,7 +64,7 @@ export class AssetTransferContract extends Contract {
@Transaction() @Transaction()
@Param('assetObj', 'Asset', 'Part formed JSON of Asset') @Param('assetObj', 'Asset', 'Part formed JSON of Asset')
async UpdateAsset(ctx: Context, assetUpdate: Asset): Promise<void> { async UpdateAsset(ctx: Context, assetUpdate: Asset): Promise<void> {
if (assetUpdate.ID === undefined) { if (!assetUpdate.ID) {
throw new Error('No asset ID specified'); throw new Error('No asset ID specified');
} }
@ -113,7 +113,7 @@ export class AssetTransferContract extends Contract {
@Returns('boolean') @Returns('boolean')
async AssetExists(ctx: Context, id: string): Promise<boolean> { async AssetExists(ctx: Context, id: string): Promise<boolean> {
const assetJson = await ctx.stub.getState(id); const assetJson = await ctx.stub.getState(id);
return assetJson?.length > 0; return assetJson.length > 0;
} }
/** /**

View file

@ -4,16 +4,14 @@
"compilerOptions": { "compilerOptions": {
"experimentalDecorators": true, "experimentalDecorators": true,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"outDir": "dist",
"declaration": true, "declaration": true,
"declarationMap": true,
"sourceMap": true, "sourceMap": true,
"outDir": "dist",
"strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noImplicitReturns": true "noImplicitReturns": true,
"forceConsistentCasingInFileNames": true
}, },
"include": [ "include": ["src/"]
"./src/**/*"
],
"exclude": [
"./src/**/*.spec.ts"
]
} }

View file

@ -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

View file

@ -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,
},
},
});

View file

@ -10,24 +10,24 @@
"build": "tsc", "build": "tsc",
"prepare": "npm run build", "prepare": "npm run build",
"clean": "rimraf dist", "clean": "rimraf dist",
"lint": "eslint . --ext .ts", "lint": "eslint src",
"start": "SOFTHSM2_CONF=${HOME}/softhsm2.conf node dist/hsm-sample.js", "start": "SOFTHSM2_CONF=${HOME}/softhsm2.conf node dist/hsm-sample.js",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"author": "", "author": "",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@grpc/grpc-js": "^1.9.7", "@grpc/grpc-js": "^1.10",
"@hyperledger/fabric-gateway": "~1.4.0" "@hyperledger/fabric-gateway": "^1.5"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node18": "^18.2.2", "@types/node": "^18.19.33",
"@types/node": "^18.18.6", "@eslint/js": "^9.3.0",
"@typescript-eslint/eslint-plugin": "^6.9.0", "@tsconfig/node18": "^18.2.4",
"@typescript-eslint/parser": "^6.9.0", "eslint": "^8.57.0",
"eslint": "^8.52.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"rimraf": "^5.0.1", "rimraf": "^5.0.1",
"typescript": "~5.2.2" "typescript": "~5.4.5",
"typescript-eslint": "^7.11.0"
} }
} }

View file

@ -13,7 +13,7 @@ import { TextDecoder } from 'util';
const mspId = 'Org1MSP'; const mspId = 'Org1MSP';
const user = 'HSMUser'; const user = 'HSMUser';
const assetId = `asset${Date.now()}`; const assetId = `asset${String(Date.now())}`;
const utf8Decoder = new TextDecoder(); const utf8Decoder = new TextDecoder();
// Sample uses fabric-ca-client generated HSM identities, certificate is located in the signcerts directory // 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<void> {
const resultBytes = await contract.evaluateTransaction('ReadAsset', assetId); const resultBytes = await contract.evaluateTransaction('ReadAsset', assetId);
const resultJson = utf8Decoder.decode(resultBytes); const resultJson = utf8Decoder.decode(resultBytes);
const result = JSON.parse(resultJson); const result: unknown = JSON.parse(resultJson);
console.log('*** Result:', result); console.log('*** Result:', result);
} }
@ -167,7 +167,7 @@ function envOrDefault(key: string, defaultValue: string): string {
return process.env[key] || defaultValue; return process.env[key] || defaultValue;
} }
main().catch(error => { main().catch((error: unknown) => {
console.error('******** FAILED to run the application:', error); console.error('******** FAILED to run the application:', error);
process.exitCode = 1; process.exitCode = 1;
}); });

View file

@ -1,18 +1,15 @@
{ {
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@tsconfig/node18/tsconfig.json", "extends": "@tsconfig/node18/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "dist",
"declaration": true, "declaration": true,
"declarationMap": true, "declarationMap": true,
"sourceMap": true, "sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"noUncheckedIndexedAccess": true,
"forceConsistentCasingInFileNames": true "forceConsistentCasingInFileNames": true
}, },
"include": [ "include": ["./src/**/*"],
"src/" "exclude": ["./src/**/*.spec.ts"]
]
} }

View file

@ -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',
},
],
},
},
],
};

View file

@ -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,
},
},
});

View file

@ -10,7 +10,7 @@
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"build:watch": "tsc -w", "build:watch": "tsc -w",
"lint": "eslint ./src --ext .ts", "lint": "eslint src",
"prepare": "npm run build", "prepare": "npm run build",
"pretest": "npm run lint", "pretest": "npm run lint",
"start": "node ./dist/app" "start": "node ./dist/app"
@ -18,16 +18,16 @@
"author": "Hyperledger", "author": "Hyperledger",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@grpc/grpc-js": "^1.9.7", "@grpc/grpc-js": "^1.10",
"@hyperledger/fabric-gateway": "~1.4.0", "@hyperledger/fabric-gateway": "^1.5",
"@hyperledger/fabric-protos": "^0.2.1" "@hyperledger/fabric-protos": "^0.2.1"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node18": "^18.2.2", "@types/node": "^18.19.33",
"@types/node": "^18.18.6", "@eslint/js": "^9.3.0",
"@typescript-eslint/eslint-plugin": "^6.9.0", "@tsconfig/node18": "^18.2.4",
"@typescript-eslint/parser": "^6.9.0", "eslint": "^8.57.0",
"eslint": "^8.52.0", "typescript": "~5.4.5",
"typescript": "~5.2.2" "typescript-eslint": "^7.11.0"
} }
} }

View file

@ -47,7 +47,7 @@ function printUsage(): void {
console.log('Available commands:', Object.keys(allCommands).join(', ')); console.log('Available commands:', Object.keys(allCommands).join(', '));
} }
main().catch(error => { main().catch((error: unknown) => {
if (error instanceof ExpectedError) { if (error instanceof ExpectedError) {
console.log(error); console.log(error);
} else { } else {

View file

@ -35,10 +35,18 @@ export function parseBlock(block: common.Block): Block {
return { return {
getNumber: () => BigInt(header.getNumber()), getNumber: () => BigInt(header.getNumber()),
getTransactions: cache( getTransactions: cache(() =>
() => getPayloads(block) getPayloads(block)
.map((payload, i) => parsePayload(payload, validationCodes[i])) .map((payload, i) =>
.filter(payload => payload.isEndorserTransaction()) parsePayload(
payload,
assertDefined(
validationCodes[i],
`Missing validation code index {String(i)}`
)
)
)
.filter((payload) => payload.isEndorserTransaction())
.map(newTransaction) .map(newTransaction)
), ),
toProto: () => block, toProto: () => block,
@ -67,15 +75,23 @@ interface ReadWriteSet {
function parsePayload(payload: common.Payload, statusCode: number): Payload { function parsePayload(payload: common.Payload, statusCode: number): Payload {
const cachedChannelHeader = cache(() => getChannelHeader(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 { return {
getChannelHeader: cachedChannelHeader, getChannelHeader: cachedChannelHeader,
getEndorserTransaction: () => { getEndorserTransaction: () => {
if (!isEndorserTransaction()) { 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); return parseEndorserTransaction(transaction);
}, },
getSignatureHeader: cache(() => getSignatureHeader(payload)), 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 { return {
getReadWriteSets: cache( getReadWriteSets: cache(() =>
() => getChaincodeActionPayloads(transaction) getChaincodeActionPayloads(transaction)
.map(payload => assertDefined(payload.getAction(), 'Missing chaincode endorsed action')) .map((payload) =>
.map(endorsedAction => endorsedAction.getProposalResponsePayload_asU8()) assertDefined(
.map(bytes => peer.ProposalResponsePayload.deserializeBinary(bytes)) payload.getAction(),
.map(responsePayload => peer.ChaincodeAction.deserializeBinary(responsePayload.getExtension_asU8())) 'Missing chaincode endorsed action'
.map(chaincodeAction => chaincodeAction.getResults_asU8()) )
.map(bytes => ledger.rwset.TxReadWriteSet.deserializeBinary(bytes)) )
.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) .map(parseReadWriteSet)
), ),
toProto: () => transaction, toProto: () => transaction,
@ -109,67 +142,88 @@ function newTransaction(payload: Payload): Transaction {
getChannelHeader: () => payload.getChannelHeader(), getChannelHeader: () => payload.getChannelHeader(),
getCreator: () => { getCreator: () => {
const creatorBytes = payload.getSignatureHeader().getCreator_asU8(); const creatorBytes = payload.getSignatureHeader().getCreator_asU8();
const creator = msp.SerializedIdentity.deserializeBinary(creatorBytes); const creator =
msp.SerializedIdentity.deserializeBinary(creatorBytes);
return { return {
mspId: creator.getMspid(), mspId: creator.getMspid(),
credentials: creator.getIdBytes_asU8(), credentials: creator.getIdBytes_asU8(),
}; };
}, },
getNamespaceReadWriteSets: () => transaction.getReadWriteSets() getNamespaceReadWriteSets: () =>
.flatMap(readWriteSet => readWriteSet.getNamespaceReadWriteSets()), transaction
.getReadWriteSets()
.flatMap((readWriteSet) =>
readWriteSet.getNamespaceReadWriteSets()
),
getValidationCode: () => payload.getTransactionValidationCode(), getValidationCode: () => payload.getTransactionValidationCode(),
isValid: () => payload.isValid(), isValid: () => payload.isValid(),
toProto: () => payload.toProto(), toProto: () => payload.toProto(),
}; };
} }
function parseReadWriteSet(readWriteSet: ledger.rwset.TxReadWriteSet): ReadWriteSet { function parseReadWriteSet(
readWriteSet: ledger.rwset.TxReadWriteSet
): ReadWriteSet {
return { return {
getNamespaceReadWriteSets: () => { getNamespaceReadWriteSets: () =>
if (readWriteSet.getDataModel() !== ledger.rwset.TxReadWriteSet.DataModel.KV) { readWriteSet.getNsRwsetList().map(parseNamespaceReadWriteSet),
throw new Error(`Unexpected read/write set data model: ${readWriteSet.getDataModel()}`);
}
return readWriteSet.getNsRwsetList().map(parseNamespaceReadWriteSet);
},
toProto: () => readWriteSet, toProto: () => readWriteSet,
}; };
} }
function parseNamespaceReadWriteSet(nsReadWriteSet: ledger.rwset.NsReadWriteSet): NamespaceReadWriteSet { function parseNamespaceReadWriteSet(
nsReadWriteSet: ledger.rwset.NsReadWriteSet
): NamespaceReadWriteSet {
return { return {
getNamespace: () => nsReadWriteSet.getNamespace(), getNamespace: () => nsReadWriteSet.getNamespace(),
getReadWriteSet: cache( getReadWriteSet: cache(() =>
() => ledger.rwset.kvrwset.KVRWSet.deserializeBinary(nsReadWriteSet.getRwset_asU8()) ledger.rwset.kvrwset.KVRWSet.deserializeBinary(
nsReadWriteSet.getRwset_asU8()
)
), ),
toProto: () => nsReadWriteSet, toProto: () => nsReadWriteSet,
}; };
} }
function getTransactionValidationCodes(block: common.Block): Uint8Array { function getTransactionValidationCodes(block: common.Block): Uint8Array {
const metadata = assertDefined(block.getMetadata(), 'Missing block metadata'); const metadata = assertDefined(
return metadata.getMetadataList_asU8()[common.BlockMetadataIndex.TRANSACTIONS_FILTER]; 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[] { function getPayloads(block: common.Block): common.Payload[] {
return (block.getData()?.getDataList_asU8() ?? []) return (block.getData()?.getDataList_asU8() ?? [])
.map(bytes => common.Envelope.deserializeBinary(bytes)) .map((bytes) => common.Envelope.deserializeBinary(bytes))
.map(envelope => envelope.getPayload_asU8()) .map((envelope) => envelope.getPayload_asU8())
.map(bytes => common.Payload.deserializeBinary(bytes)); .map((bytes) => common.Payload.deserializeBinary(bytes));
} }
function getChannelHeader(payload: common.Payload): common.ChannelHeader { function getChannelHeader(payload: common.Payload): common.ChannelHeader {
const header = assertDefined(payload.getHeader(), 'Missing payload header'); 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 { function getSignatureHeader(payload: common.Payload): common.SignatureHeader {
const header = assertDefined(payload.getHeader(), 'Missing payload header'); const header = assertDefined(payload.getHeader(), 'Missing payload header');
return common.SignatureHeader.deserializeBinary(header.getSignatureHeader_asU8()); return common.SignatureHeader.deserializeBinary(
header.getSignatureHeader_asU8()
);
} }
function getChaincodeActionPayloads(transaction: peer.Transaction): peer.ChaincodeActionPayload[] { function getChaincodeActionPayloads(
return transaction.getActionsList() transaction: peer.Transaction
.map(transactionAction => transactionAction.getPayload_asU8()) ): peer.ChaincodeActionPayload[] {
.map(bytes => peer.ChaincodeActionPayload.deserializeBinary(bytes)); return transaction
.getActionsList()
.map((transactionAction) => transactionAction.getPayload_asU8())
.map((bytes) => peer.ChaincodeActionPayload.deserializeBinary(bytes));
} }

View file

@ -70,10 +70,11 @@ async function newIdentity(): Promise<Identity> {
async function newSigner(): Promise<Signer> { async function newSigner(): Promise<Signer> {
const keyFiles = await fs.readdir(keyDirectoryPath); 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}`); 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 privateKeyPem = await fs.readFile(keyPath);
const privateKey = crypto.createPrivateKey(privateKeyPem); const privateKey = crypto.createPrivateKey(privateKeyPem);
return signers.newPrivateKeySigner(privateKey); return signers.newPrivateKeySigner(privateKey);

View file

@ -20,7 +20,10 @@ export async function main(client: Client): Promise<void> {
const smartContract = new AssetTransferBasic(contract); const smartContract = new AssetTransferBasic(contract);
const assets = await smartContract.getAllAssets(); const assets = await smartContract.getAllAssets();
const assetsJson = JSON.stringify(assets, undefined, 2); 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 { } finally {
gateway.close(); gateway.close();
} }

View file

@ -87,10 +87,10 @@ export async function main(client: Client): Promise<void> {
const network = gateway.getNetwork(channelName); const network = gateway.getNetwork(channelName);
const checkpointer = await checkpointers.file(checkpointFile); 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()); console.log('Last processed transaction ID within block:', checkpointer.getTransactionId());
if (simulatedFailureCount > 0) { 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({ const blocks = await network.getBlockEvents({
@ -135,7 +135,7 @@ class BlockProcessor {
async process(): Promise<void> { async process(): Promise<void> {
const blockNumber = this.#block.getNumber(); const blockNumber = this.#block.getNumber();
console.log(`\nReceived block ${blockNumber}`); console.log(`\nReceived block ${String(blockNumber)}`);
const validTransactions = this.#getNewTransactions() const validTransactions = this.#getNewTransactions()
.filter(transaction => transaction.isValid()); .filter(transaction => transaction.isValid());
@ -168,7 +168,7 @@ class BlockProcessor {
const blockTransactionIds = transactions.map(transaction => transaction.getChannelHeader().getTxId()); const blockTransactionIds = transactions.map(transaction => transaction.getChannelHeader().getTxId());
const lastProcessedIndex = blockTransactionIds.indexOf(lastTransactionId); const lastProcessedIndex = blockTransactionIds.indexOf(lastTransactionId);
if (lastProcessedIndex < 0) { 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); return transactions.slice(lastProcessedIndex + 1);

View file

@ -9,7 +9,8 @@
* @param values Candidate elements. * @param values Candidate elements.
*/ */
export function randomElement<T>(values: T[]): T { export function randomElement<T>(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<unknown>[]): Promise<void>
if (failures.length > 0) { if (failures.length > 0) {
const failMessages = ' - ' + failures.join('\n - '); const failMessages = ' - ' + failures.join('\n - ');
throw new Error(`${failures.length} failures:\n${failMessages}\n`); throw new Error(`${String(failures.length)} failures:\n${failMessages}\n`);
} }
} }

View file

@ -1,19 +1,15 @@
{ {
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@tsconfig/node18/tsconfig.json", "extends": "@tsconfig/node18/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "dist",
"declaration": true, "declaration": true,
"declarationMap": true, "declarationMap": true,
"sourceMap": true, "sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"noUnusedLocals": true, "noUnusedLocals": true,
"noImplicitReturns": true "noImplicitReturns": true,
"noUncheckedIndexedAccess": true,
"forceConsistentCasingInFileNames": true
}, },
"include": [ "include": ["./src/**/*"],
"src/" "exclude": ["./src/**/*.spec.ts"]
],
"exclude": [
"src/**/*.spec.ts"
]
} }