Skip to content

Commit ed4422e

Browse files
authored
Merge branch 'main' into je-ref-docs
2 parents e40f3be + 5a203f6 commit ed4422e

12 files changed

Lines changed: 822 additions & 222 deletions

etc/firebase-admin.functions.api.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,21 @@ export class Functions {
3737
//
3838
// (undocumented)
3939
readonly app: App;
40-
taskQueue<Args = Record<string, any>>(functionName: string, extensionId?: string): TaskQueue<Args>;
40+
taskQueue<Args = Record<string, any>>(functionName: string, scope?: FunctionScope): TaskQueue<Args>;
41+
// @deprecated
42+
taskQueue<Args = Record<string, any>>(functionName: string, deprecatedExtensionId: string): TaskQueue<Args>;
4143
}
4244

45+
// @public
46+
export type FunctionScope = {
47+
scope: 'current';
48+
} | {
49+
scope: 'global';
50+
} | {
51+
scope: 'extension';
52+
instance: string;
53+
};
54+
4355
// @public
4456
export const FunctionsErrorCode: {
4557
readonly ABORTED: "aborted";

package-lock.json

Lines changed: 185 additions & 134 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "firebase-admin",
3-
"version": "14.1.0",
3+
"version": "14.3.0",
44
"description": "Firebase admin SDK for Node.js",
55
"author": "Firebase <firebase-support@google.com> (https://firebase.google.com/)",
66
"license": "Apache-2.0",
@@ -215,8 +215,8 @@
215215
"jwks-rsa": "^4.0.1"
216216
},
217217
"optionalDependencies": {
218-
"@google-cloud/firestore": "^8.6.0",
219-
"@google-cloud/storage": "^7.19.0"
218+
"@google-cloud/firestore": "^8.7.1",
219+
"@google-cloud/storage": "^7.22.0"
220220
},
221221
"devDependencies": {
222222
"@eslint/js": "^10.0.1",
@@ -237,8 +237,8 @@
237237
"@types/node": "^26.0.0",
238238
"@types/sinon": "^21.0.1",
239239
"@types/sinon-chai": "^3.0.0",
240-
"@typescript-eslint/eslint-plugin": "^8.62.0",
241-
"@typescript-eslint/parser": "^8.62.0",
240+
"@typescript-eslint/eslint-plugin": "^8.67.0",
241+
"@typescript-eslint/parser": "^8.67.0",
242242
"bcrypt": "^6.0.0",
243243
"chai": "^4.2.0",
244244
"chai-as-promised": "^7.0.0",
@@ -266,7 +266,7 @@
266266
"sinon-chai": "^3.0.0",
267267
"ts-node": "^10.2.0",
268268
"typescript": "^5.7.3",
269-
"typescript-eslint": "^8.62.0",
269+
"typescript-eslint": "^8.67.0",
270270
"yargs": "^17.0.1"
271271
}
272272
}

src/data-connect/data-connect-api-client-internal.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,19 @@ const IMPERSONATE_MUTATION_ENDPOINT = 'impersonateMutation';
7070
/** @internal The maximum number of items allowed in the @allow directive's maxCount argument. */
7171
export const ALLOW_DIRECTIVE_MAX_COUNT = 10_000;
7272

