diff --git a/package-lock.json b/package-lock.json index 48a9122..cee6622 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@azure/functions", - "version": "4.7.1-preview", + "version": "4.7.2-preview", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@azure/functions", - "version": "4.7.1-preview", + "version": "4.7.2-preview", "license": "MIT", "dependencies": { - "@azure/functions-extensions-base": "0.1.0-preview", + "@azure/functions-extensions-base": "0.2.0-preview", "cookie": "^0.7.0", "long": "^4.0.0", "undici": "^5.13.0" @@ -55,18 +55,53 @@ "webpack-cli": "^4.10.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" } }, - "node_modules/@azure/functions-extensions-base": { - "version": "0.1.0-preview", - "resolved": "https://registry.npmjs.org/@azure/functions-extensions-base/-/functions-extensions-base-0.1.0-preview.tgz", - "integrity": "sha512-Bl94Xst8UpUoSMrJ70ZuFA9E+sIH5v1siKAAkgle7BETdWyn2Aq5USQ7np7LuJtVgUp3+6PeiP7/Fw8gPpzrZQ==", + "../azure-functions-nodejs-extensions/azure-functions-nodejs-extensions-base": { + "name": "@azure/functions-extensions-base", + "version": "0.2.0-preview", "license": "MIT", + "devDependencies": { + "@types/chai": "^4.2.22", + "@types/minimist": "^1.2.2", + "@types/mocha": "^9.1.1", + "@types/node": "18.0.0", + "@types/sinon": "^17.0.4", + "@typescript-eslint/eslint-plugin": "^5.12.1", + "@typescript-eslint/parser": "^5.12.1", + "chai": "^4.2.0", + "eslint": "^7.32.0", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-deprecation": "^1.3.2", + "eslint-plugin-header": "^3.1.1", + "eslint-plugin-import": "^2.29.0", + "eslint-plugin-prettier": "^4.0.0", + "eslint-plugin-simple-import-sort": "^10.0.0", + "eslint-webpack-plugin": "^3.2.0", + "fork-ts-checker-webpack-plugin": "^7.2.13", + "globby": "^11.0.0", + "minimist": "^1.2.8", + "mocha": "^11.1.0", + "mocha-junit-reporter": "^2.0.2", + "mocha-multi-reporters": "^1.5.1", + "prettier": "^2.4.1", + "sinon": "^20.0.0", + "ts-loader": "^9.3.1", + "ts-node": "^3.3.0", + "typescript": "^4.5.5", + "typescript4": "npm:typescript@~4.2.0", + "webpack": "^5.74.0", + "webpack-cli": "^4.10.0" + }, "engines": { "node": ">=18.0" } }, + "node_modules/@azure/functions-extensions-base": { + "resolved": "../azure-functions-nodejs-extensions/azure-functions-nodejs-extensions-base", + "link": true + }, "node_modules/@babel/code-frame": { "version": "7.12.11", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", diff --git a/package.json b/package.json index 93be00c..b8361ce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@azure/functions", - "version": "4.7.1-preview", + "version": "4.7.2-preview", "description": "Microsoft Azure Functions NodeJS Framework", "keywords": [ "azure", @@ -28,7 +28,7 @@ "README.md" ], "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "scripts": { "build": "webpack --mode development", @@ -41,7 +41,7 @@ "watch": "webpack --watch --mode development" }, "dependencies": { - "@azure/functions-extensions-base": "0.1.0-preview", + "@azure/functions-extensions-base": "0.2.0-preview", "cookie": "^0.7.0", "long": "^4.0.0", "undici": "^5.13.0" diff --git a/src/constants.ts b/src/constants.ts index e911704..a082113 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,6 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. -export const version = '4.7.1-preview'; +export const version = '4.7.2-preview'; export const returnBindingKey = '$return'; diff --git a/src/converters/fromRpcTypedData.ts b/src/converters/fromRpcTypedData.ts index 54ce42b..fa1a919 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,24 @@ export function fromRpcTypedData(data: RpcTypedData | null | undefined): unknown `Error: ${exception instanceof Error ? exception.message : String(exception)}` ); } + } else if ( + data.collectionModelBindingData && + isDefined(data.collectionModelBindingData.modelBindingData) && + data.collectionModelBindingData.modelBindingData.length > 0 + ) { + 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;