Starlord
Starlord2w ago

Database Triggers

https://stack.convex.dev/triggers this just doesnt work at all. no events trigger
Database Triggers
Triggers automatically run code whenever data in a table changes. A library in the convex-helpers npm package allows you to attach trigger functions t...
40 Replies
Nicolas
Nicolas2w ago
Hi, this is definitely unexpected. Did you make sure that you’re defining all your mutations using customMutation instead of the regular mutation and internalMutation functions?
Starlord
StarlordOP2w ago
triggers.register("users", async (ctx, change) => { console.log("user changed", change); }); no logs on trigger so nothing is executing
Starlord
StarlordOP2w ago
No description
Nicolas
Nicolas2w ago
This looks correct. But for triggers to work, you also need to use the correct syntax when defining mutations that will affect the users table
Nicolas
Nicolas2w ago
For instance in https://stack.convex.dev/triggers the mutations are imported from ./functions, where mutation and internalMutation are defined as:
export const mutation = customMutation(rawMutation, customCtx(triggers.wrapDB));
export const internalMutation = customMutation(rawInternalMutation, customCtx(triggers.wrapDB));
export const mutation = customMutation(rawMutation, customCtx(triggers.wrapDB));
export const internalMutation = customMutation(rawInternalMutation, customCtx(triggers.wrapDB));
Database Triggers
Triggers automatically run code whenever data in a table changes. A library in the convex-helpers npm package allows you to attach trigger functions t...
Nicolas
Nicolas2w ago
Triggers won’t run if you update the data from somewhere else (e.g. if you don’t use customMutation or edit data from the dashboard or DB imports)
Starlord
StarlordOP2w ago
i have custom mutations defined
Nicolas
Nicolas2w ago
Are you using this declaration of mutation where you write your custom functions?
Starlord
StarlordOP2w ago
what do you mean? i have this export const mutation = customMutation(rawMutation, customCtx(triggers.wrapDB)); export const internalMutation = customMutation(rawInternalMutation, customCtx(triggers.wrapDB));
Nicolas
Nicolas2w ago
For instance could you please share one example of how you’re definining particular mutations in your code? For instance the mutation you use to insert a row in playerInventory
Starlord
StarlordOP2w ago
here is mutation that is removing item from inventory
export async function tryDeductItemFromInventory(
ctx: { db: DatabaseWriter },
playerInventory: UserItem[],
itemId: Id<"items">,
amount: number
): Promise<boolean> {

const userInventory = getPlayerItem(playerInventory, itemId);
if (!userInventory) {
return false;
}

const currentAmount = userInventory.amount;
if (currentAmount < amount) {
return false;
}

await ctx.db.patch(userInventory._id, {
amount: currentAmount - amount
});

return true;
}
export async function tryDeductItemFromInventory(
ctx: { db: DatabaseWriter },
playerInventory: UserItem[],
itemId: Id<"items">,
amount: number
): Promise<boolean> {

const userInventory = getPlayerItem(playerInventory, itemId);
if (!userInventory) {
return false;
}

const currentAmount = userInventory.amount;
if (currentAmount < amount) {
return false;
}

await ctx.db.patch(userInventory._id, {
amount: currentAmount - amount
});

return true;
}
and its not triggering trigger
Nicolas
Nicolas2w ago
Do you have an example of a place where you’re calling this function? This isn’t a Convex mutation in itself but can be called from mutations
Starlord
StarlordOP2w ago
so data in the table is changed
export async function export async
export const spin = mutation({
args: {
betAmount: v.number()
},
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new ConvexError("Unauthorized");

// Check for existing pending spins
const pendingSpins = await ctx.db
.query("playerPendingSpins")
.withIndex("by_user")
.filter(q => q.eq(q.field("userId"), userId))
.collect();

// If there's a completed spin, delete it
for (const spin of pendingSpins) {
if (spin.state === SpinState.COMPLETE || !spin.rewards.length) {
await ctx.db.delete(spin._id);
} else if (spin.state === SpinState.IN_PROGRESS) {
throw new ConvexError("Cannot start new spin while previous spin is in progress");
}
}

const itemCache = await createItemIdsCache(ctx);
const serverStorage = new ServerSpinStorage(ctx, userId, itemCache);

const result = await processSpinRequest(serverStorage, args.betAmount);
if (!result.success) {
throw result.error || new ConvexError("Unknown error during spin");
}

return result;
}
});
export async function export async
export const spin = mutation({
args: {
betAmount: v.number()
},
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new ConvexError("Unauthorized");

// Check for existing pending spins
const pendingSpins = await ctx.db
.query("playerPendingSpins")
.withIndex("by_user")
.filter(q => q.eq(q.field("userId"), userId))
.collect();

// If there's a completed spin, delete it
for (const spin of pendingSpins) {
if (spin.state === SpinState.COMPLETE || !spin.rewards.length) {
await ctx.db.delete(spin._id);
} else if (spin.state === SpinState.IN_PROGRESS) {
throw new ConvexError("Cannot start new spin while previous spin is in progress");
}
}

const itemCache = await createItemIdsCache(ctx);
const serverStorage = new ServerSpinStorage(ctx, userId, itemCache);

const result = await processSpinRequest(serverStorage, args.betAmount);
if (!result.success) {
throw result.error || new ConvexError("Unknown error during spin");
}

return result;
}
});
export async function processSpinRequest(
storage: SpinStorageInterface,
betAmount: number
): Promise<{ success: boolean; error?: ConvexError<any> }> {
try {
const playerStats = await Promise.resolve(storage.getPlayerStats());
const playerStatus = await Promise.resolve(storage.getPlayerStatus());

if (!playerStats || !playerStatus) {
return { success: false, error: new ConvexError({ message: "Player data not found" }) };
}

if (playerStatus.lastSpinTimestamp) {
const timeSinceLastSpin = Date.now() - playerStatus.lastSpinTimestamp;
if (timeSinceLastSpin < playerStats.spinDuration * 0.5 * 1000) {
return { success: false, error: new ConvexError({ message: "Cannot spin right now" }) };
}
}

//await storage.regenerateSpins();

// Try to deduct spins
const deductionSuccess = await Promise.resolve(storage.deductSpins(betAmount));

}
}

