зеркало из https://github.com/mozilla/fxa.git
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:
Коммит
19e9c028b1
|
@ -9,10 +9,16 @@ const CONTENTFUL_GRAPHQL_ENVIRONMENT =
|
|||
const config: CodegenConfig = {
|
||||
overwrite: true,
|
||||
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: {
|
||||
'libs/shared/contentful/src/__generated__/': {
|
||||
preset: 'client',
|
||||
config: {
|
||||
avoidOptionals: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"test-unit": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
|
|
|
@ -19,10 +19,10 @@ const documents = {
|
|||
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':
|
||||
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':
|
||||
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':
|
||||
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.
|
||||
*/
|
||||
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'
|
||||
): (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'];
|
||||
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 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.
|
||||
*/
|
||||
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'
|
||||
): (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'];
|
||||
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 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.
|
||||
*/
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -1,5 +1,9 @@
|
|||
export * from './lib/contentful-client';
|
||||
export * from './lib/queries/purchase-with-details';
|
||||
export * from './lib/queries/offering';
|
||||
export * from './lib/errors';
|
||||
/* 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/contentful.client';
|
||||
export * from './lib/contentful.manager';
|
||||
export * from './lib/contentful.error';
|
||||
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 { ApolloQueryResult } from '@apollo/client';
|
||||
import { ContentfulClient } from './contentful-client';
|
||||
import { ContentfulClient } from './contentful.client';
|
||||
import { offeringQuery } from './queries/offering';
|
||||
import { OfferingQuery } from '../__generated__/graphql';
|
||||
import {
|
||||
ContentfulError,
|
||||
ContentfulLinkError,
|
||||
ContentfulLocaleError,
|
||||
} from './errors';
|
||||
} from './contentful.error';
|
||||
|
||||
const ApolloError = jest.requireActual('@apollo/client').ApolloError;
|
||||
|
|
@ -15,9 +15,9 @@ import {
|
|||
ContentfulExecutionError,
|
||||
ContentfulLinkError,
|
||||
ContentfulLocaleError,
|
||||
} from './errors';
|
||||
} from './contentful.error';
|
||||
import { BaseError } from '@fxa/shared/error';
|
||||
import { ContentfulClientConfig } from './contentful-client.config';
|
||||
import { ContentfulClientConfig } from './contentful.client.config';
|
||||
|
||||
@Injectable()
|
||||
export class ContentfulClient {
|
||||
|
@ -31,7 +31,7 @@ export class ContentfulClient {
|
|||
async query<Result, Variables>(
|
||||
query: TypedDocumentNode<Result, Variables>,
|
||||
variables: Variables
|
||||
): Promise<ApolloQueryResult<Result> | null> {
|
||||
): Promise<ApolloQueryResult<Result>> {
|
||||
try {
|
||||
const response = await this.client.query<Result, Variables>({
|
||||
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 { ApolloQueryResult } from '@apollo/client';
|
||||
import { TypedDocumentNode } from '@graphql-typed-document-node/core';
|
||||
import { OfferingQuery } from '../__generated__/graphql';
|
||||
import { PurchaseWithDetailsQuery } from '../__generated__/graphql';
|
||||
import {
|
||||
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 = (
|
||||
override?: Partial<OfferingQuery>
|
||||
|
@ -31,6 +71,7 @@ export const PurchaseWithDetailsQueryFactory = (
|
|||
override?: Partial<PurchaseWithDetailsQuery>
|
||||
): PurchaseWithDetailsQuery => ({
|
||||
purchase: {
|
||||
internalName: faker.string.sample(),
|
||||
description: faker.string.sample(),
|
||||
purchaseDetails: {
|
||||
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
|
||||
* 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(`
|
||||
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": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"types": ["node"]
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
"src/**/*.d.ts",
|
||||
"src/**/__mocks__/*.ts"
|
||||
]
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче