1- import { and , count , eq } from 'drizzle-orm'
1+ import { and , count , eq , sql } from 'drizzle-orm'
22import { z } from 'zod'
33import { chatbotAgent } from '../../../database/schema'
44import { requireChatbotAccess } from '../../../utils/chatbotAccess'
@@ -21,6 +21,14 @@ const bodySchema = z.object({
2121 * POST /api/chatbot/agents
2222 *
2323 * Create a custom AI agent (system prompt + persona) for the current user.
24+ *
25+ * The cap-check, default-clearing, and insert run inside a single
26+ * transaction guarded by a Postgres advisory lock keyed on (org, user).
27+ * The lock serialises concurrent create requests for the same user so the
28+ * `CHATBOT_AGENT_MAX_PER_USER` cap cannot be exceeded by overlapping calls,
29+ * and so a previous default is reliably cleared before a new default is
30+ * inserted. The partial unique index `chatbot_agent_default_per_user_idx`
31+ * is the DB-level backstop on the default invariant.
2432 */
2533export default defineEventHandler ( async ( event ) : Promise < { agent : ChatbotAgent } > => {
2634 const session = await requireChatbotAccess ( event )
@@ -29,42 +37,52 @@ export default defineEventHandler(async (event): Promise<{ agent: ChatbotAgent }
2937
3038 const body = await readValidatedBody ( event , bodySchema . parse )
3139
32- // Enforce per-user cap.
33- const [ { value : existing } = { value : 0 } ] = await db
34- . select ( { value : count ( ) } )
35- . from ( chatbotAgent )
36- . where ( and (
37- eq ( chatbotAgent . organizationId , orgId ) ,
38- eq ( chatbotAgent . userId , userId ) ,
39- ) )
40-
41- if ( existing >= CHATBOT_AGENT_MAX_PER_USER ) {
42- throw createError ( {
43- statusCode : 422 ,
44- statusMessage : `Agent limit reached (${ CHATBOT_AGENT_MAX_PER_USER } ). Delete an agent before adding another.` ,
45- } )
46- }
40+ const created = await db . transaction ( async ( tx ) => {
41+ // Serialise concurrent create requests for the same (org, user) so the
42+ // count → insert sequence is race-free. The lock is released on commit.
43+ await tx . execute (
44+ sql `select pg_advisory_xact_lock(hashtext(${ `chatbot_agent:${ orgId } :${ userId } ` } ))` ,
45+ )
4746
48- // If marking this agent as default, unset any previous default .
49- if ( body . isDefault ) {
50- await db . update ( chatbotAgent )
51- . set ( { isDefault : false , updatedAt : new Date ( ) } )
47+ // Enforce per-user cap (now race-safe under the advisory lock) .
48+ const [ { value : existing } = { value : 0 } ] = await tx
49+ . select ( { value : count ( ) } )
50+ . from ( chatbotAgent )
5251 . where ( and (
5352 eq ( chatbotAgent . organizationId , orgId ) ,
5453 eq ( chatbotAgent . userId , userId ) ,
5554 ) )
56- }
5755
58- const [ created ] = await db . insert ( chatbotAgent ) . values ( {
59- organizationId : orgId ,
60- userId,
61- name : body . name ,
62- description : body . description ?? null ,
63- icon : body . icon ?? null ,
64- systemPrompt : body . systemPrompt ,
65- temperature : typeof body . temperature === 'number' ? String ( body . temperature ) : null ,
66- isDefault : body . isDefault === true ,
67- } ) . returning ( )
56+ if ( existing >= CHATBOT_AGENT_MAX_PER_USER ) {
57+ throw createError ( {
58+ statusCode : 422 ,
59+ statusMessage : `Agent limit reached (${ CHATBOT_AGENT_MAX_PER_USER } ). Delete an agent before adding another.` ,
60+ } )
61+ }
62+
63+ // If marking this agent as default, unset any previous default.
64+ if ( body . isDefault ) {
65+ await tx . update ( chatbotAgent )
66+ . set ( { isDefault : false , updatedAt : new Date ( ) } )
67+ . where ( and (
68+ eq ( chatbotAgent . organizationId , orgId ) ,
69+ eq ( chatbotAgent . userId , userId ) ,
70+ ) )
71+ }
72+
73+ const [ row ] = await tx . insert ( chatbotAgent ) . values ( {
74+ organizationId : orgId ,
75+ userId,
76+ name : body . name ,
77+ description : body . description ?? null ,
78+ icon : body . icon ?? null ,
79+ systemPrompt : body . systemPrompt ,
80+ temperature : typeof body . temperature === 'number' ? String ( body . temperature ) : null ,
81+ isDefault : body . isDefault === true ,
82+ } ) . returning ( )
83+
84+ return row
85+ } )
6886
6987 if ( ! created ) {
7088 throw createError ( { statusCode : 500 , statusMessage : 'Failed to create agent.' } )
0 commit comments