Merge pull request #15851 from mozilla/FXA-8405--contentfulmanager-create-manager-class-and-add-m

feat: add contentful manager and eligibility helper method
This commit is contained in:
Ben Bangert 2023-10-02 08:31:25 -07:00 коммит произвёл GitHub
Родитель d74a3aac6f d802413923
Коммит 19e9c028b1
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
23 изменённых файлов: 1437 добавлений и 1253 удалений

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

@ -9,10 +9,16 @@ const CONTENTFUL_GRAPHQL_ENVIRONMENT =
const config: CodegenConfig = { const config: CodegenConfig = {
overwrite: true, overwrite: true,
schema: `${CONTENTFUL_GRAPHQL_API_URL}/spaces/${CONTENTFUL_GRAPHQL_SPACE_ID}/environments/${CONTENTFUL_GRAPHQL_ENVIRONMENT}?access_token=${CONTENTFUL_GRAPHQL_API_KEY}`, schema: `${CONTENTFUL_GRAPHQL_API_URL}/spaces/${CONTENTFUL_GRAPHQL_SPACE_ID}/environments/${CONTENTFUL_GRAPHQL_ENVIRONMENT}?access_token=${CONTENTFUL_GRAPHQL_API_KEY}`,
documents: ['libs/shared/contentful/src/lib/queries/*.ts'], documents: [
'libs/shared/contentful/src/lib/queries/*.ts',
'libs/shared/contentful/src/lib/queries/**/*.ts',
],
generates: { generates: {
'libs/shared/contentful/src/__generated__/': { 'libs/shared/contentful/src/__generated__/': {
preset: 'client', preset: 'client',
config: {
avoidOptionals: true,
},
}, },
}, },
}; };

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

