@@ -3,7 +3,11 @@ import { and, eq, sql } from 'drizzle-orm'
33import { type NextRequest , NextResponse } from 'next/server'
44import { z } from 'zod'
55import { getSession } from '@/lib/auth'
6- import { MAX_TAG_SLOTS , TAG_SLOTS } from '@/lib/constants/knowledge'
6+ import {
7+ getMaxSlotsForFieldType ,
8+ getSlotsForFieldType ,
9+ SUPPORTED_FIELD_TYPES ,
10+ } from '@/lib/constants/knowledge'
711import { createLogger } from '@/lib/logs/console/logger'
812import { checkKnowledgeBaseAccess , checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils'
913import { db } from '@/db'
@@ -14,17 +18,60 @@ export const dynamic = 'force-dynamic'
1418const logger = createLogger ( 'DocumentTagDefinitionsAPI' )
1519
1620const TagDefinitionSchema = z . object ( {
17- tagSlot : z . enum ( TAG_SLOTS as [ string , ... string [ ] ] ) ,
21+ tagSlot : z . string ( ) , // Will be validated against field type slots
1822 displayName : z . string ( ) . min ( 1 , 'Display name is required' ) . max ( 100 , 'Display name too long' ) ,
19- fieldType : z . string ( ) . default ( 'text' ) , // Currently only 'text', future: 'date', 'number', 'range'
23+ fieldType : z . enum ( SUPPORTED_FIELD_TYPES as [ string , ...string [ ] ] ) . default ( 'text' ) ,
24+ // Optional: for editing existing definitions
25+ _originalDisplayName : z . string ( ) . optional ( ) ,
2026} )
2127
2228const BulkTagDefinitionsSchema = z . object ( {
23- definitions : z
24- . array ( TagDefinitionSchema )
25- . max ( MAX_TAG_SLOTS , `Cannot define more than ${ MAX_TAG_SLOTS } tags` ) ,
29+ definitions : z . array ( TagDefinitionSchema ) ,
2630} )
2731
32+ // Helper function to get the next available slot for a knowledge base and field type
33+ async function getNextAvailableSlot (
34+ knowledgeBaseId : string ,
35+ fieldType : string ,
36+ existingBySlot ?: Map < string , any >
37+ ) : Promise < string | null > {
38+ // Get available slots for this field type
39+ const availableSlots = getSlotsForFieldType ( fieldType )
40+ let usedSlots : Set < string >
41+
42+ if ( existingBySlot ) {
43+ // Use provided map if available (for performance in batch operations)
44+ // Filter by field type
45+ usedSlots = new Set (
46+ Array . from ( existingBySlot . entries ( ) )
47+ . filter ( ( [ _ , def ] ) => def . fieldType === fieldType )
48+ . map ( ( [ slot , _ ] ) => slot )
49+ )
50+ } else {
51+ // Query database for existing tag definitions of the same field type
52+ const existingDefinitions = await db
53+ . select ( { tagSlot : knowledgeBaseTagDefinitions . tagSlot } )
54+ . from ( knowledgeBaseTagDefinitions )
55+ . where (
56+ and (
57+ eq ( knowledgeBaseTagDefinitions . knowledgeBaseId , knowledgeBaseId ) ,
58+ eq ( knowledgeBaseTagDefinitions . fieldType , fieldType )
59+ )
60+ )
61+
62+ usedSlots = new Set ( existingDefinitions . map ( ( def ) => def . tagSlot ) )
63+ }
64+
65+ // Find the first available slot for this field type
66+ for ( const slot of availableSlots ) {
67+ if ( ! usedSlots . has ( slot ) ) {
68+ return slot
69+ }
70+ }
71+
72+ return null // No available slots for this field type
73+ }
74+
2875// Helper function to clean up unused tag definitions
2976async function cleanupUnusedTagDefinitions ( knowledgeBaseId : string , requestId : string ) {
3077 try {
@@ -191,35 +238,93 @@ export async function POST(
191238
192239 const validatedData = BulkTagDefinitionsSchema . parse ( body )
193240
194- // Validate no duplicate tag slots
195- const tagSlots = validatedData . definitions . map ( ( def ) => def . tagSlot )
196- const uniqueTagSlots = new Set ( tagSlots )
197- if ( tagSlots . length !== uniqueTagSlots . size ) {
198- return NextResponse . json ( { error : 'Duplicate tag slots not allowed' } , { status : 400 } )
241+ // Validate slots are valid for their field types
242+ for ( const definition of validatedData . definitions ) {
243+ const validSlots = getSlotsForFieldType ( definition . fieldType )
244+ if ( validSlots . length === 0 ) {
245+ return NextResponse . json (
246+ { error : `Unsupported field type: ${ definition . fieldType } ` } ,
247+ { status : 400 }
248+ )
249+ }
250+
251+ if ( ! validSlots . includes ( definition . tagSlot ) ) {
252+ return NextResponse . json (
253+ {
254+ error : `Invalid slot '${ definition . tagSlot } ' for field type '${ definition . fieldType } '. Valid slots: ${ validSlots . join ( ', ' ) } ` ,
255+ } ,
256+ { status : 400 }
257+ )
258+ }
259+ }
260+
261+ // Validate no duplicate tag slots within the same field type
262+ const slotsByFieldType = new Map < string , Set < string > > ( )
263+ for ( const definition of validatedData . definitions ) {
264+ if ( ! slotsByFieldType . has ( definition . fieldType ) ) {
265+ slotsByFieldType . set ( definition . fieldType , new Set ( ) )
266+ }
267+ const slotsForType = slotsByFieldType . get ( definition . fieldType ) !
268+ if ( slotsForType . has ( definition . tagSlot ) ) {
269+ return NextResponse . json (
270+ {
271+ error : `Duplicate slot '${ definition . tagSlot } ' for field type '${ definition . fieldType } '` ,
272+ } ,
273+ { status : 400 }
274+ )
275+ }
276+ slotsForType . add ( definition . tagSlot )
199277 }
200278
201279 const now = new Date ( )
202280 const createdDefinitions : ( typeof knowledgeBaseTagDefinitions . $inferSelect ) [ ] = [ ]
203281
204- // Get existing definitions count before transaction for cleanup check
282+ // Get existing definitions
205283 const existingDefinitions = await db
206284 . select ( )
207285 . from ( knowledgeBaseTagDefinitions )
208286 . where ( eq ( knowledgeBaseTagDefinitions . knowledgeBaseId , knowledgeBaseId ) )
209287
210- // Check if we're trying to create more tag definitions than available slots
211- const existingTagNames = new Set ( existingDefinitions . map ( ( def ) => def . displayName ) )
212- const trulyNewTags = validatedData . definitions . filter (
213- ( def ) => ! existingTagNames . has ( def . displayName )
214- )
288+ // Group by field type for validation
289+ const existingByFieldType = new Map < string , number > ( )
290+ for ( const def of existingDefinitions ) {
291+ existingByFieldType . set ( def . fieldType , ( existingByFieldType . get ( def . fieldType ) || 0 ) + 1 )
292+ }
215293
216- if ( existingDefinitions . length + trulyNewTags . length > MAX_TAG_SLOTS ) {
217- return NextResponse . json (
218- {
219- error : `Cannot create ${ trulyNewTags . length } new tags. Knowledge base already has ${ existingDefinitions . length } tag definitions. Maximum is ${ MAX_TAG_SLOTS } total.` ,
220- } ,
221- { status : 400 }
294+ // Validate we don't exceed limits per field type
295+ const newByFieldType = new Map < string , number > ( )
296+ for ( const definition of validatedData . definitions ) {
297+ // Skip validation for edit operations - they don't create new slots
298+ if ( definition . _originalDisplayName ) {
299+ continue
300+ }
301+
302+ const existingTagNames = new Set (
303+ existingDefinitions
304+ . filter ( ( def ) => def . fieldType === definition . fieldType )
305+ . map ( ( def ) => def . displayName )
222306 )
307+
308+ if ( ! existingTagNames . has ( definition . displayName ) ) {
309+ newByFieldType . set (
310+ definition . fieldType ,
311+ ( newByFieldType . get ( definition . fieldType ) || 0 ) + 1
312+ )
313+ }
314+ }
315+
316+ for ( const [ fieldType , newCount ] of newByFieldType . entries ( ) ) {
317+ const existingCount = existingByFieldType . get ( fieldType ) || 0
318+ const maxSlots = getMaxSlotsForFieldType ( fieldType )
319+
320+ if ( existingCount + newCount > maxSlots ) {
321+ return NextResponse . json (
322+ {
323+ error : `Cannot create ${ newCount } new '${ fieldType } ' tags. Knowledge base already has ${ existingCount } '${ fieldType } ' tag definitions. Maximum is ${ maxSlots } per field type.` ,
324+ } ,
325+ { status : 400 }
326+ )
327+ }
223328 }
224329
225330 // Use transaction to ensure consistency
@@ -228,72 +333,93 @@ export async function POST(
228333 const existingByName = new Map ( existingDefinitions . map ( ( def ) => [ def . displayName , def ] ) )
229334 const existingBySlot = new Map ( existingDefinitions . map ( ( def ) => [ def . tagSlot , def ] ) )
230335
231- // Process each new definition
336+ // Process each definition
232337 for ( const definition of validatedData . definitions ) {
338+ if ( definition . _originalDisplayName ) {
339+ // This is an EDIT operation - find by original name and update
340+ const originalDefinition = existingByName . get ( definition . _originalDisplayName )
341+
342+ if ( originalDefinition ) {
343+ logger . info (
344+ `[${ requestId } ] Editing tag definition: ${ definition . _originalDisplayName } -> ${ definition . displayName } (slot ${ originalDefinition . tagSlot } )`
345+ )
346+
347+ await tx
348+ . update ( knowledgeBaseTagDefinitions )
349+ . set ( {
350+ displayName : definition . displayName ,
351+ fieldType : definition . fieldType ,
352+ updatedAt : now ,
353+ } )
354+ . where ( eq ( knowledgeBaseTagDefinitions . id , originalDefinition . id ) )
355+
356+ createdDefinitions . push ( {
357+ ...originalDefinition ,
358+ displayName : definition . displayName ,
359+ fieldType : definition . fieldType ,
360+ updatedAt : now ,
361+ } )
362+ continue
363+ }
364+ logger . warn (
365+ `[${ requestId } ] Could not find original definition for: ${ definition . _originalDisplayName } `
366+ )
367+ }
368+
369+ // Regular create/update logic
233370 const existingByDisplayName = existingByName . get ( definition . displayName )
234- const existingByTagSlot = existingBySlot . get ( definition . tagSlot )
235371
236372 if ( existingByDisplayName ) {
237- // Update existing definition (same display name)
238- if ( existingByDisplayName . tagSlot !== definition . tagSlot ) {
239- // Slot is changing - check if target slot is available
240- if ( existingByTagSlot && existingByTagSlot . id !== existingByDisplayName . id ) {
241- // Target slot is occupied by a different definition - this is a conflict
242- // For now, keep the existing slot to avoid constraint violation
243- logger . warn (
244- `[${ requestId } ] Slot conflict for ${ definition . displayName } : keeping existing slot ${ existingByDisplayName . tagSlot } `
245- )
246- createdDefinitions . push ( existingByDisplayName )
247- continue
248- }
249- }
373+ // Display name exists - UPDATE operation
374+ logger . info (
375+ `[${ requestId } ] Updating existing tag definition: ${ definition . displayName } (slot ${ existingByDisplayName . tagSlot } )`
376+ )
250377
251378 await tx
252379 . update ( knowledgeBaseTagDefinitions )
253380 . set ( {
254- tagSlot : definition . tagSlot ,
255381 fieldType : definition . fieldType ,
256382 updatedAt : now ,
257383 } )
258384 . where ( eq ( knowledgeBaseTagDefinitions . id , existingByDisplayName . id ) )
259385
260386 createdDefinitions . push ( {
261387 ...existingByDisplayName ,
262- tagSlot : definition . tagSlot ,
263- fieldType : definition . fieldType ,
264- updatedAt : now ,
265- } )
266- } else if ( existingByTagSlot ) {
267- // Slot is occupied by a different display name - update it
268- await tx
269- . update ( knowledgeBaseTagDefinitions )
270- . set ( {
271- displayName : definition . displayName ,
272- fieldType : definition . fieldType ,
273- updatedAt : now ,
274- } )
275- . where ( eq ( knowledgeBaseTagDefinitions . id , existingByTagSlot . id ) )
276-
277- createdDefinitions . push ( {
278- ...existingByTagSlot ,
279- displayName : definition . displayName ,
280388 fieldType : definition . fieldType ,
281389 updatedAt : now ,
282390 } )
283391 } else {
284- // Create new definition
392+ // Display name doesn't exist - CREATE operation
393+ const targetSlot = await getNextAvailableSlot (
394+ knowledgeBaseId ,
395+ definition . fieldType ,
396+ existingBySlot
397+ )
398+
399+ if ( ! targetSlot ) {
400+ logger . error (
401+ `[${ requestId } ] No available slots for new tag definition: ${ definition . displayName } `
402+ )
403+ continue
404+ }
405+
406+ logger . info (
407+ `[${ requestId } ] Creating new tag definition: ${ definition . displayName } -> ${ targetSlot } `
408+ )
409+
285410 const newDefinition = {
286411 id : randomUUID ( ) ,
287412 knowledgeBaseId,
288- tagSlot : definition . tagSlot ,
413+ tagSlot : targetSlot as any ,
289414 displayName : definition . displayName ,
290415 fieldType : definition . fieldType ,
291416 createdAt : now ,
292417 updatedAt : now ,
293418 }
294419
295420 await tx . insert ( knowledgeBaseTagDefinitions ) . values ( newDefinition )
296- createdDefinitions . push ( newDefinition )
421+ existingBySlot . set ( targetSlot as any , newDefinition )
422+ createdDefinitions . push ( newDefinition as any )
297423 }
298424 }
299425 } )
0 commit comments