import { ProductAPI } from '../datasources/product-api'; import { Orientation, PrintProduct, Product, ProductGroup, ProductListResult, ProductsFilterInput, ProductsSearchResult, } from '../types/product-types'; import * as CONFIG from '../config'; import { CategoryAPI } from '../datasources/category-api'; import { Category } from '../types/category-types'; import { KeywordAPI } from '../datasources/keyword-api'; import { Keyword } from '../types/keyword-types'; import { MarketLocaleAPI } from '../datasources/market-locale-api'; import { InteriorAPI } from '../datasources/interior-api'; import { InteriorsLambdaAPI } from '../datasources/interiors-lambda-api'; import { IdNumberResult, MarketInput } from '../types/types'; import { moveS3File } from '../aws/s3'; import { DesignerAPI } from '../datasources/designer-api'; import { PrintProductDefaultsAPI } from '../datasources/printproduct-defaults-api'; import { PrintProductDefaults } from '../types/printproduct-defaults-types'; import { GraphQLError } from 'graphql'; import { getOrientationString, isRepeatingPattern } from '../datasources/utils'; import { sendMessage } from '../aws/sqs'; const ProductBlacklist = { async market(parent, _args, { dataSources }) { return (dataSources.marketLocaleApi).getMarketById( parent.marketId, ); }, }; const Product = { async printProducts( product: { printProducts: PrintProduct[] }, input: { groups: ProductGroup[] }, ) { if (!input.groups) { return product.printProducts; } return product.printProducts.filter((printProduct) => { return input.groups.includes(printProduct.group); }); }, async categories({ id }, _args, { dataSources }): Promise> { return (dataSources.categoryApi).getProductCategories(id); }, async designer({ designerId }, _args, { dataSources }) { if (!designerId) { return null; } return dataSources.designerApi.getDesignerById(designerId); }, async keywords({ id }, _args, { dataSources }): Promise> { return (dataSources.keywordApi).getProductKeywords(id); }, async related({ id }, _args, { dataSources }) { return (dataSources.productApi).getRelatedProducts(id); }, async facets(product: Product, _args, { dataSources }) { const keywords = await (( dataSources.keywordApi )).getProductKeywords(product.id); const colors = []; const types = []; keywords.forEach((keyword) => { if (keyword.type === 'COLOR') { colors.push(keyword.value); } else if (keyword.value === 'typeillustration') { types.push('illustration'); } else if (keyword.value === 'typephotography') { types.push('photograph'); } }); const isRepeating = isRepeatingPattern(product); if (isRepeating) { types.push('repeating-pattern'); } const orientations = isRepeating ? [ Orientation[Orientation.LANDSCAPE], Orientation[Orientation.STANDING], Orientation[Orientation.SQUARE], ] : [getOrientationString(product.fields)]; return [ { attribute: 'color', values: colors }, { attribute: 'orientation', values: orientations.map((value) => value.toLowerCase()), }, { attribute: 'type', values: types }, ]; }, }; const PrintProduct = { async interiors({ id }, _args, { dataSources }) { return (dataSources.interiorApi).getPrintProductInteriors(id); }, async defaults({ id }, _args, { dataSources }) { return (( dataSources.printProductDefaultsApi )).getDefaults(id); }, }; async function getProductsResult( _, input: ProductsFilterInput, { dataSources }, ): Promise { if (input.pagination.limit > 5000) { throw new GraphQLError('Pagination limit cannot exceed 5000', { extensions: { code: 'BAD_USER_INPUT' }, }); } const totalPromise = (dataSources.productApi).getProductsTotal( input, ); const itemsPromise = (dataSources.productApi).getProducts(input); const [total, items] = await Promise.all([totalPromise, itemsPromise]); const offset = input.pagination?.offset ?? 0; return { pagination: { limit: input.pagination.limit, offset: offset, total: total, }, items: items, }; } async function getProductsSearchResult( _, { q }, { dataSources }, ): Promise { const prodApi = dataSources.productApi; const catApi = dataSources.categoryApi; const keywApi = dataSources.keywordApi; const designerApi = dataSources.designerApi; const products = await Promise.all([ prodApi.searchByName(q), prodApi.searchByArtNo(q), prodApi.searchByCompleteProductId(q), ]).then((d) => { // flatten, remove falsy (each searchBy... can return undefined above) // and finally remove duplicates. const products = {} as { [key: string]: Product }; d.flat().forEach((product) => { if (product && !products[product.id]) { products[product.id] = product; } }); return Object.values(products); }); const [batches, keywords, categories, designers, copyrights, references] = await Promise.all([ prodApi.searchByBatch(q), keywApi.searchKeywords(q), catApi.searchCategories(q), designerApi.searchDesigners(q), prodApi.searchByCopyright(q), prodApi.searchByReferences(q), ]); return { q: q, products: products, batches: batches, keywords: keywords, categories: categories, designers: designers, copyrights: copyrights, references: references, }; } async function getProduct(_, { id }, { dataSources }): Promise { return (dataSources.productApi).getProduct(id); } async function getProductByPath( _, { path }, { dataSources }, ): Promise { return (dataSources.productApi).getProductByPath(path); } export const productTypeDefs = { ProductBlacklist, Product, PrintProduct, }; export const productQueryTypeDefs = { products: getProductsResult, product: getProduct, productByPath: getProductByPath, productsSearch: getProductsSearchResult, }; // --------------------------------------------------------- // MUTATIONS // --------------------------------------------------------- export const productMutationTypeDefs = { /** * @function addProduct * @description Will add a product */ async addProduct(_, { name, batch, designerId }, { dataSources }) { const id = await (dataSources.productApi).addProduct( name, batch, designerId, ); const product = await (dataSources.productApi).getProduct(id); // Send sqs message to trigger sample ripper await sendMessage( JSON.stringify({ productId: id, artNo: product.fields.artNo }), process.env.SAMPLE_SQS_QUEUE_URL, 60 * 10, ); return product; }, /** * @function cleanupInteriors * @description Will remove all unused interior images for s3 */ async cleanupInteriors(_, { productId }, { dataSources }) { const product = await (dataSources.productApi).getProduct( productId, ); const interiors = []; for (const printProduct of product.printProducts) { const i = await (( dataSources.interiorApi )).getPrintProductInteriors(printProduct.id); interiors.push(...i); } const response = await (( dataSources.productApi )).cleanupInteriors(productId, interiors); return response; }, /** * @function productInfo * @description Mutate base information about a product */ async productInfo(_, { productId, info }, { dataSources }) { await (dataSources.productApi).updateProductInfo( productId, info, ); return (dataSources.productApi).getProduct(productId); }, /** * @function productFocusPoint * @description Mutate the focuspoint for a product */ async productFocusPoint( _, { productId, focusXpoint2, focusYpoint2 }, { dataSources }, ) { await (dataSources.productApi).updateFocusPoint( productId, focusXpoint2, focusYpoint2, ); // generate new interiors const product = await (dataSources.productApi).getProduct( productId, ); await (( dataSources.interiorsLambdaApi )).generateNewInteriors(product); return (dataSources.productApi).getProduct(productId); }, /** * @function productBlacklisting * @description Mutate the product blacklist */ async productBlacklisting(_, { productId, markets }, { dataSources }) { await (dataSources.productApi).updateBlacklisting( productId, markets, ); return (dataSources.productApi).getProduct(productId); }, /** * @function productGroup * @description Mutate the product groups, when changing group interiors lambda is triggered. */ async productGroup(_, { productId, groupIds }, { dataSources }) { await (dataSources.productApi).updateGroups( productId, groupIds, ); const product = await (dataSources.productApi).getProduct( productId, ); await (( dataSources.interiorsLambdaApi )).generateNewInteriors(product); return product; }, /** * @function generateInteriors * @description Will generate one interior for a specific product */ async generateInteriors(_, { productId, roomIds }, { dataSources }) { const product = await (dataSources.productApi).getProduct( productId, ); await (( dataSources.interiorsLambdaApi )).generateInteriors(product, roomIds); return { value: 'OK', }; }, /** * @function productGroupInteriors * @description Mutate the list of interiors on a product and its groups. */ async productGroupInteriors( _, { productId, groupId, uris }, { dataSources }, ) { await (dataSources.productApi).addInteriorsToProductGroup( productId, groupId, uris, ); return (dataSources.productApi).getProduct(productId); }, /** * @function productKeywords * @description Mutate the keywords for a product */ async productKeywords(_, { productId, keywordIds }, { dataSources }) { await (dataSources.keywordApi).setProductKeywords( productId, keywordIds, ); return (dataSources.productApi).getProduct(productId); }, /** * @function productProportionsWarning * @description Mutate the values for proportion warnings on a product */ async productProportionsWarning( _, { productId, proportions }, { dataSources }, ) { await (dataSources.productApi).setProportionsWarning( productId, proportions, ); return (dataSources.productApi).getProduct(productId); }, /** * @function addOwnInteriorToPrintProduct * @description Add a uploaded interior image to a product and its groups. */ async addOwnInteriorToPrintProduct( _, { printId, uploadedS3Key }, { dataSources }, ): Promise { return (dataSources.interiorApi).addOwnUploadToPrintId( printId, uploadedS3Key, ); }, /** * @function printProductDefaults * @description Add default values to a printproduct, this is used through admin gui. */ async printProductDefaults( _, { printId, widthMm, heightMm, cropX, cropY, border }, { dataSources }, ): Promise { return (( dataSources.printProductDefaultsApi )).updateDefaults(printId, widthMm, heightMm, cropX, cropY, border); }, /** * @function productWallpaperType * @description Set the wallpapertype for a wallpaper product */ async productWallpaperType( _, { productId, wallpaperTypes }, { dataSources }, ) { await (dataSources.productApi).setWallpaperTypes( productId, wallpaperTypes, ); return (dataSources.productApi).getProduct(productId); }, /** * @function updateProductImage * @description Set a new image for the product, image is first uploaded to inquiries * bucket then moved to photowall-images. When new image is uploaded interiors are * generated. */ async updateProductImage( _, { productId, uploadedS3Key, width, height }, { dataSources }, ) { await moveS3File( CONFIG.uploadBucket, uploadedS3Key, CONFIG.imageBucket, `products/${productId}.jpg`, ); await (dataSources.productApi).updateProductField( productId, 'width', width.toString(), ); await (dataSources.productApi).updateProductField( productId, 'height', height.toString(), ); const product = await (dataSources.productApi).getProduct( productId, ); await (( dataSources.interiorsLambdaApi )).generateNewInteriors(product); return 'success'; }, /** * @function addRelatedProducts * @description Adds a related product to a product, when doing this the related product * gets the same relationship back. So products are always related to eachother. */ async addRelatedProducts(_, { productId, articleNumbers }, { dataSources }) { await (dataSources.productApi).addRelatedProducts( productId, articleNumbers, ); return (dataSources.productApi).getProduct(productId); }, /** * @function removeRelatedProducts * @description remove relation between products */ async removeRelatedProducts( _, { productId, relatedProductIds }, { dataSources }, ) { await (dataSources.productApi).removeRelatedProducts( productId, relatedProductIds, ); return (dataSources.productApi).getProduct(productId); }, /** * @function addCategoriesToProduct * @description Add categories to a product */ async addCategoriesToProduct(_, { productId, categoryIds }, { dataSources }) { await (dataSources.productApi).addCategoriesToProduct( productId, categoryIds, ); return (dataSources.productApi).getProduct(productId); }, /** * @function removeCategoriesFromProduct * @description Remove categories from a product */ async removeCategoriesFromProduct( _, { productId, categoryIds }, { dataSources }, ) { await (dataSources.productApi).removeCategoriesFromProduct( productId, categoryIds, ); return (dataSources.productApi).getProduct(productId); }, /** * @function updateProductComments * @description Mutate product comments */ async updateProductComments(_, { productId, comments }, { dataSources }) { await (dataSources.productApi).updateComments( productId, comments, ); return (dataSources.productApi).getProduct(productId); }, /** * @function massUpdatePublishing * @description Mutate the publishing for products */ async massUpdatePublishing( _, { productIds, visible, browsable }, { dataSources }, ) { await (dataSources.productApi).massUpdatePublishing( productIds, visible, browsable, ); return { value: 'OK', }; }, /** * @function massUpdateBlacklisting * @description Mutate the blacklisting for products */ async massUpdateBlacklisting(_, { productIds, markets }, { dataSources }) { await (dataSources.productApi).massUpdateBlacklisting( productIds, markets, ); return { value: 'OK', }; }, /** * @function massUpdatePublishing * @description Mutate the Publishing for products */ async massUpdatePublishingDate(_, { productIds, date }, { dataSources }) { await (dataSources.productApi).massUpdatePublishingDate( productIds, date, ); return { value: 'OK', }; }, };