async deductSpins(amount: number) {
const inventory = await this.getInventory();
return await tryDeductSpins(this.ctx, inventory, amount, this.itemCache);
}

function tryDeductSpins(
ctx: { db: DatabaseWriter },
playerInventory: UserItem[],
amount: number,
itemCache: ItemIdsCache
): Promise<boolean> {
const spinsItemId = getItemIdByName(ItemNames.Spins, itemCache);
if (!spinsItemId) {
return false;
}

return await tryDeductItemFromInventory(ctx, playerInventory, spinsItemId, amount);
}
export async function processSpinRequest(
storage: SpinStorageInterface,
betAmount: number
): Promise<{ success: boolean; error?: ConvexError<any> }> {
try {
const playerStats = await Promise.resolve(storage.getPlayerStats());
const playerStatus = await Promise.resolve(storage.getPlayerStatus());

if (!playerStats || !playerStatus) {
return { success: false, error: new ConvexError({ message: "Player data not found" }) };
}

if (playerStatus.lastSpinTimestamp) {
const timeSinceLastSpin = Date.now() - playerStatus.lastSpinTimestamp;
if (timeSinceLastSpin < playerStats.spinDuration * 0.5 * 1000) {
return { success: false, error: new ConvexError({ message: "Cannot spin right now" }) };
}
}

//await storage.regenerateSpins();

// Try to deduct spins
const deductionSuccess = await Promise.resolve(storage.deductSpins(betAmount));

}
}

async deductSpins(amount: number) {
const inventory = await this.getInventory();
return await tryDeductSpins(this.ctx, inventory, amount, this.itemCache);
}

