Database module on TypeScript. Too many type definitions
Core Review
Hello, I'm developing relatively simple car rental service app. I decided to use TypeScript for backend.
The question is about database module. Now there are three entities: car, tariff и photo. I use MongoDB as database.
General types are extracted tp separate file src/lib/types.ts
:
// Currency
export enum Currency {
USD = "USD",
EUR = "EUR",
}
// Price
export interface Price {
value: number;
currency: Currency;
}
// DB Entity
export interface Entity {
id: string;
createdAt: Date;
}
// Tariff
export interface TariffCreateFields {
days: number;
currency: Currency;
price: number;
}
export interface Tariff extends Entity {
days: number;
currency: Currency;
price: number;
}
// Photo
export interface PhotoCreateFields {
fileKey: string;
}
export interface Photo extends Entity {
fileKey: string;
}
// Car
export interface CarCreateFields {
model: string;
year?: string;
// ... some other fields ...
collateral: Price;
tariffs: TariffCreateFields;
photos: PhotoCreateFields;
}
export interface Car extends Entity {
model: string;
year?: string;
// ... some other fields ...
collateral: Price;
tariffs: Tariff;
photos: Photo;
}
This is car entity in src/db/models/car.ts
:
import mongoose from "mongoose";
import {
Car,
CarCreateFields,
Currency,
Photo,
Tariff,
} from "../../lib/types";
import {u} from "../../lib/utils";
import {photo} from "./photo";
import {tariff} from "./tariff";
interface CarSchema {
createdAt: Date;
model: string;
year?: string;
// ... some other fields ...
collateralValue: number;
collateralCurrency: Currency;
}
interface CarCreateDocument {
model: string;
year?: string;
// ... some other fields ...
collateralValue: number;
collateralCurrency: Currency;
}
type CarModel = CarSchema & mongoose.Document;
const carSchema = new mongoose.Schema({
createdAt: {type: Date, required: true, default: () => new Date()},
model: {type: String, required: true},
year: String,
// ... some other fields ...
collateralValue: Number,
collateralCurrency: String,
});
const fromDb = (m: CarModel, tariffs: Tariff, photos: Photo): Car => ({
id: m.id,
createdAt: m.createdAt,
model: m.model,
year: u(m.year),
// ... some other fields ...
collateral: {
value: m.collateralValue || 0,
currency: m.collateralCurrency || Currency.USD,
},
tariffs,
photos,
});
const toDb = (f: CarCreateFields): CarCreateDocument => ({
model: f.model,
year: f.year,
// ... some other fields ...
collateralCurrency: f.collateral.currency,
collateralValue: f.collateral.value,
});
const CarModel = mongoose.model<CarModel>("Car", carSchema);
export namespace car {
export const create = async (fields: CarCreateFields): Promise<Car> => {
let dbCar = new CarModel(toDb(fields));
dbCar = await dbCar.save();
const tariffs = await tariff.createAllOf(dbCar.id, fields.tariffs);
const photos = await photo.createAllOf(dbCar.id, fields.photos);
return fromDb(dbCar, tariffs, photos);
};
export const list = async (): Promise<Car> => {
const dbCarList = await CarModel.find().exec();
return await Promise.all(
dbCarList.map(async (dbCar) => {
const tariffs = await tariff.getAllOf(dbCar.id);
const photos = await photo.getAllOf(dbCar.id);
return fromDb(dbCar, tariffs, photos);
}),
);
};
export const get = async (carId: string): Promise<Car | undefined> => {
const dbCar = await CarModel.findById(carId).exec();
if (dbCar === null) {
return undefined;
}
const tariffs = await tariff.getAllOf(dbCar.id);
const photos = await photo.getAllOf(dbCar.id);
return fromDb(dbCar, tariffs, photos);
};
export const update = async (carId: string, fields: CarCreateFields): Promise<void> => {
await CarModel.updateOne({_id: carId}, {$set: toDb(fields)}, {upsert: true}).then(() => undefined);
await tariff.updateAllOf(carId, fields.tariffs);
await photo.updateAllOf(carId, fields.photos);
};
export const remove = async (carId: string): Promise<void> => {
await tariff.removeAllOf(carId);
await photo.removeAllOf(carId);
await CarModel.deleteOne({_id: carId}).then(() => undefined);
};
export const drop = (): Promise<void> => {
return CarModel.deleteMany({}).then(() => undefined);
};
}
Tariff entity in src/db/models/tariff.ts
import mongoose from "mongoose";
import {Currency, Tariff, TariffCreateFields} from "../../lib/types";
interface TariffSchema {
createdAt: Date;
carId: string;
days: number;
currency: Currency;
price: number;
}
interface TariffCreateDocument {
carId: string;
days: number;
currency: Currency;
price: number;
}
type TariffModel = TariffSchema & mongoose.Document;
const tariffSchema = new mongoose.Schema({
carId: {type: mongoose.Types.ObjectId, required: true},
createdAt: {type: Date, required: true, default: () => new Date()},
days: {type: Number, required: true},
currency: {type: String, required: true},
price: {type: Number, required: true},
});
tariffSchema.index({carId: 1, days: 1}, {unique: true});
const fromDb = (m: TariffModel): Tariff => ({
id: m.id,
createdAt: m.createdAt,
days: m.days,
currency: m.currency,
price: m.price,
});
const toDb = (carId: string, f: TariffCreateFields): TariffCreateDocument => ({
carId,
days: f.days,
currency: f.currency,
price: f.price,
});
const TariffModel = mongoose.model<TariffModel>("Tariff", tariffSchema);
export namespace tariff {
export const create = (carId: string, fields: TariffCreateFields): Promise<Tariff> => {
const dbTariff = new TariffModel(toDb(carId, fields));
return dbTariff.save().then(fromDb);
};
export const createAllOf = (carId: string, fieldsList: TariffCreateFields): Promise<Tariff> => {
return Promise.all(fieldsList.map((fields) => create(carId, fields)));
};
export const list = (): Promise<Tariff> => {
return TariffModel.find().then((l) => l.map(fromDb));
};
export const getAllOf = (carId: string): Promise<Tariff> => {
return TariffModel.find({carId}).then((l) => l.map(fromDb));
};
export const updateAllOf = async (carId: string, fieldsList: TariffCreateFields): Promise<void> => {
await removeAllOf(carId);
await createAllOf(carId, fieldsList);
};
export const removeAllOf = (carId: string): Promise<void> => {
return TariffModel.deleteMany({carId}).then(() => undefined);
};
export const drop = (): Promise<void> => {
return TariffModel.deleteMany({}).then(() => undefined);
};
}
And photo entity in src/db/models/photo.ts
import mongoose from "mongoose";
import {Photo, PhotoCreateFields} from "../../lib/types";
interface PhotoSchema {
createdAt: Date;
carId: string;
fileKey: string;
}
interface PhotoCreateDocument {
carId: string;
fileKey: string;
}
type PhotoModel = PhotoSchema & mongoose.Document;
const PhotoSchema = new mongoose.Schema({
carId: {type: mongoose.Types.ObjectId, required: true},
createdAt: {type: Date, required: true, default: () => new Date()},
fileKey: {type: String, required: true},
});
PhotoSchema.index({carId: 1, fileKey: 1}, {unique: true});
const fromDb = (m: PhotoModel): Photo => ({
id: m.id,
createdAt: m.createdAt,
fileKey: m.fileKey,
});
const toDb = (carId: string, f: PhotoCreateFields): PhotoCreateDocument => ({
carId,
fileKey: f.fileKey,
});
const PhotoModel = mongoose.model<PhotoModel>("Photo", PhotoSchema);
export namespace photo {
export const create = (carId: string, fields: PhotoCreateFields): Promise<Photo> => {
const dbPhoto = new PhotoModel(toDb(carId, fields));
return dbPhoto.save().then(fromDb);
};
export const createAllOf = (carId: string, fieldsList: PhotoCreateFields): Promise<Photo> => {
return Promise.all(fieldsList.map((fields) => create(carId, fields)));
};
export const list = (): Promise<Photo> => {
return PhotoModel.find().then((l) => l.map(fromDb));
};
export const getAllOf = (carId: string): Promise<Photo> => {
return PhotoModel.find({carId}).then((l) => l.map(fromDb));
};
export const updateAllOf = async (carId: string, fieldsList: PhotoCreateFields): Promise<void> => {
await removeAllOf(carId);
await createAllOf(carId, fieldsList);
};
export const removeAllOf = (carId: string): Promise<void> => {
return PhotoModel.deleteMany({carId}).then(() => undefined);
};
export const drop = (): Promise<void> => {
return PhotoModel.deleteMany({}).then(() => undefined);
};
}
I think I have a lot of boilerplate code. Especially in type definitions: Car
, CarCreateFields
, CarSchema
and CarCreateDocument
. Also I'm not happy with fromDb
and toDb
functions. I tried to extract duplicated fields to separate types but in this case I have a naming problem. Event current naming is not good for me.
Please notice that Car
and CarCreateFields
are not the same types, they has different type for tariffs
and photos
. The same is true for CarCreateFields
and CarCreateDocument
.
Can you help me to reduce boilerplate code?
typescript mongodb mongoose
add a comment |
Core Review
Hello, I'm developing relatively simple car rental service app. I decided to use TypeScript for backend.
The question is about database module. Now there are three entities: car, tariff и photo. I use MongoDB as database.
General types are extracted tp separate file src/lib/types.ts
:
// Currency
export enum Currency {
USD = "USD",
EUR = "EUR",
}
// Price
export interface Price {
value: number;
currency: Currency;
}
// DB Entity
export interface Entity {
id: string;
createdAt: Date;
}
// Tariff
export interface TariffCreateFields {
days: number;
currency: Currency;
price: number;
}
export interface Tariff extends Entity {
days: number;
currency: Currency;
price: number;
}
// Photo
export interface PhotoCreateFields {
fileKey: string;
}
export interface Photo extends Entity {
fileKey: string;
}
// Car
export interface CarCreateFields {
model: string;
year?: string;
// ... some other fields ...
collateral: Price;
tariffs: TariffCreateFields;
photos: PhotoCreateFields;
}
export interface Car extends Entity {
model: string;
year?: string;
// ... some other fields ...
collateral: Price;
tariffs: Tariff;
photos: Photo;
}
This is car entity in src/db/models/car.ts
:
import mongoose from "mongoose";
import {
Car,
CarCreateFields,
Currency,
Photo,
Tariff,
} from "../../lib/types";
import {u} from "../../lib/utils";
import {photo} from "./photo";
import {tariff} from "./tariff";
interface CarSchema {
createdAt: Date;
model: string;
year?: string;
// ... some other fields ...
collateralValue: number;
collateralCurrency: Currency;
}
interface CarCreateDocument {
model: string;
year?: string;
// ... some other fields ...
collateralValue: number;
collateralCurrency: Currency;
}
type CarModel = CarSchema & mongoose.Document;
const carSchema = new mongoose.Schema({
createdAt: {type: Date, required: true, default: () => new Date()},
model: {type: String, required: true},
year: String,
// ... some other fields ...
collateralValue: Number,
collateralCurrency: String,
});
const fromDb = (m: CarModel, tariffs: Tariff, photos: Photo): Car => ({
id: m.id,
createdAt: m.createdAt,
model: m.model,
year: u(m.year),
// ... some other fields ...
collateral: {
value: m.collateralValue || 0,
currency: m.collateralCurrency || Currency.USD,
},
tariffs,
photos,
});
const toDb = (f: CarCreateFields): CarCreateDocument => ({
model: f.model,
year: f.year,
// ... some other fields ...
collateralCurrency: f.collateral.currency,
collateralValue: f.collateral.value,
});
const CarModel = mongoose.model<CarModel>("Car", carSchema);
export namespace car {
export const create = async (fields: CarCreateFields): Promise<Car> => {
let dbCar = new CarModel(toDb(fields));
dbCar = await dbCar.save();
const tariffs = await tariff.createAllOf(dbCar.id, fields.tariffs);
const photos = await photo.createAllOf(dbCar.id, fields.photos);
return fromDb(dbCar, tariffs, photos);
};
export const list = async (): Promise<Car> => {
const dbCarList = await CarModel.find().exec();
return await Promise.all(
dbCarList.map(async (dbCar) => {
const tariffs = await tariff.getAllOf(dbCar.id);
const photos = await photo.getAllOf(dbCar.id);
return fromDb(dbCar, tariffs, photos);
}),
);
};
export const get = async (carId: string): Promise<Car | undefined> => {
const dbCar = await CarModel.findById(carId).exec();
if (dbCar === null) {
return undefined;
}
const tariffs = await tariff.getAllOf(dbCar.id);
const photos = await photo.getAllOf(dbCar.id);
return fromDb(dbCar, tariffs, photos);
};
export const update = async (carId: string, fields: CarCreateFields): Promise<void> => {
await CarModel.updateOne({_id: carId}, {$set: toDb(fields)}, {upsert: true}).then(() => undefined);
await tariff.updateAllOf(carId, fields.tariffs);
await photo.updateAllOf(carId, fields.photos);
};
export const remove = async (carId: string): Promise<void> => {
await tariff.removeAllOf(carId);
await photo.removeAllOf(carId);
await CarModel.deleteOne({_id: carId}).then(() => undefined);
};
export const drop = (): Promise<void> => {
return CarModel.deleteMany({}).then(() => undefined);
};
}
Tariff entity in src/db/models/tariff.ts
import mongoose from "mongoose";
import {Currency, Tariff, TariffCreateFields} from "../../lib/types";
interface TariffSchema {
createdAt: Date;
carId: string;
days: number;
currency: Currency;
price: number;
}
interface TariffCreateDocument {
carId: string;
days: number;
currency: Currency;
price: number;
}
type TariffModel = TariffSchema & mongoose.Document;
const tariffSchema = new mongoose.Schema({
carId: {type: mongoose.Types.ObjectId, required: true},
createdAt: {type: Date, required: true, default: () => new Date()},
days: {type: Number, required: true},
currency: {type: String, required: true},
price: {type: Number, required: true},
});
tariffSchema.index({carId: 1, days: 1}, {unique: true});
const fromDb = (m: TariffModel): Tariff => ({
id: m.id,
createdAt: m.createdAt,
days: m.days,
currency: m.currency,
price: m.price,
});
const toDb = (carId: string, f: TariffCreateFields): TariffCreateDocument => ({
carId,
days: f.days,
currency: f.currency,
price: f.price,
});
const TariffModel = mongoose.model<TariffModel>("Tariff", tariffSchema);
export namespace tariff {
export const create = (carId: string, fields: TariffCreateFields): Promise<Tariff> => {
const dbTariff = new TariffModel(toDb(carId, fields));
return dbTariff.save().then(fromDb);
};
export const createAllOf = (carId: string, fieldsList: TariffCreateFields): Promise<Tariff> => {
return Promise.all(fieldsList.map((fields) => create(carId, fields)));
};
export const list = (): Promise<Tariff> => {
return TariffModel.find().then((l) => l.map(fromDb));
};
export const getAllOf = (carId: string): Promise<Tariff> => {
return TariffModel.find({carId}).then((l) => l.map(fromDb));
};
export const updateAllOf = async (carId: string, fieldsList: TariffCreateFields): Promise<void> => {
await removeAllOf(carId);
await createAllOf(carId, fieldsList);
};
export const removeAllOf = (carId: string): Promise<void> => {
return TariffModel.deleteMany({carId}).then(() => undefined);
};
export const drop = (): Promise<void> => {
return TariffModel.deleteMany({}).then(() => undefined);
};
}
And photo entity in src/db/models/photo.ts
import mongoose from "mongoose";
import {Photo, PhotoCreateFields} from "../../lib/types";
interface PhotoSchema {
createdAt: Date;
carId: string;
fileKey: string;
}
interface PhotoCreateDocument {
carId: string;
fileKey: string;
}
type PhotoModel = PhotoSchema & mongoose.Document;
const PhotoSchema = new mongoose.Schema({
carId: {type: mongoose.Types.ObjectId, required: true},
createdAt: {type: Date, required: true, default: () => new Date()},
fileKey: {type: String, required: true},
});
PhotoSchema.index({carId: 1, fileKey: 1}, {unique: true});
const fromDb = (m: PhotoModel): Photo => ({
id: m.id,
createdAt: m.createdAt,
fileKey: m.fileKey,
});
const toDb = (carId: string, f: PhotoCreateFields): PhotoCreateDocument => ({
carId,
fileKey: f.fileKey,
});
const PhotoModel = mongoose.model<PhotoModel>("Photo", PhotoSchema);
export namespace photo {
export const create = (carId: string, fields: PhotoCreateFields): Promise<Photo> => {
const dbPhoto = new PhotoModel(toDb(carId, fields));
return dbPhoto.save().then(fromDb);
};
export const createAllOf = (carId: string, fieldsList: PhotoCreateFields): Promise<Photo> => {
return Promise.all(fieldsList.map((fields) => create(carId, fields)));
};
export const list = (): Promise<Photo> => {
return PhotoModel.find().then((l) => l.map(fromDb));
};
export const getAllOf = (carId: string): Promise<Photo> => {
return PhotoModel.find({carId}).then((l) => l.map(fromDb));
};
export const updateAllOf = async (carId: string, fieldsList: PhotoCreateFields): Promise<void> => {
await removeAllOf(carId);
await createAllOf(carId, fieldsList);
};
export const removeAllOf = (carId: string): Promise<void> => {
return PhotoModel.deleteMany({carId}).then(() => undefined);
};
export const drop = (): Promise<void> => {
return PhotoModel.deleteMany({}).then(() => undefined);
};
}
I think I have a lot of boilerplate code. Especially in type definitions: Car
, CarCreateFields
, CarSchema
and CarCreateDocument
. Also I'm not happy with fromDb
and toDb
functions. I tried to extract duplicated fields to separate types but in this case I have a naming problem. Event current naming is not good for me.
Please notice that Car
and CarCreateFields
are not the same types, they has different type for tariffs
and photos
. The same is true for CarCreateFields
and CarCreateDocument
.
Can you help me to reduce boilerplate code?
typescript mongodb mongoose
add a comment |
Core Review
Hello, I'm developing relatively simple car rental service app. I decided to use TypeScript for backend.
The question is about database module. Now there are three entities: car, tariff и photo. I use MongoDB as database.
General types are extracted tp separate file src/lib/types.ts
:
// Currency
export enum Currency {
USD = "USD",
EUR = "EUR",
}
// Price
export interface Price {
value: number;
currency: Currency;
}
// DB Entity
export interface Entity {
id: string;
createdAt: Date;
}
// Tariff
export interface TariffCreateFields {
days: number;
currency: Currency;
price: number;
}
export interface Tariff extends Entity {
days: number;
currency: Currency;
price: number;
}
// Photo
export interface PhotoCreateFields {
fileKey: string;
}
export interface Photo extends Entity {
fileKey: string;
}
// Car
export interface CarCreateFields {
model: string;
year?: string;
// ... some other fields ...
collateral: Price;
tariffs: TariffCreateFields;
photos: PhotoCreateFields;
}
export interface Car extends Entity {
model: string;
year?: string;
// ... some other fields ...
collateral: Price;
tariffs: Tariff;
photos: Photo;
}
This is car entity in src/db/models/car.ts
:
import mongoose from "mongoose";
import {
Car,
CarCreateFields,
Currency,
Photo,
Tariff,
} from "../../lib/types";
import {u} from "../../lib/utils";
import {photo} from "./photo";
import {tariff} from "./tariff";
interface CarSchema {
createdAt: Date;
model: string;
year?: string;
// ... some other fields ...
collateralValue: number;
collateralCurrency: Currency;
}
interface CarCreateDocument {
model: string;
year?: string;
// ... some other fields ...
collateralValue: number;
collateralCurrency: Currency;
}
type CarModel = CarSchema & mongoose.Document;
const carSchema = new mongoose.Schema({
createdAt: {type: Date, required: true, default: () => new Date()},
model: {type: String, required: true},
year: String,
// ... some other fields ...
collateralValue: Number,
collateralCurrency: String,
});
const fromDb = (m: CarModel, tariffs: Tariff, photos: Photo): Car => ({
id: m.id,
createdAt: m.createdAt,
model: m.model,
year: u(m.year),
// ... some other fields ...
collateral: {
value: m.collateralValue || 0,
currency: m.collateralCurrency || Currency.USD,
},
tariffs,
photos,
});
const toDb = (f: CarCreateFields): CarCreateDocument => ({
model: f.model,
year: f.year,
// ... some other fields ...
collateralCurrency: f.collateral.currency,
collateralValue: f.collateral.value,
});
const CarModel = mongoose.model<CarModel>("Car", carSchema);
export namespace car {
export const create = async (fields: CarCreateFields): Promise<Car> => {
let dbCar = new CarModel(toDb(fields));
dbCar = await dbCar.save();
const tariffs = await tariff.createAllOf(dbCar.id, fields.tariffs);
const photos = await photo.createAllOf(dbCar.id, fields.photos);
return fromDb(dbCar, tariffs, photos);
};
export const list = async (): Promise<Car> => {
const dbCarList = await CarModel.find().exec();
return await Promise.all(
dbCarList.map(async (dbCar) => {
const tariffs = await tariff.getAllOf(dbCar.id);
const photos = await photo.getAllOf(dbCar.id);
return fromDb(dbCar, tariffs, photos);
}),
);
};
export const get = async (carId: string): Promise<Car | undefined> => {
const dbCar = await CarModel.findById(carId).exec();
if (dbCar === null) {
return undefined;
}
const tariffs = await tariff.getAllOf(dbCar.id);
const photos = await photo.getAllOf(dbCar.id);
return fromDb(dbCar, tariffs, photos);
};
export const update = async (carId: string, fields: CarCreateFields): Promise<void> => {
await CarModel.updateOne({_id: carId}, {$set: toDb(fields)}, {upsert: true}).then(() => undefined);
await tariff.updateAllOf(carId, fields.tariffs);
await photo.updateAllOf(carId, fields.photos);
};
export const remove = async (carId: string): Promise<void> => {
await tariff.removeAllOf(carId);
await photo.removeAllOf(carId);
await CarModel.deleteOne({_id: carId}).then(() => undefined);
};
export const drop = (): Promise<void> => {
return CarModel.deleteMany({}).then(() => undefined);
};
}
Tariff entity in src/db/models/tariff.ts
import mongoose from "mongoose";
import {Currency, Tariff, TariffCreateFields} from "../../lib/types";
interface TariffSchema {
createdAt: Date;
carId: string;
days: number;
currency: Currency;
price: number;
}
interface TariffCreateDocument {
carId: string;
days: number;
currency: Currency;
price: number;
}
type TariffModel = TariffSchema & mongoose.Document;
const tariffSchema = new mongoose.Schema({
carId: {type: mongoose.Types.ObjectId, required: true},
createdAt: {type: Date, required: true, default: () => new Date()},
days: {type: Number, required: true},
currency: {type: String, required: true},
price: {type: Number, required: true},
});
tariffSchema.index({carId: 1, days: 1}, {unique: true});
const fromDb = (m: TariffModel): Tariff => ({
id: m.id,
createdAt: m.createdAt,
days: m.days,
currency: m.currency,
price: m.price,
});
const toDb = (carId: string, f: TariffCreateFields): TariffCreateDocument => ({
carId,
days: f.days,
currency: f.currency,
price: f.price,
});
const TariffModel = mongoose.model<TariffModel>("Tariff", tariffSchema);
export namespace tariff {
export const create = (carId: string, fields: TariffCreateFields): Promise<Tariff> => {
const dbTariff = new TariffModel(toDb(carId, fields));
return dbTariff.save().then(fromDb);
};
export const createAllOf = (carId: string, fieldsList: TariffCreateFields): Promise<Tariff> => {
return Promise.all(fieldsList.map((fields) => create(carId, fields)));
};
export const list = (): Promise<Tariff> => {
return TariffModel.find().then((l) => l.map(fromDb));
};
export const getAllOf = (carId: string): Promise<Tariff> => {
return TariffModel.find({carId}).then((l) => l.map(fromDb));
};
export const updateAllOf = async (carId: string, fieldsList: TariffCreateFields): Promise<void> => {
await removeAllOf(carId);
await createAllOf(carId, fieldsList);
};
export const removeAllOf = (carId: string): Promise<void> => {
return TariffModel.deleteMany({carId}).then(() => undefined);
};
export const drop = (): Promise<void> => {
return TariffModel.deleteMany({}).then(() => undefined);
};
}
And photo entity in src/db/models/photo.ts
import mongoose from "mongoose";
import {Photo, PhotoCreateFields} from "../../lib/types";
interface PhotoSchema {
createdAt: Date;
carId: string;
fileKey: string;
}
interface PhotoCreateDocument {
carId: string;
fileKey: string;
}
type PhotoModel = PhotoSchema & mongoose.Document;
const PhotoSchema = new mongoose.Schema({
carId: {type: mongoose.Types.ObjectId, required: true},
createdAt: {type: Date, required: true, default: () => new Date()},
fileKey: {type: String, required: true},
});
PhotoSchema.index({carId: 1, fileKey: 1}, {unique: true});
const fromDb = (m: PhotoModel): Photo => ({
id: m.id,
createdAt: m.createdAt,
fileKey: m.fileKey,
});
const toDb = (carId: string, f: PhotoCreateFields): PhotoCreateDocument => ({
carId,
fileKey: f.fileKey,
});
const PhotoModel = mongoose.model<PhotoModel>("Photo", PhotoSchema);
export namespace photo {
export const create = (carId: string, fields: PhotoCreateFields): Promise<Photo> => {
const dbPhoto = new PhotoModel(toDb(carId, fields));
return dbPhoto.save().then(fromDb);
};
export const createAllOf = (carId: string, fieldsList: PhotoCreateFields): Promise<Photo> => {
return Promise.all(fieldsList.map((fields) => create(carId, fields)));
};
export const list = (): Promise<Photo> => {
return PhotoModel.find().then((l) => l.map(fromDb));
};
export const getAllOf = (carId: string): Promise<Photo> => {
return PhotoModel.find({carId}).then((l) => l.map(fromDb));
};
export const updateAllOf = async (carId: string, fieldsList: PhotoCreateFields): Promise<void> => {
await removeAllOf(carId);
await createAllOf(carId, fieldsList);
};
export const removeAllOf = (carId: string): Promise<void> => {
return PhotoModel.deleteMany({carId}).then(() => undefined);
};
export const drop = (): Promise<void> => {
return PhotoModel.deleteMany({}).then(() => undefined);
};
}
I think I have a lot of boilerplate code. Especially in type definitions: Car
, CarCreateFields
, CarSchema
and CarCreateDocument
. Also I'm not happy with fromDb
and toDb
functions. I tried to extract duplicated fields to separate types but in this case I have a naming problem. Event current naming is not good for me.
Please notice that Car
and CarCreateFields
are not the same types, they has different type for tariffs
and photos
. The same is true for CarCreateFields
and CarCreateDocument
.
Can you help me to reduce boilerplate code?
typescript mongodb mongoose
Core Review
Hello, I'm developing relatively simple car rental service app. I decided to use TypeScript for backend.
The question is about database module. Now there are three entities: car, tariff и photo. I use MongoDB as database.
General types are extracted tp separate file src/lib/types.ts
:
// Currency
export enum Currency {
USD = "USD",
EUR = "EUR",
}
// Price
export interface Price {
value: number;
currency: Currency;
}
// DB Entity
export interface Entity {
id: string;
createdAt: Date;
}
// Tariff
export interface TariffCreateFields {
days: number;
currency: Currency;
price: number;
}
export interface Tariff extends Entity {
days: number;
currency: Currency;
price: number;
}
// Photo
export interface PhotoCreateFields {
fileKey: string;
}
export interface Photo extends Entity {
fileKey: string;
}
// Car
export interface CarCreateFields {
model: string;
year?: string;
// ... some other fields ...
collateral: Price;
tariffs: TariffCreateFields;
photos: PhotoCreateFields;
}
export interface Car extends Entity {
model: string;
year?: string;
// ... some other fields ...
collateral: Price;
tariffs: Tariff;
photos: Photo;
}
This is car entity in src/db/models/car.ts
:
import mongoose from "mongoose";
import {
Car,
CarCreateFields,
Currency,
Photo,
Tariff,
} from "../../lib/types";
import {u} from "../../lib/utils";
import {photo} from "./photo";
import {tariff} from "./tariff";
interface CarSchema {
createdAt: Date;
model: string;
year?: string;
// ... some other fields ...
collateralValue: number;
collateralCurrency: Currency;
}
interface CarCreateDocument {
model: string;
year?: string;
// ... some other fields ...
collateralValue: number;
collateralCurrency: Currency;
}
type CarModel = CarSchema & mongoose.Document;
const carSchema = new mongoose.Schema({
createdAt: {type: Date, required: true, default: () => new Date()},
model: {type: String, required: true},
year: String,
// ... some other fields ...
collateralValue: Number,
collateralCurrency: String,
});
const fromDb = (m: CarModel, tariffs: Tariff, photos: Photo): Car => ({
id: m.id,
createdAt: m.createdAt,
model: m.model,
year: u(m.year),
// ... some other fields ...
collateral: {
value: m.collateralValue || 0,
currency: m.collateralCurrency || Currency.USD,
},
tariffs,
photos,
});
const toDb = (f: CarCreateFields): CarCreateDocument => ({
model: f.model,
year: f.year,
// ... some other fields ...
collateralCurrency: f.collateral.currency,
collateralValue: f.collateral.value,
});
const CarModel = mongoose.model<CarModel>("Car", carSchema);
export namespace car {
export const create = async (fields: CarCreateFields): Promise<Car> => {
let dbCar = new CarModel(toDb(fields));
dbCar = await dbCar.save();
const tariffs = await tariff.createAllOf(dbCar.id, fields.tariffs);
const photos = await photo.createAllOf(dbCar.id, fields.photos);
return fromDb(dbCar, tariffs, photos);
};
export const list = async (): Promise<Car> => {
const dbCarList = await CarModel.find().exec();
return await Promise.all(
dbCarList.map(async (dbCar) => {
const tariffs = await tariff.getAllOf(dbCar.id);
const photos = await photo.getAllOf(dbCar.id);
return fromDb(dbCar, tariffs, photos);
}),
);
};
export const get = async (carId: string): Promise<Car | undefined> => {
const dbCar = await CarModel.findById(carId).exec();
if (dbCar === null) {
return undefined;
}
const tariffs = await tariff.getAllOf(dbCar.id);
const photos = await photo.getAllOf(dbCar.id);
return fromDb(dbCar, tariffs, photos);
};
export const update = async (carId: string, fields: CarCreateFields): Promise<void> => {
await CarModel.updateOne({_id: carId}, {$set: toDb(fields)}, {upsert: true}).then(() => undefined);
await tariff.updateAllOf(carId, fields.tariffs);
await photo.updateAllOf(carId, fields.photos);
};
export const remove = async (carId: string): Promise<void> => {
await tariff.removeAllOf(carId);
await photo.removeAllOf(carId);
await CarModel.deleteOne({_id: carId}).then(() => undefined);
};
export const drop = (): Promise<void> => {
return CarModel.deleteMany({}).then(() => undefined);
};
}
Tariff entity in src/db/models/tariff.ts
import mongoose from "mongoose";
import {Currency, Tariff, TariffCreateFields} from "../../lib/types";
interface TariffSchema {
createdAt: Date;
carId: string;
days: number;
currency: Currency;
price: number;
}
interface TariffCreateDocument {
carId: string;
days: number;
currency: Currency;
price: number;
}
type TariffModel = TariffSchema & mongoose.Document;
const tariffSchema = new mongoose.Schema({
carId: {type: mongoose.Types.ObjectId, required: true},
createdAt: {type: Date, required: true, default: () => new Date()},
days: {type: Number, required: true},
currency: {type: String, required: true},
price: {type: Number, required: true},
});
tariffSchema.index({carId: 1, days: 1}, {unique: true});
const fromDb = (m: TariffModel): Tariff => ({
id: m.id,
createdAt: m.createdAt,
days: m.days,
currency: m.currency,
price: m.price,
});
const toDb = (carId: string, f: TariffCreateFields): TariffCreateDocument => ({
carId,
days: f.days,
currency: f.currency,
price: f.price,
});
const TariffModel = mongoose.model<TariffModel>("Tariff", tariffSchema);
export namespace tariff {
export const create = (carId: string, fields: TariffCreateFields): Promise<Tariff> => {
const dbTariff = new TariffModel(toDb(carId, fields));
return dbTariff.save().then(fromDb);
};
export const createAllOf = (carId: string, fieldsList: TariffCreateFields): Promise<Tariff> => {
return Promise.all(fieldsList.map((fields) => create(carId, fields)));
};
export const list = (): Promise<Tariff> => {
return TariffModel.find().then((l) => l.map(fromDb));
};
export const getAllOf = (carId: string): Promise<Tariff> => {
return TariffModel.find({carId}).then((l) => l.map(fromDb));
};
export const updateAllOf = async (carId: string, fieldsList: TariffCreateFields): Promise<void> => {
await removeAllOf(carId);
await createAllOf(carId, fieldsList);
};
export const removeAllOf = (carId: string): Promise<void> => {
return TariffModel.deleteMany({carId}).then(() => undefined);
};
export const drop = (): Promise<void> => {
return TariffModel.deleteMany({}).then(() => undefined);
};
}
And photo entity in src/db/models/photo.ts
import mongoose from "mongoose";
import {Photo, PhotoCreateFields} from "../../lib/types";
interface PhotoSchema {
createdAt: Date;
carId: string;
fileKey: string;
}
interface PhotoCreateDocument {
carId: string;
fileKey: string;
}
type PhotoModel = PhotoSchema & mongoose.Document;
const PhotoSchema = new mongoose.Schema({
carId: {type: mongoose.Types.ObjectId, required: true},
createdAt: {type: Date, required: true, default: () => new Date()},
fileKey: {type: String, required: true},
});
PhotoSchema.index({carId: 1, fileKey: 1}, {unique: true});
const fromDb = (m: PhotoModel): Photo => ({
id: m.id,
createdAt: m.createdAt,
fileKey: m.fileKey,
});
const toDb = (carId: string, f: PhotoCreateFields): PhotoCreateDocument => ({
carId,
fileKey: f.fileKey,
});
const PhotoModel = mongoose.model<PhotoModel>("Photo", PhotoSchema);
export namespace photo {
export const create = (carId: string, fields: PhotoCreateFields): Promise<Photo> => {
const dbPhoto = new PhotoModel(toDb(carId, fields));
return dbPhoto.save().then(fromDb);
};
export const createAllOf = (carId: string, fieldsList: PhotoCreateFields): Promise<Photo> => {
return Promise.all(fieldsList.map((fields) => create(carId, fields)));
};
export const list = (): Promise<Photo> => {
return PhotoModel.find().then((l) => l.map(fromDb));
};
export const getAllOf = (carId: string): Promise<Photo> => {
return PhotoModel.find({carId}).then((l) => l.map(fromDb));
};
export const updateAllOf = async (carId: string, fieldsList: PhotoCreateFields): Promise<void> => {
await removeAllOf(carId);
await createAllOf(carId, fieldsList);
};
export const removeAllOf = (carId: string): Promise<void> => {
return PhotoModel.deleteMany({carId}).then(() => undefined);
};
export const drop = (): Promise<void> => {
return PhotoModel.deleteMany({}).then(() => undefined);
};
}
I think I have a lot of boilerplate code. Especially in type definitions: Car
, CarCreateFields
, CarSchema
and CarCreateDocument
. Also I'm not happy with fromDb
and toDb
functions. I tried to extract duplicated fields to separate types but in this case I have a naming problem. Event current naming is not good for me.
Please notice that Car
and CarCreateFields
are not the same types, they has different type for tariffs
and photos
. The same is true for CarCreateFields
and CarCreateDocument
.
Can you help me to reduce boilerplate code?
typescript mongodb mongoose
typescript mongodb mongoose
asked 16 mins ago
atomAltera
12518
12518
add a comment |
add a comment |
active
oldest
votes
Your Answer
StackExchange.ifUsing("editor", function () {
return StackExchange.using("mathjaxEditing", function () {
StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
});
});
}, "mathjax-editing");
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "196"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: false,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f210345%2fdatabase-module-on-typescript-too-many-type-definitions%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
active
oldest
votes
active
oldest
votes
active
oldest
votes
active
oldest
votes
Thanks for contributing an answer to Code Review Stack Exchange!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
Use MathJax to format equations. MathJax reference.
To learn more, see our tips on writing great answers.
Some of your past answers have not been well-received, and you're in danger of being blocked from answering.
Please pay close attention to the following guidance:
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f210345%2fdatabase-module-on-typescript-too-many-type-definitions%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown