@boostercloud/framework-provider-azure 3.4.4-debug.2 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/helpers/query-helper.js +1 -42
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/library/api-adapter.d.ts +20 -11
- package/dist/library/api-adapter.js +41 -15
- package/dist/library/events-adapter.d.ts +1 -2
- package/dist/library/events-adapter.js +10 -2
- package/dist/library/events-stream-consumer-adapter.d.ts +32 -3
- package/dist/library/events-stream-consumer-adapter.js +55 -12
- package/dist/library/graphql-adapter.d.ts +1 -2
- package/dist/library/graphql-adapter.js +138 -17
- package/dist/library/health-adapter.d.ts +9 -3
- package/dist/library/health-adapter.js +33 -11
- package/dist/library/read-model-adapter.d.ts +7 -0
- package/dist/library/read-model-adapter.js +11 -7
- package/dist/library/scheduled-adapter.d.ts +22 -6
- package/dist/library/scheduled-adapter.js +23 -3
- package/dist/library/subscription-model.d.ts +19 -16
- package/dist/library/subscription-model.js +14 -0
- package/dist/types/azure-func-types.d.ts +116 -0
- package/dist/types/azure-func-types.js +87 -0
- package/package.json +10 -21
|
@@ -36,14 +36,6 @@ async function search(cosmosDb, config, containerName, filters, limit, afterCurs
|
|
|
36
36
|
// - Legacy cursors: Numeric cursor.id values must use OFFSET/LIMIT for backward compatibility
|
|
37
37
|
const canUseContinuationToken = !isDistinctQuery;
|
|
38
38
|
const hasLegacyCursor = typeof (afterCursor === null || afterCursor === void 0 ? void 0 : afterCursor.id) === 'string' && /^\d+$/.test(afterCursor.id);
|
|
39
|
-
logger.debug('PAGINATION DEBUG - Initial analysis:', {
|
|
40
|
-
isDistinctQuery,
|
|
41
|
-
canUseContinuationToken,
|
|
42
|
-
hasLegacyCursor,
|
|
43
|
-
afterCursor,
|
|
44
|
-
limit,
|
|
45
|
-
finalQuery,
|
|
46
|
-
});
|
|
47
39
|
// Use Cosmos DB's continuation token pagination
|
|
48
40
|
const feedOptions = {};
|
|
49
41
|
if (limit) {
|
|
@@ -52,10 +44,6 @@ async function search(cosmosDb, config, containerName, filters, limit, afterCurs
|
|
|
52
44
|
// Extract continuation token from the cursor (backward compatibility)
|
|
53
45
|
if (afterCursor === null || afterCursor === void 0 ? void 0 : afterCursor.continuationToken) {
|
|
54
46
|
feedOptions.continuationToken = afterCursor.continuationToken;
|
|
55
|
-
logger.debug('PAGINATION DEBUG - Using provided continuation token:', {
|
|
56
|
-
continuationToken: afterCursor.continuationToken,
|
|
57
|
-
feedOptions,
|
|
58
|
-
});
|
|
59
47
|
}
|
|
60
48
|
else if (!canUseContinuationToken || hasLegacyCursor) {
|
|
61
49
|
// Legacy cursor format - fallback to OFFSET for backward compatibility
|
|
@@ -65,19 +53,8 @@ async function search(cosmosDb, config, containerName, filters, limit, afterCurs
|
|
|
65
53
|
legacyQuery += ` LIMIT ${limit} `;
|
|
66
54
|
}
|
|
67
55
|
const legacyQuerySpec = { ...querySpec, query: legacyQuery };
|
|
68
|
-
logger.debug('PAGINATION DEBUG - Using LEGACY OFFSET/LIMIT approach:', {
|
|
69
|
-
reason: !canUseContinuationToken ? 'DISTINCT query detected' : 'Legacy numeric cursor detected',
|
|
70
|
-
offset,
|
|
71
|
-
legacyQuery,
|
|
72
|
-
legacyQuerySpec,
|
|
73
|
-
});
|
|
74
56
|
const { resources } = await container.items.query(legacyQuerySpec).fetchAll();
|
|
75
57
|
const processedResources = processResources(resources);
|
|
76
|
-
logger.debug('PAGINATION DEBUG - Legacy query results:', {
|
|
77
|
-
resourcesCount: (resources === null || resources === void 0 ? void 0 : resources.length) || 0,
|
|
78
|
-
processedResourcesCount: processedResources.length,
|
|
79
|
-
nextCursor: { id: (offset + processedResources.length).toString() },
|
|
80
|
-
});
|
|
81
58
|
return {
|
|
82
59
|
items: processedResources !== null && processedResources !== void 0 ? processedResources : [],
|
|
83
60
|
count: processedResources.length,
|
|
@@ -86,35 +63,17 @@ async function search(cosmosDb, config, containerName, filters, limit, afterCurs
|
|
|
86
63
|
},
|
|
87
64
|
};
|
|
88
65
|
}
|
|
89
|
-
logger.debug('PAGINATION DEBUG - About to execute continuation token query with feedOptions:', feedOptions);
|
|
90
66
|
const queryIterator = container.items.query(querySpec, feedOptions);
|
|
91
67
|
const { resources, continuationToken } = await queryIterator.fetchNext();
|
|
92
|
-
logger.debug('PAGINATION DEBUG - Cosmos SDK response:', {
|
|
93
|
-
resourcesCount: (resources === null || resources === void 0 ? void 0 : resources.length) || 0,
|
|
94
|
-
continuationTokenReceived: !!continuationToken,
|
|
95
|
-
continuationTokenValue: continuationToken,
|
|
96
|
-
continuationTokenType: typeof continuationToken,
|
|
97
|
-
continuationTokenLength: continuationToken === null || continuationToken === void 0 ? void 0 : continuationToken.length,
|
|
98
|
-
});
|
|
99
68
|
const finalResources = processResources(resources || []);
|
|
100
69
|
let cursor;
|
|
101
70
|
if (continuationToken) {
|
|
102
71
|
cursor = { continuationToken };
|
|
103
|
-
logger.debug('PAGINATION DEBUG - Setting cursor with continuation token:', cursor);
|
|
104
72
|
}
|
|
105
73
|
else if (finalResources.length > 0) {
|
|
106
74
|
const currentOffset = (afterCursor === null || afterCursor === void 0 ? void 0 : afterCursor.id) && !isNaN(parseInt(afterCursor.id)) ? parseInt(afterCursor.id) : 0;
|
|
107
|
-
cursor = { id: (currentOffset + finalResources.length).toString() };
|
|
108
|
-
logger.debug('PAGINATION DEBUG - No continuation token, setting fallback cursor:', cursor);
|
|
75
|
+
cursor = { id: (currentOffset + finalResources.length).toString() }; // Use the length of the results to calculate the next id
|
|
109
76
|
}
|
|
110
|
-
else {
|
|
111
|
-
logger.debug('PAGINATION DEBUG - No continuation token and no resources, no cursor set');
|
|
112
|
-
}
|
|
113
|
-
logger.debug('PAGINATION DEBUG - Final response:', {
|
|
114
|
-
itemsCount: finalResources.length,
|
|
115
|
-
cursor,
|
|
116
|
-
hasMoreResults: !!continuationToken,
|
|
117
|
-
});
|
|
118
77
|
return {
|
|
119
78
|
items: finalResources,
|
|
120
79
|
count: finalResources.length,
|
package/dist/index.d.ts
CHANGED
|
@@ -2,3 +2,4 @@ import { HasInfrastructure, ProviderLibrary, RocketDescriptor } from '@boostercl
|
|
|
2
2
|
export declare function loadInfrastructurePackage(packageName: string): HasInfrastructure;
|
|
3
3
|
export declare const Provider: (rockets?: RocketDescriptor[]) => ProviderLibrary;
|
|
4
4
|
export * from './constants';
|
|
5
|
+
export * from './types/azure-func-types';
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { HttpResponseInit } from '@azure/functions';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Standard HTTP response type for Azure Functions v4.
|
|
4
|
+
* Uses HttpResponseInit from '@azure/functions' v4.
|
|
4
5
|
*/
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
export type AzureHttpResponse = HttpResponseInit;
|
|
7
|
+
/**
|
|
8
|
+
* Web PubSub response types for Azure Functions v4.
|
|
9
|
+
* These are returned directly from Web PubSub triggered functions.
|
|
10
|
+
*/
|
|
11
|
+
export interface WebPubSubConnectResponse {
|
|
12
|
+
subprotocol?: string;
|
|
13
|
+
userId?: string;
|
|
14
|
+
groups?: string[];
|
|
15
|
+
roles?: string[];
|
|
16
|
+
}
|
|
17
|
+
export interface WebPubSubMessageResponse {
|
|
18
|
+
data?: unknown;
|
|
19
|
+
dataType?: 'json' | 'text' | 'binary';
|
|
11
20
|
}
|
|
12
|
-
export declare function requestSucceeded(body?: unknown, headers?: Record<string, number | string | ReadonlyArray<string>>): Promise<
|
|
13
|
-
export declare function requestFailed(error: Error): Promise<
|
|
14
|
-
export declare function healthRequestResult(body: unknown, isHealthy: boolean): Promise<
|
|
21
|
+
export declare function requestSucceeded(body?: unknown, headers?: Record<string, number | string | ReadonlyArray<string>>): Promise<AzureHttpResponse | WebPubSubConnectResponse | WebPubSubMessageResponse | void>;
|
|
22
|
+
export declare function requestFailed(error: Error): Promise<AzureHttpResponse>;
|
|
23
|
+
export declare function healthRequestResult(body: unknown, isHealthy: boolean): Promise<AzureHttpResponse>;
|
|
@@ -5,25 +5,51 @@ exports.requestFailed = requestFailed;
|
|
|
5
5
|
exports.healthRequestResult = healthRequestResult;
|
|
6
6
|
const framework_types_1 = require("@boostercloud/framework-types");
|
|
7
7
|
const WEB_SOCKET_PROTOCOL_HEADER = 'Sec-WebSocket-Protocol';
|
|
8
|
+
const WEB_SOCKET_MESSAGE_MARKER_HEADER = 'X-Booster-WebSocket-Message';
|
|
8
9
|
async function requestSucceeded(body, headers) {
|
|
10
|
+
// Check if this is a Web Pubsub CONNECT event (has the specific WebSocket header)
|
|
11
|
+
const isWebSocketConnect = headers && Object.keys(headers).includes(WEB_SOCKET_PROTOCOL_HEADER);
|
|
12
|
+
// Web PubSub CONNECT event - return subprotocol response format
|
|
13
|
+
if (isWebSocketConnect) {
|
|
14
|
+
const subprotocol = headers[WEB_SOCKET_PROTOCOL_HEADER];
|
|
15
|
+
return {
|
|
16
|
+
subprotocol: Array.isArray(subprotocol) ? subprotocol[0] : String(subprotocol),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
// Check if this is a Web Pubsub MESSAGE event (has the marker header from framework-core
|
|
20
|
+
const isWebSocketMessage = headers && headers[WEB_SOCKET_MESSAGE_MARKER_HEADER] === 'true';
|
|
21
|
+
// Web PubSub MESSAGE event - return data response format
|
|
22
|
+
if (isWebSocketMessage) {
|
|
23
|
+
return {
|
|
24
|
+
data: body,
|
|
25
|
+
dataType: 'json',
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// No body and no meaningful headers - nothing to return (e.g., DISCONNECT)
|
|
9
29
|
if (!body && (!headers || Object.keys(headers).length === 0)) {
|
|
10
30
|
return;
|
|
11
31
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
32
|
+
// Standard HTTP response for everything else
|
|
33
|
+
const responseHeaders = {
|
|
34
|
+
'Access-Control-Allow-Origin': '*',
|
|
35
|
+
'Content-Type': 'application/json',
|
|
36
|
+
};
|
|
37
|
+
if (headers) {
|
|
38
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
39
|
+
// Don't include internal Booster headers in HTTP response
|
|
40
|
+
if (key.startsWith('X-Booster-'))
|
|
41
|
+
continue;
|
|
42
|
+
responseHeaders[key] = Array.isArray(value) ? value.join(',') : String(value);
|
|
43
|
+
}
|
|
16
44
|
}
|
|
17
|
-
|
|
18
|
-
headers:
|
|
19
|
-
'Access-Control-Allow-Origin': '*',
|
|
20
|
-
'Content-Type': 'application/json',
|
|
21
|
-
...headers,
|
|
22
|
-
},
|
|
45
|
+
const response = {
|
|
46
|
+
headers: responseHeaders,
|
|
23
47
|
status: 200,
|
|
24
|
-
body: body ? JSON.stringify(body) : '',
|
|
25
|
-
...extraParams,
|
|
26
48
|
};
|
|
49
|
+
if (body) {
|
|
50
|
+
response.jsonBody = body;
|
|
51
|
+
}
|
|
52
|
+
return response;
|
|
27
53
|
}
|
|
28
54
|
async function requestFailed(error) {
|
|
29
55
|
const status = (0, framework_types_1.httpStatusCodeFor)(error);
|
|
@@ -33,11 +59,11 @@ async function requestFailed(error) {
|
|
|
33
59
|
'Content-Type': 'application/json',
|
|
34
60
|
},
|
|
35
61
|
status,
|
|
36
|
-
|
|
62
|
+
jsonBody: {
|
|
37
63
|
status,
|
|
38
64
|
title: (0, framework_types_1.toClassTitle)(error),
|
|
39
65
|
reason: error.message,
|
|
40
|
-
}
|
|
66
|
+
},
|
|
41
67
|
};
|
|
42
68
|
}
|
|
43
69
|
async function healthRequestResult(body, isHealthy) {
|
|
@@ -47,6 +73,6 @@ async function healthRequestResult(body, isHealthy) {
|
|
|
47
73
|
'Content-Type': 'application/json',
|
|
48
74
|
},
|
|
49
75
|
status: isHealthy ? 200 : 503,
|
|
50
|
-
|
|
76
|
+
jsonBody: body,
|
|
51
77
|
};
|
|
52
78
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { CosmosClient } from '@azure/cosmos';
|
|
2
2
|
import { BoosterConfig, EntitySnapshotEnvelope, EventEnvelope, NonPersistedEntitySnapshotEnvelope, UUID } from '@boostercloud/framework-types';
|
|
3
|
-
|
|
4
|
-
export declare function rawEventsToEnvelopes(context: Context): Array<EventEnvelope>;
|
|
3
|
+
export declare function rawEventsToEnvelopes(rawInput: unknown): Array<EventEnvelope>;
|
|
5
4
|
export declare function readEntityEventsSince(cosmosDb: CosmosClient, config: BoosterConfig, entityTypeName: string, entityID: UUID, since?: string): Promise<Array<EventEnvelope>>;
|
|
6
5
|
export declare function readEntityLatestSnapshot(cosmosDb: CosmosClient, config: BoosterConfig, entityTypeName: string, entityID: UUID): Promise<EntitySnapshotEnvelope | undefined>;
|
|
7
6
|
export declare function storeSnapshot(cosmosDb: CosmosClient, snapshotEnvelope: NonPersistedEntitySnapshotEnvelope, config: BoosterConfig): Promise<EntitySnapshotEnvelope>;
|
|
@@ -8,9 +8,17 @@ exports.storeDispatchedEvent = storeDispatchedEvent;
|
|
|
8
8
|
const framework_common_helpers_1 = require("@boostercloud/framework-common-helpers");
|
|
9
9
|
const constants_1 = require("../constants");
|
|
10
10
|
const partition_keys_1 = require("./partition-keys");
|
|
11
|
+
const azure_func_types_1 = require("../types/azure-func-types");
|
|
11
12
|
const originOfTime = new Date(0).toISOString();
|
|
12
|
-
function rawEventsToEnvelopes(
|
|
13
|
-
|
|
13
|
+
function rawEventsToEnvelopes(rawInput) {
|
|
14
|
+
if ((0, azure_func_types_1.isCosmosDBFunctionInput)(rawInput)) {
|
|
15
|
+
return rawInput.documents;
|
|
16
|
+
}
|
|
17
|
+
// Fallback for direct array input (used in some test scenarios)
|
|
18
|
+
if (Array.isArray(rawInput)) {
|
|
19
|
+
return rawInput;
|
|
20
|
+
}
|
|
21
|
+
throw new Error('Invalid input type for rawEventsToEnvelopes: expected AzureCosmosDBFunctionInput or Array');
|
|
14
22
|
}
|
|
15
23
|
async function readEntityEventsSince(cosmosDb, config, entityTypeName, entityID, since) {
|
|
16
24
|
const logger = (0, framework_common_helpers_1.getLogger)(config, 'events-adapter#readEntityEventsSince');
|
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
import { BoosterConfig, EventEnvelope, EventStream } from '@boostercloud/framework-types';
|
|
2
|
-
import { Context } from '@azure/functions';
|
|
3
2
|
import { CosmosClient } from '@azure/cosmos';
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import { AzureEventHubFunctionInput } from '../types/azure-func-types';
|
|
4
|
+
/**
|
|
5
|
+
* V4 Programming Model: Input wrapper for Event Hub consumer functions.
|
|
6
|
+
* Extends the base type with binding data for event metadata.
|
|
7
|
+
*/
|
|
8
|
+
export interface EventHubConsumerInput extends AzureEventHubFunctionInput {
|
|
9
|
+
/** Binding data containing Event Hub metadata arrays */
|
|
10
|
+
bindingData: {
|
|
11
|
+
partitionKeyArray: string[];
|
|
12
|
+
offsetArray: string[];
|
|
13
|
+
sequenceNumberArray: number[];
|
|
14
|
+
enqueuedTimeUtcArray: string[];
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Deduplicates events from the Event Hub stream using Cosmos DB.
|
|
19
|
+
* In v4 programming model, messages are passed in the input wrapper.
|
|
20
|
+
* @param cosmosDb - The Cosmos DB client
|
|
21
|
+
* @param config - The Booster configuration
|
|
22
|
+
* @param rawInput - The raw input from the Azure Function, expected to be of type EventHubConsumerInput
|
|
23
|
+
* @returns A promise that resolves to the deduplicated event stream
|
|
24
|
+
*/
|
|
25
|
+
export declare function dedupEventStream(cosmosDb: CosmosClient, config: BoosterConfig, rawInput: unknown): Promise<EventStream>;
|
|
26
|
+
/**
|
|
27
|
+
* Converts raw Event Hub events to Booster EventEnvelope format.
|
|
28
|
+
* In v4 programming model, binding data is passed via triggerMetadata on the context.
|
|
29
|
+
* @param config - Booster configuration
|
|
30
|
+
* @param rawInput - The raw input from the Azure Function
|
|
31
|
+
* @param dedupEventStream - The deduplicated event stream
|
|
32
|
+
* @returns An array of EventEnvelope objects
|
|
33
|
+
*/
|
|
34
|
+
export declare function rawEventsStreamToEnvelopes(config: BoosterConfig, rawInput: unknown, dedupEventStream: EventStream): Array<EventEnvelope>;
|
|
@@ -4,15 +4,32 @@ exports.dedupEventStream = dedupEventStream;
|
|
|
4
4
|
exports.rawEventsStreamToEnvelopes = rawEventsStreamToEnvelopes;
|
|
5
5
|
const framework_common_helpers_1 = require("@boostercloud/framework-common-helpers");
|
|
6
6
|
const constants_1 = require("../constants");
|
|
7
|
+
const azure_func_types_1 = require("../types/azure-func-types");
|
|
7
8
|
const DEFAULT_DEDUP_TTL = 86400;
|
|
8
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Deduplicates events from the Event Hub stream using Cosmos DB.
|
|
11
|
+
* In v4 programming model, messages are passed in the input wrapper.
|
|
12
|
+
* @param cosmosDb - The Cosmos DB client
|
|
13
|
+
* @param config - The Booster configuration
|
|
14
|
+
* @param rawInput - The raw input from the Azure Function, expected to be of type EventHubConsumerInput
|
|
15
|
+
* @returns A promise that resolves to the deduplicated event stream
|
|
16
|
+
*/
|
|
17
|
+
async function dedupEventStream(cosmosDb, config, rawInput) {
|
|
9
18
|
var _a, _b;
|
|
10
19
|
const logger = (0, framework_common_helpers_1.getLogger)(config, 'dedup-events-stream#dedupEventsStream');
|
|
11
|
-
|
|
20
|
+
let events = [];
|
|
21
|
+
if ((0, azure_func_types_1.isEventHubFunctionInput)(rawInput)) {
|
|
22
|
+
events = rawInput.messages;
|
|
23
|
+
}
|
|
24
|
+
else if (Array.isArray(rawInput)) {
|
|
25
|
+
// Direct array for testing
|
|
26
|
+
events = rawInput;
|
|
27
|
+
}
|
|
12
28
|
logger.debug(`Dedup ${events.length} events`);
|
|
13
29
|
const resources = [];
|
|
14
30
|
for (const event of events) {
|
|
15
|
-
|
|
31
|
+
// In v4, Event Hub messages may already be parsed objects, not JSON strings
|
|
32
|
+
const rawParsed = typeof event === 'string' ? JSON.parse(event) : event;
|
|
16
33
|
const eventTag = {
|
|
17
34
|
primaryKey: rawParsed._etag,
|
|
18
35
|
createdAt: new Date().toISOString(),
|
|
@@ -31,23 +48,49 @@ async function dedupEventStream(cosmosDb, config, context) {
|
|
|
31
48
|
if (error.code !== constants_1.AZURE_CONFLICT_ERROR_CODE) {
|
|
32
49
|
throw error;
|
|
33
50
|
}
|
|
34
|
-
logger.warn(`Ignoring duplicated event with etag ${eventTag}.`);
|
|
51
|
+
logger.warn(`Ignoring duplicated event with etag ${eventTag.primaryKey}.`);
|
|
35
52
|
}
|
|
36
53
|
}
|
|
37
54
|
return resources;
|
|
38
55
|
}
|
|
39
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Converts raw Event Hub events to Booster EventEnvelope format.
|
|
58
|
+
* In v4 programming model, binding data is passed via triggerMetadata on the context.
|
|
59
|
+
* @param config - Booster configuration
|
|
60
|
+
* @param rawInput - The raw input from the Azure Function
|
|
61
|
+
* @param dedupEventStream - The deduplicated event stream
|
|
62
|
+
* @returns An array of EventEnvelope objects
|
|
63
|
+
*/
|
|
64
|
+
function rawEventsStreamToEnvelopes(config, rawInput, dedupEventStream) {
|
|
40
65
|
const logger = (0, framework_common_helpers_1.getLogger)(config, 'events-adapter#rawEventsStreamToEnvelopes');
|
|
41
66
|
logger.debug(`Mapping ${dedupEventStream.length} events`);
|
|
42
|
-
|
|
67
|
+
// Extract binding data from the input
|
|
68
|
+
let bindingData;
|
|
69
|
+
if (isEventHubConsumerInput(rawInput)) {
|
|
70
|
+
bindingData = rawInput.bindingData;
|
|
71
|
+
}
|
|
43
72
|
return dedupEventStream.map((message, index) => {
|
|
44
|
-
|
|
73
|
+
// In v4, Event Hub messages may already be parsed objects, not JSON strings
|
|
74
|
+
const rawParsed = typeof message === 'string' ? JSON.parse(message) : message;
|
|
45
75
|
const instance = process.env.WEBSITE_INSTANCE_ID;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
76
|
+
if (bindingData) {
|
|
77
|
+
const partitionKeyArrayElement = bindingData.partitionKeyArray[index];
|
|
78
|
+
const offset = bindingData.offsetArray[index];
|
|
79
|
+
const sequence = bindingData.sequenceNumberArray[index];
|
|
80
|
+
const time = bindingData.enqueuedTimeUtcArray[index];
|
|
81
|
+
logger.debug(`CONSUMED_EVENT:${instance}#${partitionKeyArrayElement}=>(${index}#${offset}#${sequence}#${time})`);
|
|
82
|
+
}
|
|
51
83
|
return rawParsed;
|
|
52
84
|
});
|
|
53
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Type guard to check if the input is an Event Hub consumer input with binding data
|
|
88
|
+
* @param input - The input to check
|
|
89
|
+
* @returns True if the input is an Event Hub consumer input, false otherwise
|
|
90
|
+
*/
|
|
91
|
+
function isEventHubConsumerInput(input) {
|
|
92
|
+
var _a;
|
|
93
|
+
return ((0, azure_func_types_1.isEventHubFunctionInput)(input) &&
|
|
94
|
+
typeof input.bindingData === 'object' &&
|
|
95
|
+
Array.isArray((_a = input.bindingData) === null || _a === void 0 ? void 0 : _a.partitionKeyArray));
|
|
96
|
+
}
|
|
@@ -1,3 +1,2 @@
|
|
|
1
1
|
import { BoosterConfig, GraphQLRequestEnvelope, GraphQLRequestEnvelopeError } from '@boostercloud/framework-types';
|
|
2
|
-
|
|
3
|
-
export declare function rawGraphQLRequestToEnvelope(config: BoosterConfig, context: Context): Promise<GraphQLRequestEnvelope | GraphQLRequestEnvelopeError>;
|
|
2
|
+
export declare function rawGraphQLRequestToEnvelope(config: BoosterConfig, rawRequest: unknown): Promise<GraphQLRequestEnvelope | GraphQLRequestEnvelopeError>;
|
|
@@ -2,36 +2,157 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.rawGraphQLRequestToEnvelope = rawGraphQLRequestToEnvelope;
|
|
4
4
|
const framework_common_helpers_1 = require("@boostercloud/framework-common-helpers");
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
const azure_func_types_1 = require("../types/azure-func-types");
|
|
6
|
+
async function rawGraphQLRequestToEnvelope(config, rawRequest) {
|
|
7
7
|
const logger = (0, framework_common_helpers_1.getLogger)(config, 'graphql-adapter#rawGraphQLRequestToEnvelope');
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
// Handle HTTP requests (v4 programming model)
|
|
9
|
+
if ((0, azure_func_types_1.isHttpFunctionInput)(rawRequest)) {
|
|
10
|
+
return handleHttpRequest(config, rawRequest, logger);
|
|
11
|
+
}
|
|
12
|
+
// Handle WebSocket requests (Web PubSub v4 programming model)
|
|
13
|
+
if ((0, azure_func_types_1.isWebPubSubFunctionInput)(rawRequest)) {
|
|
14
|
+
return handleWebPubSubRequest(config, rawRequest, logger);
|
|
15
|
+
}
|
|
16
|
+
// Fallback error for unknown input types
|
|
17
|
+
const error = new Error('Unknown request type received by GraphQL adapter');
|
|
18
|
+
logger.error('Received unknown request type:', rawRequest);
|
|
19
|
+
return {
|
|
20
|
+
error,
|
|
21
|
+
requestID: 'unknown',
|
|
22
|
+
eventType: 'MESSAGE',
|
|
23
|
+
context: {
|
|
24
|
+
request: {
|
|
25
|
+
headers: {},
|
|
26
|
+
body: undefined,
|
|
27
|
+
},
|
|
28
|
+
rawContext: rawRequest,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
async function handleHttpRequest(config, input, logger) {
|
|
33
|
+
const { request, context } = input;
|
|
34
|
+
logger.debug('Received GraphQL request: ', request.url);
|
|
35
|
+
const requestID = context.invocationId;
|
|
13
36
|
try {
|
|
37
|
+
// Parse the request body
|
|
14
38
|
let graphQLValue = undefined;
|
|
15
|
-
|
|
16
|
-
graphQLValue =
|
|
39
|
+
try {
|
|
40
|
+
graphQLValue = await request.json();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Body might not be JSON, try text
|
|
44
|
+
const textBody = await request.text();
|
|
45
|
+
if (textBody) {
|
|
46
|
+
try {
|
|
47
|
+
graphQLValue = JSON.parse(textBody);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
graphQLValue = textBody;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Convert headers to a plain object
|
|
55
|
+
const headers = {};
|
|
56
|
+
request.headers.forEach((value, key) => {
|
|
57
|
+
headers[key] = value;
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
requestID,
|
|
61
|
+
eventType: 'MESSAGE',
|
|
62
|
+
token: request.headers.get('authorization') || undefined,
|
|
63
|
+
value: graphQLValue,
|
|
64
|
+
context: {
|
|
65
|
+
request: {
|
|
66
|
+
headers,
|
|
67
|
+
body: graphQLValue,
|
|
68
|
+
},
|
|
69
|
+
rawContext: input,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
const error = e;
|
|
75
|
+
logger.error('Error processing GraphQL HTTP request: ', error);
|
|
76
|
+
// Convert headers to a plain object for error response
|
|
77
|
+
const headers = {};
|
|
78
|
+
request.headers.forEach((value, key) => {
|
|
79
|
+
headers[key] = value;
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
error,
|
|
83
|
+
requestID,
|
|
84
|
+
eventType: 'MESSAGE',
|
|
85
|
+
context: {
|
|
86
|
+
request: {
|
|
87
|
+
headers,
|
|
88
|
+
body: undefined,
|
|
89
|
+
},
|
|
90
|
+
rawContext: input,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function handleWebPubSubRequest(config, input, logger) {
|
|
96
|
+
var _a, _b;
|
|
97
|
+
const { request: webPubSubRequest, context } = input;
|
|
98
|
+
const requestID = context.invocationId;
|
|
99
|
+
// Get connection context from either location (request or triggerMetadata)
|
|
100
|
+
const connectionContext = (0, azure_func_types_1.getWebPubSubConnectionContext)(input);
|
|
101
|
+
if (!connectionContext) {
|
|
102
|
+
const error = new Error('WebPubSub connectionContext not found in request or triggerMetadata');
|
|
103
|
+
logger.error('Missing connectionContext. Input structure', JSON.stringify(input, null, 2));
|
|
104
|
+
return {
|
|
105
|
+
error,
|
|
106
|
+
requestID,
|
|
107
|
+
eventType: 'MESSAGE',
|
|
108
|
+
context: {
|
|
109
|
+
request: {
|
|
110
|
+
headers: {},
|
|
111
|
+
body: undefined,
|
|
112
|
+
},
|
|
113
|
+
rawContext: input,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
logger.debug('Received GraphQL WebSocket request: ', connectionContext.eventName);
|
|
118
|
+
const connectionID = connectionContext.connectionId;
|
|
119
|
+
// Map Web PubSub event names to Booster EventType
|
|
120
|
+
let eventType = 'MESSAGE';
|
|
121
|
+
if (((_a = connectionContext.eventType) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'system') {
|
|
122
|
+
const eventName = (_b = connectionContext.eventName) === null || _b === void 0 ? void 0 : _b.toUpperCase();
|
|
123
|
+
if (eventName === 'CONNECT' || eventName === 'DISCONNECT') {
|
|
124
|
+
eventType = eventName;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
// Parse the data if it's a JSON string (Web PubSub sends data as string)
|
|
129
|
+
let messageData = webPubSubRequest === null || webPubSubRequest === void 0 ? void 0 : webPubSubRequest.data;
|
|
130
|
+
if (typeof messageData === 'string') {
|
|
131
|
+
try {
|
|
132
|
+
messageData = JSON.parse(messageData);
|
|
133
|
+
logger.debug('Parsed Web PubSub message data from string:', messageData);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
logger.debug('WebPubSub data is not valid JSON, using as-is:', messageData);
|
|
137
|
+
}
|
|
17
138
|
}
|
|
18
139
|
return {
|
|
19
140
|
requestID,
|
|
20
141
|
connectionID,
|
|
21
142
|
eventType,
|
|
22
|
-
|
|
23
|
-
value: graphQLValue,
|
|
143
|
+
value: messageData,
|
|
24
144
|
context: {
|
|
25
145
|
request: {
|
|
26
|
-
headers:
|
|
27
|
-
body:
|
|
146
|
+
headers: {},
|
|
147
|
+
body: messageData,
|
|
28
148
|
},
|
|
29
|
-
rawContext:
|
|
149
|
+
rawContext: input,
|
|
30
150
|
},
|
|
31
151
|
};
|
|
32
152
|
}
|
|
33
153
|
catch (e) {
|
|
34
154
|
const error = e;
|
|
155
|
+
logger.error('Error processing GraphQL WebSocket request: ', error);
|
|
35
156
|
return {
|
|
36
157
|
error,
|
|
37
158
|
requestID,
|
|
@@ -39,10 +160,10 @@ async function rawGraphQLRequestToEnvelope(config, context) {
|
|
|
39
160
|
eventType,
|
|
40
161
|
context: {
|
|
41
162
|
request: {
|
|
42
|
-
headers:
|
|
43
|
-
body:
|
|
163
|
+
headers: {},
|
|
164
|
+
body: undefined,
|
|
44
165
|
},
|
|
45
|
-
rawContext:
|
|
166
|
+
rawContext: input,
|
|
46
167
|
},
|
|
47
168
|
};
|
|
48
169
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { BoosterConfig, HealthEnvelope } from '@boostercloud/framework-types';
|
|
2
2
|
import { Container, CosmosClient } from '@azure/cosmos';
|
|
3
|
-
import { Context } from '@azure/functions';
|
|
4
3
|
export declare function databaseUrl(cosmosDb: CosmosClient, config: BoosterConfig): Promise<Array<string>>;
|
|
5
4
|
export declare function getContainer(cosmosDb: CosmosClient, config: BoosterConfig, containerName: string): Container;
|
|
6
5
|
export declare function isContainerUp(cosmosDb: CosmosClient, config: BoosterConfig, containerName: string): Promise<boolean>;
|
|
@@ -15,6 +14,13 @@ export declare function isRocketFunctionUp(rocketFunctionAppName: string): Promi
|
|
|
15
14
|
export declare function areRocketFunctionsUp(): Promise<{
|
|
16
15
|
[key: string]: boolean;
|
|
17
16
|
}>;
|
|
18
|
-
export declare function rawRequestToSensorHealthComponentPath(rawRequest:
|
|
19
|
-
|
|
17
|
+
export declare function rawRequestToSensorHealthComponentPath(rawRequest: unknown): string;
|
|
18
|
+
/**
|
|
19
|
+
* Converts the raw HTTP request to a HealthEnvelope.
|
|
20
|
+
* Note: Body is not parsed synchronously in v4 - it will be undefined.
|
|
21
|
+
* The health endpoint typically doesn't need the request body.
|
|
22
|
+
* @param rawRequest - The raw HTTP request from the Azure Function
|
|
23
|
+
* @returns A HealthEnvelope object
|
|
24
|
+
*/
|
|
25
|
+
export declare function rawRequestToSensorHealth(rawRequest: unknown): HealthEnvelope;
|
|
20
26
|
export declare function databaseReadModelsHealthDetails(cosmosDb: CosmosClient, config: BoosterConfig): Promise<unknown>;
|
|
@@ -17,6 +17,7 @@ exports.rawRequestToSensorHealth = rawRequestToSensorHealth;
|
|
|
17
17
|
exports.databaseReadModelsHealthDetails = databaseReadModelsHealthDetails;
|
|
18
18
|
const constants_1 = require("../constants");
|
|
19
19
|
const framework_common_helpers_1 = require("@boostercloud/framework-common-helpers");
|
|
20
|
+
const azure_func_types_1 = require("../types/azure-func-types");
|
|
20
21
|
async function databaseUrl(cosmosDb, config) {
|
|
21
22
|
const database = cosmosDb.database(config.resourceNames.applicationStack);
|
|
22
23
|
return [database.url];
|
|
@@ -117,25 +118,46 @@ async function areRocketFunctionsUp() {
|
|
|
117
118
|
return results.reduce((acc, result) => ({ ...acc, ...result }), {});
|
|
118
119
|
}
|
|
119
120
|
function rawRequestToSensorHealthComponentPath(rawRequest) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
if ((0, azure_func_types_1.isHttpFunctionInput)(rawRequest)) {
|
|
122
|
+
const input = rawRequest;
|
|
123
|
+
const url = input.request.url;
|
|
124
|
+
const parameters = url.replace(/^.*sensor\/health\/?/, '');
|
|
125
|
+
return parameters !== null && parameters !== void 0 ? parameters : '';
|
|
126
|
+
}
|
|
127
|
+
return '';
|
|
123
128
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Converts the raw HTTP request to a HealthEnvelope.
|
|
131
|
+
* Note: Body is not parsed synchronously in v4 - it will be undefined.
|
|
132
|
+
* The health endpoint typically doesn't need the request body.
|
|
133
|
+
* @param rawRequest - The raw HTTP request from the Azure Function
|
|
134
|
+
* @returns A HealthEnvelope object
|
|
135
|
+
*/
|
|
136
|
+
function rawRequestToSensorHealth(rawRequest) {
|
|
137
|
+
var _a;
|
|
138
|
+
if (!(0, azure_func_types_1.isHttpFunctionInput)(rawRequest)) {
|
|
139
|
+
throw new Error('Invalid input type for rawRequestToSensorHealth: expected AzureHttpFunctionInput');
|
|
140
|
+
}
|
|
141
|
+
const input = rawRequest;
|
|
142
|
+
const { request, context } = input;
|
|
143
|
+
const componentPath = rawRequestToSensorHealthComponentPath(rawRequest);
|
|
144
|
+
const requestID = context.invocationId;
|
|
145
|
+
// Convert headers to a plain object
|
|
146
|
+
const headers = {};
|
|
147
|
+
request.headers.forEach((value, key) => {
|
|
148
|
+
headers[key] = value;
|
|
149
|
+
});
|
|
128
150
|
return {
|
|
129
151
|
requestID: requestID,
|
|
130
152
|
context: {
|
|
131
153
|
request: {
|
|
132
|
-
headers
|
|
133
|
-
body:
|
|
154
|
+
headers,
|
|
155
|
+
body: undefined, // Body not parsed synchronously in v4
|
|
134
156
|
},
|
|
135
|
-
rawContext:
|
|
157
|
+
rawContext: rawRequest,
|
|
136
158
|
},
|
|
137
159
|
componentPath: componentPath,
|
|
138
|
-
token: (
|
|
160
|
+
token: (_a = request.headers.get('authorization')) !== null && _a !== void 0 ? _a : undefined,
|
|
139
161
|
};
|
|
140
162
|
}
|
|
141
163
|
async function databaseReadModelsHealthDetails(cosmosDb, config) {
|
|
@@ -3,4 +3,11 @@ import { BoosterConfig, ReadModelEnvelope, ReadModelInterface, ReadOnlyNonEmptyA
|
|
|
3
3
|
export declare function fetchReadModel(db: CosmosClient, config: BoosterConfig, readModelName: string, readModelID: UUID): Promise<ReadOnlyNonEmptyArray<ReadModelInterface>>;
|
|
4
4
|
export declare function storeReadModel(db: CosmosClient, config: BoosterConfig, readModelName: string, readModel: ReadModelInterface): Promise<void>;
|
|
5
5
|
export declare function deleteReadModel(db: CosmosClient, config: BoosterConfig, readModelName: string, readModel: ReadModelInterface): Promise<void>;
|
|
6
|
+
/**
|
|
7
|
+
* Converts raw read model events from Cosmos DB change feed to Booster envelopes.
|
|
8
|
+
* In v4 programming model, the input wrapper contains documents directly and the typeName.
|
|
9
|
+
* @param config - Booster configuration
|
|
10
|
+
* @param rawEvents - Raw events from the Azure Function input
|
|
11
|
+
* @returns A promise that resolves to an array of ReadModelEnvelope objects
|
|
12
|
+
*/
|
|
6
13
|
export declare function rawReadModelEventsToEnvelopes(config: BoosterConfig, rawEvents: unknown): Promise<Array<ReadModelEnvelope>>;
|
|
@@ -7,6 +7,7 @@ exports.rawReadModelEventsToEnvelopes = rawReadModelEventsToEnvelopes;
|
|
|
7
7
|
const framework_types_1 = require("@boostercloud/framework-types");
|
|
8
8
|
const framework_common_helpers_1 = require("@boostercloud/framework-common-helpers");
|
|
9
9
|
const constants_1 = require("../constants");
|
|
10
|
+
const subscription_model_1 = require("./subscription-model");
|
|
10
11
|
async function fetchReadModel(db, config, readModelName, readModelID) {
|
|
11
12
|
const logger = (0, framework_common_helpers_1.getLogger)(config, 'read-model-adapter#fetchReadModel');
|
|
12
13
|
const { resource } = await db
|
|
@@ -100,12 +101,19 @@ async function deleteReadModel(db, config, readModelName, readModel) {
|
|
|
100
101
|
logger.warn(`Read model to delete ${readModelName} ID = ${readModel.id} not found`);
|
|
101
102
|
}
|
|
102
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Converts raw read model events from Cosmos DB change feed to Booster envelopes.
|
|
106
|
+
* In v4 programming model, the input wrapper contains documents directly and the typeName.
|
|
107
|
+
* @param config - Booster configuration
|
|
108
|
+
* @param rawEvents - Raw events from the Azure Function input
|
|
109
|
+
* @returns A promise that resolves to an array of ReadModelEnvelope objects
|
|
110
|
+
*/
|
|
103
111
|
async function rawReadModelEventsToEnvelopes(config, rawEvents) {
|
|
104
112
|
const logger = (0, framework_common_helpers_1.getLogger)(config, 'read-model-adapter#rawReadModelEventsToEnvelopes');
|
|
105
113
|
logger.debug(`Parsing raw read models ${JSON.stringify(rawEvents)}`);
|
|
106
|
-
if (
|
|
107
|
-
const typeName = rawEvents
|
|
108
|
-
return
|
|
114
|
+
if ((0, subscription_model_1.isSubscriptionNotifierInput)(rawEvents)) {
|
|
115
|
+
const { documents, typeName } = rawEvents;
|
|
116
|
+
return documents.map((rawEvent) => {
|
|
109
117
|
const { _rid, _self, _st, _etag, _lsn, _ts, ...rest } = rawEvent;
|
|
110
118
|
return {
|
|
111
119
|
typeName: typeName,
|
|
@@ -116,7 +124,3 @@ async function rawReadModelEventsToEnvelopes(config, rawEvents) {
|
|
|
116
124
|
logger.warn(`Unexpected events to be parsed ${JSON.stringify(rawEvents)}`);
|
|
117
125
|
return [];
|
|
118
126
|
}
|
|
119
|
-
function isSubscriptionContext(rawRequest) {
|
|
120
|
-
return (rawRequest.bindings !== undefined &&
|
|
121
|
-
rawRequest.bindings.rawEvent !== undefined);
|
|
122
|
-
}
|
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
import { BoosterConfig, ScheduledCommandEnvelope } from '@boostercloud/framework-types';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import { AzureTimerFunctionInput } from '../types/azure-func-types';
|
|
3
|
+
/**
|
|
4
|
+
* V4 Programming Model: Input wrapper for scheduled command functions.
|
|
5
|
+
* Extends the base timer input with the command type name.
|
|
6
|
+
*/
|
|
7
|
+
export interface ScheduledCommandInput extends AzureTimerFunctionInput {
|
|
8
|
+
/** The scheduled command type name */
|
|
9
|
+
typeName: string;
|
|
6
10
|
}
|
|
7
|
-
|
|
8
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Type guard to check if the input is a scheduled command input
|
|
13
|
+
* @param input - The input to check
|
|
14
|
+
* @returns True if the input is a ScheduledCommandInput, false otherwise
|
|
15
|
+
*/
|
|
16
|
+
export declare function isScheduledCommandInput(input: unknown): input is ScheduledCommandInput;
|
|
17
|
+
/**
|
|
18
|
+
* Converts the raw timer trigger input to a Booster ScheduledCommandEnvelope.
|
|
19
|
+
* In v4 programming model, the timer info and typeName are passed in the input wrapper.
|
|
20
|
+
* @param config - Booster configuration
|
|
21
|
+
* @param input - The raw input from the Azure Function
|
|
22
|
+
* @returns A promise that resolves to a ScheduledCommandEnvelope
|
|
23
|
+
*/
|
|
24
|
+
export declare function rawScheduledInputToEnvelope(config: BoosterConfig, input: unknown): Promise<ScheduledCommandEnvelope>;
|
|
@@ -1,15 +1,35 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isScheduledCommandInput = isScheduledCommandInput;
|
|
3
4
|
exports.rawScheduledInputToEnvelope = rawScheduledInputToEnvelope;
|
|
4
5
|
const framework_types_1 = require("@boostercloud/framework-types");
|
|
5
6
|
const framework_common_helpers_1 = require("@boostercloud/framework-common-helpers");
|
|
7
|
+
const azure_func_types_1 = require("../types/azure-func-types");
|
|
8
|
+
/**
|
|
9
|
+
* Type guard to check if the input is a scheduled command input
|
|
10
|
+
* @param input - The input to check
|
|
11
|
+
* @returns True if the input is a ScheduledCommandInput, false otherwise
|
|
12
|
+
*/
|
|
13
|
+
function isScheduledCommandInput(input) {
|
|
14
|
+
return (0, azure_func_types_1.isTimerFunctionInput)(input);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Converts the raw timer trigger input to a Booster ScheduledCommandEnvelope.
|
|
18
|
+
* In v4 programming model, the timer info and typeName are passed in the input wrapper.
|
|
19
|
+
* @param config - Booster configuration
|
|
20
|
+
* @param input - The raw input from the Azure Function
|
|
21
|
+
* @returns A promise that resolves to a ScheduledCommandEnvelope
|
|
22
|
+
*/
|
|
6
23
|
async function rawScheduledInputToEnvelope(config, input) {
|
|
7
24
|
const logger = (0, framework_common_helpers_1.getLogger)(config, 'scheduled-adapter#rawScheduledInputToEnvelope');
|
|
8
25
|
logger.debug('Received AzureScheduledCommand request: ', input);
|
|
9
|
-
if (!
|
|
10
|
-
throw new Error(`
|
|
26
|
+
if (!isScheduledCommandInput(input)) {
|
|
27
|
+
throw new Error(`Invalid scheduled command input. Expected ScheduledCommandInput with typeName and timer, but received: ${JSON.stringify(input)}`);
|
|
28
|
+
}
|
|
29
|
+
const { typeName, timer } = input;
|
|
30
|
+
logger.debug(`Processing scheduled command: ${typeName}, isPastDue: ${timer.isPastDue}`);
|
|
11
31
|
return {
|
|
12
32
|
requestID: framework_types_1.UUID.generate(),
|
|
13
|
-
typeName:
|
|
33
|
+
typeName: typeName,
|
|
14
34
|
};
|
|
15
35
|
}
|
|
@@ -1,15 +1,8 @@
|
|
|
1
|
+
import { InvocationContext } from '@azure/functions';
|
|
1
2
|
import { BoosterMetadata } from '@boostercloud/framework-types';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
executionContext: ExecutionContext;
|
|
6
|
-
bindings: Bindings;
|
|
7
|
-
bindingData: unknown;
|
|
8
|
-
bindingDefinitions: unknown;
|
|
9
|
-
}
|
|
10
|
-
export interface Bindings {
|
|
11
|
-
rawEvent: RawEvent[];
|
|
12
|
-
}
|
|
3
|
+
/**
|
|
4
|
+
* Raw event structure from Cosmos DB change feed
|
|
5
|
+
*/
|
|
13
6
|
export interface RawEvent {
|
|
14
7
|
id: string;
|
|
15
8
|
_rid: string;
|
|
@@ -20,9 +13,19 @@ export interface RawEvent {
|
|
|
20
13
|
boosterMetadata: BoosterMetadata;
|
|
21
14
|
_lsn: number;
|
|
22
15
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
16
|
+
/**
|
|
17
|
+
* V4 Programming Model: Input wrapper for subscription notifier functions.
|
|
18
|
+
* In v4, documents are passed directly as the first argument to the handler.
|
|
19
|
+
*/
|
|
20
|
+
export interface SubscriptionNotifierInput {
|
|
21
|
+
documents: RawEvent[];
|
|
22
|
+
context: InvocationContext;
|
|
23
|
+
/** The read model type name, extracted from function configuration */
|
|
24
|
+
typeName: string;
|
|
28
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Type guard to check if the input is a v4 subscription notifier input
|
|
28
|
+
* @param input - The input to check
|
|
29
|
+
* @returns True if the input is a SubscriptionNotifierInput, false otherwise
|
|
30
|
+
*/
|
|
31
|
+
export declare function isSubscriptionNotifierInput(input: unknown): input is SubscriptionNotifierInput;
|
|
@@ -1,2 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isSubscriptionNotifierInput = isSubscriptionNotifierInput;
|
|
4
|
+
/**
|
|
5
|
+
* Type guard to check if the input is a v4 subscription notifier input
|
|
6
|
+
* @param input - The input to check
|
|
7
|
+
* @returns True if the input is a SubscriptionNotifierInput, false otherwise
|
|
8
|
+
*/
|
|
9
|
+
function isSubscriptionNotifierInput(input) {
|
|
10
|
+
return (typeof input === 'object' &&
|
|
11
|
+
input !== null &&
|
|
12
|
+
'documents' in input &&
|
|
13
|
+
'context' in input &&
|
|
14
|
+
'typeName' in input &&
|
|
15
|
+
Array.isArray(input.documents));
|
|
16
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure Functions v4 Programming Model Types
|
|
3
|
+
*
|
|
4
|
+
* These types provide a wrapper for Azure Functions vs inputs to standardize
|
|
5
|
+
* the interface between function handlers and Booster adapters
|
|
6
|
+
*/
|
|
7
|
+
import { HttpRequest, InvocationContext } from '@azure/functions';
|
|
8
|
+
/**
|
|
9
|
+
* Standard wrapper for HTTP-triggered Azure Function v4 inputs.
|
|
10
|
+
* This replaces the v3 Context object for HTTP triggers.
|
|
11
|
+
*/
|
|
12
|
+
export interface AzureHttpFunctionInput {
|
|
13
|
+
request: HttpRequest;
|
|
14
|
+
context: InvocationContext;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Standard wrapper for CosmosDB-triggered Azure Function v4 inputs.
|
|
18
|
+
*/
|
|
19
|
+
export interface AzureCosmosDBFunctionInput {
|
|
20
|
+
documents: unknown[];
|
|
21
|
+
context: InvocationContext;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Standard wrapper for EventHub-triggered Azure Function v4 inputs.
|
|
25
|
+
*/
|
|
26
|
+
export interface AzureEventHubFunctionInput {
|
|
27
|
+
messages: unknown[];
|
|
28
|
+
context: InvocationContext;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Standard wrapper for Timer-triggered Azure Function v4 inputs.
|
|
32
|
+
*/
|
|
33
|
+
export interface AzureTimerFunctionInput {
|
|
34
|
+
timer: {
|
|
35
|
+
isPastDue: boolean;
|
|
36
|
+
schedule: {
|
|
37
|
+
adjustForDST: boolean;
|
|
38
|
+
};
|
|
39
|
+
scheduleStatus?: {
|
|
40
|
+
last: string;
|
|
41
|
+
next: string;
|
|
42
|
+
lastUpdated: string;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
context: InvocationContext;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Web PubSub connection context structure
|
|
49
|
+
*/
|
|
50
|
+
export interface WebPubSubConnectionContext {
|
|
51
|
+
connectionId: string;
|
|
52
|
+
userId?: string;
|
|
53
|
+
hub: string;
|
|
54
|
+
eventType: string;
|
|
55
|
+
eventName: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Standard wrapper for Web PubSub-triggered Azure Function v4 inputs.
|
|
59
|
+
* Note: connectionContext can be found in either request.connectionContext
|
|
60
|
+
* or context.triggerMetadata.connectionContext depending on Azure Functions version.
|
|
61
|
+
*/
|
|
62
|
+
export interface AzureWebPubSubFunctionInput {
|
|
63
|
+
request: {
|
|
64
|
+
connectionContext?: WebPubSubConnectionContext;
|
|
65
|
+
data?: unknown;
|
|
66
|
+
dataType?: string;
|
|
67
|
+
};
|
|
68
|
+
context: InvocationContext & {
|
|
69
|
+
triggerMetadata?: {
|
|
70
|
+
connectionContext?: WebPubSubConnectionContext;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Type alias for the raw request that Booster adapters receive.
|
|
76
|
+
* This can be any of the v4 input types.
|
|
77
|
+
*/
|
|
78
|
+
export type AzureFunctionRawRequest = AzureHttpFunctionInput | AzureCosmosDBFunctionInput | AzureEventHubFunctionInput | AzureTimerFunctionInput | AzureWebPubSubFunctionInput;
|
|
79
|
+
/**
|
|
80
|
+
* Type guard to check if the input is an HTTP function input
|
|
81
|
+
* @param input - The input to check
|
|
82
|
+
* @returns True if the input is an HTTP function input, false otherwise
|
|
83
|
+
*/
|
|
84
|
+
export declare function isHttpFunctionInput(input: unknown): input is AzureHttpFunctionInput;
|
|
85
|
+
/**
|
|
86
|
+
* Type guard to check if the input is a CosmosDB function input
|
|
87
|
+
* @param input - The input to check
|
|
88
|
+
* @returns True if the input is a CosmosDB function input, false otherwise
|
|
89
|
+
*/
|
|
90
|
+
export declare function isCosmosDBFunctionInput(input: unknown): input is AzureCosmosDBFunctionInput;
|
|
91
|
+
/**
|
|
92
|
+
* Type guard to check if the input is an EventHub function input
|
|
93
|
+
* @param input - The input to check
|
|
94
|
+
* @returns True if the input is an EventHub function input, false otherwise
|
|
95
|
+
*/
|
|
96
|
+
export declare function isEventHubFunctionInput(input: unknown): input is AzureEventHubFunctionInput;
|
|
97
|
+
/**
|
|
98
|
+
* Type guard to check if the input is a Timer function input
|
|
99
|
+
* @param input - The input to check
|
|
100
|
+
* @returns True if the input is a Timer function input, false otherwise
|
|
101
|
+
*/
|
|
102
|
+
export declare function isTimerFunctionInput(input: unknown): input is AzureTimerFunctionInput;
|
|
103
|
+
/**
|
|
104
|
+
* Type guard to check if the input is a Web PubSub function input.
|
|
105
|
+
* Checks for connectionContext in both request.connectionContext and context.triggerMetadata.connectionContext
|
|
106
|
+
* as Azure Functions v4 may provide it in different locations.
|
|
107
|
+
* @param input - The input to check
|
|
108
|
+
* @returns True if the input is a Web PubSub function input, false otherwise
|
|
109
|
+
*/
|
|
110
|
+
export declare function isWebPubSubFunctionInput(input: unknown): input is AzureWebPubSubFunctionInput;
|
|
111
|
+
/**
|
|
112
|
+
* Helper to extract connectionContext from either location in Web PubSub input
|
|
113
|
+
* @param input - The Azure Web PubSub function input
|
|
114
|
+
* @returns The connection context if present, undefined otherwise
|
|
115
|
+
*/
|
|
116
|
+
export declare function getWebPubSubConnectionContext(input: AzureWebPubSubFunctionInput): WebPubSubConnectionContext | undefined;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isHttpFunctionInput = isHttpFunctionInput;
|
|
4
|
+
exports.isCosmosDBFunctionInput = isCosmosDBFunctionInput;
|
|
5
|
+
exports.isEventHubFunctionInput = isEventHubFunctionInput;
|
|
6
|
+
exports.isTimerFunctionInput = isTimerFunctionInput;
|
|
7
|
+
exports.isWebPubSubFunctionInput = isWebPubSubFunctionInput;
|
|
8
|
+
exports.getWebPubSubConnectionContext = getWebPubSubConnectionContext;
|
|
9
|
+
/**
|
|
10
|
+
* Type guard to check if the input is an HTTP function input
|
|
11
|
+
* @param input - The input to check
|
|
12
|
+
* @returns True if the input is an HTTP function input, false otherwise
|
|
13
|
+
*/
|
|
14
|
+
function isHttpFunctionInput(input) {
|
|
15
|
+
var _a;
|
|
16
|
+
return (typeof input === 'object' &&
|
|
17
|
+
input !== null &&
|
|
18
|
+
'request' in input &&
|
|
19
|
+
'context' in input &&
|
|
20
|
+
typeof ((_a = input.request) === null || _a === void 0 ? void 0 : _a.method) === 'string');
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Type guard to check if the input is a CosmosDB function input
|
|
24
|
+
* @param input - The input to check
|
|
25
|
+
* @returns True if the input is a CosmosDB function input, false otherwise
|
|
26
|
+
*/
|
|
27
|
+
function isCosmosDBFunctionInput(input) {
|
|
28
|
+
return (typeof input === 'object' &&
|
|
29
|
+
input !== null &&
|
|
30
|
+
'documents' in input &&
|
|
31
|
+
'context' in input &&
|
|
32
|
+
Array.isArray(input.documents));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Type guard to check if the input is an EventHub function input
|
|
36
|
+
* @param input - The input to check
|
|
37
|
+
* @returns True if the input is an EventHub function input, false otherwise
|
|
38
|
+
*/
|
|
39
|
+
function isEventHubFunctionInput(input) {
|
|
40
|
+
return (typeof input === 'object' &&
|
|
41
|
+
input !== null &&
|
|
42
|
+
'messages' in input &&
|
|
43
|
+
'context' in input &&
|
|
44
|
+
Array.isArray(input.messages));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Type guard to check if the input is a Timer function input
|
|
48
|
+
* @param input - The input to check
|
|
49
|
+
* @returns True if the input is a Timer function input, false otherwise
|
|
50
|
+
*/
|
|
51
|
+
function isTimerFunctionInput(input) {
|
|
52
|
+
var _a;
|
|
53
|
+
return (typeof input === 'object' &&
|
|
54
|
+
input !== null &&
|
|
55
|
+
'timer' in input &&
|
|
56
|
+
'context' in input &&
|
|
57
|
+
typeof ((_a = input.timer) === null || _a === void 0 ? void 0 : _a.isPastDue) === 'boolean');
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Type guard to check if the input is a Web PubSub function input.
|
|
61
|
+
* Checks for connectionContext in both request.connectionContext and context.triggerMetadata.connectionContext
|
|
62
|
+
* as Azure Functions v4 may provide it in different locations.
|
|
63
|
+
* @param input - The input to check
|
|
64
|
+
* @returns True if the input is a Web PubSub function input, false otherwise
|
|
65
|
+
*/
|
|
66
|
+
function isWebPubSubFunctionInput(input) {
|
|
67
|
+
var _a, _b, _c, _d, _e;
|
|
68
|
+
if (typeof input !== 'object' || input === null || !('request' in input) || !('context' in input)) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
const typedInput = input;
|
|
72
|
+
// Check connectionContext in request.connectionContext
|
|
73
|
+
if (typeof ((_b = (_a = typedInput.request) === null || _a === void 0 ? void 0 : _a.connectionContext) === null || _b === void 0 ? void 0 : _b.connectionId) === 'string') {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
// Check connectionContext in context.triggerMetadata.connectionContext
|
|
77
|
+
return typeof ((_e = (_d = (_c = typedInput.context) === null || _c === void 0 ? void 0 : _c.triggerMetadata) === null || _d === void 0 ? void 0 : _d.connectionContext) === null || _e === void 0 ? void 0 : _e.connectionId) === 'string';
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Helper to extract connectionContext from either location in Web PubSub input
|
|
81
|
+
* @param input - The Azure Web PubSub function input
|
|
82
|
+
* @returns The connection context if present, undefined otherwise
|
|
83
|
+
*/
|
|
84
|
+
function getWebPubSubConnectionContext(input) {
|
|
85
|
+
var _a, _b, _c, _d;
|
|
86
|
+
return (_b = (_a = input.request) === null || _a === void 0 ? void 0 : _a.connectionContext) !== null && _b !== void 0 ? _b : (_d = (_c = input.context) === null || _c === void 0 ? void 0 : _c.triggerMetadata) === null || _d === void 0 ? void 0 : _d.connectionContext;
|
|
87
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boostercloud/framework-provider-azure",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Handle Booster's integration with Azure",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"framework-provider-azure"
|
|
@@ -20,21 +20,21 @@
|
|
|
20
20
|
"url": "git+https://github.com/boostercloud/booster.git"
|
|
21
21
|
},
|
|
22
22
|
"engines": {
|
|
23
|
-
"node": ">=
|
|
23
|
+
"node": ">=22.0.0 <23.0.0"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@azure/cosmos": "^4.3.0",
|
|
27
|
-
"@azure/functions": "^
|
|
27
|
+
"@azure/functions": "^4.0.0",
|
|
28
28
|
"@azure/identity": "~4.7.0",
|
|
29
29
|
"@azure/event-hubs": "5.11.1",
|
|
30
|
-
"@boostercloud/framework-common-helpers": "
|
|
31
|
-
"@boostercloud/framework-types": "
|
|
30
|
+
"@boostercloud/framework-common-helpers": "^4.0.0",
|
|
31
|
+
"@boostercloud/framework-types": "^4.0.0",
|
|
32
32
|
"tslib": "^2.4.0",
|
|
33
33
|
"@effect-ts/core": "^0.60.4",
|
|
34
34
|
"@azure/web-pubsub": "~1.1.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@boostercloud/eslint-config": "
|
|
37
|
+
"@boostercloud/eslint-config": "^4.0.0",
|
|
38
38
|
"@types/chai": "4.2.18",
|
|
39
39
|
"@types/chai-as-promised": "7.1.4",
|
|
40
40
|
"@types/faker": "5.1.5",
|
|
@@ -61,27 +61,16 @@
|
|
|
61
61
|
"typescript": "5.7.3",
|
|
62
62
|
"eslint-plugin-unicorn": "~44.0.2"
|
|
63
63
|
},
|
|
64
|
+
"bugs": {
|
|
65
|
+
"url": "https://github.com/boostercloud/booster/issues"
|
|
66
|
+
},
|
|
64
67
|
"scripts": {
|
|
65
68
|
"format": "prettier --write --ext '.js,.ts' **/*.ts **/*/*.ts",
|
|
66
69
|
"lint:check": "eslint --ext '.js,.ts' **/*.ts",
|
|
67
70
|
"lint:fix": "eslint --quiet --fix --ext '.js,.ts' **/*.ts",
|
|
68
71
|
"build": "tsc -b tsconfig.json",
|
|
69
72
|
"clean": "rimraf ./dist tsconfig.tsbuildinfo",
|
|
70
|
-
"prepack": "tsc -b tsconfig.json",
|
|
71
73
|
"test:provider-azure": "npm run test",
|
|
72
74
|
"test": "nyc --extension .ts mocha --forbid-only \"test/**/*.test.ts\""
|
|
73
|
-
},
|
|
74
|
-
"bugs": {
|
|
75
|
-
"url": "https://github.com/boostercloud/booster/issues"
|
|
76
|
-
},
|
|
77
|
-
"pnpm": {
|
|
78
|
-
"overrides": {
|
|
79
|
-
"pac-resolver@<5.0.0": ">=5.0.0",
|
|
80
|
-
"underscore@>=1.3.2 <1.12.1": ">=1.13.6",
|
|
81
|
-
"node-fetch@<2.6.7": ">=2.6.7",
|
|
82
|
-
"ws@>=7.0.0 <7.4.6": ">=7.4.6",
|
|
83
|
-
"nanoid@>=3.0.0 <3.1.31": ">=3.1.31",
|
|
84
|
-
"node-fetch@<2.6.1": ">=2.6.1"
|
|
85
|
-
}
|
|
86
75
|
}
|
|
87
|
-
}
|
|
76
|
+
}
|