diff --git a/.gitignore b/.gitignore index 748e1e3..4aec6d3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,8 @@ logs *.log npm-debug.log* - +sample/bin/* +sample/obj/* # Runtime data pids *.pid diff --git a/src/trigger.ts b/src/trigger.ts index 59305e3..0a0c456 100644 --- a/src/trigger.ts +++ b/src/trigger.ts @@ -103,10 +103,18 @@ export function eventGrid(options: EventGridTriggerOptions): EventGridTrigger { } export function cosmosDB(options: CosmosDBTriggerOptions): CosmosDBTrigger { - return addTriggerBindingName({ + const binding = addTriggerBindingName({ ...options, type: 'cosmosDBTrigger', }); + + // The Functions host (Cosmos DB extension) only recognizes 'AllVersionsAndDeletes' on the wire, + // so translate the developer-facing 'FullFidelity' value before registration. + if ('changeFeedMode' in binding && binding.changeFeedMode === 'FullFidelity') { + (binding as { changeFeedMode?: string }).changeFeedMode = 'AllVersionsAndDeletes'; + } + + return binding; } export function warmup(options: WarmupTriggerOptions): WarmupTrigger { diff --git a/test/app.cosmosDB.test.ts b/test/app.cosmosDB.test.ts new file mode 100644 index 0000000..5a2b6c9 --- /dev/null +++ b/test/app.cosmosDB.test.ts @@ -0,0 +1,50 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. + +import 'mocha'; +import { RpcBindingInfo } from '@azure/functions-core'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import * as app from '../src/app'; +import * as tryGetCoreApiLazyModule from '../src/utils/tryGetCoreApiLazy'; +import { InvocationContext } from '../types'; + +describe('app.cosmosDB', () => { + const handler = (_documents: unknown[], _context: InvocationContext) => {}; + + afterEach(() => { + sinon.restore(); + }); + + it('registers changeFeedMode on function metadata', () => { + const registerFunction = sinon.stub(); + const setProgrammingModel = sinon.stub(); + + sinon.stub(tryGetCoreApiLazyModule, 'tryGetCoreApiLazy').returns({ + registerFunction, + setProgrammingModel, + } as unknown as ReturnType); + + app.cosmosDB('cosmosFn', { + handler, + connection: 'CosmosConnection', + databaseName: 'dbName', + containerName: 'containerName', + changeFeedMode: 'FullFidelity', + }); + + sinon.assert.calledOnce(registerFunction); + const metadata = registerFunction.firstCall.args[0]; + + expect(metadata.name).to.equal('cosmosFn'); + const cosmosBinding = Object.values(metadata.bindings as Record).find( + (b) => b.type === 'cosmosDBTrigger' + ); + expect(cosmosBinding).to.include({ + connection: 'CosmosConnection', + databaseName: 'dbName', + containerName: 'containerName', + changeFeedMode: 'AllVersionsAndDeletes', + }); + }); +}); diff --git a/test/converters/toCoreFunctionMetadata.test.ts b/test/converters/toCoreFunctionMetadata.test.ts index c5cfa6f..17cce16 100644 --- a/test/converters/toCoreFunctionMetadata.test.ts +++ b/test/converters/toCoreFunctionMetadata.test.ts @@ -161,6 +161,48 @@ describe('toCoreFunctionMetadata', () => { retryOptions: undefined, }); }); + + it('cosmosDB trigger maps FullFidelity changeFeedMode to AllVersionsAndDeletes', () => { + const result = toCoreFunctionMetadata('funcName', { + handler, + trigger: trigger.cosmosDB({ + connection: 'CosmosConnection', + databaseName: 'dbName', + containerName: 'containerName', + changeFeedMode: 'FullFidelity', + }), + return: output.http({}), + }); + + const cosmosBinding = Object.values(result.bindings).find((b) => b.type === 'cosmosDBTrigger'); + expect(cosmosBinding).to.include({ + connection: 'CosmosConnection', + databaseName: 'dbName', + containerName: 'containerName', + changeFeedMode: 'AllVersionsAndDeletes', + }); + }); + + it('cosmosDB trigger preserves changeFeedMode LatestVersion', () => { + const result = toCoreFunctionMetadata('funcName', { + handler, + trigger: trigger.cosmosDB({ + connection: 'CosmosConnection', + databaseName: 'dbName', + containerName: 'containerName', + changeFeedMode: 'LatestVersion', + }), + return: output.http({}), + }); + + const cosmosBinding = Object.values(result.bindings).find((b) => b.type === 'cosmosDBTrigger'); + expect(cosmosBinding).to.include({ + connection: 'CosmosConnection', + databaseName: 'dbName', + containerName: 'containerName', + changeFeedMode: 'LatestVersion', + }); + }); }); describe('toCoreFunctionMetadata sdk binding tests', () => { diff --git a/test/types/index.test.ts b/test/types/index.test.ts index c2067c4..9f6cd67 100644 --- a/test/types/index.test.ts +++ b/test/types/index.test.ts @@ -2,4 +2,60 @@ // Licensed under the MIT License. // This file will be compiled by multiple versions of TypeScript as decribed in ./test/TypesTests.ts to verify there are no errors -// Temporarily removing this code until we have updated templates for the new programming model +interface TodoItem { + id: string; + status: string; +} + +type CosmosDBv4LatestVersionFunctionOptions = + import('../../types').CosmosDBv4LatestVersionFunctionOptions; +type CosmosDBv4FullFidelityFunctionOptions = + import('../../types').CosmosDBv4FullFidelityFunctionOptions; +type CosmosDBChangeFeedItem = import('../../types').CosmosDBChangeFeedItem; + +const latestVersionOptions: CosmosDBv4LatestVersionFunctionOptions = { + connection: 'CosmosConnection', + databaseName: 'dbName', + containerName: 'containerName', + handler: (documents: TodoItem[]) => { + const item: TodoItem | undefined = documents[0]; + void item?.id; + }, +}; + +const fullFidelityOptions: CosmosDBv4FullFidelityFunctionOptions = { + connection: 'CosmosConnection', + databaseName: 'dbName', + containerName: 'containerName', + changeFeedMode: 'FullFidelity', + handler: (documents: CosmosDBChangeFeedItem[]) => { + const item: CosmosDBChangeFeedItem | undefined = documents[0]; + const currentId: string | undefined = item?.current?.id; + const previousId: string | undefined = item?.previous?.id; + const operationType: string | undefined = item?.metadata?.operationType; + void currentId; + void previousId; + void operationType; + }, +}; + +type ExpectedAvadHandler = import('../../types').CosmosDBv4Handler>; +type ValidAvadHandler = ( + documents: CosmosDBChangeFeedItem[], + context: import('../../types').InvocationContext +) => import('../../types').FunctionResult; +type InvalidAvadHandler = ( + documents: TodoItem[], + context: import('../../types').InvocationContext +) => import('../../types').FunctionResult; + +type IsValidHandlerAssignableToAvad = ValidAvadHandler extends ExpectedAvadHandler ? true : false; +type IsInvalidHandlerAssignableToAvad = InvalidAvadHandler extends ExpectedAvadHandler ? true : false; + +const validHandlerAssignableToAvad: IsValidHandlerAssignableToAvad = true; +const invalidHandlerAssignableToAvad: IsInvalidHandlerAssignableToAvad = false; + +void latestVersionOptions; +void fullFidelityOptions; +void validHandlerAssignableToAvad; +void invalidHandlerAssignableToAvad; diff --git a/types/app.d.ts b/types/app.d.ts index 9b59e4d..2658124 100644 --- a/types/app.d.ts +++ b/types/app.d.ts @@ -153,7 +153,9 @@ export function eventHub(name: string, options: EventHubFunctionOpt export function eventGrid(name: string, options: EventGridFunctionOptions): void; /** - * Registers a Cosmos DB function in your app that will be triggered whenever inserts and updates occur (not deletions) + * Registers a Cosmos DB function in your app that will be triggered by container changes. + * By default, this trigger processes latest document versions. To include delete events and + * intermediate mutations, set `changeFeedMode` to `FullFidelity`. * @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 */ diff --git a/types/cosmosDB.d.ts b/types/cosmosDB.d.ts index 2f8398e..88d81ad 100644 --- a/types/cosmosDB.d.ts +++ b/types/cosmosDB.d.ts @@ -12,6 +12,9 @@ import { CosmosDBv3TriggerOptions, } from './cosmosDB.v3'; import { + CosmosDBv4ChangeFeedItem, + CosmosDBv4ChangeFeedMetadata, + CosmosDBv4ChangeFeedMode, CosmosDBv4FunctionOptions, CosmosDBv4Handler, CosmosDBv4Input, @@ -32,5 +35,9 @@ export type CosmosDBInput = CosmosDBv3Input | CosmosDBv4Input; export type CosmosDBTriggerOptions = CosmosDBv3TriggerOptions | CosmosDBv4TriggerOptions; export type CosmosDBTrigger = CosmosDBv3Trigger | CosmosDBv4Trigger; +export type CosmosDBChangeFeedMode = CosmosDBv4ChangeFeedMode; +export type CosmosDBChangeFeedMetadata = CosmosDBv4ChangeFeedMetadata; +export type CosmosDBChangeFeedItem = CosmosDBv4ChangeFeedItem; + export type CosmosDBOutputOptions = CosmosDBv3OutputOptions | CosmosDBv4OutputOptions; export type CosmosDBOutput = CosmosDBv3Output | CosmosDBv4Output; diff --git a/types/cosmosDB.v4.d.ts b/types/cosmosDB.v4.d.ts index f9015bb..4d38741 100644 --- a/types/cosmosDB.v4.d.ts +++ b/types/cosmosDB.v4.d.ts @@ -6,11 +6,60 @@ import { InvocationContext } from './InvocationContext'; export type CosmosDBv4Handler = (documents: T[], context: InvocationContext) => FunctionResult; -export interface CosmosDBv4FunctionOptions extends CosmosDBv4TriggerOptions, Partial { +/** + * Metadata included with each change feed item when using `FullFidelity`. + * Property names follow the wire format emitted by Cosmos DB. + */ +export interface CosmosDBv4ChangeFeedMetadata { + crts: number; + lsn: number; + operationType: string; + previousImageLSN?: number; + timeToLiveExpired?: boolean; + id?: string; + partitionKey?: Record; +} + +/** + * Full-fidelity change feed item payload emitted when `changeFeedMode` is `FullFidelity`. + * Property names follow the wire format emitted by Cosmos DB. + */ +export interface CosmosDBv4ChangeFeedItem { + current: T | null; + previous?: T; + metadata?: CosmosDBv4ChangeFeedMetadata; +} + +/** + * Change feed mode for Cosmos DB trigger bindings. + */ +export type CosmosDBv4ChangeFeedMode = 'LatestVersion' | 'FullFidelity'; + +export interface CosmosDBv4LatestVersionFunctionOptions + extends Omit, + Partial { handler: CosmosDBv4Handler; trigger?: CosmosDBv4Trigger; + changeFeedMode?: 'LatestVersion'; + + /** + * 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; +} + +export interface CosmosDBv4FullFidelityFunctionOptions + extends Omit, + Partial { + handler: CosmosDBv4Handler>; + + trigger?: CosmosDBv4Trigger; + + changeFeedMode: 'FullFidelity'; + /** * 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) @@ -18,6 +67,10 @@ export interface CosmosDBv4FunctionOptions extends CosmosDBv4Trigge retry?: RetryOptions; } +export type CosmosDBv4FunctionOptions = + | CosmosDBv4LatestVersionFunctionOptions + | CosmosDBv4FullFidelityFunctionOptions; + export interface CosmosDBv4InputOptions { /** * An app setting (or environment variable) with the Cosmos DB connection string @@ -144,6 +197,7 @@ export interface CosmosDBv4TriggerOptions { * This option tells the Trigger to read changes from the beginning of the container's change history instead of starting at the current time. * Reading from the beginning only works the first time the trigger starts, as in subsequent runs, the checkpoints are already stored. * Setting this option to true when there are leases already created has no effect. + * Not supported when `changeFeedMode` is `FullFidelity`. */ startFromBeginning?: boolean; @@ -151,9 +205,17 @@ export interface CosmosDBv4TriggerOptions { * Gets or sets the date and time from which to initialize the change feed read operation. * The recommended format is ISO 8601 with the UTC designator, such as 2021-02-16T14:19:29Z. * This is only used to set the initial trigger state. After the trigger has a lease state, changing this value has no effect. + * Not supported when `changeFeedMode` is `FullFidelity`. */ startFromTime?: string; + /** + * Gets or sets the change feed mode used to process document changes. + * `LatestVersion` processes the latest document version. + * `FullFidelity` includes intermediate mutations and delete events. + */ + changeFeedMode?: CosmosDBv4ChangeFeedMode; + /** * Defines preferred locations (regions) for geo-replicated database accounts in the Azure Cosmos DB service. * Values should be comma-separated. For example, East US,South Central US,North Europe