Skip to content

Adding ServiceBus Trigger Chnages #353

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: v4.x-preview
Choose a base branch
from
Open
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
15 changes: 14 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,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
Comment on lines +50 to +51
Copy link
Preview

Copilot AI Jun 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The optional chaining on data and its properties is unnecessary here since the enclosing else if ensures collectionModelBindingData and modelBindingData are defined. You can simplify this to data.collectionModelBindingData.modelBindingData[0].source.

Suggested change
data?.collectionModelBindingData?.modelBindingData[0]?.source,
data?.collectionModelBindingData?.modelBindingData
data.collectionModelBindingData.modelBindingData[0].source,
data.collectionModelBindingData.modelBindingData

Copilot uses AI. Check for mistakes.

Comment on lines +45 to +51
Copy link
Preview

Copilot AI Jun 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider validating that modelBindingData is non-empty before accessing the first element. If the array is empty, calling createClient with an undefined source may lead to unintended behavior.

Suggested change
} 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
} 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

Copilot uses AI. Check for mistakes.

);
} 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
Copy link
Preview

Copilot AI Jun 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stubbed exception message ('String exception for collection') does not match the expected test assertion ('Sinon-provided String exception for collection'). Update either the thrown string to 'Sinon-provided String exception for collection' or adjust the expected error message in the test.

Suggested change
createClient: sinon.stub().throws('String exception for collection'), // Non-Error exception
createClient: sinon.stub().throws('Sinon-provided String exception for collection'), // Non-Error exception

Copilot uses AI. Check for mistakes.

};
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