Initial content
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
import { catalog_repository } from ".";
|
||||
import { Cart } from "./cart_models";
|
||||
import { Product } from "./catalog_models"
|
||||
|
||||
export interface CartDetail {
|
||||
lines: {
|
||||
product: Product,
|
||||
quantity: number,
|
||||
subtotal: number
|
||||
}[],
|
||||
total: number;
|
||||
}
|
||||
|
||||
export const getCartDetail = async (cart: Cart) : Promise<CartDetail> => {
|
||||
|
||||
const ids = cart.lines.map(l => l.productId);
|
||||
const db_data = await catalog_repository.getProductDetails(ids);
|
||||
|
||||
const products = Object.fromEntries(db_data.map(p => [p.id, p]));
|
||||
|
||||
const lines = cart.lines.map(line => ({
|
||||
product: products[line.productId],
|
||||
quantity: line.quantity,
|
||||
subtotal: products[line.productId].price * line.quantity
|
||||
}));
|
||||
|
||||
const total = lines.reduce((total, line) => total + line.subtotal, 0);
|
||||
|
||||
return { lines, total }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
export interface CartLine {
|
||||
productId: number;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
export interface Cart {
|
||||
lines: CartLine[];
|
||||
}
|
||||
|
||||
export const createCart = () : Cart => ({ lines: [] });
|
||||
|
||||
export const addLine = (cart: Cart, productId: number, quantity: number) => {
|
||||
const line = cart.lines.find(l => l.productId == productId);
|
||||
if (line !== undefined) {
|
||||
line.quantity += quantity;
|
||||
} else {
|
||||
cart.lines.push({ productId, quantity })
|
||||
}
|
||||
}
|
||||
|
||||
export const removeLine = (cart: Cart, productId: number) => {
|
||||
cart.lines = cart.lines.filter(l => l.productId !== productId);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
export interface Product {
|
||||
id?: number;
|
||||
name: string;
|
||||
description: string;
|
||||
price: number;
|
||||
|
||||
category?: Category;
|
||||
supplier?: Supplier;
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
id?: number;
|
||||
name: string;
|
||||
|
||||
products?: Product[];
|
||||
}
|
||||
|
||||
export interface Supplier {
|
||||
id?: number;
|
||||
name: string;
|
||||
|
||||
products?: Product[];
|
||||
}
|
||||
|
||||
export interface ProductQueryParameters {
|
||||
pageSize?: number;
|
||||
page?: number;
|
||||
category?: number;
|
||||
searchTerm?: string;
|
||||
}
|
||||
|
||||
export interface ProductQueryResult {
|
||||
products: Product[];
|
||||
totalCount: number;
|
||||
categories: Category[];
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Category, Product, Supplier, ProductQueryParameters,
|
||||
ProductQueryResult } from "./catalog_models";
|
||||
|
||||
export interface CatalogRepository {
|
||||
|
||||
getProducts(params?: ProductQueryParameters): Promise<ProductQueryResult>;
|
||||
|
||||
getProductDetails(ids: number[]): Promise<Product[]>;
|
||||
|
||||
storeProduct(p: Product): Promise<Product>;
|
||||
|
||||
getCategories() : Promise<Category[]>;
|
||||
|
||||
storeCategory(c: Category): Promise<Category>;
|
||||
|
||||
getSuppliers(): Promise<Supplier[]>;
|
||||
|
||||
storeSupplier(s: Supplier): Promise<Supplier>;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface Customer {
|
||||
id?: number;
|
||||
name: string;
|
||||
email: string;
|
||||
|
||||
federatedId?: string;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Customer } from "./customer_models";
|
||||
import { Address } from "./order_models";
|
||||
|
||||
export interface CustomerRepository {
|
||||
|
||||
getCustomer(id: number) : Promise<Customer | null>;
|
||||
|
||||
getCustomerByFederatedId(id: string): Promise<Customer | null>;
|
||||
|
||||
getCustomerAddress(id: number): Promise<Address | null>;
|
||||
|
||||
storeCustomer(customer: Customer): Promise<Customer>;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { CatalogRepository } from "./catalog_repository";
|
||||
import { CatalogRepoImpl} from "./orm";
|
||||
import { OrderRepository } from "./order_repository";
|
||||
import { CustomerRepository } from "./customer_repository";
|
||||
|
||||
const repo = new CatalogRepoImpl();
|
||||
|
||||
export const catalog_repository: CatalogRepository = repo;
|
||||
export const order_repository: OrderRepository = repo;
|
||||
export const customer_repository: CustomerRepository = repo;
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Product } from "./catalog_models";
|
||||
import { Customer } from "./customer_models";
|
||||
|
||||
export interface Order {
|
||||
id?: number;
|
||||
|
||||
customer?: Customer;
|
||||
selections?: ProductSelection[];
|
||||
address?: Address;
|
||||
|
||||
shipped: boolean;
|
||||
}
|
||||
|
||||
export interface ProductSelection {
|
||||
id?: number;
|
||||
productId?: number;
|
||||
quantity: number;
|
||||
price: number;
|
||||
}
|
||||
|
||||
export interface Address {
|
||||
id?: number;
|
||||
street: string;
|
||||
city: string;
|
||||
state: string;
|
||||
zip: string;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Order } from "./order_models";
|
||||
|
||||
export interface OrderRepository {
|
||||
|
||||
getOrder(id: number): Promise<Order| null>;
|
||||
|
||||
getOrders(excludeShipped: boolean): Promise<Order[]>;
|
||||
|
||||
storeOrder(order: Order): Promise<Order>;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Sequelize } from "sequelize";
|
||||
import { getConfig } from "../../config";
|
||||
import { initializeModels, CategoryModel, ProductModel, SupplierModel }
|
||||
from "./models";
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
const config = getConfig("catalog:orm_repo");
|
||||
const logging = config.logging
|
||||
? { logging: console.log, logQueryParameters: true}
|
||||
: { logging: false };
|
||||
|
||||
export class BaseRepo {
|
||||
sequelize: Sequelize;
|
||||
|
||||
constructor() {
|
||||
this.sequelize = new Sequelize({ ...config.settings, ...logging })
|
||||
this.initModelsAndDatabase();
|
||||
}
|
||||
|
||||
async initModelsAndDatabase() : Promise<void> {
|
||||
initializeModels(this.sequelize);
|
||||
if (config.reset_db) {
|
||||
await this.sequelize.drop();
|
||||
await this.sequelize.sync();
|
||||
await this.addSeedData();
|
||||
} else {
|
||||
await this.sequelize.sync();
|
||||
}
|
||||
}
|
||||
|
||||
async addSeedData() {
|
||||
const data = JSON.parse(readFileSync(config.seed_file).toString());
|
||||
await this.sequelize.transaction(async (transaction) => {
|
||||
await SupplierModel.bulkCreate(data.suppliers, { transaction });
|
||||
await CategoryModel.bulkCreate(data.categories, { transaction });
|
||||
await ProductModel.bulkCreate(data.products, { transaction });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export type Constructor<T = {}> = new (...args: any[]) => T;
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Customer } from "../customer_models";
|
||||
import { CustomerRepository } from "../customer_repository";
|
||||
import { Address } from "../order_models";
|
||||
import { BaseRepo, Constructor } from "./core"
|
||||
import { CustomerModel } from "./models/customer_models";
|
||||
import { AddressModel, OrderModel } from "./models/order_models";
|
||||
|
||||
export function AddCustomers<TBase extends
|
||||
Constructor<BaseRepo>>(Base: TBase) {
|
||||
|
||||
return class extends Base implements CustomerRepository {
|
||||
|
||||
getCustomer(id: number): Promise<Customer | null> {
|
||||
return CustomerModel.findByPk(id, {
|
||||
raw: true
|
||||
});
|
||||
}
|
||||
|
||||
getCustomerByFederatedId(id: string): Promise<Customer | null> {
|
||||
return CustomerModel.findOne({
|
||||
where: { federatedId: id },
|
||||
raw: true
|
||||
})
|
||||
}
|
||||
|
||||
getCustomerAddress(id: number): Promise<Address | null> {
|
||||
return AddressModel.findOne({
|
||||
include: [{
|
||||
model: OrderModel,
|
||||
where: { customerId: id },
|
||||
attributes: []
|
||||
}],
|
||||
order: [["updatedAt", "DESC"]]
|
||||
});
|
||||
}
|
||||
|
||||
async storeCustomer(customer: Customer): Promise<Customer> {
|
||||
const [data, created] = await CustomerModel.findOrCreate({
|
||||
where: { email: customer.email },
|
||||
defaults: customer,
|
||||
});
|
||||
if (!created) {
|
||||
data.name = customer.name;
|
||||
data.email = customer.email;
|
||||
data.federatedId = customer.federatedId;
|
||||
await data.save();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { BaseRepo } from "./core";
|
||||
import { AddQueries } from "./queries";
|
||||
import { AddStorage } from "./storage";
|
||||
import { AddOrderQueries } from "./order_queries";
|
||||
import { AddOrderStorage } from "./order_storage";
|
||||
import { AddCustomers } from "./customers";
|
||||
|
||||
const CatalogRepo = AddStorage(AddQueries(BaseRepo));
|
||||
const RepoWithOrders = AddOrderStorage(AddOrderQueries(CatalogRepo));
|
||||
const RepoWithCustomers = AddCustomers(RepoWithOrders);
|
||||
|
||||
export const CatalogRepoImpl = RepoWithCustomers;
|
||||
@@ -0,0 +1,35 @@
|
||||
import { DataTypes, Sequelize } from "sequelize";
|
||||
import { CategoryModel, ProductModel, SupplierModel } from "./catalog_models";
|
||||
|
||||
const primaryKey = {
|
||||
id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true }
|
||||
};
|
||||
|
||||
export const initializeCatalogModels = (sequelize: Sequelize) => {
|
||||
|
||||
ProductModel.init({
|
||||
...primaryKey,
|
||||
name: { type: DataTypes.STRING},
|
||||
description: { type: DataTypes.STRING},
|
||||
price: { type: DataTypes.DECIMAL(10, 2) }
|
||||
}, { sequelize })
|
||||
|
||||
CategoryModel.init({
|
||||
...primaryKey,
|
||||
name: { type: DataTypes.STRING}
|
||||
}, { sequelize });
|
||||
|
||||
SupplierModel.init({
|
||||
...primaryKey,
|
||||
name: { type: DataTypes.STRING}
|
||||
}, { sequelize})
|
||||
|
||||
ProductModel.belongsTo(CategoryModel,
|
||||
{ foreignKey: "categoryId", as: "category"});
|
||||
ProductModel.belongsTo(SupplierModel,
|
||||
{ foreignKey: "supplierId", as: "supplier"});
|
||||
CategoryModel.hasMany(ProductModel,
|
||||
{ foreignKey: "categoryId", as: "products"});
|
||||
SupplierModel.hasMany(ProductModel,
|
||||
{ foreignKey: "supplierId", as: "products"});
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Model, CreationOptional, ForeignKey, InferAttributes,
|
||||
InferCreationAttributes } from "sequelize";
|
||||
|
||||
export class ProductModel extends Model<InferAttributes<ProductModel>,
|
||||
InferCreationAttributes<ProductModel>> {
|
||||
|
||||
declare id?: CreationOptional<number>;
|
||||
|
||||
declare name: string;
|
||||
declare description: string;
|
||||
declare price: number;
|
||||
|
||||
declare categoryId: ForeignKey<CategoryModel["id"]>;
|
||||
declare supplierId: ForeignKey<SupplierModel["id"]>;
|
||||
|
||||
declare category?: InferAttributes<CategoryModel>
|
||||
declare supplier?: InferAttributes<SupplierModel>
|
||||
}
|
||||
|
||||
export class CategoryModel extends Model<InferAttributes<CategoryModel>,
|
||||
InferCreationAttributes<CategoryModel>> {
|
||||
|
||||
declare id?: CreationOptional<number>;
|
||||
declare name: string;
|
||||
|
||||
declare products?: InferAttributes<ProductModel>[];
|
||||
}
|
||||
|
||||
export class SupplierModel extends Model<InferAttributes<SupplierModel>,
|
||||
InferCreationAttributes<SupplierModel>> {
|
||||
|
||||
declare id?: CreationOptional<number>;
|
||||
declare name: string;
|
||||
|
||||
declare products?: InferAttributes<ProductModel>[];
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { DataTypes, Sequelize } from "sequelize";
|
||||
import { CustomerModel } from "./customer_models";
|
||||
|
||||
export const initializeCustomerModels = (sequelize: Sequelize) => {
|
||||
|
||||
CustomerModel.init({
|
||||
id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true},
|
||||
name: { type: DataTypes.STRING},
|
||||
email: { type: DataTypes.STRING },
|
||||
federatedId: { type: DataTypes.STRING }
|
||||
}, { sequelize})
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Model, CreationOptional, InferAttributes, InferCreationAttributes }
|
||||
from "sequelize";
|
||||
import { Customer } from "../../customer_models";
|
||||
|
||||
export class CustomerModel extends Model<InferAttributes<CustomerModel>,
|
||||
InferCreationAttributes<CustomerModel>> implements Customer {
|
||||
|
||||
declare id?: CreationOptional<number>;
|
||||
declare name: string;
|
||||
declare email: string;
|
||||
|
||||
declare federatedId?: string;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Sequelize } from "sequelize";
|
||||
import { initializeCatalogModels } from "./catalog_helpers";
|
||||
import { initializeCustomerModels } from "./customer_helpers";
|
||||
import { initializeOrderModels } from "./order_helpers";
|
||||
|
||||
export { ProductModel, CategoryModel, SupplierModel } from "./catalog_models";
|
||||
|
||||
export const initializeModels = (sequelize: Sequelize) => {
|
||||
initializeCatalogModels(sequelize);
|
||||
initializeCustomerModels(sequelize);
|
||||
initializeOrderModels(sequelize);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { DataTypes, Sequelize } from "sequelize";
|
||||
import { OrderModel, ProductSelectionModel, AddressModel }
|
||||
from "./order_models";
|
||||
import { CustomerModel } from "./customer_models";
|
||||
import { ProductModel } from ".";
|
||||
|
||||
const primaryKey = {
|
||||
id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true }
|
||||
};
|
||||
|
||||
export const initializeOrderModels = (sequelize: Sequelize) => {
|
||||
|
||||
OrderModel.init({
|
||||
...primaryKey, shipped: DataTypes.BOOLEAN
|
||||
}, {sequelize});
|
||||
|
||||
ProductSelectionModel.init({
|
||||
...primaryKey,
|
||||
quantity: DataTypes.INTEGER, price: DataTypes.DECIMAL(10, 2)
|
||||
}, {sequelize});
|
||||
|
||||
AddressModel.init({
|
||||
...primaryKey,
|
||||
street: DataTypes.STRING, city: DataTypes.STRING,
|
||||
state: DataTypes.STRING, zip: DataTypes.STRING,
|
||||
}, {sequelize});
|
||||
|
||||
OrderModel.belongsTo(CustomerModel, { as: "customer"});
|
||||
OrderModel.belongsTo(AddressModel,
|
||||
{foreignKey: "addressId", as: "address"});
|
||||
OrderModel.belongsToMany(ProductSelectionModel,
|
||||
{ through: "OrderProductJunction",
|
||||
foreignKey: "orderId", as: "selections" });
|
||||
ProductSelectionModel.belongsTo(ProductModel, { as: "product"});
|
||||
|
||||
AddressModel.hasMany(OrderModel, { foreignKey: "addressId"});
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Model, CreationOptional, ForeignKey, InferAttributes,
|
||||
InferCreationAttributes,
|
||||
HasManySetAssociationsMixin} from "sequelize";
|
||||
import { ProductModel } from "./catalog_models";
|
||||
import { CustomerModel } from "./customer_models";
|
||||
import { Address, Order, ProductSelection } from "../../order_models";
|
||||
|
||||
export class OrderModel extends Model<InferAttributes<OrderModel>,
|
||||
InferCreationAttributes<OrderModel>> implements Order {
|
||||
|
||||
declare id?: CreationOptional<number>;
|
||||
declare shipped: boolean;
|
||||
|
||||
declare customerId: ForeignKey<CustomerModel["id"]>;
|
||||
declare customer?: InferAttributes<CustomerModel>
|
||||
|
||||
declare addressId: ForeignKey<AddressModel["id"]>;
|
||||
declare address?: InferAttributes<AddressModel>;
|
||||
|
||||
declare selections?: InferAttributes<ProductSelectionModel>[];
|
||||
|
||||
declare setSelections:
|
||||
HasManySetAssociationsMixin<ProductSelectionModel, number>;
|
||||
}
|
||||
|
||||
export class ProductSelectionModel extends
|
||||
Model<InferAttributes<ProductSelectionModel>,
|
||||
InferCreationAttributes<ProductSelectionModel>>
|
||||
implements ProductSelection {
|
||||
|
||||
declare id?: CreationOptional<number>;
|
||||
|
||||
declare productId: ForeignKey<ProductModel["id"]>;
|
||||
declare product?: InferAttributes<ProductModel>
|
||||
|
||||
declare quantity: number;
|
||||
declare price: number;
|
||||
|
||||
declare orderId: ForeignKey<OrderModel["id"]>;
|
||||
declare order?: InferAttributes<OrderModel>;
|
||||
}
|
||||
|
||||
export class AddressModel extends Model<InferAttributes<AddressModel>,
|
||||
InferCreationAttributes<AddressModel>> implements Address {
|
||||
|
||||
declare id?: CreationOptional<number>;
|
||||
|
||||
declare street: string;
|
||||
declare city: string;
|
||||
declare state: string;
|
||||
declare zip: string;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Attributes, FindOptions } from "sequelize";
|
||||
import { Order } from "../order_models"
|
||||
import { BaseRepo, Constructor } from "./core"
|
||||
import { AddressModel, OrderModel } from "./models/order_models";
|
||||
import { CustomerModel } from "./models/customer_models";
|
||||
|
||||
const queryConfig: FindOptions<Attributes<OrderModel>> = {
|
||||
include: [
|
||||
{ model: AddressModel, as: "address"},
|
||||
{ model: CustomerModel, as: "customer" }
|
||||
],
|
||||
raw: true, nest: true
|
||||
}
|
||||
|
||||
export function AddOrderQueries<TBase
|
||||
extends Constructor<BaseRepo>>(Base: TBase) {
|
||||
|
||||
return class extends Base {
|
||||
|
||||
getOrder(id: number) : Promise<Order | null> {
|
||||
return OrderModel.findByPk(id, queryConfig);
|
||||
}
|
||||
|
||||
getOrders(excludeShipped: boolean): Promise<Order[]> {
|
||||
return OrderModel.findAll(
|
||||
excludeShipped ?
|
||||
{ ...queryConfig, where: { shipped: false}} : queryConfig
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Order } from "../order_models"
|
||||
import { BaseRepo, Constructor } from "./core"
|
||||
import { AddressModel, OrderModel, ProductSelectionModel }
|
||||
from "./models/order_models";
|
||||
import { CustomerModel } from "./models/customer_models";
|
||||
|
||||
export function AddOrderStorage<TBase extends
|
||||
Constructor<BaseRepo>>(Base: TBase) {
|
||||
|
||||
return class extends Base {
|
||||
|
||||
storeOrder(order: Order): Promise<Order> {
|
||||
return this.sequelize.transaction(async (transaction) => {
|
||||
const { id, shipped } = order;
|
||||
const [stored] =
|
||||
await OrderModel.upsert({ id, shipped }, {transaction});
|
||||
|
||||
if (order.customer) {
|
||||
|
||||
const [{id}] = await CustomerModel.findOrCreate({
|
||||
where: { email: order.customer.email},
|
||||
defaults: order.customer,
|
||||
transaction
|
||||
});
|
||||
stored.customerId = id;
|
||||
}
|
||||
if (order.address) {
|
||||
|
||||
const [{id}] = await AddressModel.findOrCreate({
|
||||
where: { ...order.address },
|
||||
defaults: order.address,
|
||||
transaction
|
||||
});
|
||||
stored.addressId = id;
|
||||
}
|
||||
await stored.save({transaction});
|
||||
if (order.selections) {
|
||||
const sels = await ProductSelectionModel.bulkCreate(
|
||||
order.selections, { transaction});
|
||||
await stored.setSelections(
|
||||
sels, { transaction });
|
||||
}
|
||||
return stored;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { CategoryModel, ProductModel, SupplierModel } from "./models";
|
||||
import { BaseRepo, Constructor } from "./core"
|
||||
import { ProductQueryParameters } from "../catalog_models";
|
||||
import { Op } from "sequelize";
|
||||
|
||||
export function AddQueries<TBase extends Constructor<BaseRepo>>(Base: TBase) {
|
||||
return class extends Base {
|
||||
|
||||
async getProducts(params?: ProductQueryParameters) {
|
||||
const opts: any = {};
|
||||
if (params?.page && params.pageSize) {
|
||||
opts.limit = params?.pageSize,
|
||||
opts.offset = (params.page -1) * params.pageSize
|
||||
}
|
||||
if(params?.searchTerm) {
|
||||
const searchOp = { [Op.like]: "%" + params.searchTerm + "%"};
|
||||
opts.where = {
|
||||
[Op.or]: { name: searchOp, description: searchOp }
|
||||
}
|
||||
}
|
||||
if (params?.category) {
|
||||
opts.where = {
|
||||
...opts.where, categoryId: params.category
|
||||
}
|
||||
}
|
||||
const result = await ProductModel.findAndCountAll({
|
||||
include: [
|
||||
{model: SupplierModel, as: "supplier" },
|
||||
{model: CategoryModel, as: "category"}],
|
||||
raw: true, nest: true,
|
||||
...opts
|
||||
});
|
||||
const categories = await this.getCategories();
|
||||
return { products: result.rows, totalCount: result.count, categories };
|
||||
}
|
||||
|
||||
getCategories() {
|
||||
return CategoryModel.findAll({raw: true, nest: true})
|
||||
}
|
||||
|
||||
getSuppliers() {
|
||||
return SupplierModel.findAll({raw: true, nest: true});
|
||||
}
|
||||
|
||||
getProductDetails(ids: number[]) {
|
||||
return ProductModel.findAll({
|
||||
where: { id: { [Op.in]: ids }}, raw: true, nest: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Transaction } from "sequelize";
|
||||
import { Category, Product, Supplier } from "../catalog_models";
|
||||
import { CategoryModel, ProductModel, SupplierModel } from "./models";
|
||||
import { BaseRepo, Constructor } from "./core"
|
||||
|
||||
export function AddStorage<TBase extends Constructor<BaseRepo>>(Base: TBase) {
|
||||
return class extends Base {
|
||||
|
||||
storeProduct(p: Product) {
|
||||
return this.sequelize.transaction(async (transaction) => {
|
||||
|
||||
if (p.category) {
|
||||
p.category = await this.storeCategory(p.category)
|
||||
}
|
||||
if (p.supplier) {
|
||||
p.supplier = await this.storeSupplier(p.supplier);
|
||||
}
|
||||
|
||||
const [stored] = await ProductModel.upsert({
|
||||
id: p.id, name: p.name, description: p.description,
|
||||
price: p.price, categoryId: p.category?.id,
|
||||
supplierId: p.supplier?.id
|
||||
}, { transaction });
|
||||
return stored;
|
||||
});
|
||||
}
|
||||
|
||||
async storeCategory(c: Category, transaction?: Transaction) {
|
||||
const [stored] = await CategoryModel.upsert({
|
||||
id: c.id, name: c.name
|
||||
}, { transaction});
|
||||
return stored;
|
||||
}
|
||||
|
||||
async storeSupplier(s: Supplier, transaction?: Transaction) {
|
||||
const [stored] = await SupplierModel.upsert({
|
||||
id: s.id, name: s.name
|
||||
}, {transaction});
|
||||
return stored;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import validator from "validator";
|
||||
import { ValidationStatus } from "./validation_types";
|
||||
|
||||
export const minLength = (min: number) => (status: ValidationStatus) => {
|
||||
if (!validator.isLength(status.value, { min })) {
|
||||
status.setInvalid(true);
|
||||
status.messages.push(`Enter at least ${min} characters`);
|
||||
}
|
||||
};
|
||||
|
||||
export const email = (status: ValidationStatus) => {
|
||||
if (!validator.isEmail(status.value)) {
|
||||
status.setInvalid(true);
|
||||
status.messages.push("Enter an email address");
|
||||
}
|
||||
};
|
||||
|
||||
export const required = (status: ValidationStatus) => {
|
||||
if (validator.isEmpty(status.value.toString(), { ignore_whitespace: true})) {
|
||||
status.setInvalid(true);
|
||||
status.messages.push("A value is required");
|
||||
}
|
||||
};
|
||||
|
||||
export const no_op = (status: ValidationStatus) => { /* do nothing */ }
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./validation_types";
|
||||
export * from "./validator";
|
||||
export * from "./basic_rules";
|
||||
export * from "./order_rules";
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Validator } from "./validator";
|
||||
import { required, minLength, email, no_op } from "./basic_rules";
|
||||
import { Address } from "../order_models";
|
||||
import { Customer } from "../customer_models";
|
||||
|
||||
export const CustomerValidator = new Validator<Customer>({
|
||||
name: [required, minLength(6)],
|
||||
email: email,
|
||||
federatedId: no_op
|
||||
});
|
||||
|
||||
export const AddressValidator = new Validator<Address>({
|
||||
street: required,
|
||||
city: required,
|
||||
state: required,
|
||||
zip: no_op
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
export class ValidationStatus {
|
||||
private invalid: boolean = false;
|
||||
|
||||
constructor(public readonly value: any) {}
|
||||
|
||||
get isInvalid() : boolean {
|
||||
return this.invalid
|
||||
}
|
||||
|
||||
setInvalid(newValue: boolean) {
|
||||
this.invalid = newValue || this.invalid;
|
||||
}
|
||||
|
||||
messages: string[] = [];
|
||||
}
|
||||
|
||||
export type ValidationRule = (status: ValidationStatus)
|
||||
=> void | Promise<void>;
|
||||
|
||||
export type ValidationRuleSet<T> = {
|
||||
[key in keyof Omit<Required<T>, "id">]: ValidationRule | ValidationRule[];
|
||||
}
|
||||
|
||||
export type ValidationResults<T> = {
|
||||
[key in keyof Omit<Required<T>, "id">]: ValidationStatus;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ValidationResults, ValidationRule, ValidationRuleSet,
|
||||
ValidationStatus } from "./validation_types";
|
||||
|
||||
export class Validator<T>{
|
||||
|
||||
constructor(public rules: ValidationRuleSet<T>,
|
||||
public breakOnInvalid = true) {}
|
||||
|
||||
async validate(data: any): Promise<ValidationResults<T>> {
|
||||
const vdata = Object.entries(this.rules).map(async ([key, rules]) => {
|
||||
const status = new ValidationStatus(data?.[key] ?? "");
|
||||
const rs = (Array.isArray(rules) ? rules: [rules]);
|
||||
for (const r of rs) {
|
||||
if (!status.isInvalid || !this.breakOnInvalid) {
|
||||
await r(status);
|
||||
}
|
||||
}
|
||||
return [key, status];
|
||||
});
|
||||
const done = await Promise.all(vdata);
|
||||
return Object.fromEntries(done);
|
||||
}
|
||||
|
||||
validateOriginal(data: any): ValidationResults<T> {
|
||||
const vdata = Object.entries(this.rules).map(([key, rules]) => {
|
||||
const status = new ValidationStatus(data?.[key] ?? "");
|
||||
(Array.isArray(rules) ? rules: [rules])
|
||||
.forEach(async (rule: ValidationRule) => {
|
||||
if (!status.isInvalid || !this.breakOnInvalid) {
|
||||
await rule(status);
|
||||
}
|
||||
});
|
||||
return [key, status];
|
||||
});
|
||||
return Object.fromEntries(vdata);
|
||||
}
|
||||
}
|
||||
|
||||
export function isValid<T>(result: ValidationResults<T>) {
|
||||
return Object.values<ValidationStatus>(result)
|
||||
.every(r => r.isInvalid === false);
|
||||
}
|
||||
|
||||
export function getData<T>(result: ValidationResults<T>): T {
|
||||
return Object.fromEntries (Object.entries<ValidationStatus>(result)
|
||||
.map(([key, status]) => [key, status.value])) as T;
|
||||
}
|
||||
Reference in New Issue
Block a user