Files
graphql-api-js/src/datasources/collection-api.ts
T
Arwid ThornströmandGitHub 3259fa702c fetch collections from new table (#179)
* fetch collections from new table

* optimization fixes

* fixed fields
2025-10-09 12:59:32 +02:00

141 lines
4.2 KiB
TypeScript

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,
(SELECT array_agg(DISTINCT ecp.product_id)
FROM editorial_collection_products ecp
WHERE ecp.editorial_page_id = ep.id
) AS product_ids
FROM editorial_pages ep
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,
(SELECT array_agg(DISTINCT ecp.product_id)
FROM editorial_collection_products ecp
WHERE ecp.editorial_page_id = ep.id
) AS product_ids
FROM editorial_pages ep
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 {
const productIds: number[] = Array.isArray(row.product_ids)
? row.product_ids
: [];
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 productIdsInt = productIds.map(Number);
const query = `
WITH collection_products AS (
SELECT
e.id,
e.name,
e.metadata,
array_agg(DISTINCT ecp.product_id) AS product_ids
FROM editorial_pages e
JOIN editorial_collection_products ecp ON ecp.editorial_page_id = e.id
WHERE e.type = 'collection'
AND e.deleted IS NULL
GROUP BY e.id, e.name, e.metadata
)
SELECT
id,
name,
metadata,
product_ids
FROM collection_products
WHERE product_ids && ?::integer[];
`;
const results = await this.knex.raw(query, [productIdsInt]);
// Group collections by product ID
const collectionsByProduct: { [key: string]: Collection[] } = {};
productIdsInt.forEach((id) => {
collectionsByProduct[id] = [];
});
results.rows.forEach((row) => {
const collection = this.createCollectionFromRow(row);
// Add this collection to each product ID that it contains
const collectionProductIds = row.product_ids || [];
collectionProductIds.forEach((productId: number) => {
if (productIdsInt.includes(productId)) {
if (!collectionsByProduct[productId]) {
collectionsByProduct[productId] = [];
}
collectionsByProduct[productId].push(collection);
}
});
});
// Return collections in the same order as the input productIds
return productIdsInt.map((id) => collectionsByProduct[id] || []);
}
}