import apiClient from './client'; import { auth, dataConnect } from '../firebase'; import { signOut } from 'firebase/auth'; import * as dcSdk from '@dataconnect/generated'; // listEvents, createEvent, etc. const MOCK_USER_ROLE_KEY = 'krow_mock_user_role'; // --- Auth Module --- const authModule = { /** * Fetches the currently authenticated user's profile from the backend. * @returns {Promise} The user profile. */ me: async () => { // 1. Firebase auth user const fbUser = auth.currentUser; if (!fbUser) { return null; // NO ESTÁ LOGGEADO } // 2. Attempt to load matching Krow User from DataConnect // (because your Krow user metadata is stored in the "users" table) let krowUser = null; try { const response = await dcSdk.getUserById(dataConnect, { id: fbUser.uid }); krowUser = response.data?.user || null; } catch (err) { console.warn("Krow user not found in DataConnect, returning Firebase-only info."); } let mockRole = null; mockRole = localStorage.getItem(MOCK_USER_ROLE_KEY); // 3. Build unified "me" object return { id: fbUser.uid, email: fbUser.email, fullName: krowUser?.fullName || fbUser.displayName || null, role: krowUser?.role || "user", user_role: mockRole || krowUser?.userRole || null, firebase: fbUser, krow: krowUser }; }, /** * Logs the user out. * @param {string} [redirectUrl] - Optional URL to redirect to after logout. */ logout: async (redirectUrl) => { localStorage.removeItem(MOCK_USER_ROLE_KEY); await signOut(auth); if (redirectUrl) { window.location.href = redirectUrl; } }, /** * Checks if a user is currently authenticated. * @returns {boolean} True if a user is authenticated. */ isAuthenticated: () => { return !!auth.currentUser; }, // ============================== // FIX: auth.updateMe para soportar RoleSwitcher (antes lo hacía el mock base44) // ============================== /** * Updates current user metadata (including role/user_role). * Used by RoleSwitcher to change between ADMIN / VENDOR / etc. * @param {{ user_role?: string, role?: string, fullName?: string }} data * @returns {Promise} updated "me" object */ updateMe: async (data) => { const fbUser = auth.currentUser; if (!fbUser) { throw new Error("Not authenticated"); } if (data.user_role) { try { localStorage.setItem(MOCK_USER_ROLE_KEY, data.user_role); } catch (err) { console.warn("Krow user role could not be saved to localStorage."); } } return authModule.me(); }, }; // --- Core Integrations Module --- const coreIntegrationsModule = { /** * Sends an email. * @param {object} params - { to, subject, body } * @returns {Promise} API response. */ SendEmail: async (params) => { const { data } = await apiClient.post('/sendEmail', params); return data; }, /** * Invokes a large language model. * @param {object} params - { prompt, response_json_schema, file_urls } * @returns {Promise} API response. */ InvokeLLM: async (params) => { const { data } = await apiClient.post('/invokeLLM', params); return data; }, /** * Uploads a public file. * @param {File} file - The file to upload. * @returns {Promise} API response with file_url. */ UploadFile: async ({ file }) => { const formData = new FormData(); formData.append('file', file); const { data } = await apiClient.post('/uploadFile', formData, { headers: { 'Content-Type': 'multipart/form-data' }, }); return data; }, /** * Uploads a private file. * @param {File} file - The file to upload. * @returns {Promise} API response with file_uri. */ UploadPrivateFile: async ({ file }) => { const formData = new FormData(); formData.append('file', file); const { data } = await apiClient.post('/uploadPrivateFile', formData, { headers: { 'Content-Type': 'multipart/form-data' }, }); return data; }, /** * Creates a temporary signed URL for a private file. * @param {object} params - { file_uri, expires_in } * @returns {Promise} API response with signed_url. */ CreateFileSignedUrl: async (params) => { const { data } = await apiClient.post('/createSignedUrl', params); return data; }, }; const dataconnectEntityConfig = { User: { list: 'listUsers', get: 'getUserById', create: 'createUser', update: 'updateUser', delete: 'deleteUser', filter: 'filterUsers', }, Event: { list: 'listEvents', create: 'createEvent', get: 'getEventById', update: 'updateEvent', delete: 'deleteEvent', filter: 'filterEvents', }, Staff: { list: 'listStaff', create: 'createStaff', get: 'getStaffById', update: 'updateStaff', delete: 'deleteStaff', filter: 'filterStaff', }, Vendor: { list: 'listVendor', get: 'getVendorById', create: 'createVendor', update: 'updateVendor', delete: 'deleteVendor', filter: 'filterVendors', }, VendorRate: { list: 'listVendorRate', get: 'getVendorRateById', create: 'createVendorRate', update: 'updateVendorRate', delete: 'deleteVendorRate', filter: 'filterVendorRates', }, VendorDefaultSetting:{ list: 'listVendorDefaultSettings', get: 'getVendorDefaultSettingById', create: 'createVendorDefaultSetting', update: 'updateVendorDefaultSetting', delete: 'deleteVendorDefaultSetting', filter: 'filterVendorDefaultSettings', }, Invoice:{ list: 'listInvoice', get: 'getInvoiceById', create: 'createInvoice', update: 'updateInvoice', delete: 'deleteInvoice', filter: 'filterInvoices', }, Business:{ list: 'listBusiness', get: 'getBusinessById', create: 'createBusiness', update: 'updateBusiness', delete: 'deleteBusiness', filter: 'filterBusiness', }, Certification:{ list: 'listCertification', get: 'getCertificationById', create: 'createCertification', update: 'updateCertification', delete: 'deleteCertification', filter: 'filterCertification', }, Team:{ list: 'listTeam', get: 'getTeamById', create: 'createTeam', update: 'updateTeam', delete: 'deleteTeam', filter: 'filterTeam', }, TeamMember: { list: 'listTeamMember', get: 'getTeamMemberById', create: 'createTeamMember', update: 'updateTeamMember', delete: 'deleteTeamMember', filter: 'filterTeamMember', }, TeamHub: { list: 'listTeamHub', get: 'getTeamHubById', create: 'createTeamHub', update: 'updateTeamHub', delete: 'deleteTeamHub', filter: 'filterTeamHub', }, TeamMemberInvite: { list: 'listTeamMemberInvite', get: 'getTeamMemberInviteById', create: 'createTeamMemberInvite', update: 'updateTeamMemberInvite', delete: 'deleteTeamMemberInvite', filter: 'filterTeamMemberInvite', }, Conversation:{ list: 'listConversation', get: 'getConversationById', create: 'createConversation', update: 'updateConversation', delete: 'deleteConversation', filter: 'filterConversation', }, Message:{ list: 'listMessage', get: 'getMessageById', create: 'createMessage', update: 'updateMessage', delete: 'deleteMessage', filter: 'filterMessage', }, ActivityLog:{ list: 'listActivityLog', get: 'getActivityLogById', create: 'createActivityLog', update: 'updateActivityLog', delete: 'deleteActivityLog', filter: 'filterActivityLog', }, Enterprise:{ list: 'listEnterprise', get: 'getEnterpriseById', create: 'createEnterprise', update: 'updateEnterprise', delete: 'deleteEnterprise', filter: 'filterEnterprise', }, Sector:{ }, Partner:{ }, Order:{ }, Shift:{ } }; // Helper for methods not implemented const notImplemented = (entityName, method) => async () => { throw new Error(`${entityName}.${method} is not implemented yet for Data Connect`); }; // helper para normalizar siempre a array (list + filter devolverán arrays) const normalizeResultToArray = (res) => { // if it is an array, perfect if (Array.isArray(res)) return res; if (!res || typeof res !== 'object') return []; const data = res.data; if (data && typeof data === 'object') { const keys = Object.keys(data); if (keys.length === 0) return []; const firstValue = data[keys[0]]; if (Array.isArray(firstValue)) return firstValue; if (firstValue == null) return []; // if it is a single object, return it as an array return [firstValue]; } return []; }; // --- Entities Module ( Data Connect, without REST Base44) --- const entitiesModule = {}; // --- Helper to convert snake_case to camelCase recursively --- const toCamel = (value) => { if (Array.isArray(value)) { return value.map(toCamel); } if (value && typeof value === "object") { return Object.entries(value).reduce((acc, [key, val]) => { const camelKey = key.replace(/_([a-z])/g, (_, c) => c.toUpperCase()); acc[camelKey] = toCamel(val); return acc; }, {}); } return value; }; // --- Helper to convert camelCase to snake_case recursively --- const toSnake = (value) => { if (Array.isArray(value)) { return value.map(toSnake); } if (value && typeof value === "object") { return Object.entries(value).reduce((acc, [key, val]) => { const snakeKey = key .replace(/([A-Z])/g, "_$1") .toLowerCase(); acc[snakeKey] = toSnake(val); return acc; }, {}); } return value; }; Object.entries(dataconnectEntityConfig).forEach(([entityName, ops]) => { entitiesModule[entityName] = { get: notImplemented(entityName, 'get'), update: notImplemented(entityName, 'update'), delete: notImplemented(entityName, 'delete'), filter: notImplemented(entityName, 'filter'), list: notImplemented(entityName, 'list'), create: notImplemented(entityName, 'create'), // list ...(ops.list && { list: async (...args) => { const fn = dcSdk[ops.list]; if (typeof fn !== 'function') { throw new Error( `Data Connect operation "${ops.list}" not found for entity "${entityName}".` ); } let sort; let limit; let baseVariables; // por si algún list usa variables (como Team) if (args.length === 1) { const [a0] = args; if (typeof a0 === "string") { // list('-created_date') sort = a0; } else if (a0 && typeof a0 === "object" && !Array.isArray(a0)) { // list({ ...vars }) -> reservado para queries que acepten variables baseVariables = a0; } } else if (args.length === 2) { const [a0, a1] = args; if (typeof a0 === "string" && typeof a1 === "number") { // list('-created_date', 50) sort = a0; limit = a1; } else if (a0 && typeof a0 === "object" && !Array.isArray(a0)) { // list({ ...vars }, '-created_date') baseVariables = a0; if (typeof a1 === "string") sort = a1; } } else if (args.length >= 3) { const [a0, a1, a2] = args; if (a0 && typeof a0 === "object" && !Array.isArray(a0)) { // list({ ...vars }, '-created_date', 50) baseVariables = a0; if (typeof a1 === "string") sort = a1; if (typeof a2 === "number") limit = a2; } } // COMMENT FIX: variables que realmente se mandan a DataConnect let variables = baseVariables; // COMMENT FIX: caso especial para Team, que SÍ tiene orderBy/limit en el query if (entityName === "Team") { variables = variables || {}; if (sort) { const desc = sort.startsWith("-"); variables.orderByCreatedDate = desc ? "DESC" : "ASC"; } if (typeof limit === "number") { variables.limit = limit; } } const res = await fn(dataConnect, variables); let items = normalizeResultToArray(res); // COMMENT FIX: para entidades que NO tienen orderBy/limit en el query, // aplicamos sort/limit en el front como fallback. if (entityName !== "Team" && sort) { const desc = sort.startsWith("-"); const field = desc ? sort.slice(1) : sort; // '-created_date' -> 'created_date' items = items.slice().sort((a, b) => { const av = a?.[field]; const bv = b?.[field]; if (av == null && bv == null) return 0; if (av == null) return 1; if (bv == null) return -1; const da = new Date(av); const db = new Date(bv); if (!isNaN(da) && !isNaN(db)) { return desc ? db - da : da - db; } if (av < bv) return desc ? 1 : -1; if (av > bv) return desc ? -1 : 1; return 0; }); } if (entityName !== "Team" && typeof limit === "number") { items = items.slice(0, limit); } //console.log(items) //return items; return items.map(toSnake); }, }), // create ...(ops.create && { create: async (params) => { const fn = dcSdk[ops.create]; if (typeof fn !== 'function') { throw new Error( `Data Connect operation "${ops.create}" not found for entity "${entityName}".` ); } if (!params || typeof params !== 'object') { throw new Error( `${entityName}.create expects an object with the fields to insert` ); } let payload; if (params.data && typeof params.data === 'object') { // caso nuevo: create({ data: { ... } }) payload = params.data; } else { // caso legacy: create({ ...fields }) payload = params; } //converting to camelCase for data connect payload = toCamel(payload); return fn(dataConnect, payload); }, }), //get ...(ops.get && { get: async (params) => { const fn = dcSdk[ops.get]; if (typeof fn !== 'function') { throw new Error( `Data Connect operation "${ops.get}" not found for entity "${entityName}".` ); } if (!params || typeof params !== 'object') { throw new Error(`${entityName}.get expects an object of variables (e.g. { id })`); } //return fn(dataConnect, params); const result = await fn(dataConnect, params); return toSnake(result); }, }), //update ...(ops.update && { update: async (id,params) => { const fn = dcSdk[ops.update]; if (typeof fn !== 'function') { throw new Error( `Data Connect operation "${ops.update}" not found for entity "${entityName}".` ); } if (!params || typeof params !== 'object') { throw new Error( `${entityName}.update expects an object of variables matching the GraphQL mutation` ); } //const { id, data } = params; let data = params; if (!id) { throw new Error(`${entityName}.update requires an "id" field`); } if (!data || typeof data !== 'object') { throw new Error( `${entityName}.update requires a "data" object with the fields to update` ); } if (data && typeof data === "object") { data = toCamel(data); } const vars = { id, ...data }; return fn(dataConnect, vars); }, }), // delete ...(ops.delete && { delete: async (params) => { const fn = dcSdk[ops.delete]; if (typeof fn !== 'function') { throw new Error( `Data Connect operation "${ops.delete}" not found for entity "${entityName}".` ); } if (!params || typeof params !== 'object') { throw new Error( `${entityName}.delete expects an object like { id }` ); } const { id } = params; if (!id) { throw new Error(`${entityName}.delete requires an "id" field`); } // Data Connect solo espera { id } como variables return fn(dataConnect, { id }); }, }), // filter ...(ops.filter && { filter: async (...args) => { const fn = dcSdk[ops.filter]; if (typeof fn !== 'function') { throw new Error( `Data Connect operation "${ops.filter}" not found for entity "${entityName}".` ); } // FIX: soportar firma vieja: filter(filters, sort, limit) const [params, sort, limit] = args; // FIX: soportar firma vieja: filter(filters, sort, limit) if (!params) { if (ops.list) {//if no params, call to list() const listFn = dcSdk[ops.list]; if (typeof listFn !== 'function') { throw new Error( `Data Connect operation "${ops.list}" not found for entity "${entityName}".` ); } //return listFn(dataConnect); //fix const resList = await listFn(dataConnect); return normalizeResultToArray(resList); //fix } throw new Error(`${entityName}.filter expects params or a list operation`); } const rawFilters = params.filters ?? params; const variables = {}; for (const [key, value] of Object.entries(rawFilters)) {//cleaning undefined/null/'' values if (value !== undefined && value !== null && value !== '') { variables[key] = value; } } // if no valid filters, call to list() if (Object.keys(variables).length === 0) { if (ops.list) { const listFn = dcSdk[ops.list]; if (typeof listFn !== 'function') { throw new Error( `Data Connect operation "${ops.list}" not found for entity "${entityName}".` ); } //return listFn(dataConnect); //fix const resList = await listFn(dataConnect); return normalizeResultToArray(resList); //fix } throw new Error(`${entityName}.filter received no valid filters and no list operation`); } //return fn(dataConnect, variables); //fix const res = await fn(dataConnect, variables); // FIX: normalizar a array (activityLogs[], messages[], etc.) let items = normalizeResultToArray(res); // FIX: soportar sort tipo '-created_date' o 'created_date' if (sort) { const desc = sort.startsWith('-'); const field = desc ? sort.slice(1) : sort; items = items.slice().sort((a, b) => { const av = a?.[field]; const bv = b?.[field]; if (av == null && bv == null) return 0; if (av == null) return 1; if (bv == null) return -1; // Intentar tratarlos como fecha si parecen fechas const da = new Date(av); const db = new Date(bv); if (!isNaN(da) && !isNaN(db)) { return desc ? db - da : da - db; } // Fallback: comparación normal if (av < bv) return desc ? 1 : -1; if (av > bv) return desc ? -1 : 1; return 0; }); } // FIX: soportar limit (ej: 50) if (typeof limit === 'number') { items = items.slice(0, limit); } return items; //fix }, }), }; }); // --- Main SDK Export --- export const krowSDK = { auth: authModule, integrations: { Core: coreIntegrationsModule, }, entities: entitiesModule, };