Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions controller/wearableDataController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const wearableDataService = require("../services/wearableDataService");

async function ingestWearableData(req, res) {
try {
const result = await wearableDataService.ingestWearableData(req.user.userId, req.body || {});
return res.status(201).json(result);
} catch (error) {
const status = error.statusCode || 500;
return res.status(status).json({
success: false,
error: status >= 500 ? "Failed to ingest wearable data" : error.message,
details: error.details || undefined,
});
}
}

async function getLatestWearableSummary(req, res) {
try {
const limit = Number.isInteger(Number(req.query.limit))
? Math.min(Math.max(parseInt(req.query.limit, 10), 1), 100)
: 50;

const result = await wearableDataService.getLatestWearableSummary(req.user.userId, limit);
return res.status(200).json(result);
} catch (error) {
const status = error.statusCode || 500;
return res.status(status).json({
success: false,
error: status >= 500 ? "Failed to load wearable data" : error.message,
details: error.details || undefined,
});
}
}

module.exports = {
getLatestWearableSummary,
ingestWearableData,
};
20 changes: 20 additions & 0 deletions database/wearable-device-data.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
create table if not exists public.wearable_device_data (
id bigint generated always as identity primary key,
user_id bigint not null references public.users(user_id) on delete cascade,
source text not null,
device_id text null,
device_name text null,
metric_type text not null,
metric_value numeric not null,
metric_unit text not null,
recorded_at timestamptz not null,
received_at timestamptz not null default now(),
timezone text null,
metadata jsonb not null default '{}'::jsonb
);

create index if not exists idx_wearable_device_data_user_recorded_at
on public.wearable_device_data(user_id, recorded_at desc);

create index if not exists idx_wearable_device_data_metric_type
on public.wearable_device_data(metric_type);
179 changes: 179 additions & 0 deletions repositories/wearable-device/authRepository.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
const { supabaseAnon, supabaseService } = require('../../services/supabaseClient');

async function findUserIdByEmail(email) {
const { data, error } = await supabaseAnon
.from('users')
.select('user_id')
.eq('email', email)
.single();

if (error) {
throw error;
}

return data || null;
}

async function createUser(userData) {
const { data, error } = await supabaseAnon
.from('users')
.insert(userData)
.select('user_id, email, name')
.single();

if (error) {
throw error;
}

return data || null;
}

async function findUserWithRoleByEmail(email) {
const { data, error } = await supabaseAnon
.from('users')
.select(`
user_id, email, password, name, role_id,
account_status, email_verified,
user_roles!inner(id, role_name)
`)
.eq('email', email)
.single();

if (error) {
throw error;
}

return data || null;
}

async function updateLastLogin(userId, lastLogin) {
const { error } = await supabaseAnon
.from('users')
.update({ last_login: lastLogin })
.eq('user_id', userId);

if (error) {
throw error;
}
}

async function deactivateSessionsByUserId(userId) {
const { error } = await supabaseService
.from('user_sessiontoken')
.update({ is_active: false })
.eq('user_id', userId);

if (error) {
throw error;
}
}

async function createRefreshSession(sessionData) {
const { error } = await supabaseService
.from('user_sessiontoken')
.insert(sessionData);

if (error) {
throw error;
}
}

async function findActiveSessionsByLookupHash(lookupHash) {
const { data, error } = await supabaseService
.from('user_sessiontoken')
.select(`
id,
user_id,
refresh_token,
refresh_token_lookup,
expires_at,
is_active
`)
.eq('refresh_token_lookup', lookupHash)
.eq('is_active', true)
.limit(1);

if (error) {
throw error;
}

return data || [];
}

async function findUserById(userId) {
const { data, error } = await supabaseAnon
.from('users')
.select(`
user_id,
email,
name,
role_id,
account_status
`)
.eq('user_id', userId)
.single();

if (error) {
throw error;
}

return data || null;
}

async function deactivateSessionById(sessionId) {
const { error } = await supabaseService
.from('user_sessiontoken')
.update({ is_active: false })
.eq('id', sessionId);

if (error) {
throw error;
}
}

async function deactivateSessionsByLookupHash(lookupHash) {
const { error } = await supabaseService
.from('user_sessiontoken')
.update({ is_active: false })
.eq('refresh_token_lookup', lookupHash);

if (error) {
throw error;
}
}

async function insertAuthLog(logEntry) {
const { error } = await supabaseAnon
.from('auth_logs')
.insert(logEntry);

if (error) {
throw error;
}
}

async function deactivateExpiredSessions(referenceTime) {
const { error } = await supabaseService
.from('user_sessiontoken')
.update({ is_active: false })
.lt('expires_at', referenceTime);

if (error) {
throw error;
}
}

module.exports = {
createRefreshSession,
createUser,
deactivateExpiredSessions,
deactivateSessionById,
deactivateSessionsByLookupHash,
deactivateSessionsByUserId,
findActiveSessionsByLookupHash,
findUserById,
findUserIdByEmail,
findUserWithRoleByEmail,
insertAuthLog,
updateLastLogin
};
51 changes: 51 additions & 0 deletions repositories/wearable-device/errorLogRepository.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
let supabase = null;

try {
const { createClient } = require('@supabase/supabase-js');
if (process.env.SUPABASE_URL && process.env.SUPABASE_ANON_KEY) {
supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY);
}
} catch {
supabase = null;
}

function isAvailable() {
return !!supabase;
}

async function insertErrorLog(dbEntry) {
if (!supabase) {
throw new Error('Supabase client not available');
}

const { data, error } = await supabase
.from('error_logs')
.insert([dbEntry])
.select()
.single();

if (error) {
throw error;
}

return data || null;
}

async function checkConnection() {
if (!supabase) {
return false;
}

try {
const { error } = await supabase.from('error_logs').select('id').limit(1);
return !error;
} catch {
return false;
}
}

module.exports = {
checkConnection,
insertErrorLog,
isAvailable
};
33 changes: 33 additions & 0 deletions repositories/wearable-device/recommendationRepository.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const supabase = require('../../dbConnection');

async function fetchRecentRecipeIds(userId) {
const { data, error } = await supabase
.from('recipe_meal')
.select('recipe_id')
.eq('user_id', userId)
.limit(20);

if (error) {
throw error;
}

return (data || []).map((row) => row.recipe_id);
}

async function fetchCandidateRecipes(limit = 50) {
const { data, error } = await supabase
.from('recipes')
.select('id, recipe_name, cuisine_id, cooking_method_id, total_servings, preparation_time, calories, fat, carbohydrates, protein, fiber, sodium, sugar, allergy, dislike')
.limit(limit);

if (error) {
throw error;
}

return data || [];
}

module.exports = {
fetchCandidateRecipes,
fetchRecentRecipeIds
};
Loading
Loading