function tryDeductSpins(
ctx: { db: DatabaseWriter },
playerInventory: UserItem[],
amount: number,
itemCache: ItemIdsCache
): Promise<boolean> {
const spinsItemId = getItemIdByName(ItemNames.Spins, itemCache);
if (!spinsItemId) {
return false;
}

return await tryDeductItemFromInventory(ctx, playerInventory, spinsItemId, amount);
}
Nicolas
Nicolas2w ago
Thanks! In this example, where do you import mutation from?
Starlord
StarlordOP2w ago
so there are multiple functions triggered from mutation till inventory is modified mutation that is called by client?
Nicolas
Nicolas2w ago
For instance, in the file where you define spin, what’s the import statement you use to import mutation?
Starlord
StarlordOP2w ago
import { mutation } from "./_generated/server";
import { mutation } from "./_generated/server";
Nicolas
Nicolas2w ago
Thanks! So here, if you want triggers to work, you need to change that import to import mutation from the file where you defined custom mutations The issue is that with the current import, your changes go straight to your database, without the triggers having any chance of detecting that there was a change
Starlord
StarlordOP2w ago
ok understand so its not possible trigger to work if data is changed in database directly?
Nicolas
Nicolas2w ago
Yes, triggers are implemented entirely as code that lives inside of your Convex functions, so you need to do this
Starlord
StarlordOP2w ago
ok understand thanks
Nicolas
Nicolas2w ago
And as I said above the triggers won’t be executed for changes that come from somewhere else (e.g. the dashboard) You're welcome!
Starlord
StarlordOP2w ago
can i call await ctx.db.patch inside trigger register? or need to call mutation because its executed now but has no effect
playerMutation:spin
log
'Trigger: Clearing stray pause time for user md7bd3av3223hcyxy1cf7fn5ys7bh3fe as spins 48/50 are below max.
playerMutation:spin
log
'Trigger: Clearing stray pause time for user md7bd3av3223hcyxy1cf7fn5ys7bh3fe as spins 48/50 are below max.
console.log(`Trigger: Clearing stray pause time for user ${userId} as spins ${amountAfter}/${maxSpins} are below max.`);
await ctx.db.patch(playerStatus._id, { regenerationPauseTime: undefined });
console.log(`Trigger: Clearing stray pause time for user ${userId} as spins ${amountAfter}/${maxSpins} are below max.`);
await ctx.db.patch(playerStatus._id, { regenerationPauseTime: undefined });
but no data was changed regenerationPauseTime of user is still defined in the database @Nicolas
Nicolas
Nicolas2w ago
Yes, this should work
Starlord
StarlordOP2w ago
ok doesnt work for me hm logs triggering but no data is changed in the database
Nicolas
Nicolas2w ago
If you go to the Convex logs in the dashboard, does the function call succeed? If there is an error the effects of the mutation will not be committed
Starlord
StarlordOP2w ago
no error
Starlord
StarlordOP2w ago
No description
Nicolas
Nicolas2w ago
Hmmm, I’m wondering if that might be a bug in the triggers library where undefined is treated the same way as if the field didn't exist at all If you try to set the value to something else than undefined, does the write go through?
Starlord
StarlordOP2w ago
will test
Nicolas
Nicolas2w ago
Thanks!
Starlord
StarlordOP2w ago
still same. i will now move away from trigger and trigger the functions directly in code ok sorry this bug was not connected to this. db patch of this table was called within same mutation somewhere else so data was overriden
Nicolas
Nicolas2w ago
Great to hear you found the source of the issue!
Starlord
StarlordOP2w ago
Did not know such conflicts possible there Is it possible somehow to trigger db patch in the code somewhere else and not to override entire row? Because one db patch is affecting one colum and another db patch another column But full document gets overriden
ian
ian2w ago
Are they happening in parallel promises? If so, they might be racing
Starlord
StarlordOP2w ago
I will check if those are parallel One is executed by mutation code and another one by trigger
ian
ian2w ago
Triggers does some locking to prevent this. If you can get a minimal repro, that'd be awesome
v
v2w ago
w ian
Starlord
StarlordOP2w ago
the problem with overriding was on my side. i fixed it. thanks

Did you find this page helpful?