73-
function getHeaders(isUsingGen: boolean): { [key: string]: string } {
73+
interface GetHeadersParameters {
74+
projectId: string;
75+
serviceId: string;
76+
isUsingGen: boolean;
77+
}
78+
79+
function getHeaders(parameters: GetHeadersParameters): { [key: string]: string } {
80+
const { projectId, serviceId, isUsingGen } = parameters;
7481
const headerValue = {
7582
'X-Firebase-Client': `fire-admin-node/${utils.getSdkVersion()}`,
7683
'X-Goog-Api-Client': utils.getMetricsHeader(),
84+
'X-Client-Version': `Node/Admin/${utils.getSdkVersion()}`,
85+
'X-Firebase-Sqlconnect-Affinity': `${projectId}${serviceId}`,
7786
};
7887
if (isUsingGen) {
7988
headerValue['X-Goog-Api-Client'] += ' admin-js/gen';
@@ -385,10 +394,15 @@ export class DataConnectApiClient {
385394
*/
386395
private async makeGqlRequest<GraphqlResponse>(url: string, data: object):
387396
Promise<ExecuteGraphqlResponse<GraphqlResponse>> {
397+
const projectId = await this.getProjectId();
388398
const request: HttpRequestConfig = {
389399
method: 'POST',
390400
url,
391-
headers: getHeaders(this.isUsingGen),
401+
headers: getHeaders({
402+
projectId,
403+
serviceId: this.connectorConfig.serviceId,
404+
isUsingGen: this.isUsingGen,
405+
}),
392406
data,
393407
};
394408
const resp = await this.httpClient.send(request);

src/functions/functions-api-client-internal.ts

Lines changed: 87 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,17 @@ import { FirebaseError, toHttpResponse } from '../utils/error';
2424
import { FirebaseFunctionsError, FunctionsErrorCode, FUNCTIONS_ERROR_CODE_MAPPING } from './error';
2525
import * as utils from '../utils/index';
2626
import * as validator from '../utils/validator';
27-
import { TaskOptions } from './functions-api';
27+
import { TaskOptions, FunctionScope } from './functions-api';
2828
import { ApplicationDefaultCredential } from '../app/credential-internal';
2929

30+
export type InternalFunctionScope = FunctionScope | {
31+
scope: 'kit';
32+
instance: string;
33+
} | {
34+
scope: 'extensionOrKit';
35+
instance: string;
36+
};
37+
3038
const CLOUD_TASKS_API_RESOURCE_PATH = 'projects/{projectId}/locations/{locationId}/queues/{resourceId}/tasks';
3139
const CLOUD_TASKS_API_URL_FORMAT = 'https://cloudtasks.googleapis.com/v2/' + CLOUD_TASKS_API_RESOURCE_PATH;
3240
const FIREBASE_FUNCTION_URL_FORMAT = 'https://{locationId}-{projectId}.cloudfunctions.net/{resourceId}';
@@ -65,10 +73,14 @@ export class FunctionsApiClient {
6573
* Deletes a task from a queue.
6674
*
6775
* @param id - The ID of the task to delete.
68-
* @param functionName - The function name of the queue.
69-
* @param extensionId - Optional canonical ID of the extension.
76+
* @param functionName - The name of the function associated with the queue.
77+
* @param scope - Optional `FunctionScope` configuration.
7078
*/
71-
public async delete(id: string, functionName: string, extensionId?: string): Promise<void> {
79+
public async delete(
80+
id: string,
81+
functionName: string,
82+
scope?: InternalFunctionScope
83+
): Promise<void> {
7284
if (!validator.isNonEmptyString(functionName)) {
7385
throw new FirebaseFunctionsError({
7486
code: 'invalid-argument',
@@ -101,13 +113,13 @@ export class FunctionsApiClient {
101113
message: 'No valid function name specified to enqueue tasks for.'
102114
});
103115
}
104-
if (typeof extensionId !== 'undefined' && validator.isNonEmptyString(extensionId)) {
105-
resources.resourceId = `ext-${extensionId}-${resources.resourceId}`;
106-
}
116+
117+
const { resourceId } = this.resolveResourceId(resources.resourceId, scope);
118+
const targetResources = { ...resources, resourceId };
107119

108120
try {
109-
const serviceUrl = tasksEmulatorUrl(resources, this.emulatorHost)?.concat('/', id)
110-
?? await this.getUrl(resources, CLOUD_TASKS_API_URL_FORMAT.concat('/', id));
121+
const serviceUrl = tasksEmulatorUrl(targetResources, this.emulatorHost)?.concat('/', id)
122+
?? await this.getUrl(targetResources, CLOUD_TASKS_API_URL_FORMAT.concat('/', id));
111123
const request: HttpRequestConfig = {
112124
method: 'DELETE',
113125
url: serviceUrl,
@@ -116,10 +128,6 @@ export class FunctionsApiClient {
116128
await this.httpClient.send(request);
117129
} catch (err: unknown) {
118130
if (err instanceof RequestResponseError) {
119-
if (err.response.status === 404) {
120-
// if no task with the provided ID exists, then ignore the delete.
121-
return;
122-
}
123131
throw this.toFirebaseError(err);
124132
} else {
125133
throw err;
@@ -131,11 +139,16 @@ export class FunctionsApiClient {
131139
* Creates a task and adds it to a queue.
132140
*
133141
* @param data - The data payload of the task.
134-
* @param functionName - The functionName of the queue.
135-
* @param extensionId - Optional canonical ID of the extension.
142+
* @param functionName - The name of the function associated with the queue.
143+
* @param scope - Optional `FunctionScope` configuration.
136144
* @param opts - Optional options when enqueuing a new task.
137145
*/
138-
public async enqueue(data: any, functionName: string, extensionId?: string, opts?: TaskOptions): Promise<void> {
146+
public async enqueue(
147+
data: any,
148+
functionName: string,
149+
scope?: InternalFunctionScope,
150+
opts?: TaskOptions
151+
): Promise<void> {
139152
if (!validator.isNonEmptyString(functionName)) {
140153
throw new FirebaseFunctionsError({
141154
code: 'invalid-argument',
@@ -161,17 +174,17 @@ export class FunctionsApiClient {
161174
message: 'No valid function name specified to enqueue tasks for.'
162175
});
163176
}
164-
if (typeof extensionId !== 'undefined' && validator.isNonEmptyString(extensionId)) {
165-
resources.resourceId = `ext-${extensionId}-${resources.resourceId}`;
166-
}
167177

168-
const task = this.validateTaskOptions(data, resources, opts);
178+
const { resourceId, extensionOrKitId } = this.resolveResourceId(resources.resourceId, scope);
179+
const targetResources = { ...resources, resourceId };
180+
169181
try {
182+
const task = this.validateTaskOptions(data, targetResources, opts);
170183
const serviceUrl =
171-
tasksEmulatorUrl(resources, this.emulatorHost) ??
172-
await this.getUrl(resources, CLOUD_TASKS_API_URL_FORMAT);
184+
tasksEmulatorUrl(targetResources, this.emulatorHost) ??
185+
await this.getUrl(targetResources, CLOUD_TASKS_API_URL_FORMAT);
173186

174-
const taskPayload = await this.updateTaskPayload(task, resources, extensionId);
187+
const taskPayload = await this.updateTaskPayload(task, targetResources, extensionOrKitId);
175188
const request: HttpRequestConfig = {
176189
method: 'POST',
177190
url: serviceUrl,
@@ -199,6 +212,51 @@ export class FunctionsApiClient {
199212
}
200213
}
201214

215+
private resolveResourceId(
216+
resourceId: string,
217+
scope: InternalFunctionScope = { scope: 'current' }
218+
): { resourceId: string; extensionOrKitId?: string } {
219+
220+
switch (scope.scope) {
221+
case 'current': {
222+
const extInstanceId = process.env.EXT_INSTANCE_ID;
223+
if (validator.isNonEmptyString(extInstanceId)) {
224+
return {
225+
resourceId: `ext-${extInstanceId}-${resourceId}`,
226+
extensionOrKitId: extInstanceId,
227+
};
228+
}
229+
const kitInstanceId = process.env.FIREBASE_KIT_INSTANCE_ID;
230+
if (validator.isNonEmptyString(kitInstanceId)) {
231+
return {
232+
resourceId: `kit-${kitInstanceId}-${resourceId}`,
233+
extensionOrKitId: kitInstanceId,
234+
};
235+
}
236+
return { resourceId };
237+
}
238+
case 'global':
239+
return { resourceId };
240+
case 'extension':
241+
return {
242+
resourceId: `ext-${scope.instance}-${resourceId}`,
243+
extensionOrKitId: scope.instance,
244+
};
245+
case 'kit': // kit scope is secretly accepted for forward compatibility
246+
return {
247+
resourceId: `kit-${scope.instance}-${resourceId}`,
248+
extensionOrKitId: scope.instance,
249+
};
250+
case 'extensionOrKit':
251+
return {
252+
resourceId: `ext-${scope.instance}-${resourceId}`,
253+
extensionOrKitId: scope.instance,
254+
};
255+
default:
256+
return { resourceId };
257+
}
258+
}
259+
202260
private getUrl(resourceName: utils.ParsedResource, urlFormat: string): Promise<string> {
203261
let { locationId } = resourceName;
204262
const { projectId, resourceId } = resourceName;
@@ -349,7 +407,11 @@ export class FunctionsApiClient {
349407
return task;
350408
}
351409

352-
private async updateTaskPayload(task: Task, resources: utils.ParsedResource, extensionId?: string): Promise<Task> {
410+
private async updateTaskPayload(
411+
task: Task,
412+
resources: utils.ParsedResource,
413+
extensionOrKitId?: string
414+
): Promise<Task> {
353415
const defaultUrl = this.emulatorHost ?
354416
''
355417
: await this.getUrl(resources, FIREBASE_FUNCTION_URL_FORMAT);
@@ -360,7 +422,7 @@ export class FunctionsApiClient {
360422

361423
task.httpRequest.url = functionUrl;
362424
// When run from a deployed extension, we should be using ComputeEngineCredentials
363-
if (validator.isNonEmptyString(extensionId) && this.app.options.credential
425+
if (validator.isNonEmptyString(extensionOrKitId) && this.app.options.credential
364426
instanceof ApplicationDefaultCredential && await this.app.options.credential.isComputeEngineCredential()) {
365427
const idToken = await this.app.options.credential.getIDToken(functionUrl);
366428
task.httpRequest.headers = { ...task.httpRequest.headers, 'Authorization': `Bearer ${idToken}` };

src/functions/functions-api.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,16 @@ export interface TaskOptionsExperimental {
108108
*/
109109
uri?: string;
110110
}
111+
112+
/**
113+
* Type representing the scope of a function in a task queue.
114+
*/
115+
export type FunctionScope = {
116+
scope: 'current';
117+
} | {
118+
scope: 'global';
119+
} | {
120+
scope: 'extension';
121+
instance: string;
122+
};
123+

0 commit comments

Comments
 (0)