@pikku/azure-functions 0.12.1 → 0.12.3
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/CHANGELOG.md +24 -1
- package/dist/azure-deployment-service.d.ts +13 -0
- package/dist/azure-deployment-service.js +60 -0
- package/dist/azure-deployment-service.js.map +1 -0
- package/dist/azure-queue-service.d.ts +11 -0
- package/dist/azure-queue-service.js +53 -0
- package/dist/azure-queue-service.js.map +1 -0
- package/dist/handler-factories.d.ts +13 -0
- package/dist/handler-factories.js +121 -0
- package/dist/handler-factories.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/package.json +11 -6
- package/run-tests.sh +4 -4
- package/src/azure-deployment-service.ts +94 -0
- package/src/azure-queue-service.ts +90 -0
- package/src/handler-factories.ts +205 -0
- package/src/index.test.ts +13 -0
- package/src/index.ts +3 -0
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
## 0.12.3
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- daec082: Drop Node 22 support — the minimum supported runtime is now Node 24 (LTS).
|
|
6
|
+
|
|
7
|
+
Node 22 deadlocks `pikku dev` at `loadUserBootstrap` (tsx `register()` + `require(esm)` cycle handling on node 22.12+), and Node 20 is already below our floor. The `engines.node` requirement is raised to `>=24` across all packages, matching `.nvmrc` and the CI test matrix. Closes #751.
|
|
8
|
+
|
|
9
|
+
- Updated dependencies [7b17b14]
|
|
10
|
+
- Updated dependencies [daec082]
|
|
11
|
+
- Updated dependencies [e0fd352]
|
|
12
|
+
- @pikku/core@0.12.58
|
|
13
|
+
|
|
14
|
+
## 0.12.2
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- 41ce2cb: Upgrade to TypeScript 6 and raise the minimum Node.js version to 22.
|
|
19
|
+
|
|
20
|
+
All packages now build against `typescript@^6.0.3` and declare `engines.node >= 22`. Internal tooling (`ts-json-schema-generator`, `zod-to-ts`) was bumped to TypeScript 6-compatible releases.
|
|
21
|
+
|
|
22
|
+
- Updated dependencies [41ce2cb]
|
|
23
|
+
- @pikku/core@0.12.44
|
|
24
|
+
|
|
1
25
|
## 0.12.0
|
|
2
26
|
|
|
3
27
|
## 0.12.1
|
|
@@ -16,7 +40,6 @@
|
|
|
16
40
|
- Updated dependencies [62a8725]
|
|
17
41
|
- Updated dependencies [62a8725]
|
|
18
42
|
- Updated dependencies [62a8725]
|
|
19
|
-
|
|
20
43
|
- @pikku/core@0.12.1
|
|
21
44
|
|
|
22
45
|
- Updated dependencies
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { DeploymentService, DeploymentConfig } from '@pikku/core/services';
|
|
2
|
+
import type { JWTService } from '@pikku/core/services';
|
|
3
|
+
import type { SecretService } from '@pikku/core/services';
|
|
4
|
+
export declare class AzureDeploymentService implements DeploymentService {
|
|
5
|
+
private jwt;
|
|
6
|
+
private secrets;
|
|
7
|
+
private bindings;
|
|
8
|
+
constructor(jwt: JWTService | undefined, secrets: SecretService, functionBindings: Record<string, string>);
|
|
9
|
+
init(): Promise<void>;
|
|
10
|
+
start(_config: DeploymentConfig): Promise<void>;
|
|
11
|
+
stop(): Promise<void>;
|
|
12
|
+
invoke(funcName: string, data: unknown, session?: unknown, traceId?: string): Promise<unknown>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { encryptJSON } from '@pikku/core/crypto-utils';
|
|
2
|
+
export class AzureDeploymentService {
|
|
3
|
+
jwt;
|
|
4
|
+
secrets;
|
|
5
|
+
bindings;
|
|
6
|
+
constructor(jwt, secrets, functionBindings) {
|
|
7
|
+
this.jwt = jwt;
|
|
8
|
+
this.secrets = secrets;
|
|
9
|
+
this.bindings = new Map(Object.entries(functionBindings));
|
|
10
|
+
}
|
|
11
|
+
async init() { }
|
|
12
|
+
async start(_config) { }
|
|
13
|
+
async stop() { }
|
|
14
|
+
async invoke(funcName, data, session, traceId) {
|
|
15
|
+
const functionUrl = this.bindings.get(funcName);
|
|
16
|
+
if (!functionUrl) {
|
|
17
|
+
throw new Error(`No Azure Function binding for function '${funcName}'. ` +
|
|
18
|
+
`Available bindings: ${[...this.bindings.keys()].join(', ')}`);
|
|
19
|
+
}
|
|
20
|
+
const headers = {
|
|
21
|
+
'content-type': 'application/json',
|
|
22
|
+
...(traceId && { 'x-request-id': traceId }),
|
|
23
|
+
};
|
|
24
|
+
let secret;
|
|
25
|
+
try {
|
|
26
|
+
secret = await this.secrets.getSecret('PIKKU_REMOTE_SECRET');
|
|
27
|
+
}
|
|
28
|
+
catch { }
|
|
29
|
+
if (secret && this.jwt) {
|
|
30
|
+
const sessionEnc = session
|
|
31
|
+
? await encryptJSON(secret, { session })
|
|
32
|
+
: undefined;
|
|
33
|
+
const token = await this.jwt.encode({ value: 5, unit: 'minute' }, {
|
|
34
|
+
aud: 'pikku-remote',
|
|
35
|
+
fn: funcName,
|
|
36
|
+
iat: Math.floor(Date.now() / 1000),
|
|
37
|
+
session: sessionEnc,
|
|
38
|
+
});
|
|
39
|
+
headers.authorization = `Bearer ${token}`;
|
|
40
|
+
}
|
|
41
|
+
const url = `${functionUrl}/remote/rpc/${encodeURIComponent(funcName)}`;
|
|
42
|
+
const response = await fetch(url, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers,
|
|
45
|
+
body: JSON.stringify({ data }),
|
|
46
|
+
});
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
let errorBody;
|
|
49
|
+
try {
|
|
50
|
+
errorBody = JSON.stringify(await response.json());
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
errorBody = await response.text();
|
|
54
|
+
}
|
|
55
|
+
throw new Error(`Remote RPC call to '${funcName}' failed: ${response.status} ${response.statusText}. ${errorBody}`);
|
|
56
|
+
}
|
|
57
|
+
return response.json();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=azure-deployment-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"azure-deployment-service.js","sourceRoot":"","sources":["../src/azure-deployment-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { QueueService, QueueJob, JobOptions } from '@pikku/core/queue';
|
|
2
|
+
export declare class AzureQueueService implements QueueService {
|
|
3
|
+
readonly supportsResults = false;
|
|
4
|
+
private queueServiceClient;
|
|
5
|
+
private queueClients;
|
|
6
|
+
constructor(connectionString?: string);
|
|
7
|
+
add<T>(queueName: string, data: T, options?: JobOptions): Promise<string>;
|
|
8
|
+
getJob<T, R>(_queueName: string, _jobId: string): Promise<QueueJob<T, R> | null>;
|
|
9
|
+
private getQueueClient;
|
|
10
|
+
private resolveQueueName;
|
|
11
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { QueueServiceClient } from '@azure/storage-queue';
|
|
2
|
+
export class AzureQueueService {
|
|
3
|
+
supportsResults = false;
|
|
4
|
+
queueServiceClient;
|
|
5
|
+
queueClients = new Map();
|
|
6
|
+
constructor(connectionString) {
|
|
7
|
+
const connStr = connectionString ?? process.env.AzureWebJobsStorage;
|
|
8
|
+
if (!connStr) {
|
|
9
|
+
throw new Error('Azure Queue connection string not provided; supply connectionString or set AzureWebJobsStorage');
|
|
10
|
+
}
|
|
11
|
+
this.queueServiceClient = QueueServiceClient.fromConnectionString(connStr);
|
|
12
|
+
}
|
|
13
|
+
async add(queueName, data, options) {
|
|
14
|
+
const client = this.getQueueClient(queueName);
|
|
15
|
+
const messageId = crypto.randomUUID();
|
|
16
|
+
const message = {
|
|
17
|
+
id: messageId,
|
|
18
|
+
queueName,
|
|
19
|
+
data,
|
|
20
|
+
pikkuUserId: options?.pikkuUserId,
|
|
21
|
+
};
|
|
22
|
+
const encoded = Buffer.from(JSON.stringify(message)).toString('base64');
|
|
23
|
+
const visibilityTimeout = options?.delay
|
|
24
|
+
? Math.min(Math.ceil(options.delay / 1000), 604800)
|
|
25
|
+
: undefined;
|
|
26
|
+
await client.sendMessage(encoded, { visibilityTimeout });
|
|
27
|
+
return messageId;
|
|
28
|
+
}
|
|
29
|
+
async getJob(_queueName, _jobId) {
|
|
30
|
+
throw new Error('AzureQueueService does not support getJob(). ' +
|
|
31
|
+
'Azure Storage Queues are fire-and-forget. ' +
|
|
32
|
+
'Use BullMQ or PgBoss if you need job result tracking.');
|
|
33
|
+
}
|
|
34
|
+
getQueueClient(queueName) {
|
|
35
|
+
if (!this.queueClients.has(queueName)) {
|
|
36
|
+
const resolvedName = this.resolveQueueName(queueName);
|
|
37
|
+
const client = this.queueServiceClient.getQueueClient(resolvedName);
|
|
38
|
+
this.queueClients.set(queueName, client);
|
|
39
|
+
}
|
|
40
|
+
return this.queueClients.get(queueName);
|
|
41
|
+
}
|
|
42
|
+
resolveQueueName(queueName) {
|
|
43
|
+
const envKey = `AZURE_QUEUE_NAME_${toScreamingSnake(queueName)}`;
|
|
44
|
+
return process.env[envKey] ?? queueName;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function toScreamingSnake(name) {
|
|
48
|
+
return name
|
|
49
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
50
|
+
.replace(/-/g, '_')
|
|
51
|
+
.toUpperCase();
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=azure-queue-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"azure-queue-service.js","sourceRoot":"","sources":["../src/azure-queue-service.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAIzD,MAAM,OAAO,iBAAiB;IACnB,eAAe,GAAG,KAAK,CAAA;IACxB,kBAAkB,CAAoB;IACtC,YAAY,GAAG,IAAI,GAAG,EAAuB,CAAA;IAErD,YAAY,gBAAyB;QACnC,MAAM,OAAO,GAAG,gBAAgB,IAAI,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAA;QACnE,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,IAAI,KAAK,CACb,gGAAgG,CACjG,CAAA;QACH,CAAC;QACD,IAAI,CAAC,kBAAkB,GAAG,kBAAkB,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAA;IAC5E,CAAC;IAED,KAAK,CAAC,GAAG,CACP,SAAiB,EACjB,IAAO,EACP,OAAoB;QAEpB,MAAM,MAAM,GAAG,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC,CAAA;QAC7C,MAAM,SAAS,GAAG,MAAM,CAAC,UAAU,EAAE,CAAA;QAErC,MAAM,OAAO,GAAG;YACd,EAAE,EAAE,SAAS;YACb,SAAS;YACT,IAAI;YACJ,WAAW,EAAE,OAAO,EAAE,WAAW;SAClC,CAAA;QAGD,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAA;QAEvE,MAAM,iBAAiB,GAAG,OAAO,EAAE,KAAK;YACtC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,EAAE,MAAM,CAAC;YACnD,CAAC,CAAC,SAAS,CAAA;QAEb,MAAM,MAAM,CAAC,WAAW,CAAC,OAAO,EAAE,EAAE,iBAAiB,EAAE,CAAC,CAAA;QACxD,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,KAAK,CAAC,MAAM,CACV,UAAkB,EAClB,MAAc;QAEd,MAAM,IAAI,KAAK,CACb,+CAA+C;YAC7C,4CAA4C;YAC5C,uDAAuD,CAC1D,CAAA;IACH,CAAC;IAEO,cAAc,CAAC,SAAiB;QACtC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;YACtC,MAAM,YAAY,GAAG,IAAI,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAA;YACrD,MAAM,MAAM,GAAG,IAAI,CAAC,kBAAkB,CAAC,cAAc,CAAC,YAAY,CAAC,CAAA;YACnE,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,SAAS,EAAE,MAAM,CAAC,CAAA;QAC1C,CAAC;QACD,OAAO,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,SAAS,CAAE,CAAA;IAC1C,CAAC;IAMO,gBAAgB,CAAC,SAAiB;QACxC,MAAM,MAAM,GAAG,oBAAoB,gBAAgB,CAAC,SAAS,CAAC,EAAE,CAAA;QAChE,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,SAAS,CAAA;IACzC,CAAC;CACF;AAED,SAAS,gBAAgB,CAAC,IAAY;IACpC,OAAO,IAAI;SACR,OAAO,CAAC,oBAAoB,EAAE,OAAO,CAAC;SACtC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC;SAClB,WAAW,EAAE,CAAA;AAClB,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { LocalVariablesService } from '@pikku/core/services';
|
|
2
|
+
import type { CoreSingletonServices } from '@pikku/core';
|
|
3
|
+
import type { HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
|
|
4
|
+
export interface AzureServiceFactories {
|
|
5
|
+
createConfig: (variables: LocalVariablesService, ...args: unknown[]) => Promise<unknown>;
|
|
6
|
+
createSingletonServices: (config: unknown, existingServices?: Partial<Record<string, unknown>>) => Promise<CoreSingletonServices>;
|
|
7
|
+
createPlatformServices?: () => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
8
|
+
}
|
|
9
|
+
export declare function createAzureHandler(factories: AzureServiceFactories, handlerTypes: string[]): Record<string, unknown>;
|
|
10
|
+
export declare function createAzureWorkerHandler(factories: AzureServiceFactories): Record<string, unknown>;
|
|
11
|
+
export declare function createAzureWebSocketHandler(_factories: AzureServiceFactories): {
|
|
12
|
+
negotiate(_request: HttpRequest, _context: InvocationContext): Promise<HttpResponseInit>;
|
|
13
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { LocalVariablesService, LocalSecretService } from '@pikku/core/services';
|
|
2
|
+
import { PikkuFetchHTTPResponse, fetchData } from '@pikku/core/http';
|
|
3
|
+
import { runQueueJob } from '@pikku/core/queue';
|
|
4
|
+
import { runScheduledTask, getScheduledTasks } from '@pikku/core/scheduler';
|
|
5
|
+
let cachedServices = null;
|
|
6
|
+
async function setupServices(factories) {
|
|
7
|
+
if (cachedServices)
|
|
8
|
+
return cachedServices;
|
|
9
|
+
const variables = new LocalVariablesService(process.env);
|
|
10
|
+
const config = await factories.createConfig(variables);
|
|
11
|
+
const secrets = new LocalSecretService(variables);
|
|
12
|
+
const platformServices = factories.createPlatformServices
|
|
13
|
+
? await factories.createPlatformServices()
|
|
14
|
+
: {};
|
|
15
|
+
cachedServices = await factories.createSingletonServices(config, {
|
|
16
|
+
variables,
|
|
17
|
+
secrets,
|
|
18
|
+
...platformServices,
|
|
19
|
+
});
|
|
20
|
+
return cachedServices;
|
|
21
|
+
}
|
|
22
|
+
export function createAzureHandler(factories, handlerTypes) {
|
|
23
|
+
const result = {};
|
|
24
|
+
if (handlerTypes.includes('fetch')) {
|
|
25
|
+
result.http = async (request, _context) => {
|
|
26
|
+
await setupServices(factories);
|
|
27
|
+
const body = request.method !== 'GET' && request.method !== 'HEAD'
|
|
28
|
+
? await request.text()
|
|
29
|
+
: undefined;
|
|
30
|
+
const webRequest = new Request(request.url, {
|
|
31
|
+
method: request.method,
|
|
32
|
+
headers: Object.fromEntries(request.headers.entries()),
|
|
33
|
+
body,
|
|
34
|
+
});
|
|
35
|
+
const response = new PikkuFetchHTTPResponse();
|
|
36
|
+
try {
|
|
37
|
+
await fetchData(webRequest, response);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
console.error('Azure HTTP handler: fetchData error', err);
|
|
41
|
+
}
|
|
42
|
+
const fetchResponse = response.toResponse();
|
|
43
|
+
const headers = {};
|
|
44
|
+
fetchResponse.headers.forEach((value, key) => {
|
|
45
|
+
headers[key] = value;
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
status: fetchResponse.status,
|
|
49
|
+
headers,
|
|
50
|
+
body: await fetchResponse.text(),
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (handlerTypes.includes('queue')) {
|
|
55
|
+
result.queue = async (queueItem, context) => {
|
|
56
|
+
await setupServices(factories);
|
|
57
|
+
let data;
|
|
58
|
+
if (typeof queueItem === 'string') {
|
|
59
|
+
try {
|
|
60
|
+
data = JSON.parse(queueItem);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
data = { body: queueItem };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
data = queueItem;
|
|
68
|
+
}
|
|
69
|
+
const queueName = data.queueName ??
|
|
70
|
+
context.triggerMetadata?.queueTrigger ??
|
|
71
|
+
'unknown';
|
|
72
|
+
const job = {
|
|
73
|
+
queueName,
|
|
74
|
+
data: data.data ?? data,
|
|
75
|
+
id: context.invocationId,
|
|
76
|
+
status: async () => 'active',
|
|
77
|
+
metadata: () => ({
|
|
78
|
+
processedAt: new Date(),
|
|
79
|
+
attemptsMade: context.triggerMetadata?.dequeueCount ?? 0,
|
|
80
|
+
maxAttempts: undefined,
|
|
81
|
+
result: undefined,
|
|
82
|
+
progress: 0,
|
|
83
|
+
createdAt: new Date(),
|
|
84
|
+
completedAt: undefined,
|
|
85
|
+
failedAt: undefined,
|
|
86
|
+
error: undefined,
|
|
87
|
+
}),
|
|
88
|
+
waitForCompletion: async () => {
|
|
89
|
+
throw new Error('Azure Storage Queues do not support waitForCompletion');
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
await runQueueJob({ job });
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (handlerTypes.includes('scheduled')) {
|
|
96
|
+
result.timer = async (_timer, _context) => {
|
|
97
|
+
await setupServices(factories);
|
|
98
|
+
const traceId = `cron-${crypto.randomUUID()}`;
|
|
99
|
+
const scheduledTasks = getScheduledTasks();
|
|
100
|
+
for (const [name] of scheduledTasks) {
|
|
101
|
+
await runScheduledTask({ name, traceId });
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
export function createAzureWorkerHandler(factories) {
|
|
108
|
+
const handlers = createAzureHandler(factories, ['fetch']);
|
|
109
|
+
return handlers;
|
|
110
|
+
}
|
|
111
|
+
export function createAzureWebSocketHandler(_factories) {
|
|
112
|
+
return {
|
|
113
|
+
async negotiate(_request, _context) {
|
|
114
|
+
return {
|
|
115
|
+
status: 501,
|
|
116
|
+
body: 'WebSocket via Azure Web PubSub not yet implemented',
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=handler-factories.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler-factories.js","sourceRoot":"","sources":["../src/handler-factories.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
export * from './pikku-az-functions-logger.js';
|
|
2
2
|
export * from './pikku-az-timer-request.js';
|
|
3
|
+
export * from './handler-factories.js';
|
|
4
|
+
export { AzureQueueService } from './azure-queue-service.js';
|
|
5
|
+
export { AzureDeploymentService } from './azure-deployment-service.js';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
export * from './pikku-az-functions-logger.js';
|
|
2
2
|
export * from './pikku-az-timer-request.js';
|
|
3
|
+
export * from './handler-factories.js';
|
|
4
|
+
export { AzureQueueService } from './azure-queue-service.js';
|
|
5
|
+
export { AzureDeploymentService } from './azure-deployment-service.js';
|
|
3
6
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gCAAgC,CAAA;AAC9C,cAAc,6BAA6B,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gCAAgC,CAAA;AAC9C,cAAc,6BAA6B,CAAA;AAC3C,cAAc,wBAAwB,CAAA;AACtC,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AAC5D,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/azure-functions",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "yasser.fadl@gmail.com",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -13,18 +13,23 @@
|
|
|
13
13
|
"release": "npm run build && npm test",
|
|
14
14
|
"test": "bash run-tests.sh",
|
|
15
15
|
"test:watch": "bash run-tests.sh --watch",
|
|
16
|
-
"test:coverage": "bash run-tests.sh --coverage"
|
|
16
|
+
"test:coverage": "bash run-tests.sh --coverage",
|
|
17
|
+
"prepublishOnly": "yarn build"
|
|
17
18
|
},
|
|
18
19
|
"peerDependencies": {
|
|
19
|
-
"@pikku/core": "^0.12.
|
|
20
|
+
"@pikku/core": "^0.12.58"
|
|
20
21
|
},
|
|
21
22
|
"devDependencies": {
|
|
22
|
-
"typescript": "^
|
|
23
|
+
"typescript": "^6.0.3"
|
|
23
24
|
},
|
|
24
25
|
"engines": {
|
|
25
|
-
"node": ">=
|
|
26
|
+
"node": ">=24"
|
|
26
27
|
},
|
|
27
28
|
"dependencies": {
|
|
28
|
-
"@azure/functions": "4.11.2"
|
|
29
|
+
"@azure/functions": "4.11.2",
|
|
30
|
+
"@azure/storage-queue": "^12.25.0"
|
|
31
|
+
},
|
|
32
|
+
"exports": {
|
|
33
|
+
".": "./dist/index.js"
|
|
29
34
|
}
|
|
30
35
|
}
|
package/run-tests.sh
CHANGED
|
@@ -38,16 +38,16 @@ if [ ${#files[@]} -eq 0 ]; then
|
|
|
38
38
|
fi
|
|
39
39
|
|
|
40
40
|
# Construct the node command
|
|
41
|
-
node_cmd=
|
|
41
|
+
node_cmd=(node --import tsx --test)
|
|
42
42
|
|
|
43
43
|
# Append options based on flags
|
|
44
44
|
if [ "$watch_mode" = true ]; then
|
|
45
|
-
node_cmd
|
|
45
|
+
node_cmd+=(--watch)
|
|
46
46
|
fi
|
|
47
47
|
|
|
48
48
|
if [ "$coverage_mode" = true ]; then
|
|
49
|
-
node_cmd
|
|
49
|
+
node_cmd+=(--test-coverage-include="src/**/*.{ts,js}" --test-coverage-exclude="**/dist/**" --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info)
|
|
50
50
|
fi
|
|
51
51
|
|
|
52
52
|
# Execute the node command with the expanded list of files
|
|
53
|
-
$node_cmd "${files[@]}"
|
|
53
|
+
"${node_cmd[@]}" "${files[@]}"
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure Functions deployment service — routes remote() calls via HTTP.
|
|
3
|
+
*
|
|
4
|
+
* When a gateway (agent, MCP, channel, workflow orchestrator) calls
|
|
5
|
+
* remote('funcName'), this service dispatches to the function's
|
|
6
|
+
* Azure Function via HTTP. Uses the function's URL endpoint.
|
|
7
|
+
*
|
|
8
|
+
* Session propagation uses the existing pikkuRemoteAuthMiddleware:
|
|
9
|
+
* JWT signed with PIKKU_REMOTE_SECRET, session encrypted in payload.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { DeploymentService, DeploymentConfig } from '@pikku/core/services'
|
|
13
|
+
import type { JWTService } from '@pikku/core/services'
|
|
14
|
+
import type { SecretService } from '@pikku/core/services'
|
|
15
|
+
import { encryptJSON } from '@pikku/core/crypto-utils'
|
|
16
|
+
|
|
17
|
+
export class AzureDeploymentService implements DeploymentService {
|
|
18
|
+
private bindings: Map<string, string>
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
private jwt: JWTService | undefined,
|
|
22
|
+
private secrets: SecretService,
|
|
23
|
+
functionBindings: Record<string, string>
|
|
24
|
+
) {
|
|
25
|
+
this.bindings = new Map(Object.entries(functionBindings))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async init(): Promise<void> {}
|
|
29
|
+
async start(_config: DeploymentConfig): Promise<void> {}
|
|
30
|
+
async stop(): Promise<void> {}
|
|
31
|
+
|
|
32
|
+
async invoke(
|
|
33
|
+
funcName: string,
|
|
34
|
+
data: unknown,
|
|
35
|
+
session?: unknown,
|
|
36
|
+
traceId?: string
|
|
37
|
+
): Promise<unknown> {
|
|
38
|
+
const functionUrl = this.bindings.get(funcName)
|
|
39
|
+
if (!functionUrl) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`No Azure Function binding for function '${funcName}'. ` +
|
|
42
|
+
`Available bindings: ${[...this.bindings.keys()].join(', ')}`
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const headers: Record<string, string> = {
|
|
47
|
+
'content-type': 'application/json',
|
|
48
|
+
...(traceId && { 'x-request-id': traceId }),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Sign session as JWT for pikkuRemoteAuthMiddleware
|
|
52
|
+
let secret: string | undefined
|
|
53
|
+
try {
|
|
54
|
+
secret = await this.secrets.getSecret('PIKKU_REMOTE_SECRET')
|
|
55
|
+
} catch {}
|
|
56
|
+
|
|
57
|
+
if (secret && this.jwt) {
|
|
58
|
+
const sessionEnc = session
|
|
59
|
+
? await encryptJSON(secret, { session })
|
|
60
|
+
: undefined
|
|
61
|
+
const token = await this.jwt.encode(
|
|
62
|
+
{ value: 5, unit: 'minute' },
|
|
63
|
+
{
|
|
64
|
+
aud: 'pikku-remote',
|
|
65
|
+
fn: funcName,
|
|
66
|
+
iat: Math.floor(Date.now() / 1000),
|
|
67
|
+
session: sessionEnc,
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
headers.authorization = `Bearer ${token}`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const url = `${functionUrl}/remote/rpc/${encodeURIComponent(funcName)}`
|
|
74
|
+
const response = await fetch(url, {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers,
|
|
77
|
+
body: JSON.stringify({ data }),
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
let errorBody: string
|
|
82
|
+
try {
|
|
83
|
+
errorBody = JSON.stringify(await response.json())
|
|
84
|
+
} catch {
|
|
85
|
+
errorBody = await response.text()
|
|
86
|
+
}
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Remote RPC call to '${funcName}' failed: ${response.status} ${response.statusText}. ${errorBody}`
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return response.json()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure Storage Queue implementation of QueueService (producer side).
|
|
3
|
+
*
|
|
4
|
+
* Sends messages to Azure Storage Queues. Queue URLs are resolved from
|
|
5
|
+
* environment variables following the convention:
|
|
6
|
+
* AZURE_QUEUE_NAME_<SCREAMING_SNAKE> (queue name) with the connection
|
|
7
|
+
* string from AzureWebJobsStorage.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { QueueServiceClient } from '@azure/storage-queue'
|
|
11
|
+
import type { QueueClient } from '@azure/storage-queue'
|
|
12
|
+
import type { QueueService, QueueJob, JobOptions } from '@pikku/core/queue'
|
|
13
|
+
|
|
14
|
+
export class AzureQueueService implements QueueService {
|
|
15
|
+
readonly supportsResults = false
|
|
16
|
+
private queueServiceClient: QueueServiceClient
|
|
17
|
+
private queueClients = new Map<string, QueueClient>()
|
|
18
|
+
|
|
19
|
+
constructor(connectionString?: string) {
|
|
20
|
+
const connStr = connectionString ?? process.env.AzureWebJobsStorage
|
|
21
|
+
if (!connStr) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
'Azure Queue connection string not provided; supply connectionString or set AzureWebJobsStorage'
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
this.queueServiceClient = QueueServiceClient.fromConnectionString(connStr)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async add<T>(
|
|
30
|
+
queueName: string,
|
|
31
|
+
data: T,
|
|
32
|
+
options?: JobOptions
|
|
33
|
+
): Promise<string> {
|
|
34
|
+
const client = this.getQueueClient(queueName)
|
|
35
|
+
const messageId = crypto.randomUUID()
|
|
36
|
+
|
|
37
|
+
const message = {
|
|
38
|
+
id: messageId,
|
|
39
|
+
queueName,
|
|
40
|
+
data,
|
|
41
|
+
pikkuUserId: options?.pikkuUserId,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Azure Storage Queue messages must be base64-encoded strings
|
|
45
|
+
const encoded = Buffer.from(JSON.stringify(message)).toString('base64')
|
|
46
|
+
|
|
47
|
+
const visibilityTimeout = options?.delay
|
|
48
|
+
? Math.min(Math.ceil(options.delay / 1000), 604800) // max 7 days
|
|
49
|
+
: undefined
|
|
50
|
+
|
|
51
|
+
await client.sendMessage(encoded, { visibilityTimeout })
|
|
52
|
+
return messageId
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async getJob<T, R>(
|
|
56
|
+
_queueName: string,
|
|
57
|
+
_jobId: string
|
|
58
|
+
): Promise<QueueJob<T, R> | null> {
|
|
59
|
+
throw new Error(
|
|
60
|
+
'AzureQueueService does not support getJob(). ' +
|
|
61
|
+
'Azure Storage Queues are fire-and-forget. ' +
|
|
62
|
+
'Use BullMQ or PgBoss if you need job result tracking.'
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private getQueueClient(queueName: string): QueueClient {
|
|
67
|
+
if (!this.queueClients.has(queueName)) {
|
|
68
|
+
const resolvedName = this.resolveQueueName(queueName)
|
|
69
|
+
const client = this.queueServiceClient.getQueueClient(resolvedName)
|
|
70
|
+
this.queueClients.set(queueName, client)
|
|
71
|
+
}
|
|
72
|
+
return this.queueClients.get(queueName)!
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Resolves the Azure queue name. Checks env var
|
|
77
|
+
* AZURE_QUEUE_NAME_<SCREAMING_SNAKE> first, falls back to the name as-is.
|
|
78
|
+
*/
|
|
79
|
+
private resolveQueueName(queueName: string): string {
|
|
80
|
+
const envKey = `AZURE_QUEUE_NAME_${toScreamingSnake(queueName)}`
|
|
81
|
+
return process.env[envKey] ?? queueName
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function toScreamingSnake(name: string): string {
|
|
86
|
+
return name
|
|
87
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
88
|
+
.replace(/-/g, '_')
|
|
89
|
+
.toUpperCase()
|
|
90
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure Functions handler factories — mirrors Lambda/CF handler factories.
|
|
3
|
+
*
|
|
4
|
+
* Creates handlers for Azure Functions v4 programming model:
|
|
5
|
+
* - HTTP triggers (HttpRequest → HttpResponseInit)
|
|
6
|
+
* - Storage Queue triggers (QueueItem → void)
|
|
7
|
+
* - Timer triggers (Timer → void)
|
|
8
|
+
*
|
|
9
|
+
* Services are cached across invocations within the same function app instance.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { LocalVariablesService, LocalSecretService } from '@pikku/core/services'
|
|
13
|
+
import type { CoreSingletonServices } from '@pikku/core'
|
|
14
|
+
import { PikkuFetchHTTPResponse, fetchData } from '@pikku/core/http'
|
|
15
|
+
import { runQueueJob } from '@pikku/core/queue'
|
|
16
|
+
import type { QueueJob, QueueJobStatus } from '@pikku/core/queue'
|
|
17
|
+
import { runScheduledTask, getScheduledTasks } from '@pikku/core/scheduler'
|
|
18
|
+
import type {
|
|
19
|
+
HttpRequest,
|
|
20
|
+
HttpResponseInit,
|
|
21
|
+
InvocationContext,
|
|
22
|
+
Timer,
|
|
23
|
+
} from '@azure/functions'
|
|
24
|
+
|
|
25
|
+
export interface AzureServiceFactories {
|
|
26
|
+
createConfig: (
|
|
27
|
+
variables: LocalVariablesService,
|
|
28
|
+
...args: unknown[]
|
|
29
|
+
) => Promise<unknown>
|
|
30
|
+
createSingletonServices: (
|
|
31
|
+
config: unknown,
|
|
32
|
+
existingServices?: Partial<Record<string, unknown>>
|
|
33
|
+
) => Promise<CoreSingletonServices>
|
|
34
|
+
createPlatformServices?: () =>
|
|
35
|
+
| Record<string, unknown>
|
|
36
|
+
| Promise<Record<string, unknown>>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let cachedServices: CoreSingletonServices | null = null
|
|
40
|
+
|
|
41
|
+
async function setupServices(
|
|
42
|
+
factories: AzureServiceFactories
|
|
43
|
+
): Promise<CoreSingletonServices> {
|
|
44
|
+
if (cachedServices) return cachedServices
|
|
45
|
+
const variables = new LocalVariablesService(
|
|
46
|
+
process.env as Record<string, string | undefined>
|
|
47
|
+
)
|
|
48
|
+
const config = await factories.createConfig(variables)
|
|
49
|
+
const secrets = new LocalSecretService(variables)
|
|
50
|
+
|
|
51
|
+
const platformServices = factories.createPlatformServices
|
|
52
|
+
? await factories.createPlatformServices()
|
|
53
|
+
: {}
|
|
54
|
+
|
|
55
|
+
cachedServices = await factories.createSingletonServices(config, {
|
|
56
|
+
variables,
|
|
57
|
+
secrets,
|
|
58
|
+
...platformServices,
|
|
59
|
+
})
|
|
60
|
+
return cachedServices
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Creates combined Azure Function handlers based on which handler types
|
|
65
|
+
* the unit needs. Returns an object with named handler functions that
|
|
66
|
+
* the entry file registers via app.http(), app.storageQueue(), app.timer().
|
|
67
|
+
*/
|
|
68
|
+
export function createAzureHandler(
|
|
69
|
+
factories: AzureServiceFactories,
|
|
70
|
+
handlerTypes: string[]
|
|
71
|
+
) {
|
|
72
|
+
const result: Record<string, unknown> = {}
|
|
73
|
+
|
|
74
|
+
if (handlerTypes.includes('fetch')) {
|
|
75
|
+
result.http = async (
|
|
76
|
+
request: HttpRequest,
|
|
77
|
+
_context: InvocationContext
|
|
78
|
+
): Promise<HttpResponseInit> => {
|
|
79
|
+
await setupServices(factories)
|
|
80
|
+
|
|
81
|
+
// Convert Azure HttpRequest to standard Request
|
|
82
|
+
const body =
|
|
83
|
+
request.method !== 'GET' && request.method !== 'HEAD'
|
|
84
|
+
? await request.text()
|
|
85
|
+
: undefined
|
|
86
|
+
const webRequest = new Request(request.url, {
|
|
87
|
+
method: request.method,
|
|
88
|
+
headers: Object.fromEntries(request.headers.entries()),
|
|
89
|
+
body,
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const response = new PikkuFetchHTTPResponse()
|
|
93
|
+
try {
|
|
94
|
+
await fetchData(webRequest, response)
|
|
95
|
+
} catch (err) {
|
|
96
|
+
console.error('Azure HTTP handler: fetchData error', err)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const fetchResponse = response.toResponse()
|
|
100
|
+
const headers: Record<string, string> = {}
|
|
101
|
+
fetchResponse.headers.forEach((value, key) => {
|
|
102
|
+
headers[key] = value
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
status: fetchResponse.status,
|
|
107
|
+
headers,
|
|
108
|
+
body: await fetchResponse.text(),
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (handlerTypes.includes('queue')) {
|
|
114
|
+
result.queue = async (
|
|
115
|
+
queueItem: unknown,
|
|
116
|
+
context: InvocationContext
|
|
117
|
+
): Promise<void> => {
|
|
118
|
+
await setupServices(factories)
|
|
119
|
+
|
|
120
|
+
let data: Record<string, unknown>
|
|
121
|
+
if (typeof queueItem === 'string') {
|
|
122
|
+
try {
|
|
123
|
+
data = JSON.parse(queueItem)
|
|
124
|
+
} catch {
|
|
125
|
+
data = { body: queueItem }
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
data = queueItem as Record<string, unknown>
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const queueName =
|
|
132
|
+
(data.queueName as string) ??
|
|
133
|
+
context.triggerMetadata?.queueTrigger ??
|
|
134
|
+
'unknown'
|
|
135
|
+
|
|
136
|
+
const job: QueueJob = {
|
|
137
|
+
queueName,
|
|
138
|
+
data: data.data ?? data,
|
|
139
|
+
id: context.invocationId,
|
|
140
|
+
status: async () => 'active' as QueueJobStatus,
|
|
141
|
+
metadata: () => ({
|
|
142
|
+
processedAt: new Date(),
|
|
143
|
+
attemptsMade: (context.triggerMetadata?.dequeueCount as number) ?? 0,
|
|
144
|
+
maxAttempts: undefined,
|
|
145
|
+
result: undefined,
|
|
146
|
+
progress: 0,
|
|
147
|
+
createdAt: new Date(),
|
|
148
|
+
completedAt: undefined,
|
|
149
|
+
failedAt: undefined,
|
|
150
|
+
error: undefined,
|
|
151
|
+
}),
|
|
152
|
+
waitForCompletion: async () => {
|
|
153
|
+
throw new Error(
|
|
154
|
+
'Azure Storage Queues do not support waitForCompletion'
|
|
155
|
+
)
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
await runQueueJob({ job })
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (handlerTypes.includes('scheduled')) {
|
|
164
|
+
result.timer = async (
|
|
165
|
+
_timer: Timer,
|
|
166
|
+
_context: InvocationContext
|
|
167
|
+
): Promise<void> => {
|
|
168
|
+
await setupServices(factories)
|
|
169
|
+
const traceId = `cron-${crypto.randomUUID()}`
|
|
170
|
+
const scheduledTasks = getScheduledTasks()
|
|
171
|
+
for (const [name] of scheduledTasks) {
|
|
172
|
+
await runScheduledTask({ name, traceId })
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return result
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Creates an Azure Function handler with an HTTP trigger for gateway units.
|
|
182
|
+
*/
|
|
183
|
+
export function createAzureWorkerHandler(factories: AzureServiceFactories) {
|
|
184
|
+
const handlers = createAzureHandler(factories, ['fetch'])
|
|
185
|
+
return handlers
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Creates Azure Function handlers for WebSocket channel units.
|
|
190
|
+
* Uses HTTP negotiation endpoint for Azure Web PubSub.
|
|
191
|
+
*/
|
|
192
|
+
export function createAzureWebSocketHandler(_factories: AzureServiceFactories) {
|
|
193
|
+
return {
|
|
194
|
+
async negotiate(
|
|
195
|
+
_request: HttpRequest,
|
|
196
|
+
_context: InvocationContext
|
|
197
|
+
): Promise<HttpResponseInit> {
|
|
198
|
+
// Web PubSub negotiation — to be implemented with @azure/web-pubsub
|
|
199
|
+
return {
|
|
200
|
+
status: 501,
|
|
201
|
+
body: 'WebSocket via Azure Web PubSub not yet implemented',
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, test } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import * as azureFunctions from './index.js'
|
|
5
|
+
|
|
6
|
+
describe('@pikku/azure-functions', () => {
|
|
7
|
+
test('exports the azure runtime API', () => {
|
|
8
|
+
assert.equal(typeof azureFunctions.createAzureHandler, 'function')
|
|
9
|
+
assert.equal(typeof azureFunctions.AzureDeploymentService, 'function')
|
|
10
|
+
assert.equal(typeof azureFunctions.AzureQueueService, 'function')
|
|
11
|
+
assert.equal(typeof azureFunctions.AzInvocationLogger, 'function')
|
|
12
|
+
})
|
|
13
|
+
})
|
package/src/index.ts
CHANGED
package/tsconfig.tsbuildinfo
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["./src/index.ts","./src/pikku-az-functions-logger.ts","./src/pikku-az-timer-request.ts"],"version":"
|
|
1
|
+
{"root":["./src/azure-deployment-service.ts","./src/azure-queue-service.ts","./src/handler-factories.ts","./src/index.ts","./src/pikku-az-functions-logger.ts","./src/pikku-az-timer-request.ts"],"version":"6.0.3"}
|