diff --git a/server/src/api/controllers/Accounts.ts b/server/src/api/controllers/Accounts.ts index 98790f1d9..f4ce2c961 100644 --- a/server/src/api/controllers/Accounts.ts +++ b/server/src/api/controllers/Accounts.ts @@ -7,6 +7,7 @@ import AccountsService from 'services/Accounts/AccountsService'; import { IAccountDTO, IAccountsFilter } from 'interfaces'; import { ServiceError } from 'exceptions'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; +import { DATATYPES_LENGTH } from 'data/DataTypes'; @Service() export default class AccountsController extends BaseController{ @@ -112,7 +113,7 @@ export default class AccountsController extends BaseController{ return [ check('name') .exists() - .isLength({ min: 3, max: 255 }) + .isLength({ min: 3, max: DATATYPES_LENGTH.STRING }) .trim() .escape(), check('code') @@ -122,16 +123,16 @@ export default class AccountsController extends BaseController{ .escape(), check('account_type_id') .exists() - .isNumeric() + .isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 }) .toInt(), check('description') .optional({ nullable: true }) - .isLength({ max: 512 }) + .isLength({ max: DATATYPES_LENGTH.TEXT }) .trim() .escape(), check('parent_account_id') .optional({ nullable: true }) - .isNumeric() + .isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 }) .toInt(), ]; } diff --git a/server/src/api/controllers/Authentication.ts b/server/src/api/controllers/Authentication.ts index e219e5a81..b5c50649c 100644 --- a/server/src/api/controllers/Authentication.ts +++ b/server/src/api/controllers/Authentication.ts @@ -6,6 +6,7 @@ import asyncMiddleware from 'api/middleware/asyncMiddleware'; import AuthenticationService from 'services/Authentication'; import { ILoginDTO, ISystemUser, IRegisterOTD } from 'interfaces'; import { ServiceError, ServiceErrors } from "exceptions"; +import { DATATYPES_LENGTH } from 'data/DataTypes'; @Service() export default class AuthenticationController extends BaseController{ @@ -60,12 +61,12 @@ export default class AuthenticationController extends BaseController{ */ get registerSchema(): ValidationChain[] { return [ - check('first_name').exists().trim().escape(), - check('last_name').exists().trim().escape(), - check('email').exists().isEmail().trim().escape(), - check('phone_number').exists().trim().escape(), - check('password').exists().trim().escape(), - check('country').exists().trim().escape(), + check('first_name').exists().isString().trim().escape().isLength({ max: DATATYPES_LENGTH.STRING }), + check('last_name').exists().isString().trim().escape().isLength({ max: DATATYPES_LENGTH.STRING }), + check('email').exists().isString().isEmail().trim().escape().isLength({ max: DATATYPES_LENGTH.STRING }), + check('phone_number').exists().isString().trim().escape().isLength({ max: DATATYPES_LENGTH.STRING }), + check('password').exists().isString().trim().escape().isLength({ max: DATATYPES_LENGTH.STRING }), + check('country').exists().isString().trim().escape().isLength({ max: DATATYPES_LENGTH.STRING }), ]; } diff --git a/server/src/api/controllers/Contacts/Contacts.ts b/server/src/api/controllers/Contacts/Contacts.ts index 312938242..4d8f845f3 100644 --- a/server/src/api/controllers/Contacts/Contacts.ts +++ b/server/src/api/controllers/Contacts/Contacts.ts @@ -1,5 +1,6 @@ import { check, param, query, body, ValidationChain } from 'express-validator'; import BaseController from "api/controllers/BaseController"; +import { DATATYPES_LENGTH } from 'data/DataTypes'; export default class ContactsController extends BaseController { /** @@ -7,37 +8,37 @@ export default class ContactsController extends BaseController { */ get contactDTOSchema(): ValidationChain[] { return [ - check('salutation').optional().trim().escape(), - check('first_name').optional().trim().escape(), - check('last_name').optional().trim().escape(), + check('salutation').optional().isString().trim().escape().isLength({ max: DATATYPES_LENGTH.STRING }), + check('first_name').optional().isString().trim().escape().isLength({ max: DATATYPES_LENGTH.STRING }), + check('last_name').optional().isString().trim().escape().isLength({ max: DATATYPES_LENGTH.STRING }), - check('company_name').optional().trim().escape(), - check('display_name').exists().trim().escape(), + check('company_name').optional().isString().trim().escape().isLength({ max: DATATYPES_LENGTH.STRING }), + check('display_name').exists().isString().trim().escape().isLength({ max: DATATYPES_LENGTH.STRING }), - check('email').optional({ nullable: true }).normalizeEmail().isEmail(), - check('website').optional().trim().escape(), - check('work_phone').optional().trim().escape(), - check('personal_phone').optional().trim().escape(), + check('email').optional({ nullable: true }).isString().normalizeEmail().isEmail().isLength({ max: DATATYPES_LENGTH.STRING }), + check('website').optional().isString().trim().isURL().isLength({ max: DATATYPES_LENGTH.STRING }), + check('work_phone').optional().isString().trim().escape().isLength({ max: DATATYPES_LENGTH.STRING }), + check('personal_phone').optional().isString().trim().escape().isLength({ max: DATATYPES_LENGTH.STRING }), - check('billing_address_1').optional().trim().escape(), - check('billing_address_2').optional().trim().escape(), - check('billing_address_city').optional().trim().escape(), - check('billing_address_country').optional().trim().escape(), - check('billing_address_email').optional().isEmail().trim().escape(), - check('billing_address_postcode').optional().trim().escape(), - check('billing_address_phone').optional().trim().escape(), - check('billing_address_state').optional().trim().escape(), + check('billing_address_1').optional().isString().trim().escape().isLength({ max: DATATYPES_LENGTH.STRING }), + check('billing_address_2').optional().isString().trim().escape().isLength({ max: DATATYPES_LENGTH.STRING }), + check('billing_address_city').optional().isString().trim().escape().isLength({ max: DATATYPES_LENGTH.STRING }), + check('billing_address_country').optional().isString().trim().escape().isLength({ max: DATATYPES_LENGTH.STRING }), + check('billing_address_email').optional().isString().isEmail().trim().escape().isLength({ max: DATATYPES_LENGTH.STRING }), + check('billing_address_postcode').optional().isString().trim().escape().isLength({ max: DATATYPES_LENGTH.STRING }), + check('billing_address_phone').optional().isString().trim().escape().isLength({ max: DATATYPES_LENGTH.STRING }), + check('billing_address_state').optional().isString().trim().escape().isLength({ max: DATATYPES_LENGTH.STRING }), - check('shipping_address_1').optional().trim().escape(), - check('shipping_address_2').optional().trim().escape(), - check('shipping_address_city').optional().trim().escape(), - check('shipping_address_country').optional().trim().escape(), - check('shipping_address_email').optional().isEmail().trim().escape(), - check('shipping_address_postcode').optional().trim().escape(), - check('shipping_address_phone').optional().trim().escape(), - check('shipping_address_state').optional().trim().escape(), + check('shipping_address_1').optional().isString().trim().escape().isLength({ max: DATATYPES_LENGTH.STRING }), + check('shipping_address_2').optional().isString().trim().escape().isLength({ max: DATATYPES_LENGTH.STRING }), + check('shipping_address_city').optional().isString().trim().escape().isLength({ max: DATATYPES_LENGTH.STRING }), + check('shipping_address_country').optional().isString().trim().escape().isLength({ max: DATATYPES_LENGTH.STRING }), + check('shipping_address_email').optional().isString().isEmail().trim().escape().isLength({ max: DATATYPES_LENGTH.STRING }), + check('shipping_address_postcode').optional().isString().trim().escape().isLength({ max: DATATYPES_LENGTH.STRING }), + check('shipping_address_phone').optional().isString().trim().escape().isLength({ max: DATATYPES_LENGTH.STRING }), + check('shipping_address_state').optional().isString().trim().escape().isLength({ max: DATATYPES_LENGTH.STRING }), - check('note').optional().trim().escape(), + check('note').optional().isString().trim().escape().isLength({ max: DATATYPES_LENGTH.TEXT }), check('active').optional().isBoolean().toBoolean(), ]; } @@ -48,8 +49,10 @@ export default class ContactsController extends BaseController { */ get contactNewDTOSchema(): ValidationChain[] { return [ - check('opening_balance').optional({ nullable: true }).isNumeric().toInt(), - body('opening_balance_at').if(body('opening_balance').exists()).exists(), + check('opening_balance').optional({ nullable: true }).isInt({ min: 0, max: DATATYPES_LENGTH.DECIMAL_13_3 }).toInt(), + body('opening_balance_at') + .if(body('opening_balance').exists()).exists() + .isISO8601(), ]; } diff --git a/server/src/api/controllers/Contacts/Customers.ts b/server/src/api/controllers/Contacts/Customers.ts index 433f74b51..80a0d5ed8 100644 --- a/server/src/api/controllers/Contacts/Customers.ts +++ b/server/src/api/controllers/Contacts/Customers.ts @@ -90,7 +90,7 @@ export default class CustomersController extends ContactsController { */ get createCustomerDTOSchema() { return [ - check('currency_code').optional().trim().escape(), + check('currency_code').optional().isString().trim().escape().isLength({ max: 3, min: 3 }), ]; } diff --git a/server/src/api/controllers/Contacts/Vendors.ts b/server/src/api/controllers/Contacts/Vendors.ts index 5a7041823..8c6566194 100644 --- a/server/src/api/controllers/Contacts/Vendors.ts +++ b/server/src/api/controllers/Contacts/Vendors.ts @@ -73,7 +73,12 @@ export default class VendorsController extends ContactsController { */ get vendorDTOSchema(): ValidationChain[] { return [ - check('currency_code').optional().trim().escape(), + check('currency_code') + .optional() + .isString() + .trim() + .escape() + .isLength({ min: 3, max: 3 }), ]; } diff --git a/server/src/api/controllers/ItemCategories.ts b/server/src/api/controllers/ItemCategories.ts index 3b8a63e40..10c9255c0 100644 --- a/server/src/api/controllers/ItemCategories.ts +++ b/server/src/api/controllers/ItemCategories.ts @@ -11,6 +11,7 @@ import { IItemCategoryOTD } from 'interfaces'; import { ServiceError } from 'exceptions'; import BaseController from 'api/controllers/BaseController'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; +import { DATATYPES_LENGTH } from 'data/DataTypes'; @Service() export default class ItemsCategoriesController extends BaseController { @@ -78,26 +79,32 @@ export default class ItemsCategoriesController extends BaseController { */ get categoryValidationSchema() { return [ - check('name').exists().trim().escape(), + check('name') + .exists() + .trim() + .escape() + .isLength({ min: 0, max: DATATYPES_LENGTH.STRING }), check('parent_category_id') .optional({ nullable: true }) - .isNumeric() + .isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 }) .toInt(), check('description') .optional() + .isString() .trim() - .escape(), + .escape() + .isLength({ max: DATATYPES_LENGTH.TEXT }), check('sell_account_id') .optional({ nullable: true }) - .isNumeric() + .isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 }) .toInt(), check('cost_account_id') .optional({ nullable: true }) - .isNumeric() + .isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 }) .toInt(), check('inventory_account_id') .optional({ nullable: true }) - .isNumeric() + .isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 }) .toInt(), ] } diff --git a/server/src/api/controllers/Items.ts b/server/src/api/controllers/Items.ts index 804f37684..b0f83caec 100644 --- a/server/src/api/controllers/Items.ts +++ b/server/src/api/controllers/Items.ts @@ -7,6 +7,7 @@ import BaseController from 'api/controllers/BaseController'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; import { ServiceError } from 'exceptions'; import { IItemDTO } from 'interfaces'; +import { DATATYPES_LENGTH } from 'data/DataTypes'; @Service() export default class ItemsController extends BaseController { @@ -78,44 +79,75 @@ export default class ItemsController extends BaseController { */ get validateItemSchema(): ValidationChain[] { return [ - check('name').exists(), - check('type').exists().trim().escape() + check('name').exists().isString().isLength({ max: DATATYPES_LENGTH.STRING }), + check('type').exists() + .isString() + .trim() + .escape() .isIn(['service', 'non-inventory', 'inventory']), - check('code').optional({ nullable: true }).trim().escape(), + check('code') + .optional({ nullable: true }) + .isString() + .trim() + .escape() + .isLength({ max: DATATYPES_LENGTH.STRING }), // Purchase attributes. check('purchasable').optional().isBoolean().toBoolean(), check('cost_price') + .optional({ nullable: true }) + .isFloat({ min: 0, max: DATATYPES_LENGTH.DECIMAL_13_3 }) + .toFloat() .if(check('purchasable').equals('true')) - .exists() - .isNumeric() - .toFloat(), + .exists(), check('cost_account_id') + .optional({ nullable: true }) + .isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 }) + .toInt() .if(check('purchasable').equals('true')) - .exists() - .isInt() - .toInt(), + .exists(), // Sell attributes. check('sellable').optional().isBoolean().toBoolean(), check('sell_price') + .optional({ nullable: true }) + .isFloat({ min: 0, max: DATATYPES_LENGTH.DECIMAL_13_3 }) + .toFloat() .if(check('sellable').equals('true')) - .exists() - .isNumeric() - .toFloat(), + .exists(), check('sell_account_id') + .optional({ nullable: true }) + .isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 }) + .toInt() .if(check('sellable').equals('true')) - .exists() - .isInt() - .toInt(), + .exists(), check('inventory_account_id') + .optional({ nullable: true }) + .isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 }) + .toInt() .if(check('type').equals('inventory')) - .exists() - .isInt() + .exists(), + check('sell_description') + .optional({ nullable: true }) + .isString() + .trim() + .escape() + .isLength({ max: DATATYPES_LENGTH.TEXT }), + check('cost_description') + .optional({ nullable: true }) + .isString() + .trim() + .escape() + .isLength({ max: DATATYPES_LENGTH.TEXT }), + check('category_id') + .optional({ nullable: true }) + .isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 }) .toInt(), - check('sell_description').optional({ nullable: true }).trim().escape(), - check('cost_description').optional({ nullable: true }).trim().escape(), - - check('category_id').optional({ nullable: true }).isInt().toInt(), - check('note').optional(), + check('note') + .optional() + .isString() + .trim() + .escape() + .isLength({ max: DATATYPES_LENGTH.TEXT }), + check('active').optional().isBoolean().toBoolean(), check('media_ids').optional().isArray(), check('media_ids.*').exists().isNumeric().toInt(), diff --git a/server/src/api/controllers/ManualJournals.ts b/server/src/api/controllers/ManualJournals.ts index b4ad00dc8..b375d7076 100644 --- a/server/src/api/controllers/ManualJournals.ts +++ b/server/src/api/controllers/ManualJournals.ts @@ -6,6 +6,7 @@ import ManualJournalsService from 'services/ManualJournals/ManualJournalsService import { Inject, Service } from "typedi"; import { ServiceError } from 'exceptions'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; +import { DATATYPES_LENGTH } from 'data/DataTypes'; @Service() export default class ManualJournalsController extends BaseController { @@ -113,30 +114,54 @@ export default class ManualJournalsController extends BaseController { get manualJournalValidationSchema() { return [ check('date').exists().isISO8601(), - check('journal_number').exists().trim().escape(), - check('journal_type').optional({ nullable: true }).trim().escape(), - check('reference').optional({ nullable: true }), - check('description').optional().trim().escape(), + check('journal_number') + .exists() + .isString() + .trim() + .escape() + .isLength({ max: DATATYPES_LENGTH.STRING }), + check('journal_type') + .optional({ nullable: true }) + .isString() + .trim() + .escape() + .isLength({ max: DATATYPES_LENGTH.STRING }), + check('reference') + .optional({ nullable: true }) + .trim() + .escape() + .isLength({ max: DATATYPES_LENGTH.STRING }), + check('description') + .optional() + .isString() + .trim() + .escape() + .isLength({ max: DATATYPES_LENGTH.TEXT }), check('status').optional().isBoolean().toBoolean(), check('entries').isArray({ min: 2 }), - check('entries.*.index').exists().isNumeric().toInt(), + check('entries.*.index') + .exists() + .isInt({ max: DATATYPES_LENGTH.INT_10 }).toInt(), check('entries.*.credit') .optional({ nullable: true }) .isNumeric() .isDecimal() - .isFloat({ max: 9999999999.999 }) // 13, 3 + .isFloat({ max: DATATYPES_LENGTH.INT_13_3 }) // 13, 3 .toFloat(), check('entries.*.debit') .optional({ nullable: true }) .isNumeric() .isDecimal() - .isFloat({ max: 9999999999.999 }) // 13, 3 + .isFloat({ max: DATATYPES_LENGTH.INT_13_3 }) // 13, 3 .toFloat(), - check('entries.*.account_id').isNumeric().toInt(), - check('entries.*.note').optional(), + check('entries.*.account_id').isInt({ max: DATATYPES_LENGTH.INT_10 }).toInt(), + check('entries.*.note') + .optional() + .isString() + .isLength({ max: DATATYPES_LENGTH.STRING }), check('entries.*.contact_id') .optional({ nullable: true }) - .isNumeric() + .isInt({ max: DATATYPES_LENGTH.INT_10 }) .toInt(), check('entries.*.contact_type').optional().isIn(['vendor', 'customer']), ] diff --git a/server/src/data/DataTypes.js b/server/src/data/DataTypes.js new file mode 100644 index 000000000..d5ab1086f --- /dev/null +++ b/server/src/data/DataTypes.js @@ -0,0 +1,8 @@ + +export const DATATYPES_LENGTH = { + STRING: 255, + TEXT: 65535, + INT_10: 4294967295, + DECIMAL_13_3: 9999999999.999, + DECIMAL_15_5: 999999999999.999, +}; diff --git a/server/src/database/migrations/20190822214306_create_items_table.js b/server/src/database/migrations/20190822214306_create_items_table.js index b85d66e7d..83f03280a 100644 --- a/server/src/database/migrations/20190822214306_create_items_table.js +++ b/server/src/database/migrations/20190822214306_create_items_table.js @@ -18,6 +18,7 @@ exports.up = function (knex) { table.text('purchase_description').nullable(); table.integer('quantity_on_hand'); table.text('note').nullable(); + table.boolean('active'); table.integer('category_id').unsigned().index().references('id').inTable('items_categories'); table.integer('user_id').unsigned().index(); table.timestamps(); diff --git a/server/src/interfaces/Item.ts b/server/src/interfaces/Item.ts index 2726fbae0..f5cf5726f 100644 --- a/server/src/interfaces/Item.ts +++ b/server/src/interfaces/Item.ts @@ -22,6 +22,7 @@ export interface IItem{ quantityOnHand: number, note: string, + active: boolean, categoryId: number, userId: number, @@ -52,6 +53,7 @@ export interface IItemDTO { quantityOnHand: number, note: string, + active: boolean, categoryId: number, } diff --git a/server/src/services/Contacts/CustomersService.ts b/server/src/services/Contacts/CustomersService.ts index 2f801d0e2..509512dfe 100644 --- a/server/src/services/Contacts/CustomersService.ts +++ b/server/src/services/Contacts/CustomersService.ts @@ -69,7 +69,7 @@ export default class CustomersService { private transformContactToCustomer(contactModel: IContact) { return { - ...omit(contactModel, ['contactService', 'contactType']), + ...omit(contactModel.toJSON(), ['contactService', 'contactType']), customerType: contactModel.contactType, }; } @@ -174,7 +174,11 @@ export default class CustomersService { public async getCustomersList( tenantId: number, customersFilter: ICustomersFilter - ): Promise<{ customers: ICustomer[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> { + ): Promise<{ + customers: ICustomer[], + pagination: IPaginationMeta, + filterMeta: IFilterMeta, + }> { const { Contact } = this.tenancy.models(tenantId); const dynamicList = await this.dynamicListService.dynamicList(tenantId, Contact, customersFilter); diff --git a/server/src/services/ItemCategories/ItemCategoriesService.ts b/server/src/services/ItemCategories/ItemCategoriesService.ts index 3626bf88b..d7e9e73b2 100644 --- a/server/src/services/ItemCategories/ItemCategoriesService.ts +++ b/server/src/services/ItemCategories/ItemCategoriesService.ts @@ -224,7 +224,11 @@ export default class ItemCategoriesService implements IItemCategoriesService { */ public async deleteItemCategory(tenantId: number, itemCategoryId: number, authorizedUser: ISystemUser) { this.logger.info('[item_category] trying to delete item category.', { tenantId, itemCategoryId }); + + // Retrieve item category or throw not found error. await this.getItemCategoryOrThrowError(tenantId, itemCategoryId); + + // Unassociate items with item category. await this.unassociateItemsWithCategories(tenantId, itemCategoryId); const { ItemCategory } = this.tenancy.models(tenantId); @@ -265,6 +269,9 @@ export default class ItemCategoriesService implements IItemCategoriesService { const dynamicList = await this.dynamicListService.dynamicList(tenantId, ItemCategory, filter); const itemCategories = await ItemCategory.query().onBuild((query) => { + // Subquery to calculate sumation of assocaited items to the item category. + query.select('*', ItemCategory.relatedQuery('items').count().as('count')); + dynamicList.buildQuery()(query); }); return { itemCategories, filterMeta: dynamicList.getResponseMeta() }; @@ -276,11 +283,14 @@ export default class ItemCategoriesService implements IItemCategoriesService { * @param {number|number[]} itemCategoryId - * @return {Promise} */ - private async unassociateItemsWithCategories(tenantId: number, itemCategoryId: number|number[]): Promise { + private async unassociateItemsWithCategories( + tenantId: number, + itemCategoryId: number | number[], + ): Promise { const { Item } = this.tenancy.models(tenantId); const ids = Array.isArray(itemCategoryId) ? itemCategoryId : [itemCategoryId]; - await Item.query().whereIn('id', ids).patch({ category_id: null }); + await Item.query().whereIn('category_id', ids).patch({ category_id: null }); } /** @@ -288,7 +298,11 @@ export default class ItemCategoriesService implements IItemCategoriesService { * @param {number} tenantId * @param {number[]} itemCategoriesIds */ - public async deleteItemCategories(tenantId: number, itemCategoriesIds: number[], authorizedUser: ISystemUser) { + public async deleteItemCategories( + tenantId: number, + itemCategoriesIds: number[], + authorizedUser: ISystemUser, + ) { this.logger.info('[item_category] trying to delete item categories.', { tenantId, itemCategoriesIds }); const { ItemCategory } = this.tenancy.models(tenantId); diff --git a/server/src/services/Items/ItemsService.ts b/server/src/services/Items/ItemsService.ts index 3597219c4..298cb3468 100644 --- a/server/src/services/Items/ItemsService.ts +++ b/server/src/services/Items/ItemsService.ts @@ -1,4 +1,4 @@ -import { difference } from "lodash"; +import { defaultTo, difference } from "lodash"; import { Service, Inject } from "typedi"; import { IItemsFilter, IItemsService, IItemDTO, IItem } from 'interfaces'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; @@ -176,7 +176,10 @@ export default class ItemsService implements IItemsService { if (itemDTO.inventoryAccountId) { await this.validateItemInventoryAccountExistance(tenantId, itemDTO.inventoryAccountId); } - const storedItem = await Item.query().insertAndFetch({ ...itemDTO }); + const storedItem = await Item.query().insertAndFetch({ + ...itemDTO, + active: defaultTo(itemDTO.active, 1), + }); this.logger.info('[items] item inserted successfully.', { tenantId, itemDTO }); return storedItem;