Merge pull request #16417 from mozilla/firestore-cacheable

feat(auth): type-cacheable firestore and networkfirst adapter
This commit is contained in:
Julian Poyourow 2024-02-14 10:02:19 -08:00 коммит произвёл GitHub
Родитель 242c0c9c91 573a18be60
Коммит 9d2b26eecc
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
34 изменённых файлов: 730 добавлений и 25 удалений

Просмотреть файл

@ -40,14 +40,14 @@ export class CapabilityManager {
): Promise<Record<string, string[]>> {
if (!subscribedPrices.length) return {};
const purchaseDetails =
await this.contentfulManager.getPurchaseDetailsForCapabilityServiceByPlanIds(
[...subscribedPrices]
);
const result: Record<string, string[]> = {};
for (const subscribedPrice of subscribedPrices) {
const purchaseDetails =
await this.contentfulManager.getPurchaseDetailsForCapabilityServiceByPlanIds(
[subscribedPrice]
);
const capabilityOffering =
purchaseDetails.capabilityOfferingForPlanId(subscribedPrice);

Просмотреть файл

@ -20,6 +20,12 @@ export class ContentfulClientConfig {
@IsString()
public readonly graphqlEnvironment!: string;
@IsString()
public readonly firestoreCacheCollectionName!: string;
@IsNumber()
public readonly cacheTTL?: number;
public readonly memCacheTTL?: number;
@IsNumber()
public readonly firestoreCacheTTL?: number;
}

Просмотреть файл

@ -22,6 +22,15 @@ jest.mock('graphql-request', () => ({
},
}));
jest.mock('@fxa/shared/db/type-cacheable', () => ({
FirestoreCacheable: () => {
return (target: any, propertyKey: any, descriptor: any) => {
return descriptor;
};
},
NetworkFirstStrategy: function () {},
}));
describe('ContentfulClient', () => {
let contentfulClient: ContentfulClient;
const onCallback = jest.fn();
@ -33,6 +42,7 @@ describe('ContentfulClient', () => {
graphqlApiUri: faker.string.uuid(),
graphqlSpaceId: faker.string.uuid(),
graphqlEnvironment: faker.string.uuid(),
firestoreCacheCollectionName: faker.string.uuid(),
});
contentfulClient.on('response', onCallback);
});

Просмотреть файл

@ -2,9 +2,9 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { OperationVariables } from '@apollo/client';
import type { OperationVariables } from '@apollo/client';
import { GraphQLClient } from 'graphql-request';
import { TypedDocumentNode } from '@graphql-typed-document-node/core';
import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
import { Injectable } from '@nestjs/common';
import { determineLocale } from '@fxa/shared/l10n';
import { DEFAULT_LOCALE } from './constants';
@ -16,8 +16,14 @@ import {
} from './contentful.error';
import { ContentfulErrorResponse } from './types';
import EventEmitter from 'events';
import {
FirestoreCacheable,
NetworkFirstStrategy,
} from '@fxa/shared/db/type-cacheable';
import { CONTENTFUL_QUERY_CACHE_KEY, cacheKeyForQuery } from './util';
const DEFAULT_CACHE_TTL = 300000; // Milliseconds
const DEFAULT_FIRESTORE_CACHE_TTL = 604800; // Seconds. 604800 is 7 days.
const DEFAULT_MEM_CACHE_TTL = 300; // Seconds
interface EventResponse {
method: string;
@ -36,12 +42,12 @@ export class ContentfulClient {
`${this.contentfulClientConfig.graphqlApiUri}/spaces/${this.contentfulClientConfig.graphqlSpaceId}/environments/${this.contentfulClientConfig.graphqlEnvironment}?access_token=${this.contentfulClientConfig.graphqlApiKey}`
);
private locales: string[] = [];
private graphqlResultCache: Record<string, unknown> = {};
private emitter: EventEmitter;
public on: (
event: 'response',
listener: (response: EventResponse) => void
) => EventEmitter;
private graphqlMemCache: Record<string, unknown> = {};
constructor(private contentfulClientConfig: ContentfulClientConfig) {
this.setupCacheBust();
@ -58,33 +64,42 @@ export class ContentfulClient {
return result;
}
@FirestoreCacheable(
{
cacheKey: (args: any) => cacheKeyForQuery(args[0], args[1]),
strategy: new NetworkFirstStrategy(),
ttlSeconds: (_, context) =>
context.contentfulClientConfig.firestoreCacheTTL ||
DEFAULT_FIRESTORE_CACHE_TTL,
},
{
collectionName: (_, context) =>
context.contentfulClientConfig.firestoreCacheCollectionName ||
CONTENTFUL_QUERY_CACHE_KEY,
}
)
async query<Result, Variables extends OperationVariables>(
query: TypedDocumentNode<Result, Variables>,
variables: Variables
): Promise<Result> {
// Sort variables prior to stringifying to not be caller order dependent
const variablesString = JSON.stringify(
variables,
Object.keys(variables as Record<string, unknown>).sort()
);
const cacheKey = variablesString + query;
const cacheKey = cacheKeyForQuery(query, variables);
const emitterResponse = {
method: 'query',
query,
variables: variablesString,
variables: JSON.stringify(variables),
requestStartTime: Date.now(),
cache: false,
};
if (this.graphqlResultCache[cacheKey]) {
if (this.graphqlMemCache[cacheKey]) {
this.emitter.emit('response', {
...emitterResponse,
requestEndTime: emitterResponse.requestStartTime,
elapsed: 0,
cache: true,
});
return this.graphqlResultCache[cacheKey] as Result;
return this.graphqlMemCache[cacheKey] as Result;
}
try {
@ -100,7 +115,7 @@ export class ContentfulClient {
requestEndTime,
});
this.graphqlResultCache[cacheKey] = response;
this.graphqlMemCache[cacheKey] = response;
return response;
} catch (e) {
@ -178,11 +193,12 @@ export class ContentfulClient {
}
private setupCacheBust() {
const cacheTTL = this.contentfulClientConfig.cacheTTL || DEFAULT_CACHE_TTL;
const cacheTTL =
this.contentfulClientConfig.memCacheTTL || DEFAULT_MEM_CACHE_TTL * 1000;
setInterval(() => {
this.locales = [];
this.graphqlResultCache = {};
this.graphqlMemCache = {};
}, cacheTTL);
}
}

Просмотреть файл

@ -21,6 +21,15 @@ import { PurchaseWithDetailsOfferingContentUtil } from './queries/purchase-with-
import { PurchaseWithDetailsOfferingContentByPlanIdsResultFactory } from './queries/purchase-with-details-offering-content/factories';
import { StatsD } from 'hot-shots';
jest.mock('@fxa/shared/db/type-cacheable', () => ({
FirestoreCacheable: () => {
return (target: any, propertyKey: any, descriptor: any) => {
return descriptor;
};
},
NetworkFirstStrategy: function () {},
}));
describe('ContentfulManager', () => {
let manager: ContentfulManager;
let mockClient: ContentfulClient;

Просмотреть файл

@ -0,0 +1,26 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import type { OperationVariables, TypedDocumentNode } from '@apollo/client';
import { createHash } from 'crypto';
export const CONTENTFUL_QUERY_CACHE_KEY = 'contentfulQueryCache';
export const cacheKeyForQuery = function <
Result,
Variables extends OperationVariables
>(query: TypedDocumentNode<Result, Variables>, variables: Variables): string {
// Sort variables prior to stringifying to not be caller order dependent
const variablesString = JSON.stringify(
variables,
Object.keys(variables as Record<string, unknown>).sort()
);
const queryHash = createHash('sha256')
.update(JSON.stringify(query))
.digest('hex');
const variableHash = createHash('sha256')
.update(variablesString)
.digest('hex');
return `${CONTENTFUL_QUERY_CACHE_KEY}:${queryHash}:${variableHash}`;
};

Просмотреть файл

@ -0,0 +1,18 @@
{
"extends": ["../../../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

Просмотреть файл

@ -0,0 +1,7 @@
# shared-db-firestore
Instantiates a NestJS injectable instance of firestore
## Running unit tests
Run `nx test shared-db-firestore` to execute the unit tests via [Jest](https://jestjs.io).

Просмотреть файл

@ -0,0 +1,11 @@
/* eslint-disable */
export default {
displayName: 'shared-db-firestore',
preset: '../../../../jest.preset.js',
testEnvironment: 'node',
transform: {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../../../coverage/libs/shared/db/firestore',
};

Просмотреть файл

@ -0,0 +1,4 @@
{
"name": "@fxa/shared/db/firestore",
"version": "0.0.0"
}

Просмотреть файл

@ -0,0 +1,41 @@
{
"name": "shared-db-firestore",
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/shared/db/firestore/src",
"projectType": "library",
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/libs/shared/db/firestore",
"tsConfig": "libs/shared/db/firestore/tsconfig.lib.json",
"packageJson": "libs/shared/db/firestore/package.json",
"main": "libs/shared/db/firestore/src/index.ts",
"assets": ["libs/shared/db/firestore/*.md"]
}
},
"lint": {
"executor": "@nx/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["libs/shared/db/firestore/**/*.ts"]
}
},
"test-unit": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/shared/db/firestore/jest.config.ts",
"passWithNoTests": true
},
"configurations": {
"ci": {
"ci": true,
"codeCoverage": true
}
}
}
},
"tags": ["scope:shared:lib"]
}

Просмотреть файл

@ -0,0 +1,5 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
export * from './lib/firestore.provider';
export * from './lib/firestore-typedi-token';

Просмотреть файл

@ -0,0 +1,8 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { Firestore } from '@google-cloud/firestore';
import { Token } from 'typedi';
export const AuthFirestore = new Token<Firestore>('AUTH_FIRESTORE');

Просмотреть файл

@ -0,0 +1,57 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { Firestore } from '@google-cloud/firestore';
import * as grpc from '@grpc/grpc-js';
import { Provider } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
/**
* Creates a firestore instance from a settings object.
* @param config
* @returns A firestore instance
*/
export function setupFirestore(config: FirebaseFirestore.Settings) {
const fsConfig = Object.assign({}, config);
// keyFilename takes precedence over credentials
if (fsConfig.keyFilename) {
delete fsConfig.credentials;
}
const testing = !(fsConfig.keyFilename || fsConfig.credentials);
// Utilize the local firestore emulator when the env indicates
if (process.env.FIRESTORE_EMULATOR_HOST || testing) {
return new Firestore({
customHeaders: {
Authorization: 'Bearer owner',
},
port: 9090,
projectId: 'demo-fxa',
servicePath: 'localhost',
sslCreds: grpc.credentials.createInsecure(),
});
} else {
return new Firestore(fsConfig);
}
}
/**
* Factory for providing access to firestore
*/
export const FirestoreService = Symbol('FIRESTORE');
export const FirestoreFactory: Provider<Firestore> = {
provide: FirestoreService,
useFactory: (configService: ConfigService) => {
const firestoreConfig = configService.get('authFirestore');
if (firestoreConfig == null) {
throw new Error(
"Could not locate config for firestore missing 'authFirestore';"
);
}
const firestore = setupFirestore(firestoreConfig);
return firestore;
},
inject: [ConfigService],
};

Просмотреть файл

@ -0,0 +1,16 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs"
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

Просмотреть файл

@ -0,0 +1,11 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"outDir": "../../../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"],
"include": ["src/**/*.ts"]
}

Просмотреть файл

@ -0,0 +1,14 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

Просмотреть файл

@ -0,0 +1,18 @@
{
"extends": ["../../../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

Просмотреть файл

@ -0,0 +1,7 @@
# shared-db-type-cacheable
Customized implementations for the type-cacheable library.
## Running unit tests
Run `nx test shared-db-type-cacheable` to execute the unit tests via [Jest](https://jestjs.io).

Просмотреть файл

@ -0,0 +1,11 @@
/* eslint-disable */
export default {
displayName: 'shared-db-type-cacheable',
preset: '../../../../jest.preset.js',
testEnvironment: 'node',
transform: {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../../../coverage/libs/shared/db/type-cacheable',
};

Просмотреть файл

@ -0,0 +1,4 @@
{
"name": "@fxa/shared/db/type-cacheable",
"version": "0.0.0"
}

Просмотреть файл

@ -0,0 +1,41 @@
{
"name": "shared-db-type-cacheable",
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/shared/db/type-cacheable/src",
"projectType": "library",
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/libs/shared/db/type-cacheable",
"tsConfig": "libs/shared/db/type-cacheable/tsconfig.lib.json",
"packageJson": "libs/shared/db/type-cacheable/package.json",
"main": "libs/shared/db/type-cacheable/src/index.ts",
"assets": ["libs/shared/db/type-cacheable/*.md"]
}
},
"lint": {
"executor": "@nx/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["libs/shared/db/type-cacheable/**/*.ts"]
}
},
"test-unit": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/shared/db/type-cacheable/jest.config.ts",
"passWithNoTests": true
},
"configurations": {
"ci": {
"ci": true,
"codeCoverage": true
}
}
}
},
"tags": ["scope:shared:lib"]
}

