K
Kysely•2y ago
NazCodeland

writing a transaction that calls functions

Hey everyone, I like what I've got going here but before I did this for my whole code base I just wanted to make sure I was on the right path
export async function createUser(user: NewUser, trx: Transaction<DB>) {
return db
.insertInto('User')
.values({
...user
})
.returningAll()
.executeTakeFirstOrThrow();
}

export async function createProfile(profile: NewProfile, trx: Transaction<DB>) {
return db
.insertInto('Profile')
.values({
...profile
})
.returningAll()
.execute();
}

export async function createBusiness(business: NewBusiness, trx: Transaction<trx>) {
return await db
.insertInto('Business')
.values({
...business
})
.returningAll()
.execute();
}

export async function createUserProfileBusiness(
user: NewUser,
profile: NewProfile,
business: NewBusiness
) {
try {
const result = await db.transaction().execute(async (trx) => {
const newUser = await createUser(user, trx);
const newProfile = await createProfile({ ...profile, userId: newUser.id }, trx);
const newBusiness = await createBusiness(business, trx);
return { newUser, newProfile, newBusiness };
});
return result;
} catch (error) {
// Handle the error here
console.error(error);
}
}
export async function createUser(user: NewUser, trx: Transaction<DB>) {
return db
.insertInto('User')
.values({
...user
})
.returningAll()
.executeTakeFirstOrThrow();
}

export async function createProfile(profile: NewProfile, trx: Transaction<DB>) {
return db
.insertInto('Profile')
.values({
...profile
})
.returningAll()
.execute();
}

export async function createBusiness(business: NewBusiness, trx: Transaction<trx>) {
return await db
.insertInto('Business')
.values({
...business
})
.returningAll()
.execute();
}

export async function createUserProfileBusiness(
user: NewUser,
profile: NewProfile,
business: NewBusiness
) {
try {
const result = await db.transaction().execute(async (trx) => {
const newUser = await createUser(user, trx);
const newProfile = await createProfile({ ...profile, userId: newUser.id }, trx);
const newBusiness = await createBusiness(business, trx);
return { newUser, newProfile, newBusiness };
});
return result;
} catch (error) {
// Handle the error here
console.error(error);
}
}
11 Replies
NazCodeland
NazCodelandOP•2y ago
at first I was calling each function from another file, so they are exported but after realizing tha I want them to fail if another function fails, I learned about transactions I still wanna keep it as functions instead of having the code for all the functions as queries within the transaction callback function but ya, if anyone can point out anything that isn't good practice, I rather know now, thanks
Igal
Igal•2y ago
Hey 👋 Generally that's a reasonable pattern. You could make the code less specific to transactions, so you could use all these functions outside of that context if you needed to (e.g. running on main instance, running on a test instance, running on a single connection instance, etc). Since a Transaction extends Kysely it would work.
async function createX(x: X, executor: Kysely<Database> = db) {...}
async function createX(x: X, executor: Kysely<Database> = db) {...}
NazCodeland
NazCodelandOP•2y ago
I just realized that my SvelteAuth adapter file calls these functions individually, so I need to be usable outside of trx as you are mentioning I oped for this
async function createAddress(address: NewAddress, trx?: Transaction<DB>) {
return (trx ?? db)
...
async function createAddress(address: NewAddress, trx?: Transaction<DB>) {
return (trx ?? db)
...
but wait a minute
Igal
Igal•2y ago
Again, that's too specific to transactions
NazCodeland
NazCodelandOP•2y ago
what is the difference between this executor: Kysely<Database> = db and
const db = new Kysely<DB>({
dialect: new PostgresDialect({
pool: new Pool({
host: DB_HOST,
port: Number(DB_PORT),
user: DB_NAME,
password: DB_PASSWORD,
database: DB_USERNAME
})
})
});
const db = new Kysely<DB>({
dialect: new PostgresDialect({
pool: new Pool({
host: DB_HOST,
port: Number(DB_PORT),
user: DB_NAME,
password: DB_PASSWORD,
database: DB_USERNAME
})
})
});
Igal
Igal•2y ago
by default it uses this db instance, but you can "inject" anything that matches Kysely<DB> type.. including trx, or a test instance. it makes the code more testable and reusable
createAddress(address); // uses main db instance
createAddress(address, db);
createAddress(address, transaction);
createAddress(address, testInstance);
createAddress(address, singleConnectionInstance);
createAddress(address); // uses main db instance
createAddress(address, db);
createAddress(address, transaction);
createAddress(address, testInstance);
createAddress(address, singleConnectionInstance);
NazCodeland
NazCodelandOP•2y ago
async function createX(x: X, executor = db) {...}
async function createX(x: X, executor = db) {...}
this would work of course but, when we pass in trx, it would not automatically assign it the type of Transaction<DB> which is a problem. -- if I am on the right train of thought, to get around that, whatever get's passed to executor will have the type of Kysely<Database> and since Transaction extends from this class, you're saying it's okay for it to have that type but does that mean, it will still be a little limiting for trx to have that type instead of the Transaction<DB> type, since whatever it extends will not be included in Kysely<DB>?
Igal
Igal•2y ago
Unless you're doing something specific to transactions in those functions, which you're not, this is completely legit
NazCodeland
NazCodelandOP•2y ago
got it thank you so much for showing me that!
Igal
Igal•2y ago
Glad to share this, as I've used this in production in the past.
Unknown User
Unknown User•15mo ago
Message Not Public
Sign In & Join Server To View
Want results from more Discord servers?
Add your server