Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion etc/firebase-admin.functions.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,21 @@ export class Functions {
//
// (undocumented)
readonly app: App;
taskQueue<Args = Record<string, any>>(functionName: string, extensionId?: string): TaskQueue<Args>;
taskQueue<Args = Record<string, any>>(functionName: string, scope?: FunctionScope): TaskQueue<Args>;
// @deprecated
taskQueue<Args = Record<string, any>>(functionName: string, deprecatedExtensionId: string): TaskQueue<Args>;
}

// @public
export type FunctionScope = {
scope: 'current';
} | {
scope: 'global';
} | {
scope: 'extension';
instance: string;
};

// @public
export const FunctionsErrorCode: {
readonly ABORTED: "aborted";
Expand Down
112 changes: 87 additions & 25 deletions src/functions/functions-api-client-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,17 @@ import { FirebaseError, toHttpResponse } from '../utils/error';
import { FirebaseFunctionsError, FunctionsErrorCode, FUNCTIONS_ERROR_CODE_MAPPING } from './error';
import * as utils from '../utils/index';
import * as validator from '../utils/validator';
import { TaskOptions } from './functions-api';
import { TaskOptions, FunctionScope } from './functions-api';
import { ApplicationDefaultCredential } from '../app/credential-internal';

export type InternalFunctionScope = FunctionScope | {
scope: 'kit';
instance: string;
} | {
scope: 'extensionOrKit';
instance: string;
};

const CLOUD_TASKS_API_RESOURCE_PATH = 'projects/{projectId}/locations/{locationId}/queues/{resourceId}/tasks';
const CLOUD_TASKS_API_URL_FORMAT = 'https://cloudtasks.googleapis.com/v2/' + CLOUD_TASKS_API_RESOURCE_PATH;
const FIREBASE_FUNCTION_URL_FORMAT = 'https://{locationId}-{projectId}.cloudfunctions.net/{resourceId}';
Expand Down Expand Up @@ -65,10 +73,14 @@ export class FunctionsApiClient {
* Deletes a task from a queue.
*
* @param id - The ID of the task to delete.
* @param functionName - The function name of the queue.
* @param extensionId - Optional canonical ID of the extension.
* @param functionName - The name of the function associated with the queue.
* @param scope - Optional `FunctionScope` configuration.
*/
public async delete(id: string, functionName: string, extensionId?: string): Promise<void> {
public async delete(
id: string,
functionName: string,
scope?: InternalFunctionScope
): Promise<void> {
if (!validator.isNonEmptyString(functionName)) {
throw new FirebaseFunctionsError({
code: 'invalid-argument',
Expand Down Expand Up @@ -101,13 +113,13 @@ export class FunctionsApiClient {
message: 'No valid function name specified to enqueue tasks for.'
});
}
if (typeof extensionId !== 'undefined' && validator.isNonEmptyString(extensionId)) {
resources.resourceId = `ext-${extensionId}-${resources.resourceId}`;
}

const { resourceId } = this.resolveResourceId(resources.resourceId, scope);
const targetResources = { ...resources, resourceId };

try {
const serviceUrl = tasksEmulatorUrl(resources, this.emulatorHost)?.concat('/', id)
?? await this.getUrl(resources, CLOUD_TASKS_API_URL_FORMAT.concat('/', id));
const serviceUrl = tasksEmulatorUrl(targetResources, this.emulatorHost)?.concat('/', id)
?? await this.getUrl(targetResources, CLOUD_TASKS_API_URL_FORMAT.concat('/', id));
const request: HttpRequestConfig = {
method: 'DELETE',
url: serviceUrl,
Expand All @@ -116,10 +128,6 @@ export class FunctionsApiClient {
await this.httpClient.send(request);
} catch (err: unknown) {
if (err instanceof RequestResponseError) {
if (err.response.status === 404) {
// if no task with the provided ID exists, then ignore the delete.
return;
}
throw this.toFirebaseError(err);
} else {
throw err;
Expand All @@ -131,11 +139,16 @@ export class FunctionsApiClient {
* Creates a task and adds it to a queue.
*
* @param data - The data payload of the task.
* @param functionName - The functionName of the queue.
* @param extensionId - Optional canonical ID of the extension.
* @param functionName - The name of the function associated with the queue.
* @param scope - Optional `FunctionScope` configuration.
* @param opts - Optional options when enqueuing a new task.
*/
public async enqueue(data: any, functionName: string, extensionId?: string, opts?: TaskOptions): Promise<void> {
public async enqueue(
data: any,
functionName: string,
scope?: InternalFunctionScope,
opts?: TaskOptions
): Promise<void> {
if (!validator.isNonEmptyString(functionName)) {
throw new FirebaseFunctionsError({
code: 'invalid-argument',
Expand All @@ -161,17 +174,17 @@ export class FunctionsApiClient {
message: 'No valid function name specified to enqueue tasks for.'
});
}
if (typeof extensionId !== 'undefined' && validator.isNonEmptyString(extensionId)) {
resources.resourceId = `ext-${extensionId}-${resources.resourceId}`;
}

const task = this.validateTaskOptions(data, resources, opts);
const { resourceId, extensionOrKitId } = this.resolveResourceId(resources.resourceId, scope);
const targetResources = { ...resources, resourceId };

try {
const task = this.validateTaskOptions(data, targetResources, opts);
const serviceUrl =
tasksEmulatorUrl(resources, this.emulatorHost) ??
await this.getUrl(resources, CLOUD_TASKS_API_URL_FORMAT);
tasksEmulatorUrl(targetResources, this.emulatorHost) ??
await this.getUrl(targetResources, CLOUD_TASKS_API_URL_FORMAT);

const taskPayload = await this.updateTaskPayload(task, resources, extensionId);
const taskPayload = await this.updateTaskPayload(task, targetResources, extensionOrKitId);
const request: HttpRequestConfig = {
method: 'POST',
url: serviceUrl,
Expand Down Expand Up @@ -199,6 +212,51 @@ export class FunctionsApiClient {
}
}

private resolveResourceId(
resourceId: string,
scope: InternalFunctionScope = { scope: 'current' }
): { resourceId: string; extensionOrKitId?: string } {

switch (scope.scope) {
case 'current': {
const extInstanceId = process.env.EXT_INSTANCE_ID;
if (validator.isNonEmptyString(extInstanceId)) {
return {
resourceId: `ext-${extInstanceId}-${resourceId}`,
extensionOrKitId: extInstanceId,
};
}
const kitInstanceId = process.env.FIREBASE_KIT_INSTANCE_ID;
if (validator.isNonEmptyString(kitInstanceId)) {
return {
resourceId: `kit-${kitInstanceId}-${resourceId}`,
extensionOrKitId: kitInstanceId,
};
}
return { resourceId };
}
case 'global':
return { resourceId };
case 'extension':
return {
resourceId: `ext-${scope.instance}-${resourceId}`,
extensionOrKitId: scope.instance,
};
case 'kit': // kit scope is secretly accepted for forward compatibility
return {
resourceId: `kit-${scope.instance}-${resourceId}`,
extensionOrKitId: scope.instance,
};
case 'extensionOrKit':
return {
resourceId: `ext-${scope.instance}-${resourceId}`,
extensionOrKitId: scope.instance,
};
default:
return { resourceId };
}
}

private getUrl(resourceName: utils.ParsedResource, urlFormat: string): Promise<string> {
let { locationId } = resourceName;
const { projectId, resourceId } = resourceName;
Expand Down Expand Up @@ -349,7 +407,11 @@ export class FunctionsApiClient {
return task;
}

private async updateTaskPayload(task: Task, resources: utils.ParsedResource, extensionId?: string): Promise<Task> {
private async updateTaskPayload(
task: Task,
resources: utils.ParsedResource,
extensionOrKitId?: string
): Promise<Task> {
const defaultUrl = this.emulatorHost ?
''
: await this.getUrl(resources, FIREBASE_FUNCTION_URL_FORMAT);
Expand All @@ -360,7 +422,7 @@ export class FunctionsApiClient {

task.httpRequest.url = functionUrl;
// When run from a deployed extension, we should be using ComputeEngineCredentials
if (validator.isNonEmptyString(extensionId) && this.app.options.credential
if (validator.isNonEmptyString(extensionOrKitId) && this.app.options.credential
instanceof ApplicationDefaultCredential && await this.app.options.credential.isComputeEngineCredential()) {
const idToken = await this.app.options.credential.getIDToken(functionUrl);
task.httpRequest.headers = { ...task.httpRequest.headers, 'Authorization': `Bearer ${idToken}` };
Expand Down
13 changes: 13 additions & 0 deletions src/functions/functions-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,16 @@ export interface TaskOptionsExperimental {
*/
uri?: string;
}

/**
* Type representing the scope of a function in a task queue.
*/
export type FunctionScope = {
scope: 'current';
} | {
scope: 'global';
} | {
scope: 'extension';
instance: string;
};

Loading
Loading