Просмотреть файл

@ -0,0 +1,6 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
export * from './lib/firestore-cacheable';
export * from './lib/type-cachable-network-first';
export * from './lib/type-cachable-firestore-adapter';

Просмотреть файл

@ -0,0 +1,65 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { Inject } from '@nestjs/common';
import { Cacheable, CacheOptions } from '@type-cacheable/core';
import { AuthFirestore, FirestoreService } from '@fxa/shared/db/firestore';
import { FirestoreAdapter } from './type-cachable-firestore-adapter';
import { Firestore } from '@google-cloud/firestore';
import Container from 'typedi';
export interface FirestoreCacheableOptions {
collectionName: string | ((args: any[], context: any) => string);
}
export function FirestoreCacheable(
cacheableOptions: CacheOptions,
firestoreOptions: FirestoreCacheableOptions
) {
// We try to fetch Firestore here with Nest DI, but need typedi compatibility where there's no Nest for now.
let injectFirestore: ReturnType<typeof Inject<Firestore>> | undefined;
try {
injectFirestore = Inject(FirestoreService);
} catch (e) {}
return (
target: any,
propertyKey: string,
propertyDescriptor: PropertyDescriptor
) => {
injectFirestore?.(target, 'firestore');
const originalMethod = propertyDescriptor.value;
propertyDescriptor.value = async function (...args: any[]) {
// We try to fetch typedi Firestore if available in case this was loaded in a non-Nest environment
let typediFirestore: Firestore | undefined;
try {
typediFirestore = Container.get<Firestore>(AuthFirestore);
} catch (e) {}
const firestore = typediFirestore || (this as any).firestore;
if (!firestore) {
throw new Error('Could not load Firestore from Nest or typedi');
}
const collectionName =
typeof firestoreOptions.collectionName === 'string'
? firestoreOptions.collectionName
: firestoreOptions.collectionName(args, this);
const newDescriptor = Cacheable({
...cacheableOptions,
client: new FirestoreAdapter(firestore, collectionName),
})(target, propertyKey, {
...propertyDescriptor,
value: originalMethod,
});
return await newDescriptor.value.apply(this, args);
};
return propertyDescriptor;
};
}

