Skip to content

Commit fad8184

Browse files
committed
feat(node-sdk): accept googleapis gcs clients
1 parent 5eca688 commit fad8184

3 files changed

Lines changed: 141 additions & 31 deletions

File tree

packages/node-sdk/src/flagsFallbackProvider.ts

Lines changed: 87 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,44 @@ export type S3FallbackProviderOptions = {
3939
keyPrefix?: string;
4040
};
4141

42+
export type GCSLegacyClient = {
43+
bucket(name: string): {
44+
file(path: string): {
45+
exists(): Promise<[boolean]>;
46+
download(): Promise<[Uint8Array]>;
47+
save(body: string, options: { contentType: string }): Promise<unknown>;
48+
};
49+
};
50+
};
51+
52+
export type GCSGoogleApisClient = {
53+
objects: {
54+
get(
55+
params: {
56+
bucket: string;
57+
object: string;
58+
alt?: string;
59+
},
60+
options?: {
61+
responseType?: "arraybuffer";
62+
},
63+
): Promise<{
64+
data: unknown;
65+
}>;
66+
insert(params: {
67+
bucket: string;
68+
name: string;
69+
uploadType: "media";
70+
media: {
71+
mimeType: string;
72+
body: string;
73+
};
74+
}): Promise<unknown>;
75+
};
76+
};
77+
78+
export type GCSFallbackProviderClient = GCSLegacyClient | GCSGoogleApisClient;
79+
4280
export type GCSFallbackProviderOptions = {
4381
/**
4482
* Bucket where snapshots are stored.
@@ -48,19 +86,12 @@ export type GCSFallbackProviderOptions = {
4886
/**
4987
* Optional GCS client. A default client is created when omitted.
5088
*
51-
* TODO(next major): Replace this legacy `bucket().file()` client shape with
52-
* a simpler object-store interface that doesn't mirror the deprecated
53-
* `@google-cloud/storage` API.
89+
* Accepts either a legacy `bucket().file()` client or a generated
90+
* `@googleapis/storage` client.
91+
*
92+
* TODO(next major): Replace this with a simpler object-store interface.
5493
*/
55-
client?: {
56-
bucket(name: string): {
57-
file(path: string): {
58-
exists(): Promise<[boolean]>;
59-
download(): Promise<[Uint8Array]>;
60-
save(body: string, options: { contentType: string }): Promise<unknown>;
61-
};
62-
};
63-
};
94+
client?: GCSFallbackProviderClient;
6495

6596
/**
6697
* Prefix for generated per-environment keys.
@@ -70,8 +101,6 @@ export type GCSFallbackProviderOptions = {
70101
keyPrefix?: string;
71102
};
72103

73-
type LegacyGCSClient = NonNullable<GCSFallbackProviderOptions["client"]>;
74-
75104
type GCSObjectStore = {
76105
exists(bucket: string, objectPath: string): Promise<boolean>;
77106
download(bucket: string, objectPath: string): Promise<Uint8Array>;
@@ -227,15 +256,18 @@ async function createDefaultS3Client() {
227256
return new S3Client({});
228257
}
229258

230-
function createGCSObjectStore(client: LegacyGCSClient): GCSObjectStore {
259+
function createLegacyGCSObjectStore(client: GCSLegacyClient): GCSObjectStore {
231260
return {
232261
async exists(bucket, objectPath) {
233262
const [exists] = await client.bucket(bucket).file(objectPath).exists();
234263
return exists;
235264
},
236265

237266
async download(bucket, objectPath) {
238-
const [contents] = await client.bucket(bucket).file(objectPath).download();
267+
const [contents] = await client
268+
.bucket(bucket)
269+
.file(objectPath)
270+
.download();
239271
return contents;
240272
},
241273

@@ -245,19 +277,13 @@ function createGCSObjectStore(client: LegacyGCSClient): GCSObjectStore {
245277
};
246278
}
247279

248-
async function createDefaultGCSObjectStore(): Promise<GCSObjectStore> {
249-
const { auth, storage } = await import("@googleapis/storage");
250-
const gcs = storage({
251-
version: "v1",
252-
auth: new auth.GoogleAuth({
253-
scopes: ["https://www.googleapis.com/auth/devstorage.read_write"],
254-
}),
255-
});
256-
280+
function createGoogleApisGCSObjectStore(
281+
client: GCSGoogleApisClient,
282+
): GCSObjectStore {
257283
return {
258284
async exists(bucket, objectPath) {
259285
try {
260-
await gcs.objects.get({
286+
await client.objects.get({
261287
bucket,
262288
object: objectPath,
263289
});
@@ -271,7 +297,7 @@ async function createDefaultGCSObjectStore(): Promise<GCSObjectStore> {
271297
},
272298

273299
async download(bucket, objectPath) {
274-
const response = await gcs.objects.get(
300+
const response = await client.objects.get(
275301
{
276302
bucket,
277303
object: objectPath,
@@ -293,7 +319,7 @@ async function createDefaultGCSObjectStore(): Promise<GCSObjectStore> {
293319
},
294320

295321
async save(bucket, objectPath, body, options) {
296-
await gcs.objects.insert({
322+
await client.objects.insert({
297323
bucket,
298324
name: objectPath,
299325
uploadType: "media",
@@ -306,6 +332,39 @@ async function createDefaultGCSObjectStore(): Promise<GCSObjectStore> {
306332
};
307333
}
308334

335+
function isGoogleApisGCSClient(
336+
client: GCSFallbackProviderClient,
337+
): client is GCSGoogleApisClient {
338+
return (
339+
"objects" in client &&
340+
isObject(client.objects) &&
341+
typeof client.objects.get === "function" &&
342+
typeof client.objects.insert === "function"
343+
);
344+
}
345+
346+
function createGCSObjectStore(
347+
client: GCSFallbackProviderClient,
348+
): GCSObjectStore {
349+
if (isGoogleApisGCSClient(client)) {
350+
return createGoogleApisGCSObjectStore(client);
351+
}
352+
353+
return createLegacyGCSObjectStore(client);
354+
}
355+
356+
async function createDefaultGCSObjectStore(): Promise<GCSObjectStore> {
357+
const { auth, storage } = await import("@googleapis/storage");
358+
return createGoogleApisGCSObjectStore(
359+
storage({
360+
version: "v1",
361+
auth: new auth.GoogleAuth({
362+
scopes: ["https://www.googleapis.com/auth/devstorage.read_write"],
363+
}),
364+
}),
365+
);
366+
}
367+
309368
export function createStaticFallbackProvider({
310369
flags,
311370
}: StaticFallbackProviderOptions): FlagsFallbackProvider {

packages/node-sdk/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ export { BoundReflagClient, ReflagClient } from "./client";
1010
export { EdgeClient, EdgeClientOptions } from "./edgeClient";
1111
export type {
1212
FileFallbackProviderOptions,
13+
GCSFallbackProviderClient,
1314
GCSFallbackProviderOptions,
15+
GCSGoogleApisClient,
16+
GCSLegacyClient,
1417
RedisFallbackProviderOptions,
1518
S3FallbackProviderOptions,
1619
StaticFallbackProviderOptions,

packages/node-sdk/test/flagsFallbackProvider.test.ts

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,55 @@ describe("flagsFallbackProvider", () => {
242242
});
243243
});
244244

245+
it("accepts a provided @googleapis/storage client", async () => {
246+
const get = vi
247+
.fn()
248+
.mockResolvedValueOnce({ data: { kind: "storage#object" } })
249+
.mockResolvedValueOnce({
250+
data: Buffer.from(JSON.stringify(snapshot), "utf-8"),
251+
});
252+
const insert = vi.fn().mockResolvedValue({});
253+
254+
const provider = fallbackProviders.gcs({
255+
bucket: "bucket-name",
256+
client: {
257+
objects: {
258+
get,
259+
insert,
260+
},
261+
},
262+
keyPrefix: "reflag/flags-fallback///",
263+
});
264+
265+
await expect(provider.load(context)).resolves.toEqual(snapshot);
266+
await provider.save(context, snapshot);
267+
268+
expect(get).toHaveBeenNthCalledWith(1, {
269+
bucket: "bucket-name",
270+
object: `reflag/flags-fallback/flags-fallback-${context.secretKeyHash.slice(0, 16)}.json`,
271+
});
272+
expect(get).toHaveBeenNthCalledWith(
273+
2,
274+
{
275+
bucket: "bucket-name",
276+
object: `reflag/flags-fallback/flags-fallback-${context.secretKeyHash.slice(0, 16)}.json`,
277+
alt: "media",
278+
},
279+
{
280+
responseType: "arraybuffer",
281+
},
282+
);
283+
expect(insert).toHaveBeenCalledWith({
284+
bucket: "bucket-name",
285+
name: `reflag/flags-fallback/flags-fallback-${context.secretKeyHash.slice(0, 16)}.json`,
286+
uploadType: "media",
287+
media: {
288+
mimeType: "application/json",
289+
body: JSON.stringify(snapshot),
290+
},
291+
});
292+
});
293+
245294
it("creates the default GCS client from @googleapis/storage", async () => {
246295
vi.resetModules();
247296

@@ -266,9 +315,8 @@ describe("flagsFallbackProvider", () => {
266315
}));
267316

268317
try {
269-
const { createGCSFallbackProvider } = await import(
270-
"../src/flagsFallbackProvider"
271-
);
318+
const { createGCSFallbackProvider } =
319+
await import("../src/flagsFallbackProvider");
272320
const provider = createGCSFallbackProvider({
273321
bucket: "bucket-name",
274322
keyPrefix: "reflag/flags-fallback///",

0 commit comments

Comments
 (0)