diff --git a/src/converters/fromRpcTypedData.ts b/src/converters/fromRpcTypedData.ts index 54ce42b..0dc25b6 100644 --- a/src/converters/fromRpcTypedData.ts +++ b/src/converters/fromRpcTypedData.ts @@ -33,7 +33,6 @@ export function fromRpcTypedData(data: RpcTypedData | null | undefined): unknown return data.collectionSint64.sint64; } else if (data.modelBindingData && isDefined(data.modelBindingData.content)) { try { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call const resourceFactoryResolver: ResourceFactoryResolver = ResourceFactoryResolver.getInstance(); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call return resourceFactoryResolver.createClient(data.modelBindingData.source, data.modelBindingData); @@ -43,6 +42,20 @@ export function fromRpcTypedData(data: RpcTypedData | null | undefined): unknown `Error: ${exception instanceof Error ? exception.message : String(exception)}` ); } + } else if (data.collectionModelBindingData && isDefined(data.collectionModelBindingData.modelBindingData)) { + try { + const resourceFactoryResolver: ResourceFactoryResolver = ResourceFactoryResolver.getInstance(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + return resourceFactoryResolver.createClient( + data?.collectionModelBindingData?.modelBindingData[0]?.source, + data?.collectionModelBindingData?.modelBindingData + ); + } catch (exception) { + throw new Error( + 'Unable to create client. Please register the extensions library with your function app. ' + + `Error: ${exception instanceof Error ? exception.message : String(exception)}` + ); + } } } diff --git a/test/converters/fromRpcTypedData.test.ts b/test/converters/fromRpcTypedData.test.ts index 45d1545..40e70ed 100644 --- a/test/converters/fromRpcTypedData.test.ts +++ b/test/converters/fromRpcTypedData.test.ts @@ -270,3 +270,164 @@ describe('fromRpcTypedData - modelBindingData path', () => { ); }); }); +describe('fromRpcTypedData - collectionModelBindingData path', () => { + let sandbox: sinon.SinonSandbox; + let originalGetInstance: typeof ResourceFactoryResolver.getInstance; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + originalGetInstance = ResourceFactoryResolver.getInstance.bind(ResourceFactoryResolver); + }); + + afterEach(() => { + sandbox.restore(); + ResourceFactoryResolver.getInstance = originalGetInstance; + }); + + it('should successfully create a client when collectionModelBindingData is valid', () => { + const mockClient = { name: 'testCollectionClient' }; + const mockResolver = { + createClient: sinon.stub().returns(mockClient), + }; + ResourceFactoryResolver.getInstance = sinon.stub().returns(mockResolver); + + const collectionModelBindingData = { + modelBindingData: [ + { + content: Buffer.from('test-content-1'), + source: 'blob', + contentType: 'application/octet-stream', + }, + { + content: Buffer.from('test-content-2'), + source: 'blob', + contentType: 'application/octet-stream', + }, + ], + }; + + const data: RpcTypedData = { + collectionModelBindingData, + }; + + const result = fromRpcTypedData(data); + + sinon.assert.calledWith(mockResolver.createClient, 'blob', collectionModelBindingData.modelBindingData); + expect(result).to.equal(mockClient); + }); + + it('should handle collectionModelBindingData with undefined source', () => { + const mockClient = { name: 'testCollectionClient' }; + const mockResolver = { + createClient: sinon.stub().returns(mockClient), + }; + ResourceFactoryResolver.getInstance = sinon.stub().returns(mockResolver); + + const collectionModelBindingData = { + modelBindingData: [ + { + content: Buffer.from('test-content-1'), + // source is undefined + contentType: 'application/octet-stream', + }, + ], + }; + + const data: RpcTypedData = { + collectionModelBindingData, + }; + + const result = fromRpcTypedData(data); + + expect(mockResolver.createClient.calledWith(undefined, collectionModelBindingData.modelBindingData)).to.be.true; + expect(result).to.equal(mockClient); + }); + + it('should throw enhanced error when ResourceFactoryResolver.createClient throws for collectionModelBindingData', () => { + const originalError = new Error('Collection factory not registered'); + const mockResolver = { + createClient: sinon.stub().throws(originalError), + }; + ResourceFactoryResolver.getInstance = sinon.stub().returns(mockResolver); + + const collectionModelBindingData = { + modelBindingData: [ + { + content: Buffer.from('test-content-1'), + source: 'blob', + contentType: 'application/octet-stream', + }, + ], + }; + + const data: RpcTypedData = { + collectionModelBindingData, + }; + + expect(() => fromRpcTypedData(data)).to.throw( + 'Unable to create client. Please register the extensions library with your function app. ' + + 'Error: Collection factory not registered' + ); + }); + + it('should throw enhanced error when ResourceFactoryResolver.getInstance throws for collectionModelBindingData', () => { + const originalError = new Error('Collection resolver not initialized'); + ResourceFactoryResolver.getInstance = sinon.stub().throws(originalError); + + const collectionModelBindingData = { + modelBindingData: [ + { + content: Buffer.from('test-content-1'), + source: 'blob', + contentType: 'application/octet-stream', + }, + ], + }; + + const data: RpcTypedData = { + collectionModelBindingData, + }; + + expect(() => fromRpcTypedData(data)).to.throw( + 'Unable to create client. Please register the extensions library with your function app. ' + + 'Error: Collection resolver not initialized' + ); + }); + + it('should handle non-Error exceptions by converting to string for collectionModelBindingData', () => { + const mockResolver = { + createClient: sinon.stub().throws('String exception for collection'), // Non-Error exception + }; + ResourceFactoryResolver.getInstance = sinon.stub().returns(mockResolver); + + const collectionModelBindingData = { + modelBindingData: [ + { + content: Buffer.from('test-content-1'), + source: 'blob', + contentType: 'application/octet-stream', + }, + ], + }; + + const data: RpcTypedData = { + collectionModelBindingData, + }; + + expect(() => fromRpcTypedData(data)).to.throw( + 'Unable to create client. Please register the extensions library with your function app. ' + + 'Error: Sinon-provided String exception for collection' + ); + }); +}); + +describe('fromRpcTypedData - fallback/undefined cases', () => { + it('should return undefined for unknown data shape', () => { + const data: RpcTypedData = { foo: 'bar' } as any; + expect(fromRpcTypedData(data)).to.be.undefined; + }); + + it('should return undefined for empty object', () => { + expect(fromRpcTypedData({} as RpcTypedData)).to.be.undefined; + }); +}); diff --git a/types-core/index.d.ts b/types-core/index.d.ts index d86c3c4..fdcbdf4 100644 --- a/types-core/index.d.ts +++ b/types-core/index.d.ts @@ -441,6 +441,12 @@ declare module '@azure/functions-core' { collectionSint64?: RpcCollectionSInt64 | null; modelBindingData?: ModelBindingData | null; + + collectionModelBindingData?: CollectionModelBindingData | null; + } + + export interface CollectionModelBindingData { + modelBindingData?: ModelBindingData[] | null; } export interface ModelBindingData { diff --git a/types/serviceBus.d.ts b/types/serviceBus.d.ts index 0a45d7a..2234623 100644 --- a/types/serviceBus.d.ts +++ b/types/serviceBus.d.ts @@ -28,10 +28,22 @@ export interface ServiceBusQueueTriggerOptions { */ isSessionsEnabled?: boolean; + /** + * Gets or sets a value indicating whether the trigger should automatically complete the message after successful processing. + * If not explicitly set, the behavior will be based on the autoCompleteMessages configuration in host.json. + * For more information, " + */ + autoCompleteMessages?: boolean; + /** * Set to `many` in order to enable batching. If omitted or set to `one`, a single message is passed to the function. */ cardinality?: 'many' | 'one'; + + /** + * Whether to use sdk binding for this blob operation. + * */ + sdkBinding?: boolean; } export type ServiceBusQueueTrigger = FunctionTrigger & ServiceBusQueueTriggerOptions;