Files
Krow-workspace/frontend-web-free/src/api/krowSDK.js

736 lines
20 KiB
JavaScript

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<object>} 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<object>} 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<object>} 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<object>} 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<object>} 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<object>} 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<object>} 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,
};