Просмотреть файл

@ -0,0 +1,97 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { Firestore } from '@google-cloud/firestore';
import cacheManager, {
CacheClient,
CacheManagerOptions,
} from '@type-cacheable/core';
export class FirestoreAdapter implements CacheClient {
constructor(firestoreClient: Firestore, collectionName: string) {
this.firestoreClient = firestoreClient;
this.collectionName = collectionName;
this.get = this.get.bind(this);
this.del = this.del.bind(this);
this.delHash = this.delHash.bind(this);
this.getClientTTL = this.getClientTTL.bind(this);
this.keys = this.keys.bind(this);
this.set = this.set.bind(this);
}
private readonly firestoreClient: Firestore;
private readonly collectionName: string;
public async get(cacheKey: string): Promise<any> {
const result = await this.firestoreClient
.collection(this.collectionName)
.doc(cacheKey)
.get();
const data = result?.data();
const cachedValue = data?.value;
return cachedValue;
}
public async set(cacheKey: string, value: any, ttl?: number): Promise<any> {
await this.firestoreClient
.collection(this.collectionName)
.doc(cacheKey)
.set({
value,
ttl: ttl || 0,
updatedAt: new Date(),
});
}
public getClientTTL(): number {
return 0;
}
public async del(keyOrKeys: string | string[]): Promise<any> {
if (Array.isArray(keyOrKeys) && !keyOrKeys.length) {
return 0;
}
const keysToDelete = Array.isArray(keyOrKeys) ? keyOrKeys : [keyOrKeys];
for (const cacheKey of keysToDelete) {
await this.firestoreClient
.collection(this.collectionName)
.doc(cacheKey)
.delete();
}
}
public async keys(_: string): Promise<string[]> {
throw new Error('keys() not supported for cachable FirestoreAdapter');
}
public async delHash(_: string | string[]): Promise<any> {
throw new Error('delHash() not supported for cachable FirestoreAdapter');
}
}
export const useAdapter = (
client: Firestore,
collectionName: string,
asFallback?: boolean,
options?: CacheManagerOptions
): FirestoreAdapter => {
const firestoreAdapter = new FirestoreAdapter(client, collectionName);
if (asFallback) {
cacheManager.setFallbackClient(firestoreAdapter);
} else {
cacheManager.setClient(firestoreAdapter);
}
if (options) {
cacheManager.setOptions(options);
}
return firestoreAdapter;
};

