Add collections (#172)
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
|||||||
} from './cognito/cognito-client';
|
} from './cognito/cognito-client';
|
||||||
import { dbConfig } from './config';
|
import { dbConfig } from './config';
|
||||||
import { CategoryAPI } from './datasources/category-api';
|
import { CategoryAPI } from './datasources/category-api';
|
||||||
|
import { CollectionAPI } from './datasources/collection-api';
|
||||||
import { DesignerAPI } from './datasources/designer-api';
|
import { DesignerAPI } from './datasources/designer-api';
|
||||||
import { ImageServerApi } from './datasources/imageserver-api';
|
import { ImageServerApi } from './datasources/imageserver-api';
|
||||||
import { InteriorAPI } from './datasources/interior-api';
|
import { InteriorAPI } from './datasources/interior-api';
|
||||||
@@ -37,6 +38,7 @@ export class ContextValue {
|
|||||||
keywordApi: KeywordAPI;
|
keywordApi: KeywordAPI;
|
||||||
orderApi: OrderAPI;
|
orderApi: OrderAPI;
|
||||||
productApi: ProductAPI;
|
productApi: ProductAPI;
|
||||||
|
collectionApi: CollectionAPI;
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
@@ -84,6 +86,8 @@ export class ContextValue {
|
|||||||
textsApi,
|
textsApi,
|
||||||
printProductDefaultsApi,
|
printProductDefaultsApi,
|
||||||
);
|
);
|
||||||
|
const collectionApi = new CollectionAPI(options, knexConfig);
|
||||||
|
|
||||||
this.dataSources = {
|
this.dataSources = {
|
||||||
productApi,
|
productApi,
|
||||||
marketLocaleApi,
|
marketLocaleApi,
|
||||||
@@ -96,6 +100,7 @@ export class ContextValue {
|
|||||||
printProductDefaultsApi,
|
printProductDefaultsApi,
|
||||||
keywordApi,
|
keywordApi,
|
||||||
orderApi,
|
orderApi,
|
||||||
|
collectionApi,
|
||||||
};
|
};
|
||||||
|
|
||||||
return this;
|
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,
|
marketsQueryTypeDefs,
|
||||||
marketTypeDefs,
|
marketTypeDefs,
|
||||||
} from './markets-locale-resolver';
|
} from './markets-locale-resolver';
|
||||||
|
import { collectionResolvers } from './collections-resolver';
|
||||||
|
|
||||||
import { DateTimeResolver, DateResolver, JSONResolver } from 'graphql-scalars';
|
import { DateTimeResolver, DateResolver, JSONResolver } from 'graphql-scalars';
|
||||||
import { interiorsQueryTypeDefs, interiorTypeDefs } from './interiors-resolver';
|
import { interiorsQueryTypeDefs, interiorTypeDefs } from './interiors-resolver';
|
||||||
@@ -32,6 +33,7 @@ const resolvers = {
|
|||||||
...interiorsQueryTypeDefs,
|
...interiorsQueryTypeDefs,
|
||||||
...imagesQueryTypeDefs,
|
...imagesQueryTypeDefs,
|
||||||
...TextsQueryTypeDefs,
|
...TextsQueryTypeDefs,
|
||||||
|
...collectionResolvers.Query,
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
...productMutationTypeDefs,
|
...productMutationTypeDefs,
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ const Product = {
|
|||||||
async categories({ id }, _args, { dataSources }): Promise<Array<Category>> {
|
async categories({ id }, _args, { dataSources }): Promise<Array<Category>> {
|
||||||
return (<CategoryAPI>dataSources.categoryApi).getProductCategories(id);
|
return (<CategoryAPI>dataSources.categoryApi).getProductCategories(id);
|
||||||
},
|
},
|
||||||
|
async collections({ id }, _args, { dataSources }) {
|
||||||
|
return dataSources.collectionApi.getProductCollections(id);
|
||||||
|
},
|
||||||
async designer({ designerId }, _args, { dataSources }) {
|
async designer({ designerId }, _args, { dataSources }) {
|
||||||
if (!designerId) {
|
if (!designerId) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ type Query {
|
|||||||
signedFileUploadURL(config: SignedUploadURLInput): SignedUploadURL
|
signedFileUploadURL(config: SignedUploadURLInput): SignedUploadURL
|
||||||
@cacheControl(maxAge: 0)
|
@cacheControl(maxAge: 0)
|
||||||
translate(input: TranslateInput!): StringResult!
|
translate(input: TranslateInput!): StringResult!
|
||||||
|
collections: [Collection]
|
||||||
|
collection(id: Int!): Collection
|
||||||
}
|
}
|
||||||
|
|
||||||
type HealthCheck {
|
type HealthCheck {
|
||||||
@@ -260,6 +262,7 @@ type Product {
|
|||||||
printProducts(groups: [ProductGroup]): [PrintProduct]
|
printProducts(groups: [ProductGroup]): [PrintProduct]
|
||||||
blacklisting: [ProductBlacklist]
|
blacklisting: [ProductBlacklist]
|
||||||
categories: [Category]
|
categories: [Category]
|
||||||
|
collections: [Collection]
|
||||||
designerId: Int
|
designerId: Int
|
||||||
designer: Designer
|
designer: Designer
|
||||||
keywords: [Keyword]
|
keywords: [Keyword]
|
||||||
@@ -553,3 +556,9 @@ type Mutation {
|
|||||||
): StringResult
|
): StringResult
|
||||||
massUpdatePublishingDate(productIds: [Int]!, date: DateTime!): 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