3183 get locale data for categories (#143)
This commit is contained in:
@@ -13,6 +13,7 @@ export enum Scopes {
|
|||||||
PRODUCTS_PUBLIC_READ = 'products.public.read',
|
PRODUCTS_PUBLIC_READ = 'products.public.read',
|
||||||
PRODUCTS_WRITE = 'products.write',
|
PRODUCTS_WRITE = 'products.write',
|
||||||
CATEGORIES_READ = 'categories.read',
|
CATEGORIES_READ = 'categories.read',
|
||||||
|
CATEGORIES_PUBLIC_READ = 'categories.public.read',
|
||||||
CATEGORIES_WRITE = 'categories.write',
|
CATEGORIES_WRITE = 'categories.write',
|
||||||
MARKETS_READ = 'markets.read',
|
MARKETS_READ = 'markets.read',
|
||||||
KEYWORDS_READ = 'keywords.read',
|
KEYWORDS_READ = 'keywords.read',
|
||||||
|
|||||||
+111
-22
@@ -1,12 +1,47 @@
|
|||||||
|
import DataLoader from 'dataloader';
|
||||||
import { ScopeAccess, Scopes } from '../cognito/access-control';
|
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 { DataSourceOptions } from '../types/types';
|
||||||
import { BaseSQLDataSource } from './BaseSQLDataSource';
|
import { BaseSQLDataSource } from './BaseSQLDataSource';
|
||||||
import { MINUTE } from './utils';
|
import { createQuestionMarksFromList, MINUTE } from './utils';
|
||||||
|
|
||||||
export class CategoryAPI extends BaseSQLDataSource {
|
export class CategoryAPI extends BaseSQLDataSource {
|
||||||
|
private categoryLoader: DataLoader<number, Category>;
|
||||||
|
private localeDataLoader: DataLoader<number, CategoryLocaleData[]>;
|
||||||
|
private localeNamesLoader: DataLoader<number, JSON>;
|
||||||
|
|
||||||
constructor(options: DataSourceOptions, config) {
|
constructor(options: DataSourceOptions, config) {
|
||||||
super(options, 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<Array<Category>> {
|
async getProductCategories(productId: number): Promise<Array<Category>> {
|
||||||
@@ -30,10 +65,30 @@ WHERE product_category.product_id = ?
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCategories(): Promise<Array<Category>> {
|
async getCategoryById(id: number): Promise<Category> {
|
||||||
ScopeAccess.validate(this.user).all([Scopes.CATEGORIES_READ]);
|
// Dataloaders does not need scopes validation since they
|
||||||
// @ts-ignore
|
// use other functions.
|
||||||
return this.knex.select('*').from('v_categorytree').cache(MINUTE);
|
return this.categoryLoader.load(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCategories(ids: number[]): Promise<Array<Category>> {
|
||||||
|
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<Array<Category>> {
|
async searchCategories(name: string): Promise<Array<Category>> {
|
||||||
@@ -44,29 +99,63 @@ WHERE product_category.product_id = ?
|
|||||||
.whereRaw(`LOWER(name) LIKE ?`, [`${name}`]);
|
.whereRaw(`LOWER(name) LIKE ?`, [`${name}`]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCategoryById(id: number): Promise<Category> {
|
async getLocaleNameForId(id: number): Promise<JSON> {
|
||||||
ScopeAccess.validate(this.user).all([Scopes.CATEGORIES_READ]);
|
// Dataloaders does not need scopes validation since they
|
||||||
return (
|
// use other functions.
|
||||||
this.knex
|
return this.localeNamesLoader.load(id);
|
||||||
.select('*')
|
|
||||||
.from('v_categorytree')
|
|
||||||
.where('id', id)
|
|
||||||
.first()
|
|
||||||
// @ts-ignore
|
|
||||||
.cache(MINUTE)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLocaleNameForId(id: number): Promise<JSON> {
|
async getLocaleNameForIds(
|
||||||
ScopeAccess.validate(this.user).all([Scopes.LOCALES_READ]);
|
ids: number[],
|
||||||
|
): Promise<{ id: number; locale_names: JSON }[]> {
|
||||||
|
ScopeAccess.validate(this.user).all([
|
||||||
|
Scopes.CATEGORIES_READ,
|
||||||
|
Scopes.LOCALES_READ,
|
||||||
|
]);
|
||||||
const res = await this.cachedRaw(
|
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 = ?',
|
`SELECT id, json_object_agg(locale, name) as locale_names FROM v_esales_categorytree_i18n WHERE id IN (${createQuestionMarksFromList(
|
||||||
[id],
|
ids,
|
||||||
|
)}) GROUP BY id`,
|
||||||
|
ids,
|
||||||
)
|
)
|
||||||
.cache(MINUTE * 5)
|
.cache(MINUTE * 5)
|
||||||
.then((data) => data.rows);
|
.then((data) => data.rows);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
return res[0].locale_names;
|
async getLocaleDataForId(
|
||||||
|
id: number,
|
||||||
|
locale?: string,
|
||||||
|
): Promise<CategoryLocaleData[]> {
|
||||||
|
// 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<CategoryLocaleData[]> {
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -14,10 +14,16 @@ const Category = {
|
|||||||
async localeNames({ id }, _, { dataSources }) {
|
async localeNames({ id }, _, { dataSources }) {
|
||||||
return (<CategoryAPI>dataSources.categoryApi).getLocaleNameForId(id);
|
return (<CategoryAPI>dataSources.categoryApi).getLocaleNameForId(id);
|
||||||
},
|
},
|
||||||
|
async localeData({ id }, input: { locale: string }, { dataSources }) {
|
||||||
|
return (<CategoryAPI>dataSources.categoryApi).getLocaleDataForId(
|
||||||
|
id,
|
||||||
|
input.locale,
|
||||||
|
);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getCategories(_, _args, { dataSources }) {
|
async function getCategories(_, { ids }, { dataSources }) {
|
||||||
return (<CategoryAPI>dataSources.categoryApi).getCategories();
|
return (<CategoryAPI>dataSources.categoryApi).getCategories(ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getCategory(_, { id }, { dataSources }) {
|
async function getCategory(_, { id }, { dataSources }) {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const getClientAccessToken = async (req) => {
|
|||||||
data: {
|
data: {
|
||||||
grant_type: 'client_credentials',
|
grant_type: 'client_credentials',
|
||||||
client_id: clientId,
|
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}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -27,7 +27,7 @@ type Query {
|
|||||||
product(id: Int!): Product
|
product(id: Int!): Product
|
||||||
productByPath(path: String!): Product
|
productByPath(path: String!): Product
|
||||||
productsSearch(q: String!): ProductsSearchResult
|
productsSearch(q: String!): ProductsSearchResult
|
||||||
categories: [Category]
|
categories(ids: [Int]): [Category]
|
||||||
category(id: Int!): Category
|
category(id: Int!): Category
|
||||||
keywords: [Keyword]
|
keywords: [Keyword]
|
||||||
keyword(id: Int!): Keyword
|
keyword(id: Int!): Keyword
|
||||||
@@ -207,6 +207,10 @@ type OrderRowData {
|
|||||||
pwintySku: String
|
pwintySku: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CategoryLocaleData {
|
||||||
|
path: String, name: String, locale: String
|
||||||
|
}
|
||||||
|
|
||||||
type Category {
|
type Category {
|
||||||
id: ID!
|
id: ID!
|
||||||
name: String
|
name: String
|
||||||
@@ -222,6 +226,7 @@ type Category {
|
|||||||
"""
|
"""
|
||||||
products(includeAllSubCategories: Boolean): [Product]
|
products(includeAllSubCategories: Boolean): [Product]
|
||||||
localeNames: JSON
|
localeNames: JSON
|
||||||
|
localeData(locale: String): [CategoryLocaleData]!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Batch {
|
type Batch {
|
||||||
|
|||||||
@@ -9,4 +9,12 @@ export interface Category {
|
|||||||
lft: number;
|
lft: number;
|
||||||
rgt: number;
|
rgt: number;
|
||||||
localeNames: Array<Record<string, string>>;
|
localeNames: Array<Record<string, string>>;
|
||||||
|
localeData: CategoryLocaleData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CategoryLocaleData {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
locale: string;
|
||||||
|
id: number;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user