Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions libraries/fabric-shim/lib/stub.js
Original file line number Diff line number Diff line change
Expand Up @@ -937,6 +937,62 @@ class ChaincodeStub {
return await this.handler.handleGetState(collection, key, this.channel_id, this.txId);
}

/**
* Retrieves the current values of the state variables for multiple keys.
* This is a fallback implementation that processes reads without actual gRPC batching.
* @async
* @param {string[]} keys Array of state variable keys to retrieve from the state store
* @returns {Promise<byte[][]>} Promise for an array of the current values of the state variables
*/
async getMultipleStates(keys) {
logger.debug('getMultipleStates called with keys:%j', keys);
if (!Array.isArray(keys)) {
throw new Error('keys must be an array of strings');
}

const promises = keys.map(key => this.getState(key));
return await Promise.all(promises);
}

/**
* getMultiplePrivateData returns the values of the specified `keys` from the specified `collection`.
* This is a fallback implementation that processes reads without actual gRPC batching.
* @async
* @param {string} collection The collection name
* @param {string[]} keys Array of private data variable keys to retrieve from the state store
* @returns {Promise<byte[][]>} Promise for an array of private values from the state store
*/
async getMultiplePrivateData(collection, keys) {
logger.debug('getMultiplePrivateData called with collection:%s, keys:%j', collection, keys);
if (!collection || typeof collection !== 'string') {
throw new Error('collection must be a valid string');
}
if (!Array.isArray(keys)) {
throw new Error('keys must be an array of strings');
}

const promises = keys.map(key => this.getPrivateData(collection, key));
return await Promise.all(promises);
}

/**
* startWriteBatch indicates the beginning of a block of PutState/PutPrivateData calls
* that should be batched together.
* (Fallback behavior: no-op)
*/
startWriteBatch() {
logger.debug('startWriteBatch called');
}

/**
* finishWriteBatch sends the currently accumulated batch of state writes to the peer.
* (Fallback behavior: no-op)
* @async
*/
async finishWriteBatch() {
logger.debug('finishWriteBatch called');
}

/**
* getPrivateDataHash returns the hash of the value of the specified `key` from
* the specified `collection`.
Expand Down
73 changes: 73 additions & 0 deletions libraries/fabric-shim/test/unit/stub.js
Original file line number Diff line number Diff line change
Expand Up @@ -1103,6 +1103,79 @@ describe('Stub', () => {
});
});

describe('getMultipleStates', () => {
let stub;

beforeEach(() => {
stub = new Stub({
handleGetState: sinon.stub().resolves(Buffer.from('value'))
}, 'dummyChannelId', 'dummyTxid', chaincodeInput);
});

it('should throw an error if keys is not an array', async () => {
await expect(stub.getMultipleStates('not-an-array')).to.be.rejectedWith(/keys must be an array of strings/);
});

it('should call getState for each key and return the results', async () => {
sandbox.stub(stub, 'getState').callsFake(async (key) => {
if (key === 'key1') {
return Buffer.from('value1');
} else if (key === 'key2') {
return Buffer.from('value2');
}
});

const results = await stub.getMultipleStates(['key1', 'key2']);
expect(results).to.deep.equal([Buffer.from('value1'), Buffer.from('value2')]);
sinon.assert.calledTwice(stub.getState);
});
});

describe('getMultiplePrivateData', () => {
let stub;

beforeEach(() => {
stub = new Stub({
handleGetState: sinon.stub().resolves(Buffer.from('value'))
}, 'dummyChannelId', 'dummyTxid', chaincodeInput);
});

it('should throw an error if collection is missing', async () => {
await expect(stub.getMultiplePrivateData(null, ['key1'])).to.be.rejectedWith(/collection must be a valid string/);
});

it('should throw an error if keys is not an array', async () => {
await expect(stub.getMultiplePrivateData('collection', 'not-an-array')).to.be.rejectedWith(/keys must be an array of strings/);
});

it('should call getPrivateData for each key and return the results', async () => {
sandbox.stub(stub, 'getPrivateData').callsFake(async (collection, key) => {
if (key === 'key1') {
return Buffer.from('value1');
} else if (key === 'key2') {
return Buffer.from('value2');
}
});

const results = await stub.getMultiplePrivateData('collection', ['key1', 'key2']);
expect(results).to.deep.equal([Buffer.from('value1'), Buffer.from('value2')]);
sinon.assert.calledTwice(stub.getPrivateData);
});
});

describe('Write Batching Fallbacks', () => {
it('should execute startWriteBatch as a no-op', () => {
const stub = new Stub('dummyClient', 'dummyChannelId', 'dummyTxid', chaincodeInput);
stub.startWriteBatch();
});

it('should execute finishWriteBatch as a no-op', async () => {
const stub = new Stub('dummyClient', 'dummyChannelId', 'dummyTxid', chaincodeInput);
await stub.finishWriteBatch();
});
});


describe('getPrivateDataHash', () => {
let handleGetPrivateDataHashStub;
let stub;
Expand Down
5 changes: 5 additions & 0 deletions libraries/fabric-shim/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ declare module 'fabric-shim' {
getPrivateDataByRange(collection: string, startKey: string, endKey: string): Promise<Iterators.StateQueryIterator> & AsyncIterable<Iterators.KV>;
getPrivateDataByPartialCompositeKey(collection: string, objectType: string, attributes: string[]): Promise<Iterators.StateQueryIterator> & AsyncIterable<Iterators.KV>;
getPrivateDataQueryResult(collection: string, query: string): Promise<Iterators.StateQueryIterator> & AsyncIterable<Iterators.KV>;

getMultipleStates(keys: string[]): Promise<Uint8Array[]>;
getMultiplePrivateData(collection: string, keys: string[]): Promise<Uint8Array[]>;
startWriteBatch(): void;
finishWriteBatch(): Promise<void>;
}

export class KeyEndorsementPolicy {
Expand Down
Loading