1- const { Project } = require ( '@urbackend/common/src/models' ) ;
1+ const { Project, Developer } = require ( '@urbackend/common/src/models' ) ;
22const { forwardToPythonService } = require ( '../utils/internalPythonClient' ) ;
3- const { AppError, ApiResponse, getProjectAccessQuery } = require ( '@urbackend/common' ) ;
3+ const { AppError, ApiResponse, getProjectAccessQuery, resolveEffectivePlan } = require ( '@urbackend/common' ) ;
4+ const { decrypt } = require ( '@urbackend/common/src/utils/encryption' ) ;
5+ const { encryptForTransit } = require ( '../utils/transitEncryption' ) ;
46
57/**
68 * Controller to handle AI Query Builder requests.
@@ -33,18 +35,26 @@ const queryBuilder = async (req, res, next) => {
3335 throw new AppError ( 403 , "Cannot query the users collection via AI" ) ;
3436 }
3537
36- // 1. Fetch the project and specifically the requested collection schema
37- const project = await Project . findOne (
38- { ...getProjectAccessQuery ( req . user . _id ) , _id : projectId , "collections.name" : safeCollectionName } ,
39- { "collections.$" : 1 }
38+ // 1. Load collection and project details
39+ const project = await Project . findOne ( {
40+ _id : projectId ,
41+ ...getProjectAccessQuery ( req . user . _id )
42+ } ) ;
43+
44+ if ( ! project ) {
45+ throw new AppError ( 404 , "Project not found or access denied" ) ;
46+ }
47+
48+ const collection = project . collections . find (
49+ col => col . name . toLowerCase ( ) === safeCollectionName . toLowerCase ( )
4050 ) ;
4151
42- if ( ! project || ! project . collections || project . collections . length === 0 ) {
43- throw new AppError ( 404 , " Collection not found or access denied" ) ;
52+ if ( ! collection ) {
53+ throw new AppError ( 404 , ` Collection ' ${ safeCollectionName } ' not found in this project` ) ;
4454 }
4555
46- const collection = project . collections [ 0 ] ;
47- const allowedFields = new Set ( [
56+ // Add index checking fields
57+ const schemaFieldsCheck = new Set ( [
4858 ...collection . model . map ( field => field . key ) ,
4959 '_id' ,
5060 'createdAt' ,
@@ -65,10 +75,41 @@ const queryBuilder = async (req, res, next) => {
6575 { key : "updatedAt" , type : "DATE" }
6676 ) ;
6777
78+ // ── Hierarchical BYOK Resolution ──
79+ let resolvedKey = null ;
80+
81+ // 1. Check project-level BYOK
82+ const projectByok = await Project . findOne ( {
83+ _id : projectId ,
84+ ...getProjectAccessQuery ( req . user . _id )
85+ } )
86+ . select ( '+byok.groqKey.encrypted +byok.groqKey.iv +byok.groqKey.tag' )
87+ . lean ( ) ;
88+ if ( projectByok ?. byok ?. groqKey ?. encrypted ) {
89+ resolvedKey = decrypt ( projectByok . byok . groqKey ) ;
90+ }
91+
92+ // 2. Fallback to developer-level BYOK
93+ const dev = await Developer . findById ( req . user . _id ) . select ( '+byok' ) ;
94+ if ( ! resolvedKey && dev ?. byok ?. groqKey ?. encrypted ) {
95+ resolvedKey = decrypt ( dev . byok . groqKey ) ;
96+ }
97+
98+ // 3. Encrypt for secure transit to Python
99+ const encryptedByok = resolvedKey
100+ ? { groqKey : encryptForTransit ( resolvedKey ) }
101+ : null ;
102+
103+ // Resolve effective plan dynamically based on DB (handling subscription expiry safely)
104+ const effectivePlan = dev ? resolveEffectivePlan ( dev ) : 'free' ;
105+
68106 // 3. Forward request to Python Service
69107 const aiResponse = await forwardToPythonService ( '/ai/query-builder' , {
70108 prompt : safePrompt ,
71- schema_fields : schemaFields
109+ schema_fields : schemaFields ,
110+ developer_id : req . user . _id . toString ( ) ,
111+ plan : effectivePlan ,
112+ encrypted_byok : encryptedByok
72113 } ) ;
73114
74115 // 4. Return the structured JSON to the frontend
0 commit comments