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
51 changes: 43 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -28,7 +28,7 @@
"README.md"
],
"engines": {
"node": ">=18.0"
"node": ">=20.0"
},
"scripts": {
"build": "webpack --mode development",
Expand All @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
@@ -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';
19 changes: 18 additions & 1 deletion src/converters/fromRpcTypedData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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)}`
);
}
}
}

Expand Down
161 changes: 161 additions & 0 deletions test/converters/fromRpcTypedData.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
});
6 changes: 6 additions & 0 deletions types-core/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
12 changes: 12 additions & 0 deletions types/serviceBus.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, <see cref="https://aka.ms/AAp8dm9"/>"
*/
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;

Expand Down