diff --git a/src/cognito/access-control.ts b/src/cognito/access-control.ts index 9c218de..c0c7176 100644 --- a/src/cognito/access-control.ts +++ b/src/cognito/access-control.ts @@ -13,6 +13,7 @@ export enum Scopes { PRODUCTS_PUBLIC_READ = 'products.public.read', PRODUCTS_WRITE = 'products.write', CATEGORIES_READ = 'categories.read', + CATEGORIES_PUBLIC_READ = 'categories.public.read', CATEGORIES_WRITE = 'categories.write', MARKETS_READ = 'markets.read', KEYWORDS_READ = 'keywords.read', diff --git a/src/datasources/category-api.ts b/src/datasources/category-api.ts index c4cae4e..c29a019 100644 --- a/src/datasources/category-api.ts +++ b/src/datasources/category-api.ts @@ -1,12 +1,47 @@ +import DataLoader from 'dataloader'; import { ScopeAccess, Scopes } from '../cognito/access-control'; -import { Category } from '../types/category-types'; +import { Category, CategoryLocaleData } from '../types/category-types'; import { DataSourceOptions } from '../types/types'; import { BaseSQLDataSource } from './BaseSQLDataSource'; -import { MINUTE } from './utils'; +import { createQuestionMarksFromList, MINUTE } from './utils'; export class CategoryAPI extends BaseSQLDataSource { + private categoryLoader: DataLoader; + private localeDataLoader: DataLoader; + private localeNamesLoader: DataLoader; + constructor(options: DataSourceOptions, config) { super(options, config); + this.categoryLoader = new DataLoader(async (keys: number[]) => { + const categoriesResult = await this.getCategories(keys); + return keys.map( + (key) => + categoriesResult.find( + (categoryResult) => categoryResult.id === key, + ) ?? null, + ); + }); + + this.localeDataLoader = new DataLoader(async (keys: number[]) => { + const localeDataResults = await this.getLocaleDataForIds(keys); + return keys.map( + (key) => + localeDataResults.filter( + (localeDataResult) => localeDataResult.id === key, + ) ?? null, + ); + }); + + this.localeNamesLoader = new DataLoader(async (keys: number[]) => { + const localeNameResults = await this.getLocaleNameForIds(keys); + return keys.map((key) => { + const row = + localeNameResults.find( + (localeDataResult) => localeDataResult.id === key, + ) ?? null; + return row?.locale_names; + }); + }); } async getProductCategories(productId: number): Promise> { @@ -30,10 +65,30 @@ WHERE product_category.product_id = ? return res; } - async getCategories(): Promise> { - ScopeAccess.validate(this.user).all([Scopes.CATEGORIES_READ]); - // @ts-ignore - return this.knex.select('*').from('v_categorytree').cache(MINUTE); + async getCategoryById(id: number): Promise { + // Dataloaders does not need scopes validation since they + // use other functions. + return this.categoryLoader.load(id); + } + + async getCategories(ids: number[]): Promise> { + ScopeAccess.validate(this.user).some([ + Scopes.CATEGORIES_READ, + Scopes.CATEGORIES_PUBLIC_READ, + ]); + + return ( + this.knex + .select('*') + .modify((queryBuilder) => { + if (ids) { + queryBuilder.whereIn('id', ids); + } + }) + .from('v_categorytree') + // @ts-ignore + .cache(MINUTE) + ); } async searchCategories(name: string): Promise> { @@ -44,29 +99,63 @@ WHERE product_category.product_id = ? .whereRaw(`LOWER(name) LIKE ?`, [`${name}`]); } - async getCategoryById(id: number): Promise { - ScopeAccess.validate(this.user).all([Scopes.CATEGORIES_READ]); - return ( - this.knex - .select('*') - .from('v_categorytree') - .where('id', id) - .first() - // @ts-ignore - .cache(MINUTE) - ); + async getLocaleNameForId(id: number): Promise { + // Dataloaders does not need scopes validation since they + // use other functions. + return this.localeNamesLoader.load(id); } - async getLocaleNameForId(id: number): Promise { - ScopeAccess.validate(this.user).all([Scopes.LOCALES_READ]); + async getLocaleNameForIds( + ids: number[], + ): Promise<{ id: number; locale_names: JSON }[]> { + ScopeAccess.validate(this.user).all([ + Scopes.CATEGORIES_READ, + Scopes.LOCALES_READ, + ]); const res = await this.cachedRaw( - 'SELECT json_object_agg(ec.locale, ec.name) as locale_names FROM v_esales_categorytree_i18n ec WHERE ec.id = ?', - [id], + `SELECT id, json_object_agg(locale, name) as locale_names FROM v_esales_categorytree_i18n WHERE id IN (${createQuestionMarksFromList( + ids, + )}) GROUP BY id`, + ids, ) .cache(MINUTE * 5) .then((data) => data.rows); + return res; + } - return res[0].locale_names; + async getLocaleDataForId( + id: number, + locale?: string, + ): Promise { + // Dataloaders does not need scopes validation since they + // use other functions. + const res = await this.localeDataLoader.load(id); + if (locale) { + return res.filter((row) => row.locale === locale); + } + return res; + } + + async getLocaleDataForIds( + ids: number[], + locale?: string, + ): Promise { + ScopeAccess.validate(this.user).some([ + Scopes.LOCALES_READ, + Scopes.CATEGORIES_PUBLIC_READ, + ]); + const res = await this.cachedRaw( + `SELECT ec.id, ec.locale, getSafeXmlPath(ec.path, 2) as path, ec.name FROM v_esales_categorytree_i18n ec WHERE ec.id IN (${createQuestionMarksFromList( + ids, + )})`, + ids, + ) + .cache(MINUTE * 5) + .then((data) => data.rows); + if (locale) { + return res.filter((row) => row.locale === locale); + } + return res; } /** diff --git a/src/resolvers/categories-resolver.ts b/src/resolvers/categories-resolver.ts index d46b40c..8e7e905 100644 --- a/src/resolvers/categories-resolver.ts +++ b/src/resolvers/categories-resolver.ts @@ -14,10 +14,16 @@ const Category = { async localeNames({ id }, _, { dataSources }) { return (dataSources.categoryApi).getLocaleNameForId(id); }, + async localeData({ id }, input: { locale: string }, { dataSources }) { + return (dataSources.categoryApi).getLocaleDataForId( + id, + input.locale, + ); + }, }; -async function getCategories(_, _args, { dataSources }) { - return (dataSources.categoryApi).getCategories(); +async function getCategories(_, { ids }, { dataSources }) { + return (dataSources.categoryApi).getCategories(ids); } async function getCategory(_, { id }, { dataSources }) { diff --git a/src/rest/cognitoService.ts b/src/rest/cognitoService.ts index 9b6f8ef..5850039 100644 --- a/src/rest/cognitoService.ts +++ b/src/rest/cognitoService.ts @@ -29,7 +29,7 @@ export const getClientAccessToken = async (req) => { data: { grant_type: 'client_credentials', client_id: clientId, - scope: `https://graphql.photowall.com/${Scopes.PRODUCTS_PUBLIC_READ} https://graphql.photowall.com/${Scopes.DESIGNERS_PUBLIC_READ}`, + scope: `https://graphql.photowall.com/${Scopes.PRODUCTS_PUBLIC_READ} https://graphql.photowall.com/${Scopes.DESIGNERS_PUBLIC_READ} https://graphql.photowall.com/${Scopes.CATEGORIES_PUBLIC_READ}`, }, }); diff --git a/src/schema.graphql b/src/schema.graphql index 3144efa..a120351 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -27,7 +27,7 @@ type Query { product(id: Int!): Product productByPath(path: String!): Product productsSearch(q: String!): ProductsSearchResult - categories: [Category] + categories(ids: [Int]): [Category] category(id: Int!): Category keywords: [Keyword] keyword(id: Int!): Keyword @@ -207,6 +207,10 @@ type OrderRowData { pwintySku: String } +type CategoryLocaleData { + path: String, name: String, locale: String +} + type Category { id: ID! name: String @@ -222,6 +226,7 @@ type Category { """ products(includeAllSubCategories: Boolean): [Product] localeNames: JSON + localeData(locale: String): [CategoryLocaleData]! } type Batch { diff --git a/src/types/category-types.ts b/src/types/category-types.ts index 8909213..3204f2b 100644 --- a/src/types/category-types.ts +++ b/src/types/category-types.ts @@ -9,4 +9,12 @@ export interface Category { lft: number; rgt: number; localeNames: Array>; + localeData: CategoryLocaleData[]; +} + +export interface CategoryLocaleData { + name: string; + path: string; + locale: string; + id: number; }