Add collections (#172)
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
} from './cognito/cognito-client';
|
||||
import { dbConfig } from './config';
|
||||
import { CategoryAPI } from './datasources/category-api';
|
||||
import { CollectionAPI } from './datasources/collection-api';
|
||||
import { DesignerAPI } from './datasources/designer-api';
|
||||
import { ImageServerApi } from './datasources/imageserver-api';
|
||||
import { InteriorAPI } from './datasources/interior-api';
|
||||
@@ -37,6 +38,7 @@ export class ContextValue {
|
||||
keywordApi: KeywordAPI;
|
||||
orderApi: OrderAPI;
|
||||
productApi: ProductAPI;
|
||||
collectionApi: CollectionAPI;
|
||||
};
|
||||
|
||||
constructor({
|
||||
@@ -84,6 +86,8 @@ export class ContextValue {
|
||||
textsApi,
|
||||
printProductDefaultsApi,
|
||||
);
|
||||
const collectionApi = new CollectionAPI(options, knexConfig);
|
||||
|
||||
this.dataSources = {
|
||||
productApi,
|
||||
marketLocaleApi,
|
||||
@@ -96,6 +100,7 @@ export class ContextValue {
|
||||
printProductDefaultsApi,
|
||||
keywordApi,
|
||||
orderApi,
|
||||
collectionApi,
|
||||
};
|
||||
|
||||
return this;
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { BaseSQLDataSource } from './BaseSQLDataSource';
|
||||
import { DataSourceOptions, Maybe } from '../types/types';
|
||||
import { ScopeAccess, Scopes } from '../cognito/access-control';
|
||||
import { Collection } from '../types/collection-types';
|
||||
import DataLoader from 'dataloader';
|
||||
|
||||
export class CollectionAPI extends BaseSQLDataSource {
|
||||
productCollectionsLoader: DataLoader<number, Collection[]>;
|
||||
|
||||
constructor(options: DataSourceOptions, config) {
|
||||
super(options, config);
|
||||
|
||||
// DataLoader for batching product-to-collections lookups
|
||||
this.productCollectionsLoader = new DataLoader(
|
||||
async (productIds: readonly number[]) => {
|
||||
return this.batchGetProductCollections(productIds);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async getCollections(): Promise<Array<Collection>> {
|
||||
ScopeAccess.validate(this.user).some([
|
||||
Scopes.PRODUCTS_READ,
|
||||
Scopes.PRODUCTS_PUBLIC_READ,
|
||||
]);
|
||||
const query = `
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
metadata
|
||||
FROM editorial_pages
|
||||
WHERE type = 'collection' AND deleted IS NULL
|
||||
ORDER BY name
|
||||
`;
|
||||
const collections = await this.knex.raw(query).then((data) => {
|
||||
return data.rows.map((row) => this.createCollectionFromRow(row));
|
||||
});
|
||||
return collections;
|
||||
}
|
||||
|
||||
async getCollection(id: number): Promise<Maybe<Collection>> {
|
||||
ScopeAccess.validate(this.user).some([
|
||||
Scopes.PRODUCTS_READ,
|
||||
Scopes.PRODUCTS_PUBLIC_READ,
|
||||
]);
|
||||
const query = `
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
metadata
|
||||
FROM editorial_pages
|
||||
WHERE id = ? AND type = 'collection' AND deleted IS NULL
|
||||
`;
|
||||
const collection = await this.knex.raw(query, [id]).then((data) => {
|
||||
if (data.rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return this.createCollectionFromRow(data.rows[0]);
|
||||
});
|
||||
return collection;
|
||||
}
|
||||
|
||||
createCollectionFromRow(row: any): Collection {
|
||||
let productIds: number[] = [];
|
||||
if (row.metadata && Array.isArray(row.metadata.collection_products)) {
|
||||
productIds = row.metadata.collection_products.map((id: string | number) =>
|
||||
Number(id),
|
||||
);
|
||||
}
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
productIds,
|
||||
};
|
||||
}
|
||||
|
||||
async getProductCollections(productId: number): Promise<Collection[]> {
|
||||
ScopeAccess.validate(this.user).some([
|
||||
Scopes.PRODUCTS_READ,
|
||||
Scopes.PRODUCTS_PUBLIC_READ,
|
||||
]);
|
||||
return this.productCollectionsLoader.load(productId);
|
||||
}
|
||||
|
||||
private async batchGetProductCollections(
|
||||
productIds: readonly number[],
|
||||
): Promise<Collection[][]> {
|
||||
const productIdsStr = productIds.map(String);
|
||||
const query = `
|
||||
WITH collection_products AS (
|
||||
SELECT
|
||||
e.id,
|
||||
e.name,
|
||||
e.metadata,
|
||||
jsonb_array_elements_text(e.metadata->'collection_products') AS product_id
|
||||
FROM editorial_pages e
|
||||
WHERE e.type = 'collection'
|
||||
AND e.deleted IS NULL
|
||||
AND e.metadata->'collection_products' IS NOT NULL
|
||||
)
|
||||
SELECT * FROM collection_products
|
||||
WHERE product_id = ANY (?)
|
||||
`;
|
||||
const results = await this.knex.raw(query, [productIdsStr]);
|
||||
// Group collections by product ID
|
||||
const collectionsByProduct: { [key: string]: Collection[] } = {};
|
||||
productIdsStr.forEach((id) => {
|
||||
collectionsByProduct[id] = [];
|
||||
});
|
||||
results.rows.forEach((row) => {
|
||||
const productId = row.product_id;
|
||||
if (!collectionsByProduct[productId])
|
||||
collectionsByProduct[productId] = [];
|
||||
collectionsByProduct[productId].push(this.createCollectionFromRow(row));
|
||||
});
|
||||
// Return collections in the same order as the input productIds
|
||||
return productIdsStr.map((id) => collectionsByProduct[id] || []);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Collection } from '../types/collection-types';
|
||||
import { CollectionAPI } from '../datasources/collection-api';
|
||||
|
||||
async function getCollections(
|
||||
_,
|
||||
_args,
|
||||
{ dataSources },
|
||||
): Promise<Collection[]> {
|
||||
return (<CollectionAPI>dataSources.collectionApi).getCollections();
|
||||
}
|
||||
|
||||
async function getCollection(_, { id }, { dataSources }): Promise<Collection> {
|
||||
return (<CollectionAPI>dataSources.collectionApi).getCollection(id);
|
||||
}
|
||||
|
||||
export const collectionResolvers = {
|
||||
Query: {
|
||||
collections: getCollections,
|
||||
collection: getCollection,
|
||||
},
|
||||
};
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
marketsQueryTypeDefs,
|
||||
marketTypeDefs,
|
||||
} from './markets-locale-resolver';
|
||||
import { collectionResolvers } from './collections-resolver';
|
||||
|
||||
import { DateTimeResolver, DateResolver, JSONResolver } from 'graphql-scalars';
|
||||
import { interiorsQueryTypeDefs, interiorTypeDefs } from './interiors-resolver';
|
||||
@@ -32,6 +33,7 @@ const resolvers = {
|
||||
...interiorsQueryTypeDefs,
|
||||
...imagesQueryTypeDefs,
|
||||
...TextsQueryTypeDefs,
|
||||
...collectionResolvers.Query,
|
||||
},
|
||||
Mutation: {
|
||||
...productMutationTypeDefs,
|
||||
|
||||
@@ -48,6 +48,9 @@ const Product = {
|
||||
async categories({ id }, _args, { dataSources }): Promise<Array<Category>> {
|
||||
return (<CategoryAPI>dataSources.categoryApi).getProductCategories(id);
|
||||
},
|
||||
async collections({ id }, _args, { dataSources }) {
|
||||
return dataSources.collectionApi.getProductCollections(id);
|
||||
},
|
||||
async designer({ designerId }, _args, { dataSources }) {
|
||||
if (!designerId) {
|
||||
return null;
|
||||
|
||||
@@ -41,6 +41,8 @@ type Query {
|
||||
signedFileUploadURL(config: SignedUploadURLInput): SignedUploadURL
|
||||
@cacheControl(maxAge: 0)
|
||||
translate(input: TranslateInput!): StringResult!
|
||||
collections: [Collection]
|
||||
collection(id: Int!): Collection
|
||||
}
|
||||
|
||||
type HealthCheck {
|
||||
@@ -260,6 +262,7 @@ type Product {
|
||||
printProducts(groups: [ProductGroup]): [PrintProduct]
|
||||
blacklisting: [ProductBlacklist]
|
||||
categories: [Category]
|
||||
collections: [Collection]
|
||||
designerId: Int
|
||||
designer: Designer
|
||||
keywords: [Keyword]
|
||||
@@ -553,3 +556,9 @@ type Mutation {
|
||||
): StringResult
|
||||
massUpdatePublishingDate(productIds: [Int]!, date: DateTime!): StringResult
|
||||
}
|
||||
|
||||
type Collection {
|
||||
id: ID!
|
||||
name: String!
|
||||
productIds: [Int!]!
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface Collection {
|
||||
id: string | number;
|
||||
name: string;
|
||||
productIds: number[];
|
||||
}
|
||||
Reference in New Issue
Block a user