3010: Apollo 4 major update and complete security overhaul to prepare for public api support
* 2919: rewrite for update to apollo 4 (#131) * rewrite for update to apollo 4 * removed container tests, edited make to publish any branch to staging * revert makefile * cleanup * update 1 * more fixes to get it working on aws * enabled csrf prevention protection, testing if build works * switched to express version * changed the healthcheck url * added cache to restdatasources * 3011: create a login mutation on graphql server that responds with a access token (#133) * 3011 added login rest endpoint and general scope fixes * changed login auth to basic auth username and password * minor changes from CR feedback * force update * added tighter timeout for idle knex connection * Refactor authentication and scopes (#134) * big refactor of scopes in graphql * cr fixes * some security fixes (#135) * some security fixes * cleanup * update staging * minor readme change
This commit is contained in:
@@ -1,15 +1,14 @@
|
||||
import { SQLDataSource } from 'datasource-sql';
|
||||
const { InMemoryLRUCache } = require('apollo-server-caching');
|
||||
import crypto from 'crypto';
|
||||
|
||||
const MINUTE = 60;
|
||||
import { InMemoryLRUCache } from '@apollo/utils.keyvaluecache';
|
||||
import { DataSourceOptions, User } from '../types/types';
|
||||
|
||||
export class RawBuilder {
|
||||
_cache: any;
|
||||
query: any;
|
||||
constructor(knex, cache, query) {
|
||||
constructor(knex, cache, query, bindings) {
|
||||
this._cache = cache;
|
||||
this.query = knex.raw(query);
|
||||
this.query = bindings ? knex.raw(query, bindings) : knex.raw(query);
|
||||
}
|
||||
|
||||
async cache(ttl: number) {
|
||||
@@ -36,16 +35,15 @@ export class RawBuilder {
|
||||
|
||||
export class BaseSQLDataSource extends SQLDataSource {
|
||||
cache: any;
|
||||
constructor(config) {
|
||||
user: User;
|
||||
constructor(options: DataSourceOptions, config) {
|
||||
super(config);
|
||||
this.cache = config.cache || new InMemoryLRUCache();
|
||||
this.cache = options.cache || new InMemoryLRUCache();
|
||||
this.user = options.user;
|
||||
this.initialize({ cache: options.cache, context: null });
|
||||
}
|
||||
|
||||
getSQLDate(date: Date): string {
|
||||
return date.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
cachedRaw(query: string): RawBuilder {
|
||||
return new RawBuilder(this.knex, this.cache, query);
|
||||
cachedRaw(query: string, bindings: any[] = null): RawBuilder {
|
||||
return new RawBuilder(this.knex, this.cache, query, bindings);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { ScopeAccess, Scopes } from '../cognito/access-control';
|
||||
import { Category } from '../types/category-types';
|
||||
import { DataSourceOptions } from '../types/types';
|
||||
import { BaseSQLDataSource } from './BaseSQLDataSource';
|
||||
|
||||
const MINUTE = 60;
|
||||
import { MINUTE } from './utils';
|
||||
|
||||
export class CategoryAPI extends BaseSQLDataSource {
|
||||
constructor(config) {
|
||||
super(config);
|
||||
constructor(options: DataSourceOptions, config) {
|
||||
super(options, config);
|
||||
}
|
||||
|
||||
async getProductCategories(productId: number): Promise<Array<Category>> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.CATEGORIES_READ]);
|
||||
const query = /* sql */ `
|
||||
SELECT product_category.category_id as id,
|
||||
categories.*
|
||||
@@ -29,11 +31,13 @@ WHERE product_category.product_id = ?
|
||||
}
|
||||
|
||||
async getCategories(): Promise<Array<Category>> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.CATEGORIES_READ]);
|
||||
// @ts-ignore
|
||||
return this.knex.select('*').from('v_categorytree').cache(MINUTE);
|
||||
}
|
||||
|
||||
async searchCategories(name: string): Promise<Array<Category>> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.CATEGORIES_READ]);
|
||||
return this.knex
|
||||
.select('*')
|
||||
.from('v_categorytree')
|
||||
@@ -41,6 +45,7 @@ WHERE product_category.product_id = ?
|
||||
}
|
||||
|
||||
async getCategoryById(id: number): Promise<Category> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.CATEGORIES_READ]);
|
||||
return (
|
||||
this.knex
|
||||
.select('*')
|
||||
@@ -53,12 +58,10 @@ WHERE product_category.product_id = ?
|
||||
}
|
||||
|
||||
async getLocaleNameForId(id: number): Promise<JSON> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.LOCALES_READ]);
|
||||
const res = await this.cachedRaw(
|
||||
`
|
||||
SELECT json_object_agg(ec.locale, ec.name) as locale_names
|
||||
FROM v_esales_categorytree_i18n ec
|
||||
WHERE ec.id = ${id}
|
||||
`,
|
||||
'SELECT json_object_agg(ec.locale, ec.name) as locale_names FROM v_esales_categorytree_i18n ec WHERE ec.id = ?',
|
||||
[id],
|
||||
)
|
||||
.cache(MINUTE * 5)
|
||||
.then((data) => data.rows);
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { BaseSQLDataSource } from './BaseSQLDataSource';
|
||||
import { Designer } from '../types/designer-types';
|
||||
import { Maybe } from '../types/types';
|
||||
import { DataSourceOptions, Maybe } from '../types/types';
|
||||
import { MINUTE } from './utils';
|
||||
import { ScopeAccess, Scopes } from '../cognito/access-control';
|
||||
|
||||
const MINUTE = 60;
|
||||
export class DesignerAPI extends BaseSQLDataSource {
|
||||
constructor(config) {
|
||||
super(config);
|
||||
constructor(options: DataSourceOptions, config) {
|
||||
super(options, config);
|
||||
}
|
||||
|
||||
async getDesignerById(id: number): Promise<Designer> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.DESIGNERS_READ]);
|
||||
return (
|
||||
this.knex
|
||||
.select('*')
|
||||
@@ -28,6 +30,7 @@ export class DesignerAPI extends BaseSQLDataSource {
|
||||
}
|
||||
|
||||
async searchDesigners(name: string): Promise<Array<Designer>> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.DESIGNERS_READ]);
|
||||
return this.knex
|
||||
.select('*')
|
||||
.from('designers')
|
||||
@@ -44,6 +47,7 @@ export class DesignerAPI extends BaseSQLDataSource {
|
||||
}
|
||||
|
||||
async getDesigners(limit: Maybe<number>): Promise<Array<Designer>> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.DESIGNERS_READ]);
|
||||
limit = limit ?? 5000;
|
||||
return (
|
||||
this.knex
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
import { RESTDataSource } from 'apollo-datasource-rest';
|
||||
|
||||
import { RESTDataSource } from '@apollo/datasource-rest';
|
||||
import { CacheOptions } from '@apollo/datasource-rest/dist/RESTDataSource';
|
||||
import { FetcherResponse, FetcherRequestInit } from '@apollo/utils.fetcher';
|
||||
import { DataSourceOptions } from '../types/types';
|
||||
export class ImageServerApi extends RESTDataSource {
|
||||
constructor() {
|
||||
super();
|
||||
this.baseURL =
|
||||
process.env.ENVIRONMENT_NAME === 'production'
|
||||
? 'http://images.photowall.com'
|
||||
: 'http://images-dev.photowall.com';
|
||||
override baseURL =
|
||||
process.env.ENVIRONMENT_NAME === 'production'
|
||||
? 'https://images.photowall.com'
|
||||
: 'https://images-dev.photowall.com';
|
||||
|
||||
constructor(options: DataSourceOptions) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
protected override cacheOptionsFor(
|
||||
url: string,
|
||||
response: FetcherResponse,
|
||||
request: FetcherRequestInit,
|
||||
): CacheOptions {
|
||||
return { ttl: 5 } as CacheOptions;
|
||||
}
|
||||
|
||||
async getInteriorsForProduct(
|
||||
productId: number,
|
||||
): Promise<Array<{ uri: string }>> {
|
||||
return this.get(`/rooms/${productId}/?ts=${Date.now()}`, null, {
|
||||
cacheOptions: { ttl: 5 },
|
||||
});
|
||||
return this.get(`/rooms/${productId}/?ts=${Date.now()}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,35 @@
|
||||
import { ApolloServerErrorCode } from '@apollo/server/errors';
|
||||
import {
|
||||
Interior,
|
||||
InteriorsFilter,
|
||||
InteriorType,
|
||||
} from '../types/interior-types';
|
||||
import { Orientation } from '../types/product-types';
|
||||
import { IdNumberResult, Maybe } from '../types/types';
|
||||
import { BaseSQLDataSource } from './BaseSQLDataSource';
|
||||
import { DataSourceOptions, IdNumberResult, Maybe } from '../types/types';
|
||||
import { ImageServerApi } from './imageserver-api';
|
||||
import * as CONFIG from '../config';
|
||||
import { moveS3File } from '../aws/s3';
|
||||
|
||||
const MINUTE = 60;
|
||||
import { BaseSQLDataSource } from './BaseSQLDataSource';
|
||||
import { GraphQLError } from 'graphql';
|
||||
import { ScopeAccess, Scopes } from '../cognito/access-control';
|
||||
|
||||
export class InteriorAPI extends BaseSQLDataSource {
|
||||
imageServerApi: ImageServerApi;
|
||||
constructor(config, imageServerApi: ImageServerApi) {
|
||||
super(config);
|
||||
|
||||
constructor(
|
||||
options: DataSourceOptions,
|
||||
config,
|
||||
imageServerApi: ImageServerApi,
|
||||
) {
|
||||
super(options, config);
|
||||
this.imageServerApi = imageServerApi;
|
||||
}
|
||||
|
||||
// -------------------------------
|
||||
// INTERNAL
|
||||
// Functions used inside this class
|
||||
// -------------------------------
|
||||
|
||||
getInteriorType(roomTypePart: string): string {
|
||||
switch (roomTypePart) {
|
||||
case 'wallpaper':
|
||||
@@ -30,7 +41,9 @@ export class InteriorAPI extends BaseSQLDataSource {
|
||||
case 'framed-print':
|
||||
return InteriorType[InteriorType.FRAMED_PRINT];
|
||||
default:
|
||||
throw new Error(`Unknown room type: ${roomTypePart}`);
|
||||
throw new GraphQLError(`Unknown room type: ${roomTypePart}`, {
|
||||
extensions: { code: ApolloServerErrorCode.BAD_USER_INPUT },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +56,12 @@ export class InteriorAPI extends BaseSQLDataSource {
|
||||
case 'square':
|
||||
return Orientation[Orientation.SQUARE];
|
||||
default:
|
||||
throw new Error(`Unknown room orientation: ${roomOrientationPart}`);
|
||||
throw new GraphQLError(
|
||||
`Unknown room orientation: ${roomOrientationPart}`,
|
||||
{
|
||||
extensions: { code: ApolloServerErrorCode.BAD_USER_INPUT },
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,13 +79,19 @@ export class InteriorAPI extends BaseSQLDataSource {
|
||||
return row;
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// PUBLIC
|
||||
// Functions used in resolvers or other datasources
|
||||
// ----------------------------
|
||||
|
||||
async getInteriors(filter: Maybe<InteriorsFilter>): Promise<Array<Interior>> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.INTERIORS_READ]);
|
||||
const sql = /* sql */ `
|
||||
SELECT rooms.*, room_types.name roomType FROM rooms
|
||||
LEFT JOIN room_types ON rooms.room_type_id = room_types.id
|
||||
`;
|
||||
|
||||
const allInteriors = await this.cachedRaw(sql)
|
||||
const allInteriors = await this.cachedRaw(sql, null)
|
||||
.cache(60)
|
||||
.then((data) =>
|
||||
data.rows.map((row) => {
|
||||
@@ -113,6 +137,10 @@ export class InteriorAPI extends BaseSQLDataSource {
|
||||
}
|
||||
|
||||
async getPrintProductInteriors(printId: number): Promise<Array<Interior>> {
|
||||
ScopeAccess.validate(this.user).all([
|
||||
Scopes.INTERIORS_READ,
|
||||
Scopes.PRODUCTS_READ,
|
||||
]);
|
||||
return this.knex
|
||||
.select('*')
|
||||
.from('v_interior_image_urls')
|
||||
@@ -130,11 +158,18 @@ export class InteriorAPI extends BaseSQLDataSource {
|
||||
});
|
||||
}
|
||||
|
||||
// Mutations
|
||||
// ---------------------------------------------------------
|
||||
// MUTATIONS
|
||||
// ---------------------------------------------------------
|
||||
|
||||
async addOwnUploadToPrintId(
|
||||
printId: number,
|
||||
uploadedS3Key: string,
|
||||
): Promise<IdNumberResult> {
|
||||
ScopeAccess.validate(this.user).all([
|
||||
Scopes.INTERIORS_WRITE,
|
||||
Scopes.PRODUCTS_WRITE,
|
||||
]);
|
||||
// Get the last position entered in interiors for this printId
|
||||
const positionRes: any = await this.knex
|
||||
.queryBuilder()
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { RESTDataSource } from 'apollo-datasource-rest';
|
||||
import { RESTDataSource } from '@apollo/datasource-rest';
|
||||
import { ScopeAccess, Scopes } from '../cognito/access-control';
|
||||
import { Product, ProductGroup } from '../types/product-types';
|
||||
import { DataSourceOptions, User } from '../types/types';
|
||||
|
||||
export class InteriorsLambdaAPI extends RESTDataSource {
|
||||
constructor() {
|
||||
super();
|
||||
this.baseURL = process.env.INTERIORS_URL;
|
||||
override baseURL = process.env.INTERIORS_URL;
|
||||
user: User;
|
||||
constructor(options: DataSourceOptions) {
|
||||
super(options);
|
||||
this.user = options.user;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -17,6 +21,8 @@ export class InteriorsLambdaAPI extends RESTDataSource {
|
||||
* </code>
|
||||
*/
|
||||
async generateNewInteriors(product: Product): Promise<any> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.INTERIORS_WRITE]);
|
||||
|
||||
const found = product.printProducts.find(
|
||||
(pr) => pr.groupId === ProductGroup.WALLPAPER,
|
||||
);
|
||||
@@ -36,6 +42,6 @@ export class InteriorsLambdaAPI extends RESTDataSource {
|
||||
body['print_file_height'] = product.fields.printFileHeight;
|
||||
body['print_file_dpi'] = product.fields.printFileDpi;
|
||||
}
|
||||
return this.post('/batch', body);
|
||||
return this.post('/batch', { body: body });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
import { SQLDataSource } from 'datasource-sql';
|
||||
import { ScopeAccess, Scopes } from '../cognito/access-control';
|
||||
import { Keyword, KeywordType } from '../types/keyword-types';
|
||||
import { DataSourceOptions, User } from '../types/types';
|
||||
import { TextsAPI } from './texts-api';
|
||||
|
||||
const MINUTE = 60;
|
||||
import { MINUTE } from './utils';
|
||||
|
||||
export class KeywordAPI extends SQLDataSource {
|
||||
textsApi: TextsAPI;
|
||||
constructor(config, textsApi) {
|
||||
user: User;
|
||||
constructor(options: DataSourceOptions, config, textsApi) {
|
||||
super(config);
|
||||
this.user = options.user;
|
||||
this.initialize({ cache: options.cache, context: null });
|
||||
this.textsApi = textsApi;
|
||||
}
|
||||
|
||||
// -------------------------------
|
||||
// INTERNAL
|
||||
// Functions used inside this class
|
||||
// -------------------------------
|
||||
|
||||
getType(data: any) {
|
||||
// raw queries doesn't camelCase...
|
||||
const typeObject = data.type_id ?? data.typeId;
|
||||
@@ -22,7 +31,13 @@ export class KeywordAPI extends SQLDataSource {
|
||||
};
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// PUBLIC
|
||||
// Functions used in resolvers or other datasources
|
||||
// ----------------------------
|
||||
|
||||
async getProductKeywords(productId: number): Promise<Array<Keyword>> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.KEYWORDS_READ]);
|
||||
const query = /* sql */ `
|
||||
SELECT keywords.*, keyword_type.type_id FROM keywords
|
||||
LEFT JOIN keyword_type ON keyword_type.keyword_id = keywords.id
|
||||
@@ -37,6 +52,7 @@ ORDER BY keywords.value
|
||||
}
|
||||
|
||||
async getKeywords(): Promise<Array<Keyword>> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.KEYWORDS_READ]);
|
||||
const query = /* sql */ `
|
||||
SELECT keywords.*, keyword_type.type_id FROM keywords
|
||||
LEFT JOIN keyword_type ON keyword_type.keyword_id = keywords.id
|
||||
@@ -48,6 +64,7 @@ ORDER BY keywords.value
|
||||
}
|
||||
|
||||
async getKeywordById(id: number): Promise<Keyword> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.KEYWORDS_READ]);
|
||||
return (
|
||||
this.knex
|
||||
.select('*')
|
||||
@@ -62,6 +79,7 @@ ORDER BY keywords.value
|
||||
}
|
||||
|
||||
async searchKeywords(name: string): Promise<Array<Keyword>> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.KEYWORDS_READ]);
|
||||
return this.knex
|
||||
.select('*')
|
||||
.from('keywords')
|
||||
@@ -69,10 +87,18 @@ ORDER BY keywords.value
|
||||
.whereRaw(`LOWER(value) LIKE ?`, [`${name}`]);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// MUTATIONS
|
||||
// ---------------------------------------------------------
|
||||
|
||||
async setProductKeywords(
|
||||
productId: number,
|
||||
keywordIds: Array<number>,
|
||||
): Promise<any> {
|
||||
ScopeAccess.validate(this.user).all([
|
||||
Scopes.KEYWORDS_WRITE,
|
||||
Scopes.PRODUCTS_WRITE,
|
||||
]);
|
||||
await this.knex('product_keyword').where('product_id', productId).del();
|
||||
|
||||
const fieldsToInsert = keywordIds.map((keywordId) => ({
|
||||
@@ -83,8 +109,8 @@ ORDER BY keywords.value
|
||||
return this.knex('product_keyword').insert(fieldsToInsert);
|
||||
}
|
||||
|
||||
// Mutations
|
||||
async addKeyword(name: string, type: KeywordType): Promise<number> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.KEYWORDS_WRITE]);
|
||||
const res = await this.knex('keywords')
|
||||
.insert({ value: name })
|
||||
.returning('id');
|
||||
|
||||
@@ -1,15 +1,28 @@
|
||||
import { SQLDataSource } from 'datasource-sql';
|
||||
import { ScopeAccess, Scopes } from '../cognito/access-control';
|
||||
import { Market, Locale } from '../types/market-locale-types';
|
||||
|
||||
const MINUTE = 60;
|
||||
import { DataSourceOptions, User } from '../types/types';
|
||||
import { MINUTE } from './utils';
|
||||
|
||||
// TODO: perhaps rename to MarketLocale API and add market_locales and locales here.
|
||||
export class MarketLocaleAPI extends SQLDataSource {
|
||||
constructor(config) {
|
||||
user: User;
|
||||
|
||||
constructor(options: DataSourceOptions, config) {
|
||||
super(config);
|
||||
this.user = options.user;
|
||||
this.initialize({ cache: options.cache, context: null });
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// PUBLIC
|
||||
// Functions used in resolvers or other datasources
|
||||
// ----------------------------
|
||||
|
||||
// MARKETS
|
||||
|
||||
async getMarketById(id: number): Promise<Market> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.MARKETS_READ]);
|
||||
return (
|
||||
this.knex
|
||||
.select('*')
|
||||
@@ -22,6 +35,7 @@ export class MarketLocaleAPI extends SQLDataSource {
|
||||
}
|
||||
|
||||
async getMarketByName(name: string): Promise<Market> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.MARKETS_READ]);
|
||||
return (
|
||||
this.knex
|
||||
.select('*')
|
||||
@@ -34,6 +48,7 @@ export class MarketLocaleAPI extends SQLDataSource {
|
||||
}
|
||||
|
||||
async getMarkets(): Promise<Array<Market>> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.MARKETS_READ]);
|
||||
return (
|
||||
this.knex
|
||||
.select('*')
|
||||
@@ -43,7 +58,10 @@ export class MarketLocaleAPI extends SQLDataSource {
|
||||
);
|
||||
}
|
||||
|
||||
// LOCALES
|
||||
|
||||
async getLocaleById(id: number): Promise<Locale> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.LOCALES_READ]);
|
||||
return (
|
||||
this.knex
|
||||
.select('*')
|
||||
@@ -56,6 +74,7 @@ export class MarketLocaleAPI extends SQLDataSource {
|
||||
}
|
||||
|
||||
async getLocaleByValue(value: string): Promise<Locale> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.LOCALES_READ]);
|
||||
return (
|
||||
this.knex
|
||||
.select('*')
|
||||
@@ -68,6 +87,7 @@ export class MarketLocaleAPI extends SQLDataSource {
|
||||
}
|
||||
|
||||
async getLocales(): Promise<Array<Locale>> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.LOCALES_READ]);
|
||||
return (
|
||||
this.knex
|
||||
.select('*')
|
||||
|
||||
@@ -1,14 +1,54 @@
|
||||
import { BaseSQLDataSource } from './BaseSQLDataSource';
|
||||
import { Address, Order, OrderRow } from '../types/order-types';
|
||||
import { GeneralInput, Maybe } from '../types/types';
|
||||
import { DataSourceOptions, GeneralInput, Maybe } from '../types/types';
|
||||
import { MINUTE } from './utils';
|
||||
import { ScopeAccess, Scopes } from '../cognito/access-control';
|
||||
|
||||
const MINUTE = 60;
|
||||
export class OrderAPI extends BaseSQLDataSource {
|
||||
constructor(config) {
|
||||
super(config);
|
||||
constructor(options: DataSourceOptions, config) {
|
||||
super(options, config);
|
||||
}
|
||||
|
||||
// -------------------------------
|
||||
// INTERNAL
|
||||
// Functions used inside this class
|
||||
// -------------------------------
|
||||
|
||||
private getOrdersQuery(input: GeneralInput) {
|
||||
ScopeAccess.validate(this.user).all([Scopes.ORDERS_READ]);
|
||||
let query = this.knex.table('orders');
|
||||
if (input?.filter?.dates?.from) {
|
||||
query = query.where('inserted', '>=', input.filter.dates.from);
|
||||
}
|
||||
|
||||
if (input?.filter?.dates?.to) {
|
||||
query = query.where('inserted', '<=', input.filter.dates.to);
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
private createOrderRows(rows: Array<any>): Array<OrderRow> {
|
||||
return rows.map((row) => {
|
||||
return {
|
||||
...row,
|
||||
data: {
|
||||
...row.data,
|
||||
pwintySku: row.data.pwinty_sku ?? null,
|
||||
pwintyImageId: row.data.pwinty_image_id ?? null,
|
||||
frameColor: row.data.frame_color ?? null,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// PUBLIC
|
||||
// Functions used in resolvers or other datasources
|
||||
// ----------------------------
|
||||
|
||||
async getAddress(addressId: number): Promise<Maybe<Address>> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.ORDERS_READ]);
|
||||
const row = await this.knex
|
||||
.select('*')
|
||||
.from('addresses')
|
||||
@@ -31,6 +71,7 @@ export class OrderAPI extends BaseSQLDataSource {
|
||||
}
|
||||
|
||||
async getOrdersTotal(input: GeneralInput): Promise<number> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.ORDERS_READ]);
|
||||
const res = await this.getOrdersQuery(input)
|
||||
.clone()
|
||||
.count()
|
||||
@@ -41,6 +82,7 @@ export class OrderAPI extends BaseSQLDataSource {
|
||||
}
|
||||
|
||||
async getOrders(input: GeneralInput): Promise<Array<Order>> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.ORDERS_READ]);
|
||||
const offset = input.pagination.offset ?? 0;
|
||||
const limit = input.pagination.limit ?? 100;
|
||||
let query = this.getOrdersQuery(input).limit(limit).offset(offset);
|
||||
@@ -50,20 +92,8 @@ export class OrderAPI extends BaseSQLDataSource {
|
||||
return query.cache(MINUTE);
|
||||
}
|
||||
|
||||
private getOrdersQuery(input: GeneralInput) {
|
||||
let query = this.knex.table('orders');
|
||||
if (input?.filter?.dates?.from) {
|
||||
query = query.where('inserted', '>=', input.filter.dates.from);
|
||||
}
|
||||
|
||||
if (input?.filter?.dates?.to) {
|
||||
query = query.where('inserted', '<=', input.filter.dates.to);
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
async getOrderById(id: number): Promise<Order> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.ORDERS_READ]);
|
||||
return this.knex.select('*').from('orders').where('id', id).first();
|
||||
}
|
||||
|
||||
@@ -71,6 +101,7 @@ export class OrderAPI extends BaseSQLDataSource {
|
||||
klarnaOrderId: string,
|
||||
paypalTransactionId: string,
|
||||
): Promise<Order> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.ORDERS_READ]);
|
||||
const column = paypalTransactionId
|
||||
? 'paypal_transaction_id'
|
||||
: 'klarna_order_id';
|
||||
@@ -89,6 +120,7 @@ export class OrderAPI extends BaseSQLDataSource {
|
||||
}
|
||||
|
||||
async getOrderRowsByOrderId(orderId: number): Promise<Array<OrderRow>> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.ORDERS_READ]);
|
||||
return this.knex
|
||||
.select('*')
|
||||
.from('order_rows')
|
||||
@@ -100,6 +132,11 @@ export class OrderAPI extends BaseSQLDataSource {
|
||||
designerId: number,
|
||||
input: Maybe<GeneralInput>,
|
||||
): Promise<Array<OrderRow>> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.ORDERS_READ]);
|
||||
const getSQLDate = (date: Date): string => {
|
||||
return date.toISOString().split('T')[0];
|
||||
};
|
||||
|
||||
const from = input?.filter?.dates?.from
|
||||
? input.filter.dates.from
|
||||
: new Date('2000-01-01');
|
||||
@@ -112,22 +149,9 @@ export class OrderAPI extends BaseSQLDataSource {
|
||||
.select('*')
|
||||
.from('order_rows')
|
||||
.where('designer_id', designerId)
|
||||
.whereBetween('inserted', [this.getSQLDate(from), this.getSQLDate(to)])
|
||||
.whereBetween('inserted', [getSQLDate(from), getSQLDate(to)])
|
||||
.orderBy('inserted', 'desc')
|
||||
.limit(limit)
|
||||
.then((rows) => this.createOrderRows(rows));
|
||||
}
|
||||
createOrderRows(rows: Array<any>): Array<OrderRow> {
|
||||
return rows.map((row) => {
|
||||
return {
|
||||
...row,
|
||||
data: {
|
||||
...row.data,
|
||||
pwintySku: row.data.pwinty_sku ?? null,
|
||||
pwintyImageId: row.data.pwinty_image_id ?? null,
|
||||
frameColor: row.data.frame_color ?? null,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { ApolloServerErrorCode } from '@apollo/server/errors';
|
||||
import { GraphQLError } from 'graphql';
|
||||
import { Maybe } from 'graphql/jsutils/Maybe';
|
||||
import {
|
||||
BorderType,
|
||||
PrintProductDefaults,
|
||||
} from '../types/printproduct-defaults-types';
|
||||
import { DataSourceOptions } from '../types/types';
|
||||
import { BaseSQLDataSource } from './BaseSQLDataSource';
|
||||
|
||||
type TPosterDefaultPositioning = {
|
||||
@@ -130,10 +133,15 @@ export const calculateDefaultPositioning = (
|
||||
};
|
||||
|
||||
export class PrintProductDefaultsAPI extends BaseSQLDataSource {
|
||||
constructor(config) {
|
||||
super(config);
|
||||
constructor(options: DataSourceOptions, config) {
|
||||
super(options, config);
|
||||
}
|
||||
|
||||
// -------------------------------
|
||||
// INTERNAL
|
||||
// Functions used inside this class
|
||||
// -------------------------------
|
||||
|
||||
/**
|
||||
* @function getBorderType
|
||||
* @description Will convert lowercase none | white | black to
|
||||
@@ -150,6 +158,60 @@ export class PrintProductDefaultsAPI extends BaseSQLDataSource {
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// PUBLIC
|
||||
// Functions used in resolvers or other datasources
|
||||
// ----------------------------
|
||||
|
||||
/**
|
||||
* @function insertDefaults
|
||||
* @description Inserts a new printproduct into printproducts_defaults, sets the default
|
||||
* values the same way we did when settings default values for all older printproducts.
|
||||
*/
|
||||
async insertDefaults(printId: number): Promise<0 | 1> {
|
||||
const product = await this.knex.raw(
|
||||
`
|
||||
SELECT width, height, "focusXpoint2", "focusYpoint2" FROM
|
||||
"product-printproducts" ppp
|
||||
JOIN v_product vp ON ppp.productid = vp.id
|
||||
WHERE printid = ?`,
|
||||
[printId],
|
||||
);
|
||||
|
||||
const { width, height, focusXpoint2, focusYpoint2 } = product.rows[0];
|
||||
|
||||
// GUARDS
|
||||
if (!width || !height) {
|
||||
throw new GraphQLError(
|
||||
'PrintProductDefaultsAPI.insertDefaults: width and height must be defined for a motif to be able to set defaults',
|
||||
{
|
||||
extensions: { code: ApolloServerErrorCode.BAD_USER_INPUT },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const { width_mm, height_mm, crop_x, crop_y } = calculateDefaultPositioning(
|
||||
width,
|
||||
height,
|
||||
focusXpoint2,
|
||||
focusYpoint2,
|
||||
);
|
||||
|
||||
return this.knex
|
||||
.raw(
|
||||
`
|
||||
INSERT INTO printproducts_defaults (print_id, width_mm, height_mm, crop_x, crop_y, border)
|
||||
VALUES (?, ?, ?, ?, ?, 'none');
|
||||
`,
|
||||
[printId, width_mm, height_mm, crop_x, crop_y],
|
||||
)
|
||||
.catch((e) => {
|
||||
throw new GraphQLError(e.message, {
|
||||
extensions: { code: ApolloServerErrorCode.INTERNAL_SERVER_ERROR },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @function getDefaults
|
||||
* @description This function will fetch the default values from printproducts_defaults table
|
||||
@@ -184,49 +246,6 @@ export class PrintProductDefaultsAPI extends BaseSQLDataSource {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @function insertDefaults
|
||||
* @description Inserts a new printproduct into printproducts_defaults, sets the default
|
||||
* values the same way we did when settings default values for all older printproducts.
|
||||
*/
|
||||
async insertDefaults(printId: number): Promise<0 | 1> {
|
||||
const product = await this.knex.raw(
|
||||
`
|
||||
SELECT width, height, "focusXpoint2", "focusYpoint2" FROM
|
||||
"product-printproducts" ppp
|
||||
JOIN v_product vp ON ppp.productid = vp.id
|
||||
WHERE printid = ?`,
|
||||
[printId],
|
||||
);
|
||||
|
||||
const { width, height, focusXpoint2, focusYpoint2 } = product.rows[0];
|
||||
|
||||
// GUARDS
|
||||
if (!width || !height)
|
||||
throw new Error(
|
||||
'PrintProductDefaultsAPI.insertDefaults: width and height must be defined for a motif to be able to set defaults',
|
||||
);
|
||||
|
||||
const { width_mm, height_mm, crop_x, crop_y } = calculateDefaultPositioning(
|
||||
width,
|
||||
height,
|
||||
focusXpoint2,
|
||||
focusYpoint2,
|
||||
);
|
||||
|
||||
return this.knex
|
||||
.raw(
|
||||
`
|
||||
INSERT INTO printproducts_defaults (print_id, width_mm, height_mm, crop_x, crop_y, border)
|
||||
VALUES (?, ?, ?, ?, ?, 'none');
|
||||
`,
|
||||
[printId, width_mm, height_mm, crop_x, crop_y],
|
||||
)
|
||||
.catch((e) => {
|
||||
throw new Error(e);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// MUTATIONS
|
||||
// ---------------------------------------------------------
|
||||
@@ -271,8 +290,11 @@ export class PrintProductDefaultsAPI extends BaseSQLDataSource {
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
throw new Error(
|
||||
throw new GraphQLError(
|
||||
`Something went wrong when trying to update with printid [${printId}][${e.message}]`,
|
||||
{
|
||||
extensions: { code: ApolloServerErrorCode.INTERNAL_SERVER_ERROR },
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
+230
-137
@@ -1,3 +1,4 @@
|
||||
import { ApolloServerErrorCode } from '@apollo/server/errors';
|
||||
import { BaseSQLDataSource } from './BaseSQLDataSource';
|
||||
import { camelcase } from 'stringcase';
|
||||
import slug from 'slug';
|
||||
@@ -20,26 +21,32 @@ import {
|
||||
Batch,
|
||||
Copyright,
|
||||
} from '../types/product-types';
|
||||
import { Maybe } from '../types/types';
|
||||
import { DataSourceOptions, Maybe } from '../types/types';
|
||||
import {
|
||||
booleanStringField,
|
||||
createQuestionMarksFromList,
|
||||
isInt,
|
||||
nullOrNumber,
|
||||
roundOrDefaultTo,
|
||||
} from './utils';
|
||||
import { UserInputError } from 'apollo-server';
|
||||
import { sendPathChangedCmd } from '../bernard/client';
|
||||
import { TextsAPI } from './texts-api';
|
||||
import { PrintProductDefaultsAPI } from './printproduct-defaults-api';
|
||||
|
||||
const MINUTE = 60;
|
||||
import { GraphQLError } from 'graphql';
|
||||
import { MINUTE } from './utils';
|
||||
import { ScopeAccess, Scopes } from '../cognito/access-control';
|
||||
|
||||
export class ProductAPI extends BaseSQLDataSource {
|
||||
textsApi: TextsAPI;
|
||||
printProductDefaultsApi: PrintProductDefaultsAPI;
|
||||
|
||||
constructor(config, textsApi, printProductDefaultsApi) {
|
||||
super(config);
|
||||
constructor(
|
||||
options: DataSourceOptions,
|
||||
config,
|
||||
textsApi,
|
||||
printProductDefaultsApi,
|
||||
) {
|
||||
super(options, config);
|
||||
this.textsApi = textsApi;
|
||||
this.printProductDefaultsApi = printProductDefaultsApi;
|
||||
}
|
||||
@@ -49,6 +56,11 @@ export class ProductAPI extends BaseSQLDataSource {
|
||||
// SELECT materialid, material, (price / 100::float) as price FROM "product-materials"
|
||||
// }
|
||||
|
||||
// -------------------------------
|
||||
// INTERNAL
|
||||
// Functions used inside this class
|
||||
// -------------------------------
|
||||
|
||||
getWallpaperTypes(row: any): Array<ProductWallpaperType> {
|
||||
if (!row.wallpapertypes) {
|
||||
return [];
|
||||
@@ -155,123 +167,17 @@ export class ProductAPI extends BaseSQLDataSource {
|
||||
}
|
||||
|
||||
createProductFromRow(row: any) {
|
||||
row.publishingDate = row.publishing_date;
|
||||
row.blacklisting = this.getBlacklisting(row);
|
||||
row.printProducts = this.getPrintProducts(row);
|
||||
row.type = this.getProductTypes(row);
|
||||
row.wallpaperTypes = this.getWallpaperTypes(row);
|
||||
row.fields = this.getProductFields(row);
|
||||
row.designerId = row.designerid;
|
||||
row.orientation = this.getOrientation(row.fields);
|
||||
row.publishingDate = row.publishing_date;
|
||||
row.printProducts = this.getPrintProducts(row);
|
||||
row.blacklisting = this.getBlacklisting(row);
|
||||
row.fields = this.getProductFields(row);
|
||||
return row;
|
||||
}
|
||||
|
||||
async getProductsTotal(input: ProductsFilterInput): Promise<number> {
|
||||
const query = sql.productsTotal(input);
|
||||
const res = await this.cachedRaw(query)
|
||||
.cache(MINUTE * 5)
|
||||
.then((data) => data.rows);
|
||||
return res[0]['count'] as number; // Optimize later to return Promise
|
||||
}
|
||||
|
||||
async getProducts(input: ProductsFilterInput): Promise<Array<Product>> {
|
||||
const query = sql.products(input);
|
||||
|
||||
return this.knex
|
||||
.raw(query)
|
||||
.then((data) => data.rows.map((row) => this.createProductFromRow(row)));
|
||||
}
|
||||
|
||||
async getProduct(id: number): Promise<Maybe<Product>> {
|
||||
const query = sql.product(id);
|
||||
|
||||
const res = await this.knex.raw(query).then((data) =>
|
||||
data.rows.map((row) => {
|
||||
const prod = this.createProductFromRow(row);
|
||||
return prod;
|
||||
}),
|
||||
);
|
||||
return res.find(Boolean);
|
||||
}
|
||||
|
||||
async getProductByPath(path: string): Promise<Maybe<Product>> {
|
||||
const query = sql.productByPath(path);
|
||||
|
||||
const res = await this.knex.raw(query).then((data) =>
|
||||
data.rows.map((row) => {
|
||||
const prod = this.createProductFromRow(row);
|
||||
return prod;
|
||||
}),
|
||||
);
|
||||
return res.find(Boolean);
|
||||
}
|
||||
|
||||
async getDesignerProducts(designerId: number): Promise<Array<Product>> {
|
||||
const query = sql.designerProducts();
|
||||
|
||||
return this.knex
|
||||
.raw(query, designerId)
|
||||
.then((data) => data.rows.map((row) => this.createProductFromRow(row)));
|
||||
}
|
||||
|
||||
async getCategoryProducts(categoryId: number): Promise<Array<Product>> {
|
||||
const query = sql.categoryProducts(categoryId);
|
||||
|
||||
return this.knex
|
||||
.raw(query)
|
||||
.then((data) => data.rows.map((row) => this.createProductFromRow(row)));
|
||||
}
|
||||
|
||||
async getCategoryWithSubCategoriesProducts(
|
||||
categoryId: number,
|
||||
): Promise<Array<Product>> {
|
||||
const query = sql.categoryWithAllSubCategoriesProducts(categoryId);
|
||||
|
||||
return this.knex
|
||||
.raw(query)
|
||||
.then((data) => data.rows.map((row) => this.createProductFromRow(row)));
|
||||
}
|
||||
|
||||
async getKeywordProducts(keywordId: number): Promise<Array<Product>> {
|
||||
const query = sql.keywordProducts(keywordId);
|
||||
|
||||
return this.knex
|
||||
.raw(query)
|
||||
.then((data) => data.rows.map((row) => this.createProductFromRow(row)));
|
||||
}
|
||||
|
||||
async searchByName(name: string): Promise<Array<Product>> {
|
||||
const query = sql.searchByFieldValue(2); // name
|
||||
|
||||
return this.knex
|
||||
.raw(query, name)
|
||||
.then((data) => data.rows.map((row) => this.createProductFromRow(row)));
|
||||
}
|
||||
|
||||
async searchByArtNo(name: string): Promise<Array<Product>> {
|
||||
const query = sql.searchByFieldValue(1); // artNo
|
||||
|
||||
return this.knex
|
||||
.raw(query, name)
|
||||
.then((data) => data.rows.map((row) => this.createProductFromRow(row)));
|
||||
}
|
||||
|
||||
async searchByCompleteProductId(q: string): Promise<Maybe<Product>> {
|
||||
const queryAsNumber = Number(q.replaceAll('%', ''));
|
||||
if (!queryAsNumber) {
|
||||
return;
|
||||
}
|
||||
return this.getProduct(queryAsNumber);
|
||||
}
|
||||
|
||||
async searchByBatch(name: string): Promise<Array<Batch>> {
|
||||
return this.searchByFieldValue<Batch>(name, 36, 'batch');
|
||||
}
|
||||
|
||||
async searchByCopyright(name: string): Promise<Array<Copyright>> {
|
||||
return this.searchByFieldValue<Copyright>(name, 25, 'copyright');
|
||||
}
|
||||
|
||||
async searchByFieldValue<T>(
|
||||
name: string,
|
||||
fieldNo: number,
|
||||
@@ -299,7 +205,144 @@ export class ProductAPI extends BaseSQLDataSource {
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// PUBLIC
|
||||
// Functions used in resolvers or other datasources
|
||||
// ----------------------------
|
||||
|
||||
async getProductsTotal(input: ProductsFilterInput): Promise<number> {
|
||||
ScopeAccess.validate(this.user).some([Scopes.PRODUCTS_READ]);
|
||||
const query = sql.productsTotal(input);
|
||||
const res = await this.cachedRaw(query, null)
|
||||
.cache(MINUTE * 5)
|
||||
.then((data) => data.rows);
|
||||
return res[0]['count'] as number; // Optimize later to return Promise
|
||||
}
|
||||
|
||||
async getProducts(input: ProductsFilterInput): Promise<Array<Product>> {
|
||||
ScopeAccess.validate(this.user).some([Scopes.PRODUCTS_READ]);
|
||||
const query = sql.products(input);
|
||||
|
||||
return this.knex
|
||||
.raw(query)
|
||||
.then((data) => data.rows.map((row) => this.createProductFromRow(row)));
|
||||
}
|
||||
|
||||
async getProduct(id: number): Promise<Maybe<Product>> {
|
||||
ScopeAccess.validate(this.user).some([Scopes.PRODUCTS_READ]);
|
||||
const query = sql.product();
|
||||
|
||||
const res = await this.knex.raw(query, [id]).then((data) =>
|
||||
data.rows.map((row) => {
|
||||
const prod = this.createProductFromRow(row);
|
||||
return prod;
|
||||
}),
|
||||
);
|
||||
return res.find(Boolean);
|
||||
}
|
||||
|
||||
async getProductByPath(path: string): Promise<Maybe<Product>> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.PRODUCTS_READ]);
|
||||
const query = sql.productByPath();
|
||||
|
||||
const res = await this.knex.raw(query, [path]).then((data) =>
|
||||
data.rows.map((row) => {
|
||||
const prod = this.createProductFromRow(row);
|
||||
return prod;
|
||||
}),
|
||||
);
|
||||
return res.find(Boolean);
|
||||
}
|
||||
|
||||
async getDesignerProducts(designerId: number): Promise<Array<Product>> {
|
||||
ScopeAccess.validate(this.user).all([
|
||||
Scopes.DESIGNERS_READ,
|
||||
Scopes.PRODUCTS_READ,
|
||||
]);
|
||||
const query = sql.designerProducts();
|
||||
|
||||
return this.knex
|
||||
.raw(query, designerId)
|
||||
.then((data) => data.rows.map((row) => this.createProductFromRow(row)));
|
||||
}
|
||||
|
||||
async getCategoryProducts(categoryId: number): Promise<Array<Product>> {
|
||||
ScopeAccess.validate(this.user).all([
|
||||
Scopes.CATEGORIES_READ,
|
||||
Scopes.PRODUCTS_READ,
|
||||
]);
|
||||
const query = sql.categoryProducts();
|
||||
|
||||
return this.knex
|
||||
.raw(query, categoryId)
|
||||
.then((data) => data.rows.map((row) => this.createProductFromRow(row)));
|
||||
}
|
||||
|
||||
async getCategoryWithSubCategoriesProducts(
|
||||
categoryId: number,
|
||||
): Promise<Array<Product>> {
|
||||
ScopeAccess.validate(this.user).all([
|
||||
Scopes.CATEGORIES_READ,
|
||||
Scopes.PRODUCTS_READ,
|
||||
]);
|
||||
const query = sql.categoryWithAllSubCategoriesProducts();
|
||||
|
||||
return this.knex
|
||||
.raw(query, categoryId)
|
||||
.then((data) => data.rows.map((row) => this.createProductFromRow(row)));
|
||||
}
|
||||
|
||||
async getKeywordProducts(keywordId: number): Promise<Array<Product>> {
|
||||
ScopeAccess.validate(this.user).all([
|
||||
Scopes.KEYWORDS_READ,
|
||||
Scopes.PRODUCTS_READ,
|
||||
]);
|
||||
const query = sql.keywordProducts();
|
||||
|
||||
return this.knex
|
||||
.raw(query, keywordId)
|
||||
.then((data) => data.rows.map((row) => this.createProductFromRow(row)));
|
||||
}
|
||||
|
||||
async searchByName(name: string): Promise<Array<Product>> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.PRODUCTS_READ]);
|
||||
const query = sql.searchByFieldValue(2); // name
|
||||
|
||||
return this.knex
|
||||
.raw(query, name)
|
||||
.then((data) => data.rows.map((row) => this.createProductFromRow(row)));
|
||||
}
|
||||
|
||||
async searchByArtNo(name: string): Promise<Array<Product>> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.PRODUCTS_READ]);
|
||||
const query = sql.searchByFieldValue(1); // artNo
|
||||
|
||||
return this.knex
|
||||
.raw(query, name)
|
||||
.then((data) => data.rows.map((row) => this.createProductFromRow(row)));
|
||||
}
|
||||
|
||||
async searchByCompleteProductId(q: string): Promise<Maybe<Product>> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.PRODUCTS_READ]);
|
||||
const queryAsNumber = Number(q.replaceAll('%', ''));
|
||||
if (!queryAsNumber) {
|
||||
return;
|
||||
}
|
||||
return this.getProduct(queryAsNumber);
|
||||
}
|
||||
|
||||
async searchByBatch(name: string): Promise<Array<Batch>> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.PRODUCTS_READ]);
|
||||
return this.searchByFieldValue<Batch>(name, 36, 'batch');
|
||||
}
|
||||
|
||||
async searchByCopyright(name: string): Promise<Array<Copyright>> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.PRODUCTS_READ]);
|
||||
return this.searchByFieldValue<Copyright>(name, 25, 'copyright');
|
||||
}
|
||||
|
||||
async searchByReferences(name: string): Promise<Array<Product>> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.PRODUCTS_READ]);
|
||||
const query = sql.referenceProducts(); // ref1, ref2, ref3
|
||||
|
||||
return this.knex
|
||||
@@ -308,31 +351,25 @@ export class ProductAPI extends BaseSQLDataSource {
|
||||
}
|
||||
|
||||
async getRelatedProducts(id: number): Promise<Array<Product>> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.PRODUCTS_READ]);
|
||||
const ids = await this.knex
|
||||
.raw(
|
||||
`SELECT productid2 FROM "product-products_products" WHERE productid1 = ${id}`,
|
||||
`SELECT productid2 FROM "product-products_products" WHERE productid1 = ?`,
|
||||
id,
|
||||
)
|
||||
.then((data) => data.rows.map((r) => r.productid2));
|
||||
|
||||
return ids.map(async (id) => this.getProduct(id));
|
||||
}
|
||||
|
||||
//
|
||||
// mutations below
|
||||
async updateProductField(
|
||||
productId: number,
|
||||
field: string,
|
||||
value: string,
|
||||
): Promise<void> {
|
||||
return this.knex.raw(
|
||||
/* sql */ `
|
||||
INSERT INTO "product-products_fields" (productid, fieldid, value, inserted, updated)
|
||||
VALUES (?, (SELECT fieldid FROM "product-fields" WHERE field = ?), ?, now(), now())
|
||||
ON CONFLICT (productid, fieldid)
|
||||
DO UPDATE SET "value" = ?`,
|
||||
[productId, field, value, value],
|
||||
);
|
||||
}
|
||||
// ---------------------------------------------------------
|
||||
// MUTATIONS
|
||||
// ---------------------------------------------------------
|
||||
|
||||
// -------------------------------
|
||||
// INTERNAL
|
||||
// Functions used inside this class
|
||||
// -------------------------------
|
||||
|
||||
/**
|
||||
* @function getUniqueProductPath
|
||||
@@ -391,6 +428,31 @@ export class ProductAPI extends BaseSQLDataSource {
|
||||
return newPath;
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// PUBLIC
|
||||
// Functions used in resolvers or other datasources
|
||||
// ----------------------------
|
||||
|
||||
/**
|
||||
* @function updateProductField
|
||||
* @description Update a field for a product
|
||||
*/
|
||||
async updateProductField(
|
||||
productId: number,
|
||||
field: string,
|
||||
value: string,
|
||||
): Promise<void> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.PRODUCTS_WRITE]);
|
||||
return this.knex.raw(
|
||||
/* sql */ `
|
||||
INSERT INTO "product-products_fields" (productid, fieldid, value, inserted, updated)
|
||||
VALUES (?, (SELECT fieldid FROM "product-fields" WHERE field = ?), ?, now(), now())
|
||||
ON CONFLICT (productid, fieldid)
|
||||
DO UPDATE SET "value" = ?`,
|
||||
[productId, field, value, value],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @function addProduct
|
||||
* @description Adds a new product to system, will use name to
|
||||
@@ -401,6 +463,7 @@ export class ProductAPI extends BaseSQLDataSource {
|
||||
batch: string,
|
||||
designerId: number,
|
||||
): Promise<number> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.PRODUCTS_WRITE]);
|
||||
const fixedBatch = !!batch ? batch : '';
|
||||
const productPath = slug(name);
|
||||
const safePath = await this.getUniqueProductPath(productPath, null);
|
||||
@@ -429,6 +492,7 @@ export class ProductAPI extends BaseSQLDataSource {
|
||||
productId: number,
|
||||
info: ProductInfoInput,
|
||||
): Promise<any[]> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.PRODUCTS_WRITE]);
|
||||
const promises = [];
|
||||
|
||||
// Check path and insert new unique path if taken
|
||||
@@ -499,7 +563,9 @@ export class ProductAPI extends BaseSQLDataSource {
|
||||
* @function productPublishing
|
||||
* @description updates a products publishing
|
||||
*/
|
||||
// TODO: This is never used, why?
|
||||
async productPublishing(productId: number, date: Date) {
|
||||
ScopeAccess.validate(this.user).all([Scopes.PRODUCTS_WRITE]);
|
||||
return this.knex
|
||||
.table('product-products')
|
||||
.update({
|
||||
@@ -513,6 +579,7 @@ export class ProductAPI extends BaseSQLDataSource {
|
||||
* @description Mass update publishing date
|
||||
*/
|
||||
async massUpdatePublishingDate(productIds: Array<number>, date: Date) {
|
||||
ScopeAccess.validate(this.user).all([Scopes.PRODUCTS_WRITE]);
|
||||
return this.knex
|
||||
.table('product-products')
|
||||
.update({
|
||||
@@ -530,6 +597,7 @@ export class ProductAPI extends BaseSQLDataSource {
|
||||
visible: boolean,
|
||||
browsable: boolean,
|
||||
) {
|
||||
ScopeAccess.validate(this.user).all([Scopes.PRODUCTS_WRITE]);
|
||||
return this.knex
|
||||
.table('product-products')
|
||||
.update({
|
||||
@@ -547,6 +615,7 @@ export class ProductAPI extends BaseSQLDataSource {
|
||||
productIds: Array<number>,
|
||||
markets: Array<ProductBlacklistMarketInput>,
|
||||
) {
|
||||
ScopeAccess.validate(this.user).all([Scopes.PRODUCTS_WRITE]);
|
||||
const promises = productIds.map((pId) =>
|
||||
this.updateBlacklisting(pId, markets),
|
||||
);
|
||||
@@ -563,6 +632,7 @@ export class ProductAPI extends BaseSQLDataSource {
|
||||
focusXpoint2: number,
|
||||
focusYpoint2: number,
|
||||
): Promise<Promise<void>[]> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.PRODUCTS_WRITE]);
|
||||
const promises = [];
|
||||
promises.push(
|
||||
this.updateProductField(
|
||||
@@ -580,6 +650,7 @@ export class ProductAPI extends BaseSQLDataSource {
|
||||
}
|
||||
|
||||
async updateComments(productId: number, comments: string): Promise<void> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.PRODUCTS_WRITE]);
|
||||
return this.updateProductField(productId, 'comments', comments);
|
||||
}
|
||||
|
||||
@@ -591,6 +662,7 @@ export class ProductAPI extends BaseSQLDataSource {
|
||||
productId: number,
|
||||
markets: Array<ProductBlacklistMarketInput>,
|
||||
) {
|
||||
ScopeAccess.validate(this.user).all([Scopes.PRODUCTS_WRITE]);
|
||||
const sql = /* sql */ `
|
||||
INSERT INTO product_blacklist (product_id, group_id, market_id, created_at, updated_at)
|
||||
VALUES (?, ?, ?, now(), now())
|
||||
@@ -623,6 +695,7 @@ export class ProductAPI extends BaseSQLDataSource {
|
||||
productId: number,
|
||||
wallpaperTypes: Array<ProductWallpaperType>,
|
||||
): Promise<any[]> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.PRODUCTS_WRITE]);
|
||||
await this.knex('product_wallpapertypes')
|
||||
.where('product_id', productId)
|
||||
.del();
|
||||
@@ -643,6 +716,7 @@ export class ProductAPI extends BaseSQLDataSource {
|
||||
* poster. Note that group 3 is repeating wallpapers.
|
||||
*/
|
||||
async updateGroups(productId: number, groupIds: number[]): Promise<any[]> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.PRODUCTS_WRITE]);
|
||||
// Fetch the current state
|
||||
const allPrintProducts = await this.knex.raw(
|
||||
`SELECT printid, groupid FROM "product-printproducts" WHERE productid = ?`,
|
||||
@@ -797,6 +871,10 @@ export class ProductAPI extends BaseSQLDataSource {
|
||||
groupId: number,
|
||||
uris: string[],
|
||||
): Promise<Promise<void>[]> {
|
||||
ScopeAccess.validate(this.user).all([
|
||||
Scopes.PRODUCTS_WRITE,
|
||||
Scopes.INTERIORS_WRITE,
|
||||
]);
|
||||
// Create object to hold data as we go along all the db calls
|
||||
// This object needs to support new own uploaded images, these should have
|
||||
// some other folder prefix, like /interiors-buffer/. These will
|
||||
@@ -896,6 +974,7 @@ export class ProductAPI extends BaseSQLDataSource {
|
||||
productId: number,
|
||||
proportions: ProductProportionsInput,
|
||||
): Promise<Promise<void>[]> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.PRODUCTS_WRITE]);
|
||||
const promises = [];
|
||||
promises.push(
|
||||
this.updateProductField(
|
||||
@@ -937,18 +1016,23 @@ export class ProductAPI extends BaseSQLDataSource {
|
||||
productId: number,
|
||||
articleNumbers: Array<string>,
|
||||
): Promise<Promise<void>[]> {
|
||||
const whereClause = articleNumbers.map((a) => `'${a}'`).join(',');
|
||||
ScopeAccess.validate(this.user).all([Scopes.PRODUCTS_WRITE]);
|
||||
|
||||
const query = /* sql */ `
|
||||
SELECT pp.productid, ppf.value FROM "product-products" pp
|
||||
LEFT JOIN "product-products_fields" ppf
|
||||
ON ppf.productid = pp.productid AND ppf.fieldid = 1
|
||||
WHERE ppf.value IN (${whereClause});
|
||||
WHERE ppf.value IN (${createQuestionMarksFromList(articleNumbers)});
|
||||
`;
|
||||
|
||||
const res = await this.knex.raw(query).then((data) => data.rows);
|
||||
const res = await this.knex
|
||||
.raw(query, articleNumbers)
|
||||
.then((data) => data.rows);
|
||||
|
||||
if (!res.length) {
|
||||
throw new UserInputError('No articles found');
|
||||
throw new GraphQLError('No articles found', {
|
||||
extensions: { code: ApolloServerErrorCode.BAD_USER_INPUT },
|
||||
});
|
||||
}
|
||||
const fieldsToInsert = res.map((item) => ({
|
||||
productid1: productId,
|
||||
@@ -977,6 +1061,7 @@ export class ProductAPI extends BaseSQLDataSource {
|
||||
productId,
|
||||
relatedProductIds,
|
||||
): Promise<Promise<void>[]> {
|
||||
ScopeAccess.validate(this.user).all([Scopes.PRODUCTS_WRITE]);
|
||||
// remove connection between the friends id and this produdId
|
||||
const promises = relatedProductIds.map((pId) => {
|
||||
return this.knex('product-products_products')
|
||||
@@ -1001,6 +1086,10 @@ export class ProductAPI extends BaseSQLDataSource {
|
||||
productId,
|
||||
categoryIds,
|
||||
): Promise<Promise<void>[]> {
|
||||
ScopeAccess.validate(this.user).all([
|
||||
Scopes.PRODUCTS_WRITE,
|
||||
Scopes.CATEGORIES_WRITE,
|
||||
]);
|
||||
const promises = categoryIds.map((categoryId) =>
|
||||
this.knex('product_category').insert({
|
||||
product_id: productId,
|
||||
@@ -1018,6 +1107,10 @@ export class ProductAPI extends BaseSQLDataSource {
|
||||
productId,
|
||||
categoryIds,
|
||||
): Promise<Promise<void>[]> {
|
||||
ScopeAccess.validate(this.user).all([
|
||||
Scopes.PRODUCTS_WRITE,
|
||||
Scopes.CATEGORIES_WRITE,
|
||||
]);
|
||||
const promises = categoryIds.map((categoryId) =>
|
||||
this.knex('product_category')
|
||||
.where({
|
||||
|
||||
@@ -110,35 +110,35 @@ export function productsTotal(input: ProductsFilterInput) {
|
||||
`;
|
||||
}
|
||||
|
||||
export function product(id: number) {
|
||||
export function product() {
|
||||
return (
|
||||
baseQuery +
|
||||
/* sql */ `
|
||||
WHERE products.productid = ${id}
|
||||
WHERE products.productid = ?
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
export function productByPath(path: string) {
|
||||
export function productByPath() {
|
||||
return (
|
||||
baseQuery +
|
||||
/* sql */ `
|
||||
WHERE products.path = '${path}'
|
||||
WHERE products.path = ?
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
export function categoryProducts(categoryId: number) {
|
||||
export function categoryProducts() {
|
||||
return (
|
||||
baseQuery +
|
||||
/* sql */ `
|
||||
WHERE products.productid IN (SELECT product_id FROM product_category WHERE category_id = ${categoryId})
|
||||
WHERE products.productid IN (SELECT product_id FROM product_category WHERE category_id = ?)
|
||||
ORDER BY products.inserted DESC
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
export function categoryWithAllSubCategoriesProducts(categoryId: number) {
|
||||
export function categoryWithAllSubCategoriesProducts() {
|
||||
return (
|
||||
baseQuery +
|
||||
/* sql */ `
|
||||
@@ -151,7 +151,7 @@ export function categoryWithAllSubCategoriesProducts(categoryId: number) {
|
||||
JOIN (
|
||||
SELECT id, lft, rgt
|
||||
FROM categories
|
||||
WHERE id = ${categoryId}
|
||||
WHERE id = ?
|
||||
) tree
|
||||
ON cat.lft >= tree.lft AND cat.rgt <= tree.rgt
|
||||
)
|
||||
@@ -180,11 +180,11 @@ export function referenceProducts() {
|
||||
);
|
||||
}
|
||||
|
||||
export function keywordProducts(keywordId: number) {
|
||||
export function keywordProducts() {
|
||||
return (
|
||||
baseQuery +
|
||||
/* sql */ `
|
||||
WHERE products.productid IN (SELECT product_id FROM product_keyword WHERE keyword_id = ${keywordId})
|
||||
WHERE products.productid IN (SELECT product_id FROM product_keyword WHERE keyword_id = ?)
|
||||
ORDER BY products.inserted DESC
|
||||
`
|
||||
);
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { SQLDataSource } from 'datasource-sql';
|
||||
import { TranslateInput, translateText } from '../aws/translate';
|
||||
import { Locale } from '../types/market-locale-types';
|
||||
import { DataSourceOptions } from '../types/types';
|
||||
import { MarketLocaleAPI } from './market-locale-api';
|
||||
|
||||
const MINUTE = 60;
|
||||
|
||||
// TODO: perhaps rename to MarketLocale API and add market_locales and locales here.
|
||||
export class TextsAPI extends SQLDataSource {
|
||||
marketLocaleApi: MarketLocaleAPI;
|
||||
|
||||
constructor(config, marketLocaleApi: MarketLocaleAPI) {
|
||||
constructor(
|
||||
options: DataSourceOptions,
|
||||
config,
|
||||
marketLocaleApi: MarketLocaleAPI,
|
||||
) {
|
||||
super(config);
|
||||
this.initialize({ cache: options.cache, context: null });
|
||||
this.marketLocaleApi = marketLocaleApi;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
type InputType = string | undefined | null;
|
||||
|
||||
export const MINUTE = 60;
|
||||
|
||||
function isNumeric(n: InputType) {
|
||||
return !isNaN(parseFloat(n));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user