Просмотреть файл

@ -0,0 +1,140 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import {
CacheClient,
CacheStrategy,
CacheStrategyContext,
} from '@type-cacheable/core';
export class NetworkFirstStrategy implements CacheStrategy {
private pendingCacheRequestMap = new Map<string, Promise<any>>();
private pendingMethodCallMap = new Map<string, Promise<any>>();
private findCachedValue = async (client: CacheClient, key: string) => {
let cachedValue: any;
try {
if (this.pendingCacheRequestMap.has(key)) {
cachedValue = await this.pendingCacheRequestMap.get(key);
} else {
const cachePromise = client.get(key);
this.pendingCacheRequestMap.set(key, cachePromise);
cachedValue = await cachePromise;
}
} catch (err) {
throw err;
} finally {
this.pendingCacheRequestMap.delete(key);
}
return cachedValue;
};
async handle(context: CacheStrategyContext): Promise<any> {
let result: any;
const pendingMethodRun = this.pendingMethodCallMap.get(context.key);
if (pendingMethodRun) {
result = await pendingMethodRun;
} else {
const methodPromise = new Promise(async (resolve, reject) => {
let returnValue;
let isCacheable = false;
try {
returnValue = await context.originalMethod.apply(
context.originalMethodScope,
context.originalMethodArgs
);
isCacheable = context.isCacheable(returnValue);
} catch (err) {
reject(err);
return;
}
if (!isCacheable) {
resolve(returnValue);
return;
}
try {
await context.client.set(context.key, returnValue, context.ttl);
} catch (err) {
console.error(err);
if (context.fallbackClient) {
try {
await context.fallbackClient.set(
context.key,
returnValue,
context.ttl
);
} catch (err) {}
}
if (context.debug) {
console.warn(
`type-cacheable Cacheable set cache failure on method ${
context.originalMethod.name
} due to client error: ${(err as Error).message}`
);
}
}
resolve(returnValue);
});
try {
this.pendingMethodCallMap.set(context.key, methodPromise);
result = await methodPromise;
} catch (err) {
console.error(err);
} finally {
this.pendingMethodCallMap.delete(context.key);
}
}
if (result) {
return result;
}
try {
const cachedValue = await this.findCachedValue(
context.client,
context.key
);
// If a value for the cacheKey was found in cache, simply return that.
if (cachedValue !== undefined && cachedValue !== null) {
return cachedValue;
}
} catch (err) {
console.error(err);
if (context.fallbackClient) {
try {
const cachedValue = await this.findCachedValue(
context.fallbackClient,
context.key
);
// If a value for the cacheKey was found in cache, simply return that.
if (cachedValue !== undefined && cachedValue !== null) {
return cachedValue;
}
} catch (fallbackErr) {}
}
if (context.debug) {
console.warn(
`type-cacheable Cacheable cache miss on method ${
context.originalMethod.name
} due to client error: ${(err as Error).message}`
);
}
}
return result;
}
}

