Reputation: 410
I have a function that returns other functions like so:
export const makeAudienceDb = () => {
async function insert({ ...params }: AudienceAttributes) {
const audience = await AudienceModel.create({ ...params })
const audienceToJson = audience.toJSON()
return audienceToJson
}
async function findById({ id }: { id: number }) {
const user = await AudienceModel.findByPk(id)
return user?.toJSON()
}
async function remove({ id }: { id: number }) {
return AudienceModel.destroy({ where: { id } })
}
async function update({
id,
...changes
}: { id: number } & AudienceAttributes) {
const updated = await AudienceModel.update(
{ ...changes },
{ where: { id } }
)
return updated
}
return Object.freeze({
insert,
findById,
remove,
update,
})
}
I have other models, e.g UserModel
, PostModel
which have the same database operations as makeAudienceDb
. e.g
export const makeUsersDb = ({ hashPassword, createToken }: DBDeps) => {
async function insert({ ...params }: User) {
if (params.password) {
params.password = await hashPassword(params.password)
}
const newUser = await UserModel.create({ ...params })
const returnedUser = newUser.toJSON()
const { id } = newUser
const { name, username, email, password } = returnedUser as User
const payload = {
id,
email,
}
const token = createToken(payload)
const user = { id, name, username, password, email }
return { user, token }
}
async function findById({ id }: { id: number }) {
const user = await UserModel.findByPk(id)
return user?.toJSON()
}
async function findByEmail({ email }: { email: string }) {
const user = await UserModel.findOne({ where: { email } })
return user?.toJSON()
}
async function remove({ id }: { id: number }) {
return UserModel.destroy({ where: { id } })
}
async function update({ id, ...changes }: { id: number } & User) {
const updated = await UserModel.update({ ...changes }, { where: { id } })
return updated
}
return Object.freeze({
insert,
findByEmail,
findById,
remove,
update,
})
}
This leads to code duplication across various levels. I want to know how to create one major function that I can reuse for other database operations. So instead of me having makeAudienceDb
, makeUsersDb
, I could just have one major function e.g majorDatabaseOps
where makeAudienceDb
and makeUsersDb
could just inherit from. I know this is possible with ES6 Classes and Interfaces
but I was just wondering how I could implement the same in a functional way. Any contribution is welcome. Thank you very much!
Upvotes: 1
Views: 1074
Reputation: 2838
How about defining the common functions outside of the majorDatabaseOps
function? Will let you reuse them in different places.
//defined outside for reusability
function findById(model, id) {
return model.findByPk(id)
}
const majorDatabaseOps = model => {
function removeById(model, id) {
return model.remove(id);
}
return Object.freeze({
removeById: id => removeById(model, id),
findById: id => findById(model, id),
})
}
//mocking models for demonstration
const UserModel = {
modelName: "UserModel",
findByPk: function(id) {
return console.log(id + " was found in " + this.modelName)
},
remove: function(id) {
return console.log(id + " was removed from " + this.modelName)
}
}
const PostModel = {
modelName: "PostModel",
findByPk: function(id) {
return console.log(id + " was found in " + this.modelName)
},
remove: function(id) {
return console.log(id + " was removed from " + this.modelName)
}
}
const userDbOps = majorDatabaseOps(UserModel);
userDbOps.findById(1);
userDbOps.removeById(1);
const postDbOps = majorDatabaseOps(PostModel);
postDbOps.findById(2);
postDbOps.removeById(2);
Upvotes: 0
Reputation: 42218
It looks like what you are trying to do is bind shortcuts to particular sequelize methods. The shared functionality can be implemented using typescript generics. Overriding specific behaviors, like hashing a password for a User
makes this a bit more complex.
My first instinct is to use a class-based approach. But you can do it with functions by copying all of the methods from the base and then overriding or adding specific ones, along these lines:
const userDb = Object.freeze({
...makeDb(UserModel),
findByEmail: async ({email}: {email: string}) => {
}
})
We want to create a function that takes the model as an argument. It will use typescript generics to describe the types associated with that model. The generics will be inferred from the model
variable when calling the function.
A sequelize Model
has two generic values: TModelAttributes
and TCreationAttributes
which is optional and defaults to TModelAttributes
. We also want to require that all of our model attributes must include {id: number}
.
You could potentially add additional typings to get better support for toJSON
. The sequelize package just declares the return type from Model.toJSON
as object
which is vague and unhelpful.
Our general function looks like this:
import {Model, ModelCtor} from "sequelize";
export const makeDb = <TModelAttributes extends {id: number} = any, TCreationAttributes extends {} = TModelAttributes>(
model: ModelCtor<Model<TModelAttributes, TCreationAttributes>>
) => {
async function insert({ ...params }: TCreationAttributes) {
const created = await model.create({ ...params });
return created.toJSON();
}
async function findById({ id }: { id: number }) {
const found = await model.findByPk(id)
return found?.toJSON()
}
async function remove({ id }: { id: number }) {
return model.destroy({ where: { id } })
}
async function update({ ...changes }: { id: number } & Partial<TModelAttributes>) {
const { id } = changes; // I get a TS error when destructuring this in the args
const updated = await model.update(
{ ...changes },
{ where: { id } }
);
return updated;
}
return Object.freeze({
insert,
findById,
remove,
update,
});
}
For audience, you would simply call:
const audienceDb = makeDb(AudienceModel);
Or you could define it as a function if you wanted to:
const makeAudienceDb = () => makeDb(AudienceModel);
For the users database, we need to override insert
, add findByEmail
, and take additional arguments hashPassword
and createToken
.
This is not elegant, but it should work. I don't love that your return type for user insert
is incompatible with the insert
returned value from the general makeDb
.
export const makeUsersDb = ({ hashPassword, createToken }: DBDeps) => {
// declaring this up top so that you could call methods on it in your overrides
const db = makeDb(UserModel);
async function insert({ ...params }: User) {
if (params.password) {
params.password = await hashPassword(params.password)
}
const newUser = await UserModel.create({ ...params })
const returnedUser = newUser.toJSON()
const { id } = newUser
const { name, username, email, password } = returnedUser as User
const payload = {
id,
email,
}
const token = createToken(payload)
const user = { id, name, username, password, email }
return { user, token }
}
async function findByEmail({ email }: { email: string }) {
const user = await UserModel.findOne({ where: { email } })
return user?.toJSON()
}
return Object.freeze({
...db,
insert,
findByEmail
})
}
Upvotes: 1