diff --git a/src/app.ts b/src/app.ts index 9659545..54b2995 100644 --- a/src/app.ts +++ b/src/app.ts @@ -3,6 +3,7 @@ import { CosmosDBFunctionOptions, + CosmosDBMongoFunctionOptions, EventGridFunctionOptions, EventHubFunctionOptions, FunctionTrigger, @@ -131,6 +132,11 @@ export function cosmosDB(name: string, options: CosmosDBFunctionOptions): void { generic(name, convertToGenericOptions(options, trigger.cosmosDB)); } +export function cosmosDBMongo(name: string, options: CosmosDBMongoFunctionOptions): void { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + generic(name, convertToGenericOptions(options, trigger.cosmosDBMongo)); +} + export function warmup(name: string, options: WarmupFunctionOptions): void { generic(name, convertToGenericOptions(options, trigger.warmup)); } diff --git a/src/input.ts b/src/input.ts index 60e3713..ad19a90 100644 --- a/src/input.ts +++ b/src/input.ts @@ -4,6 +4,8 @@ import { CosmosDBInput, CosmosDBInputOptions, + CosmosDBMongoInput, + CosmosDBMongoInputOptions, FunctionInput, GenericInputOptions, MySqlInput, @@ -42,6 +44,13 @@ export function cosmosDB(options: CosmosDBInputOptions): CosmosDBInput { }); } +export function cosmosDBMongo(options: CosmosDBMongoInputOptions): CosmosDBMongoInput { + return addInputBindingName({ + ...options, + type: 'cosmosDBMongo', + }); +} + export function sql(options: SqlInputOptions): SqlInput { return addInputBindingName({ ...options, diff --git a/src/output.ts b/src/output.ts index 0f0a781..50c926a 100644 --- a/src/output.ts +++ b/src/output.ts @@ -2,6 +2,8 @@ // Licensed under the MIT License. import { + CosmosDBMongoOutput, + CosmosDBMongoOutputOptions, CosmosDBOutput, CosmosDBOutputOptions, EventGridOutput, @@ -94,6 +96,13 @@ export function cosmosDB(options: CosmosDBOutputOptions): CosmosDBOutput { }); } +export function cosmosDBMongo(options: CosmosDBMongoOutputOptions): CosmosDBMongoOutput { + return addOutputBindingName({ + ...options, + type: 'cosmosDBMongo', + }); +} + export function sql(options: SqlOutputOptions): SqlOutput { return addOutputBindingName({ ...options, diff --git a/src/trigger.ts b/src/trigger.ts index 90103e5..80137d6 100644 --- a/src/trigger.ts +++ b/src/trigger.ts @@ -2,6 +2,8 @@ // Licensed under the MIT License. import { + CosmosDBMongoTrigger, + CosmosDBMongoTriggerOptions, CosmosDBTrigger, CosmosDBTriggerOptions, EventGridTrigger, @@ -104,6 +106,13 @@ export function cosmosDB(options: CosmosDBTriggerOptions): CosmosDBTrigger { }); } +export function cosmosDBMongo(options: CosmosDBMongoTriggerOptions): CosmosDBMongoTrigger { + return addTriggerBindingName({ + ...options, + type: 'cosmosDBMongoTrigger', + }); +} + export function warmup(options: WarmupTriggerOptions): WarmupTrigger { return addTriggerBindingName({ ...options, diff --git a/test/cosmosDBMongo.test.ts b/test/cosmosDBMongo.test.ts new file mode 100644 index 0000000..5fca9d4 --- /dev/null +++ b/test/cosmosDBMongo.test.ts @@ -0,0 +1,193 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. + +import 'mocha'; +import { expect } from 'chai'; +import { input, output, trigger } from '../src'; +import { toCoreFunctionMetadata } from '../src/converters/toCoreFunctionMetadata'; +import { InvocationContext } from '../types'; + +describe('cosmosDBMongo bindings', () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const handler = (_doc: unknown, _context: InvocationContext) => {}; + + const minimalTriggerOptions = { + connectionStringSetting: 'CosmosDBMongo', + databaseName: 'MyDatabase', + collectionName: 'MyCollection', + }; + + // ------------------------------------------------------------------------- + // trigger + // ------------------------------------------------------------------------- + + describe('trigger.cosmosDBMongo', () => { + it('produces correct type string', () => { + const trig = trigger.cosmosDBMongo(minimalTriggerOptions); + expect(trig.type).to.equal('cosmosDBMongoTrigger'); + }); + + it('copies all trigger options onto the binding object', () => { + const trig = trigger.cosmosDBMongo({ + connectionStringSetting: 'CosmosDBMongo', + databaseName: 'db', + collectionName: 'coll', + createIfNotExists: true, + triggerLevel: 'Database', + leaseDatabaseName: 'leaseDb', + leaseCollectionName: 'leases', + leaseConnectionStringSetting: 'LeaseConn', + tenantId: 'tenant-abc', + managedIdentityClientId: 'mi-abc', + leaseTenantId: 'lease-tenant', + leaseManagedIdentityClientId: 'lease-mi', + }); + expect(trig.connectionStringSetting).to.equal('CosmosDBMongo'); + expect(trig.databaseName).to.equal('db'); + expect(trig.collectionName).to.equal('coll'); + expect(trig.createIfNotExists).to.equal(true); + expect(trig.triggerLevel).to.equal('Database'); + expect(trig.leaseDatabaseName).to.equal('leaseDb'); + expect(trig.leaseCollectionName).to.equal('leases'); + expect(trig.leaseConnectionStringSetting).to.equal('LeaseConn'); + expect(trig.tenantId).to.equal('tenant-abc'); + expect(trig.managedIdentityClientId).to.equal('mi-abc'); + expect(trig.leaseTenantId).to.equal('lease-tenant'); + expect(trig.leaseManagedIdentityClientId).to.equal('lease-mi'); + }); + + it('generates a deterministic binding name with Trigger suffix', () => { + const trig1 = trigger.cosmosDBMongo(minimalTriggerOptions); + const trig2 = trigger.cosmosDBMongo(minimalTriggerOptions); + expect(trig1.name).to.equal(trig2.name); + expect(trig1.name).to.include('cosmosDBMongoTrigger'); + }); + + it('sets direction = in via toCoreFunctionMetadata', () => { + const result = toCoreFunctionMetadata('mongoTrigFunc', { + handler, + trigger: trigger.cosmosDBMongo(minimalTriggerOptions), + }); + const bindingValues = Object.values(result.bindings) as Record[]; + const trig = bindingValues.find((b) => b['type'] === 'cosmosDBMongoTrigger'); + expect(trig).to.exist; + expect(trig!['direction']).to.equal('in'); + }); + }); + + // ------------------------------------------------------------------------- + // input + // ------------------------------------------------------------------------- + + describe('input.cosmosDBMongo', () => { + it('produces correct type string', () => { + const inp = input.cosmosDBMongo({ + connectionStringSetting: 'CosmosDBMongo', + databaseName: 'db', + collectionName: 'coll', + }); + expect(inp.type).to.equal('cosmosDBMongo'); + }); + + it('copies all input options onto the binding object', () => { + const inp = input.cosmosDBMongo({ + connectionStringSetting: 'CosmosDBMongo', + databaseName: 'db', + collectionName: 'coll', + queryString: '{"status": "active"}', + createIfNotExists: false, + tenantId: 'tenant-xyz', + managedIdentityClientId: 'mi-xyz', + }); + expect(inp.connectionStringSetting).to.equal('CosmosDBMongo'); + expect(inp.databaseName).to.equal('db'); + expect(inp.collectionName).to.equal('coll'); + expect(inp.queryString).to.equal('{"status": "active"}'); + expect(inp.createIfNotExists).to.equal(false); + expect(inp.tenantId).to.equal('tenant-xyz'); + expect(inp.managedIdentityClientId).to.equal('mi-xyz'); + }); + + it('generates a deterministic binding name with Input suffix', () => { + const inp = input.cosmosDBMongo({ + connectionStringSetting: 'CosmosDBMongo', + databaseName: 'db', + collectionName: 'coll', + }); + expect(inp.name).to.include('Input'); + }); + + it('sets direction = in via toCoreFunctionMetadata extra input', () => { + const inp = input.cosmosDBMongo({ + connectionStringSetting: 'CosmosDBMongo', + databaseName: 'db', + collectionName: 'coll', + }); + const result = toCoreFunctionMetadata('mongoInputFunc', { + handler: () => {}, + trigger: trigger.cosmosDBMongo(minimalTriggerOptions), + extraInputs: [inp], + }); + const bindingValues = Object.values(result.bindings) as Record[]; + const inputBinding = bindingValues.find((b) => b['type'] === 'cosmosDBMongo' && b['direction'] === 'in'); + expect(inputBinding).to.exist; + }); + }); + + // ------------------------------------------------------------------------- + // output + // ------------------------------------------------------------------------- + + describe('output.cosmosDBMongo', () => { + it('produces correct type string', () => { + const out = output.cosmosDBMongo({ + connectionStringSetting: 'CosmosDBMongo', + databaseName: 'db', + collectionName: 'coll', + }); + expect(out.type).to.equal('cosmosDBMongo'); + }); + + it('copies all output options onto the binding object', () => { + const out = output.cosmosDBMongo({ + connectionStringSetting: 'CosmosDBMongo', + databaseName: 'db', + collectionName: 'coll', + createIfNotExists: true, + tenantId: 'tenant-out', + managedIdentityClientId: 'mi-out', + }); + expect(out.connectionStringSetting).to.equal('CosmosDBMongo'); + expect(out.databaseName).to.equal('db'); + expect(out.collectionName).to.equal('coll'); + expect(out.createIfNotExists).to.equal(true); + expect(out.tenantId).to.equal('tenant-out'); + expect(out.managedIdentityClientId).to.equal('mi-out'); + }); + + it('generates a deterministic binding name with Output suffix', () => { + const out = output.cosmosDBMongo({ + connectionStringSetting: 'CosmosDBMongo', + databaseName: 'db', + collectionName: 'coll', + }); + expect(out.name).to.include('Output'); + }); + + it('sets direction = out via toCoreFunctionMetadata extra output', () => { + const out = output.cosmosDBMongo({ + connectionStringSetting: 'CosmosDBMongo', + databaseName: 'db', + collectionName: 'coll', + }); + const result = toCoreFunctionMetadata('mongoOutputFunc', { + handler: () => {}, + trigger: trigger.cosmosDBMongo(minimalTriggerOptions), + extraOutputs: [out], + }); + const bindingValues = Object.values(result.bindings) as Record[]; + const outputBinding = bindingValues.find((b) => b['type'] === 'cosmosDBMongo' && b['direction'] === 'out'); + expect(outputBinding).to.exist; + }); + }); +}); diff --git a/types/app.d.ts b/types/app.d.ts index a736187..461a0f2 100644 --- a/types/app.d.ts +++ b/types/app.d.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { CosmosDBFunctionOptions } from './cosmosDB'; +import { CosmosDBMongoFunctionOptions } from './cosmosDBMongo'; import { EventGridEvent, EventGridFunctionOptions } from './eventGrid'; import { EventHubFunctionOptions } from './eventHub'; import { GenericFunctionOptions } from './generic'; @@ -157,6 +158,13 @@ export function eventGrid(name: string, options: EventGridFu */ export function cosmosDB(name: string, options: CosmosDBFunctionOptions): void; +/** + * Registers an Azure Cosmos DB for MongoDB function in your app that will be triggered whenever change stream events occur + * @param name The name of the function. The name must be unique within your app and will mostly be used for your own tracking purposes + * @param options Configuration options describing the inputs, outputs, and handler for this function + */ +export function cosmosDBMongo(name: string, options: CosmosDBMongoFunctionOptions): void; + /** * Registers a function in your app that will be triggered when an instance is added to scale a running function app. * The warmup trigger is only called during scale-out operations, not during restarts or other non-scale startups. diff --git a/types/cosmosDBMongo.d.ts b/types/cosmosDBMongo.d.ts new file mode 100644 index 0000000..917833a --- /dev/null +++ b/types/cosmosDBMongo.d.ts @@ -0,0 +1,187 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. + +import { FunctionInput, FunctionOptions, FunctionOutput, FunctionResult, FunctionTrigger, RetryOptions } from './index'; +import { InvocationContext } from './InvocationContext'; + +/** + * Handler type for CosmosDB Mongo trigger functions. + * The trigger delivers the change stream document serialized as JSON. + */ +export type CosmosDBMongoHandler = (documents: T, context: InvocationContext) => FunctionResult; + +/** + * Options for registering a CosmosDB Mongo-triggered function via `app.cosmosDBMongo()`. + */ +export interface CosmosDBMongoFunctionOptions extends CosmosDBMongoTriggerOptions, Partial { + handler: CosmosDBMongoHandler; + + trigger?: CosmosDBMongoTrigger; + + /** + * An optional retry policy to rerun a failed execution until either successful completion occurs + * or the maximum number of retries is reached. + * Learn more [here](https://learn.microsoft.com/azure/azure-functions/functions-bindings-error-pages) + */ + retry?: RetryOptions; +} + +/** + * Options for configuring a CosmosDB Mongo trigger binding. + */ +export interface CosmosDBMongoTriggerOptions { + /** + * An app setting (or environment variable) with the MongoDB connection string. + * Defaults to "CosmosDBMongo" if not specified. + */ + connectionStringSetting: string; + + /** + * The name of the database being monitored. + */ + databaseName: string; + + /** + * The name of the collection being monitored. + * Optional when triggerLevel is "Database" or "Cluster". + */ + collectionName?: string; + + /** + * Whether to create the collection and lease collection if they do not exist. + * Default is false. + */ + createIfNotExists?: boolean; + + /** + * The level at which the trigger monitors for changes. + * Accepted values: "Collection" | "Database" | "Cluster". + * Default is "Collection". + */ + triggerLevel?: 'Collection' | 'Database' | 'Cluster'; + + /** + * The name of the database that holds the lease collection. + * Defaults to the monitored database name. + */ + leaseDatabaseName?: string; + + /** + * The name of the collection used to store leases. + * Defaults to "leases". + */ + leaseCollectionName?: string; + + /** + * An app setting name for the connection string of the lease account. + * If not set, uses the monitored account connection string. + */ + leaseConnectionStringSetting?: string; + + /** + * The Azure AD tenant ID used for managed identity authentication on the monitored account. + */ + tenantId?: string; + + /** + * The managed identity client ID for the monitored account. + * Used for user-assigned managed identity authentication. + */ + managedIdentityClientId?: string; + + /** + * The Azure AD tenant ID used for managed identity authentication on the lease account. + */ + leaseTenantId?: string; + + /** + * The managed identity client ID for the lease account. + */ + leaseManagedIdentityClientId?: string; +} + +export type CosmosDBMongoTrigger = FunctionTrigger & CosmosDBMongoTriggerOptions; + +/** + * Options for configuring a CosmosDB Mongo input binding. + */ +export interface CosmosDBMongoInputOptions { + /** + * An app setting (or environment variable) with the MongoDB connection string. + * Defaults to "CosmosDBMongo" if not specified. + */ + connectionStringSetting: string; + + /** + * The name of the database to read from. + */ + databaseName: string; + + /** + * The name of the collection to read from. + */ + collectionName: string; + + /** + * An optional MongoDB filter document as a JSON string. + * Supports binding expressions, e.g. {"id": "{Query.id}"}. + */ + queryString?: string; + + /** + * Whether to create the collection if it does not exist. + * Default is false. + */ + createIfNotExists?: boolean; + + /** + * The Azure AD tenant ID used for managed identity authentication. + */ + tenantId?: string; + + /** + * The managed identity client ID for user-assigned managed identity authentication. + */ + managedIdentityClientId?: string; +} + +export type CosmosDBMongoInput = FunctionInput & CosmosDBMongoInputOptions; + +/** + * Options for configuring a CosmosDB Mongo output binding. + */ +export interface CosmosDBMongoOutputOptions { + /** + * An app setting (or environment variable) with the MongoDB connection string. + * Defaults to "CosmosDBMongo" if not specified. + */ + connectionStringSetting: string; + + /** + * The name of the database to write to. + */ + databaseName: string; + + /** + * The name of the collection to write to. + */ + collectionName: string; + + /** + * Whether to create the collection if it does not exist. + * Default is false. + */ + createIfNotExists?: boolean; + + /** + * The Azure AD tenant ID used for managed identity authentication. + */ + tenantId?: string; + + /** + * The managed identity client ID for user-assigned managed identity authentication. + */ + managedIdentityClientId?: string; +} + +export type CosmosDBMongoOutput = FunctionOutput & CosmosDBMongoOutputOptions; diff --git a/types/index.d.ts b/types/index.d.ts index b45bb76..7897c25 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -7,6 +7,7 @@ export * as app from './app'; export * from './cosmosDB'; export * from './cosmosDB.v3'; export * from './cosmosDB.v4'; +export * from './cosmosDBMongo'; export * from './eventGrid'; export * from './eventHub'; export * from './generic'; diff --git a/types/input.d.ts b/types/input.d.ts index b264405..0769a7e 100644 --- a/types/input.d.ts +++ b/types/input.d.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { CosmosDBInput, CosmosDBInputOptions } from './cosmosDB'; +import { CosmosDBMongoInput, CosmosDBMongoInputOptions } from './cosmosDBMongo'; import { GenericInputOptions } from './generic'; import { FunctionInput } from './index'; import { MySqlInput, MySqlInputOptions } from './mySql'; @@ -30,6 +31,11 @@ export function table(options: TableInputOptions): TableInput; */ export function cosmosDB(options: CosmosDBInputOptions): CosmosDBInput; +/** + * [Link to docs and examples](tdb, will update later) + */ +export function cosmosDBMongo(options: CosmosDBMongoInputOptions): CosmosDBMongoInput; + /** * [Link to docs and examples](https://docs.microsoft.com/azure/azure-functions/functions-bindings-azure-sql-input?pivots=programming-language-javascript) */ diff --git a/types/output.d.ts b/types/output.d.ts index e9e08d9..0abccc1 100644 --- a/types/output.d.ts +++ b/types/output.d.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { CosmosDBOutput, CosmosDBOutputOptions } from './cosmosDB'; +import { CosmosDBMongoOutput, CosmosDBMongoOutputOptions } from './cosmosDBMongo'; import { EventGridOutput, EventGridOutputOptions } from './eventGrid'; import { EventHubOutput, EventHubOutputOptions } from './eventHub'; import { GenericOutputOptions } from './generic'; @@ -64,6 +65,11 @@ export function eventGrid(options: EventGridOutputOptions): EventGridOutput; */ export function cosmosDB(options: CosmosDBOutputOptions): CosmosDBOutput; +/** + * [Link to docs and examples](tdb, will update later) + */ +export function cosmosDBMongo(options: CosmosDBMongoOutputOptions): CosmosDBMongoOutput; + /** * [Link to docs and examples](https://docs.microsoft.com/azure/azure-functions/functions-bindings-azure-sql-output?pivots=programming-language-javascript) */ diff --git a/types/trigger.d.ts b/types/trigger.d.ts index c91a958..7d0e28b 100644 --- a/types/trigger.d.ts +++ b/types/trigger.d.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { CosmosDBTrigger, CosmosDBTriggerOptions } from './cosmosDB'; +import { CosmosDBMongoTrigger, CosmosDBMongoTriggerOptions } from './cosmosDBMongo'; import { EventGridTrigger, EventGridTriggerOptions } from './eventGrid'; import { EventHubTrigger, EventHubTriggerOptions } from './eventHub'; import { GenericTriggerOptions } from './generic'; @@ -72,6 +73,11 @@ export function eventGrid(options: EventGridTriggerOptions): EventGridTrigger; */ export function cosmosDB(options: CosmosDBTriggerOptions): CosmosDBTrigger; +/** + * [Link to docs and examples](tdb, will update later) + */ +export function cosmosDBMongo(options: CosmosDBMongoTriggerOptions): CosmosDBMongoTrigger; + /** * [Link to docs and examples](https://learn.microsoft.com/azure/azure-functions/functions-bindings-warmup?tabs=isolated-process&pivots=programming-language-javascript) */