Просмотреть файл

@ -0,0 +1,16 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs"
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

Просмотреть файл

@ -0,0 +1,11 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"outDir": "../../../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"],
"include": ["src/**/*.ts"]
}

Просмотреть файл

@ -0,0 +1,14 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

Просмотреть файл

@ -120,7 +120,8 @@ async function run(config) {
config.contentful.graphqlUrl &&
config.contentful.apiKey &&
config.contentful.spaceId &&
config.contentful.environment
config.contentful.environment &&
config.contentful.firestoreCacheCollectionName
) {
const contentfulClient = new ContentfulClient({
cdnApiUri: config.contentful.cdnUrl,
@ -128,6 +129,8 @@ async function run(config) {
graphqlApiKey: config.contentful.apiKey,
graphqlSpaceId: config.contentful.spaceId,
graphqlEnvironment: config.contentful.environment,
firestoreCacheCollectionName:
config.contentful.firestoreCacheCollectionName,
});
const contentfulManager = new ContentfulManager(contentfulClient, statsd);
Container.set(ContentfulManager, contentfulManager);

Просмотреть файл

@ -2011,6 +2011,12 @@ const convictConf = convict({
env: 'CONTENTFUL_GRAPHQL_ENVIRONMENT',
format: String,
},
firestoreCacheCollectionName: {
default: '',
doc: 'Firestore collection name to store Contentful query cache',
env: 'CONTENTFUL_FIRESTORE_CACHE_COLLECTION_NAME',
format: String,
},
},
cloudTasks: {

Просмотреть файл

@ -4,7 +4,6 @@
import { Request, RequestApplicationState } from '@hapi/hapi';
import { Token } from 'typedi';
import { Logger } from 'mozlog';
import { Firestore } from '@google-cloud/firestore';
import { ConfigType } from '../config';
/**
@ -87,7 +86,7 @@ export interface AuthLogger extends Logger {
// Container token types
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const AuthLogger = new Token<AuthLogger>('AUTH_LOGGER');
export const AuthFirestore = new Token<Firestore>('AUTH_FIRESTORE');
export const AppConfig = new Token<ConfigType>('APP_CONFIG');
export { AuthFirestore } from '@fxa/shared/db/firestore';
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const ProfileClient = new Token<ProfileClient>('PROFILE_CLIENT');

Просмотреть файл

@ -72,6 +72,12 @@ const conf = convict({
env: 'CONTENTFUL_GRAPHQL_ENVIRONMENT',
default: '',
},
firestoreCacheCollectionName: {
doc: 'Firestore collection name to store Contentful query cache',
format: String,
env: 'CONTENTFUL_FIRESTORE_CACHE_COLLECTION_NAME',
default: '',
},
},
corsOrigin: {
doc: 'Value for the Access-Control-Allow-Origin response header',

Просмотреть файл

@ -36,6 +36,8 @@
"libs/shared/account/account/src/index.ts"
],
"@fxa/shared/contentful": ["libs/shared/contentful/src/index.ts"],
"@fxa/shared/db/type-cacheable": ["libs/shared/db/type-cacheable/src/index.ts"],
"@fxa/shared/db/firestore": ["libs/shared/db/firestore/src/index.ts"],
"@fxa/shared/db/mysql/account": [
"libs/shared/db/mysql/account/src/index.ts"
],