736 lines
20 KiB
JavaScript
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,
|
|
};
|