@ -31,7 +31,7 @@
] ]
} }
}, },
"test": { "test-unit": {
"executor": "@nx/jest:jest", "executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"], "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": { "options": {

12
libs/shared/contentful/src/__generated__/gql.ts сгенерированный
Просмотреть файл

@ -19,10 +19,10 @@ const documents = {
types.EligibilityContentByPlanIdsDocument, types.EligibilityContentByPlanIdsDocument,
'\n query Offering($id: String!, $locale: String!) {\n offering(id: $id, locale: $locale) {\n stripeProductId\n countries\n defaultPurchase {\n purchaseDetails {\n productName\n details\n subtitle\n webIcon\n }\n }\n }\n }\n': '\n query Offering($id: String!, $locale: String!) {\n offering(id: $id, locale: $locale) {\n stripeProductId\n countries\n defaultPurchase {\n purchaseDetails {\n productName\n details\n subtitle\n webIcon\n }\n }\n }\n }\n':
types.OfferingDocument, types.OfferingDocument,
'\n query PurchaseWithDetails($id: String!, $locale: String!) {\n purchase(id: $id, locale: $locale) {\n internalName\n description\n purchaseDetails {\n productName\n details\n webIcon\n }\n }\n }\n':
types.PurchaseWithDetailsDocument,
'\n query PurchaseWithDetailsOfferingContent(\n $skip: Int!\n $limit: Int!\n $locale: String!\n $stripePlanIds: [String]!\n ) {\n purchaseCollection(\n skip: $skip\n limit: $limit\n locale: $locale\n where: { stripePlanChoices_contains_some: $stripePlanIds }\n ) {\n items {\n stripePlanChoices\n purchaseDetails {\n details\n productName\n subtitle\n webIcon\n }\n offering {\n stripeProductId\n commonContent {\n privacyNoticeUrl\n privacyNoticeDownloadUrl\n termsOfServiceUrl\n termsOfServiceDownloadUrl\n cancellationUrl\n emailIcon\n successActionButtonUrl\n successActionButtonLabel\n }\n }\n }\n }\n }\n': '\n query PurchaseWithDetailsOfferingContent(\n $skip: Int!\n $limit: Int!\n $locale: String!\n $stripePlanIds: [String]!\n ) {\n purchaseCollection(\n skip: $skip\n limit: $limit\n locale: $locale\n where: { stripePlanChoices_contains_some: $stripePlanIds }\n ) {\n items {\n stripePlanChoices\n purchaseDetails {\n details\n productName\n subtitle\n webIcon\n }\n offering {\n stripeProductId\n commonContent {\n privacyNoticeUrl\n privacyNoticeDownloadUrl\n termsOfServiceUrl\n termsOfServiceDownloadUrl\n cancellationUrl\n emailIcon\n successActionButtonUrl\n successActionButtonLabel\n }\n }\n }\n }\n }\n':
types.PurchaseWithDetailsOfferingContentDocument, types.PurchaseWithDetailsOfferingContentDocument,
'\n query PurchaseWithDetails($id: String!, $locale: String!) {\n purchase(id: $id, locale: $locale) {\n internalName\n description\n purchaseDetails {\n productName\n details\n webIcon\n }\n }\n }\n':
types.PurchaseWithDetailsDocument,
'\n query ServicesWithCapabilities($skip: Int!, $limit: Int!, $locale: String!) {\n serviceCollection(skip: $skip, limit: $limit, locale: $locale) {\n items {\n oauthClientId\n capabilitiesCollection(skip: $skip, limit: $limit) {\n items {\n slug\n }\n }\n }\n }\n }\n': '\n query ServicesWithCapabilities($skip: Int!, $limit: Int!, $locale: String!) {\n serviceCollection(skip: $skip, limit: $limit, locale: $locale) {\n items {\n oauthClientId\n capabilitiesCollection(skip: $skip, limit: $limit) {\n items {\n slug\n }\n }\n }\n }\n }\n':
types.ServicesWithCapabilitiesDocument, types.ServicesWithCapabilitiesDocument,
}; };
@ -63,14 +63,14 @@ export function graphql(
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
export function graphql( export function graphql(
source: '\n query PurchaseWithDetails($id: String!, $locale: String!) {\n purchase(id: $id, locale: $locale) {\n internalName\n description\n purchaseDetails {\n productName\n details\n webIcon\n }\n }\n }\n' source: '\n query PurchaseWithDetailsOfferingContent(\n $skip: Int!\n $limit: Int!\n $locale: String!\n $stripePlanIds: [String]!\n ) {\n purchaseCollection(\n skip: $skip\n limit: $limit\n locale: $locale\n where: { stripePlanChoices_contains_some: $stripePlanIds }\n ) {\n items {\n stripePlanChoices\n purchaseDetails {\n details\n productName\n subtitle\n webIcon\n }\n offering {\n stripeProductId\n commonContent {\n privacyNoticeUrl\n privacyNoticeDownloadUrl\n termsOfServiceUrl\n termsOfServiceDownloadUrl\n cancellationUrl\n emailIcon\n successActionButtonUrl\n successActionButtonLabel\n }\n }\n }\n }\n }\n'
): (typeof documents)['\n query PurchaseWithDetails($id: String!, $locale: String!) {\n purchase(id: $id, locale: $locale) {\n internalName\n description\n purchaseDetails {\n productName\n details\n webIcon\n }\n }\n }\n']; ): (typeof documents)['\n query PurchaseWithDetailsOfferingContent(\n $skip: Int!\n $limit: Int!\n $locale: String!\n $stripePlanIds: [String]!\n ) {\n purchaseCollection(\n skip: $skip\n limit: $limit\n locale: $locale\n where: { stripePlanChoices_contains_some: $stripePlanIds }\n ) {\n items {\n stripePlanChoices\n purchaseDetails {\n details\n productName\n subtitle\n webIcon\n }\n offering {\n stripeProductId\n commonContent {\n privacyNoticeUrl\n privacyNoticeDownloadUrl\n termsOfServiceUrl\n termsOfServiceDownloadUrl\n cancellationUrl\n emailIcon\n successActionButtonUrl\n successActionButtonLabel\n }\n }\n }\n }\n }\n'];
/** /**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
export function graphql( export function graphql(
source: '\n query PurchaseWithDetailsOfferingContent(\n $skip: Int!\n $limit: Int!\n $locale: String!\n $stripePlanIds: [String]!\n ) {\n purchaseCollection(\n skip: $skip\n limit: $limit\n locale: $locale\n where: { stripePlanChoices_contains_some: $stripePlanIds }\n ) {\n items {\n stripePlanChoices\n purchaseDetails {\n details\n productName\n subtitle\n webIcon\n }\n offering {\n stripeProductId\n commonContent {\n privacyNoticeUrl\n privacyNoticeDownloadUrl\n termsOfServiceUrl\n termsOfServiceDownloadUrl\n cancellationUrl\n emailIcon\n successActionButtonUrl\n successActionButtonLabel\n }\n }\n }\n }\n }\n' source: '\n query PurchaseWithDetails($id: String!, $locale: String!) {\n purchase(id: $id, locale: $locale) {\n internalName\n description\n purchaseDetails {\n productName\n details\n webIcon\n }\n }\n }\n'
): (typeof documents)['\n query PurchaseWithDetailsOfferingContent(\n $skip: Int!\n $limit: Int!\n $locale: String!\n $stripePlanIds: [String]!\n ) {\n purchaseCollection(\n skip: $skip\n limit: $limit\n locale: $locale\n where: { stripePlanChoices_contains_some: $stripePlanIds }\n ) {\n items {\n stripePlanChoices\n purchaseDetails {\n details\n productName\n subtitle\n webIcon\n }\n offering {\n stripeProductId\n commonContent {\n privacyNoticeUrl\n privacyNoticeDownloadUrl\n termsOfServiceUrl\n termsOfServiceDownloadUrl\n cancellationUrl\n emailIcon\n successActionButtonUrl\n successActionButtonLabel\n }\n }\n }\n }\n }\n']; ): (typeof documents)['\n query PurchaseWithDetails($id: String!, $locale: String!) {\n purchase(id: $id, locale: $locale) {\n internalName\n description\n purchaseDetails {\n productName\n details\n webIcon\n }\n }\n }\n'];
/** /**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */

2417
libs/shared/contentful/src/__generated__/graphql.ts сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@ -1,5 +1,9 @@
export * from './lib/contentful-client'; /* This Source Code Form is subject to the terms of the Mozilla Public
export * from './lib/queries/purchase-with-details'; * License, v. 2.0. If a copy of the MPL was not distributed with this
export * from './lib/queries/offering'; * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
export * from './lib/errors';
export * from './lib/contentful.client';
export * from './lib/contentful.manager';
export * from './lib/contentful.error';
export * from './lib/factories'; export * from './lib/factories';
export * from './lib/queries/eligibility-content-by-plan-ids';

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

@ -0,0 +1,9 @@
export const mockQuery = jest
.fn()
.mockResolvedValue({ data: { purchaseCollection: { items: [] } } });
const mock = jest.fn().mockImplementation(() => {
return {
query: mockQuery,
};
});
export const ContentfulClient = mock;

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

@ -4,14 +4,14 @@
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import { ApolloQueryResult } from '@apollo/client'; import { ApolloQueryResult } from '@apollo/client';
import { ContentfulClient } from './contentful-client'; import { ContentfulClient } from './contentful.client';
import { offeringQuery } from './queries/offering'; import { offeringQuery } from './queries/offering';
import { OfferingQuery } from '../__generated__/graphql'; import { OfferingQuery } from '../__generated__/graphql';
import { import {
ContentfulError, ContentfulError,
ContentfulLinkError, ContentfulLinkError,
ContentfulLocaleError, ContentfulLocaleError,
} from './errors'; } from './contentful.error';
const ApolloError = jest.requireActual('@apollo/client').ApolloError; const ApolloError = jest.requireActual('@apollo/client').ApolloError;

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

@ -15,9 +15,9 @@ import {
ContentfulExecutionError, ContentfulExecutionError,
ContentfulLinkError, ContentfulLinkError,
ContentfulLocaleError, ContentfulLocaleError,
} from './errors'; } from './contentful.error';
import { BaseError } from '@fxa/shared/error'; import { BaseError } from '@fxa/shared/error';
import { ContentfulClientConfig } from './contentful-client.config'; import { ContentfulClientConfig } from './contentful.client.config';
@Injectable() @Injectable()
export class ContentfulClient { export class ContentfulClient {
@ -31,7 +31,7 @@ export class ContentfulClient {
async query<Result, Variables>( async query<Result, Variables>(
query: TypedDocumentNode<Result, Variables>, query: TypedDocumentNode<Result, Variables>,
variables: Variables variables: Variables
): Promise<ApolloQueryResult<Result> | null> { ): Promise<ApolloQueryResult<Result>> {
try { try {
const response = await this.client.query<Result, Variables>({ const response = await this.client.query<Result, Variables>({
query, query,

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

@ -0,0 +1,55 @@
/* 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 { Test, TestingModule } from '@nestjs/testing';
import { ContentfulClient } from './contentful.client';
import { ContentfulManager } from './contentful.manager';
import { EligibilityContentByPlanIdsQueryFactory } from './factories';
jest.mock('./contentful.client');
describe('ContentfulManager', () => {
let manager: ContentfulManager;
let mockClient: ContentfulClient;
beforeEach(async () => {
(ContentfulClient as jest.Mock).mockClear();
mockClient = new ContentfulClient({
graphqlApiKey: 'test',
graphqlApiUri: 'test',
graphqlEnvironment: 'test',
graphqlSpaceId: 'test',
});
const module: TestingModule = await Test.createTestingModule({
providers: [
{ provide: ContentfulClient, useValue: mockClient },
ContentfulManager,
],
}).compile();
manager = module.get<ContentfulManager>(ContentfulManager);
});
it('should be defined', () => {
expect(manager).toBeDefined();
});
describe('getPurchaseDetailsForEligibility', () => {
it('should return empty result', async () => {
const result = await manager.getPurchaseDetailsForEligibility(['test']);
expect(result).toBeDefined();
expect(result.purchaseCollection.items).toHaveLength(0);
});
it('should return successfully with subgroups and offering', async () => {
const queryData = EligibilityContentByPlanIdsQueryFactory();
mockClient.query = jest.fn().mockResolvedValueOnce({ data: queryData });
const result = await manager.getPurchaseDetailsForEligibility(['test']);
const planId = result.purchaseCollection.items[0].stripePlanChoices[0];
expect(result).toBeDefined();
expect(result.getSubgroupsForPlanId(planId)).toHaveLength(1);
expect(result.getOfferingForPlanId(planId)).toBeDefined();
});
});
});

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

@ -0,0 +1,36 @@
/* 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 { Injectable } from '@nestjs/common';
import { EligibilityContentByPlanIdsQuery } from '../__generated__/graphql';
import { ContentfulClient } from './contentful.client';
import {
eligibilityContentByPlanIdsQuery,
EligibilityContentByPlanIdsResultUtil,
} from './queries/eligibility-content-by-plan-ids';
import { DeepNonNullable } from './types';
@Injectable()
export class ContentfulManager {
constructor(private client: ContentfulClient) {}
async getPurchaseDetailsForEligibility(
stripePlanIds: string[]
): Promise<EligibilityContentByPlanIdsResultUtil> {
const queryResult = await this.client.query(
eligibilityContentByPlanIdsQuery,
{
skip: 0,
limit: 100,
locale: 'en-US',
stripePlanIds,
}
);
return new EligibilityContentByPlanIdsResultUtil(
queryResult.data as DeepNonNullable<EligibilityContentByPlanIdsQuery>
);
}
}

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

@ -6,8 +6,48 @@ import { faker } from '@faker-js/faker';
import { NetworkStatus } from '@apollo/client'; import { NetworkStatus } from '@apollo/client';
import { ApolloQueryResult } from '@apollo/client'; import { ApolloQueryResult } from '@apollo/client';
import { TypedDocumentNode } from '@graphql-typed-document-node/core'; import { TypedDocumentNode } from '@graphql-typed-document-node/core';
import { OfferingQuery } from '../__generated__/graphql'; import {
import { PurchaseWithDetailsQuery } from '../__generated__/graphql'; OfferingQuery,
PurchaseWithDetailsQuery,
EligibilityContentByPlanIdsQuery,
} from '../__generated__/graphql';
export const EligibilityContentByPlanIdsQueryFactory = (
override?: Partial<EligibilityContentByPlanIdsQuery>
): EligibilityContentByPlanIdsQuery => {
const stripeProductId = faker.string.sample();
return {
purchaseCollection: {
items: [
{
stripePlanChoices: [faker.string.sample()],
offering: {
stripeProductId,
countries: [faker.string.sample()],
linkedFrom: {
subGroupCollection: {
items: [
{
groupName: faker.string.sample(),
offeringCollection: {
items: [
{
stripeProductId,
countries: [faker.string.sample()],
},
],
},
},
],
},
},
},
},
],
},
...override,
};
};
export const OfferingQueryFactory = ( export const OfferingQueryFactory = (
override?: Partial<OfferingQuery> override?: Partial<OfferingQuery>
@ -31,6 +71,7 @@ export const PurchaseWithDetailsQueryFactory = (
override?: Partial<PurchaseWithDetailsQuery> override?: Partial<PurchaseWithDetailsQuery>
): PurchaseWithDetailsQuery => ({ ): PurchaseWithDetailsQuery => ({
purchase: { purchase: {
internalName: faker.string.sample(),
description: faker.string.sample(), description: faker.string.sample(),
purchaseDetails: { purchaseDetails: {
productName: faker.string.sample(), productName: faker.string.sample(),

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

@ -0,0 +1,7 @@
/* 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 './query';
export * from './types';
export * from './util';

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

@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { graphql } from '../../__generated__/gql'; import { graphql } from '../../../__generated__/gql';
export const eligibilityContentByPlanIdsQuery = graphql(` export const eligibilityContentByPlanIdsQuery = graphql(`
query EligibilityContentByPlanIds( query EligibilityContentByPlanIds(

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

@ -0,0 +1,36 @@
/* 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 interface EligibilitySubgroupOfferingResult {
stripeProductId: string;
countries: string[];
}
export interface EligibilitySubgroupResult {
groupName: string;
offeringCollection: {
items: EligibilitySubgroupOfferingResult[];
};
}
export interface EligibilityOfferingResult {
stripeProductId: string;
countries: string[];
linkedFrom: {
subGroupCollection: {
items: EligibilitySubgroupResult[];
};
};
}
export interface EligibilityPurchaseResult {
stripePlanChoices: string[];
offering: EligibilityOfferingResult;
}
export interface EligibilityContentByPlanIdsResult {
purchaseCollection: {
items: EligibilityPurchaseResult[];
};
}

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

@ -0,0 +1,31 @@
/* 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 {
EligibilityContentByPlanIdsResult,
EligibilityOfferingResult,
EligibilitySubgroupResult,
} from './types';
export class EligibilityContentByPlanIdsResultUtil {
constructor(private rawResult: EligibilityContentByPlanIdsResult) {}
getOfferingForPlanId(planId: string): EligibilityOfferingResult | undefined {
return this.rawResult.purchaseCollection.items.find((purchase) =>
purchase.stripePlanChoices.includes(planId)
)?.offering;
}
getSubgroupsForPlanId(planId: string): EligibilitySubgroupResult[] {
return (
this.rawResult.purchaseCollection.items.find((purchase) =>
purchase.stripePlanChoices.includes(planId)
)?.offering.linkedFrom.subGroupCollection.items || []
);
}
get purchaseCollection() {
return this.rawResult.purchaseCollection;
}
}

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

@ -0,0 +1,3 @@
export type DeepNonNullable<T> = {
[K in keyof T]: Exclude<DeepNonNullable<T[K]>, null | undefined>;
};

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

@ -3,7 +3,7 @@
"compilerOptions": { "compilerOptions": {
"outDir": "../../../dist/out-tsc", "outDir": "../../../dist/out-tsc",
"declaration": true, "declaration": true,
"types": ["node"] "types": ["jest", "node"]
}, },
"include": ["src/**/*.ts"], "include": ["src/**/*.ts"],
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]

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

@ -9,6 +9,7 @@
"jest.config.ts", "jest.config.ts",
"src/**/*.test.ts", "src/**/*.test.ts",
"src/**/*.spec.ts", "src/**/*.spec.ts",
"src/**/*.d.ts" "src/**/*.d.ts",
"src/**/__mocks__/*.ts"
] ]
} }