Add collections (#172)

This commit is contained in:
Fredrik Ringqvist
2025-04-29 14:00:35 +02:00
committed by GitHub
parent efab961a29
commit 10fa25b230
7 changed files with 164 additions and 0 deletions
+5
View File
@@ -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;
+119
View File
@@ -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] || []);
}
}
+21
View File
@@ -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,
},
};
+2
View File
@@ -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,
+3
View File
@@ -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;
+9
View File
@@ -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!]!
}
+5
View File
@@ -0,0 +1,5 @@
export interface Collection {
id: string | number;
name: string;
productIds: number[];
}