Question about how Convex makes transactions in a mutation
I'm testing out convex right now and I'm slowly falling in love with it, one of the features that blew my mind is how it converts mutations into transactions using plain typescript. However, there might be repetitive logic and I figured out a way to reuse that logic but I'm not sure if that is going to break the functionality of converting it into a transaction
Let me show you the code below
import { mutation } from "@/convex/_generated/server";
import { v } from "convex/values";
import { GenericMutationCtx } from "convex/server";
import { api } from "@/convex/_generated/api";
import {
getOrCreateGlobalDomain,
getOrCreateGlobalUrl,
getOrCreateUserObjectDomain,
getOrCreateUserObjectUrl,
} from "@/utils/convex-query-helpers";
export const uploadHistoryItemMutation = mutation({
args: {
url: v.string(),
domainUrl: v.string(),
},
handler: async (ctx, args) => {
// Check if user is authenticated
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error("User must be authenticated to create a history item");
}
const userId = identity.subject;
const { url, domainUrl } = args;
// Get or create all required entities using helper functions
const globalDomainId = await getOrCreateGlobalDomain(ctx, domainUrl);
const globalUrlId = await getOrCreateGlobalUrl(ctx, url, globalDomainId);
const userObjectDomainId = await getOrCreateUserObjectDomain(
ctx,
userId,
globalDomainId
);
const userObjectUrlId = await getOrCreateUserObjectUrl(
ctx,
userId,
globalUrlId,
globalDomainId,
userObjectDomainId
);
// Create the history item
const historyItemId = await ctx.db.insert("historyItems", {
url,
globalDomainId,
globalUrlId,
userObjectUrlId,
userObjectDomainId,
userId,
});
return {
historyItemId,
globalDomainId,
globalUrlId,
};
},
});
import { mutation } from "@/convex/_generated/server";
import { v } from "convex/values";
import { GenericMutationCtx } from "convex/server";
import { api } from "@/convex/_generated/api";
import {
getOrCreateGlobalDomain,
getOrCreateGlobalUrl,
getOrCreateUserObjectDomain,
getOrCreateUserObjectUrl,
} from "@/utils/convex-query-helpers";
export const uploadHistoryItemMutation = mutation({
args: {
url: v.string(),
domainUrl: v.string(),
},
handler: async (ctx, args) => {
// Check if user is authenticated
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error("User must be authenticated to create a history item");
}
const userId = identity.subject;
const { url, domainUrl } = args;
// Get or create all required entities using helper functions
const globalDomainId = await getOrCreateGlobalDomain(ctx, domainUrl);
const globalUrlId = await getOrCreateGlobalUrl(ctx, url, globalDomainId);
const userObjectDomainId = await getOrCreateUserObjectDomain(
ctx,
userId,
globalDomainId
);
const userObjectUrlId = await getOrCreateUserObjectUrl(
ctx,
userId,
globalUrlId,
globalDomainId,
userObjectDomainId
);
// Create the history item
const historyItemId = await ctx.db.insert("historyItems", {
url,
globalDomainId,
globalUrlId,
userObjectUrlId,
userObjectDomainId,
userId,
});
return {
historyItemId,
globalDomainId,
globalUrlId,
};
},
});
3 Replies
Thanks for posting in <#1088161997662724167>.
Reminder: If you have a Convex Pro account, use the Convex Dashboard to file support tickets.
- Provide context: What are you trying to achieve, what is the end-user interaction, what are you seeing? (full error message, command output, etc.)
- Use search.convex.dev to search Docs, Stack, and Discord all at once.
- Additionally, you can post your questions in the Convex Community's <#1228095053885476985> channel to receive a response from AI.
- Avoid tagging staff unless specifically instructed.
Thank you!
Below is the original code without the helper functions
Below is the helper functions
Will this break the "transaction" functionality of the original mutation?
/* ORIGINAL CODE (commented out for reference):
// Check if globalDomain exists, if not create one
let globalDomain = await ctx.db
.query("globalDomains")
.filter((q) => q.eq(q.field("domainUrl"), domainUrl))
.first();
let globalDomainId;
if (!globalDomain) {
// Create new globalDomain
globalDomainId = await ctx.db.insert("globalDomains", {
domainUrl,
faviconUrl: undefined, // You can add logic to fetch favicon later
});
} else {
globalDomainId = globalDomain._id;
}
// Check if globalUrl exists, if not create one
let globalUrl = await ctx.db
.query("globalUrls")
.filter((q) => q.eq(q.field("url"), url))
.first();
let globalUrlId;
if (!globalUrl) {
// Create new globalUrl
globalUrlId = await ctx.db.insert("globalUrls", {
url,
globalDomainId,
faviconUrl: undefined, // You can add logic to fetch favicon later
});
} else {
globalUrlId = globalUrl._id;
}
let userObjectDomain = await ctx.db
.query("userObjectDomains")
.filter((q) => q.eq(q.field("userId"), userId))
.filter((q) => q.eq(q.field("globalDomainId"), globalDomainId))
.first();
let userObjectDomainId;
if (!userObjectDomain) {
// Create new userObject
const userObject = await ctx.db.insert("userObjects", {
userId,
objectType: "MEMORY_MAP",
});
// Create new userObjectDomain
userObjectDomainId = await ctx.db.insert("userObjectDomains", {
userId,
userObjectId: userObject,
globalDomainId,
});
} else {
userObjectDomainId = userObjectDomain._id;
}
// Check if the userObjectUrl exists, if not create one
let userObjectUrl = await ctx.db
.query("userObjectUrls")
.filter((q) => q.eq(q.field("userId"), userId))
.filter((q) => q.eq(q.field("globalUrlId"), globalUrlId))
.first();
let userObjectUrlId;
if (!userObjectUrl) {
// Create new userObject
const userObject = await ctx.db.insert("userObjects", {
userId,
objectType: "URL",
});
// Create new userObjectUrl
userObjectUrlId = await ctx.db.insert("userObjectUrls", {
userId,
userObjectId: userObject,
userObjectDomainId,
globalUrlId,
globalDomainId,
});
} else {
userObjectUrlId = userObjectUrl._id;
}
// Create the history item
const historyItemId = await ctx.db.insert("historyItems", {
url,
globalDomainId,
globalUrlId,
userObjectUrlId,
userObjectDomainId,
userId,
});
return {
historyItemId,
globalDomainId,
globalUrlId,
};
*/
/* ORIGINAL CODE (commented out for reference):
// Check if globalDomain exists, if not create one
let globalDomain = await ctx.db
.query("globalDomains")
.filter((q) => q.eq(q.field("domainUrl"), domainUrl))
.first();
let globalDomainId;
if (!globalDomain) {
// Create new globalDomain
globalDomainId = await ctx.db.insert("globalDomains", {
domainUrl,
faviconUrl: undefined, // You can add logic to fetch favicon later
});
} else {
globalDomainId = globalDomain._id;
}
// Check if globalUrl exists, if not create one
let globalUrl = await ctx.db
.query("globalUrls")
.filter((q) => q.eq(q.field("url"), url))
.first();
let globalUrlId;
if (!globalUrl) {
// Create new globalUrl
globalUrlId = await ctx.db.insert("globalUrls", {
url,
globalDomainId,
faviconUrl: undefined, // You can add logic to fetch favicon later
});
} else {
globalUrlId = globalUrl._id;
}
let userObjectDomain = await ctx.db
.query("userObjectDomains")
.filter((q) => q.eq(q.field("userId"), userId))
.filter((q) => q.eq(q.field("globalDomainId"), globalDomainId))
.first();
let userObjectDomainId;
if (!userObjectDomain) {
// Create new userObject
const userObject = await ctx.db.insert("userObjects", {
userId,
objectType: "MEMORY_MAP",
});
// Create new userObjectDomain
userObjectDomainId = await ctx.db.insert("userObjectDomains", {
userId,
userObjectId: userObject,
globalDomainId,
});
} else {
userObjectDomainId = userObjectDomain._id;
}
// Check if the userObjectUrl exists, if not create one
let userObjectUrl = await ctx.db
.query("userObjectUrls")
.filter((q) => q.eq(q.field("userId"), userId))
.filter((q) => q.eq(q.field("globalUrlId"), globalUrlId))
.first();
let userObjectUrlId;
if (!userObjectUrl) {
// Create new userObject
const userObject = await ctx.db.insert("userObjects", {
userId,
objectType: "URL",
});
// Create new userObjectUrl
userObjectUrlId = await ctx.db.insert("userObjectUrls", {
userId,
userObjectId: userObject,
userObjectDomainId,
globalUrlId,
globalDomainId,
});
} else {
userObjectUrlId = userObjectUrl._id;
}
// Create the history item
const historyItemId = await ctx.db.insert("historyItems", {
url,
globalDomainId,
globalUrlId,
userObjectUrlId,
userObjectDomainId,
userId,
});
return {
historyItemId,
globalDomainId,
globalUrlId,
};
*/
import { GenericMutationCtx } from "convex/server";
import { Id, DataModel } from "@/convex/_generated/dataModel";
/**
* Helper function to get or create a global domain
*/
export async function getOrCreateGlobalDomain(
ctx: GenericMutationCtx<DataModel>,
domainUrl: string
): Promise<Id<"globalDomains">> {
let globalDomain = await ctx.db
.query("globalDomains")
.filter((q) => q.eq(q.field("domainUrl"), domainUrl))
.first();
if (!globalDomain) {
return await ctx.db.insert("globalDomains", {
domainUrl,
faviconUrl: undefined,
});
}
return globalDomain._id;
}
/**
* Helper function to get or create a global URL
*/
export async function getOrCreateGlobalUrl(
ctx: GenericMutationCtx<DataModel>,
url: string,
globalDomainId: Id<"globalDomains">
): Promise<Id<"globalUrls">> {
let globalUrl = await ctx.db
.query("globalUrls")
.filter((q) => q.eq(q.field("url"), url))
.first();
if (!globalUrl) {
return await ctx.db.insert("globalUrls", {
url,
globalDomainId,
faviconUrl: undefined,
});
}
return globalUrl._id;
}
/**
* Helper function to get or create a user object domain
*/
export async function getOrCreateUserObjectDomain(
ctx: GenericMutationCtx<DataModel>,
userId: string,
globalDomainId: Id<"globalDomains">
): Promise<Id<"userObjectDomains">> {
let userObjectDomain = await ctx.db
.query("userObjectDomains")
.filter((q) => q.eq(q.field("userId"), userId))
.filter((q) => q.eq(q.field("globalDomainId"), globalDomainId))
.first();
if (!userObjectDomain) {
// Create new userObject
const userObject = await ctx.db.insert("userObjects", {
userId,
objectType: "MEMORY_MAP",
});
// Create new userObjectDomain
return await ctx.db.insert("userObjectDomains", {
userId,
userObjectId: userObject,
globalDomainId,
});
}
return userObjectDomain._id;
}
/**
* Helper function to get or create a user object URL
*/
export async function getOrCreateUserObjectUrl(
ctx: GenericMutationCtx<DataModel>,
userId: string,
globalUrlId: Id<"globalUrls">,
globalDomainId: Id<"globalDomains">,
userObjectDomainId: Id<"userObjectDomains">
): Promise<Id<"userObjectUrls">> {
let userObjectUrl = await ctx.db
.query("userObjectUrls")
.filter((q) => q.eq(q.field("userId"), userId))
.filter((q) => q.eq(q.field("globalUrlId"), globalUrlId))
.first();
if (!userObjectUrl) {
// Create new userObject
const userObject = await ctx.db.insert("userObjects", {
userId,
objectType: "URL",
});
// Create new userObjectUrl
return await ctx.db.insert("userObjectUrls", {
userId,
userObjectId: userObject,
userObjectDomainId,
globalUrlId,
globalDomainId,
});
}
return userObjectUrl._id;
}
import { GenericMutationCtx } from "convex/server";
import { Id, DataModel } from "@/convex/_generated/dataModel";
/**
* Helper function to get or create a global domain
*/
export async function getOrCreateGlobalDomain(
ctx: GenericMutationCtx<DataModel>,
domainUrl: string
): Promise<Id<"globalDomains">> {
let globalDomain = await ctx.db
.query("globalDomains")
.filter((q) => q.eq(q.field("domainUrl"), domainUrl))
.first();
if (!globalDomain) {
return await ctx.db.insert("globalDomains", {
domainUrl,
faviconUrl: undefined,
});
}
return globalDomain._id;
}
/**
* Helper function to get or create a global URL
*/
export async function getOrCreateGlobalUrl(
ctx: GenericMutationCtx<DataModel>,
url: string,
globalDomainId: Id<"globalDomains">
): Promise<Id<"globalUrls">> {
let globalUrl = await ctx.db
.query("globalUrls")
.filter((q) => q.eq(q.field("url"), url))
.first();
if (!globalUrl) {
return await ctx.db.insert("globalUrls", {
url,
globalDomainId,
faviconUrl: undefined,
});
}
return globalUrl._id;
}
/**
* Helper function to get or create a user object domain
*/
export async function getOrCreateUserObjectDomain(
ctx: GenericMutationCtx<DataModel>,
userId: string,
globalDomainId: Id<"globalDomains">
): Promise<Id<"userObjectDomains">> {
let userObjectDomain = await ctx.db
.query("userObjectDomains")
.filter((q) => q.eq(q.field("userId"), userId))
.filter((q) => q.eq(q.field("globalDomainId"), globalDomainId))
.first();
if (!userObjectDomain) {
// Create new userObject
const userObject = await ctx.db.insert("userObjects", {
userId,
objectType: "MEMORY_MAP",
});
// Create new userObjectDomain
return await ctx.db.insert("userObjectDomains", {
userId,
userObjectId: userObject,
globalDomainId,
});
}
return userObjectDomain._id;
}
/**
* Helper function to get or create a user object URL
*/
export async function getOrCreateUserObjectUrl(
ctx: GenericMutationCtx<DataModel>,
userId: string,
globalUrlId: Id<"globalUrls">,
globalDomainId: Id<"globalDomains">,
userObjectDomainId: Id<"userObjectDomains">
): Promise<Id<"userObjectUrls">> {
let userObjectUrl = await ctx.db
.query("userObjectUrls")
.filter((q) => q.eq(q.field("userId"), userId))
.filter((q) => q.eq(q.field("globalUrlId"), globalUrlId))
.first();
if (!userObjectUrl) {
// Create new userObject
const userObject = await ctx.db.insert("userObjects", {
userId,
objectType: "URL",
});
// Create new userObjectUrl
return await ctx.db.insert("userObjectUrls", {
userId,
userObjectId: userObject,
userObjectDomainId,
globalUrlId,
globalDomainId,
});
}
return userObjectUrl._id;
}
Helpers are fine and recommended - mutations are always transactional, that isn't dependent on your code.