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:
Arwid Thornström
2022-12-12 12:58:24 +01:00
committed by GitHub
parent 4248465031
commit c68c182b64
78 changed files with 3783 additions and 14606 deletions
+11 -13
View File
@@ -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);
}
}
+12 -9
View File
@@ -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);
+8 -4
View File
@@ -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
+20 -11
View File
@@ -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()}`);
}
}
+45 -10
View File
@@ -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()
+11 -5
View File
@@ -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 });
}
}
+30 -4
View File
@@ -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');
+23 -3
View File
@@ -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('*')
+55 -31
View File
@@ -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,
},
};
});
}
}
+68 -46
View File
@@ -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
View File
@@ -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({
+10 -10
View File
@@ -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
`
);
+7 -3
View File
@@ -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;
}
+2
View File
@@ -1,5 +1,7 @@
type InputType = string | undefined | null;
export const MINUTE = 60;
function isNumeric(n: InputType) {
return !isNaN(parseFloat(n));
}