From 5233c4699d427aae6457000024d74914f9c00557 Mon Sep 17 00:00:00 2001 From: Jon Hnefill Jakobsson Date: Fri, 6 Mar 2026 01:02:39 +0100 Subject: [PATCH] fix: migrate API to Turso, fix Vercel config, fix app TS errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit API changes: - Migrate from MongoDB to Turso (@libsql/client) - Add SQL schema for users and receipts tables - Export serverless handler for Vercel deployment - Fix vercel.json (version 2, remove invalid env block) - Add missing deps: @fastify/rate-limit, fastify-plugin - Make Swagger host dynamic (VERCEL_URL or localhost) - Fix security.ts getResponseTime → elapsedTime (Fastify 5) App changes: - Fix broken import paths in App.tsx (./app/ → ./) - Fix non-existent selectUser import in UsersScreen - Fix user.email reference in BeaconsScreen (not on User type) - Fix thumbColor → thumbTintColor in CreateUserScreen - Fix missing commonStyles.loadingText in StatsScreen - Fix headers type mismatch in api service - Rename mongodb.service → api.service, MongoDBService → ApiService - Add .env.example Co-Authored-By: Oz --- api/package.json | 4 +- api/src/index.ts | 385 +++++++-------- api/src/plugins/security.ts | 2 +- api/src/routes/receipts.ts | 98 ++-- api/src/routes/users.ts | 293 +++++------- api/src/utils/db.ts | 47 ++ api/src/utils/mongodb.ts | 46 -- api/src/utils/schema.ts | 29 ++ api/vercel.json | 12 +- app/.env.example | 2 + app/App.tsx | 6 +- app/screens/BeaconsScreen.tsx | 2 +- app/screens/CreateUserScreen.tsx | 2 +- app/screens/StatsScreen.tsx | 2 +- app/screens/UsersScreen.tsx | 4 +- .../{mongodb.service.ts => api.service.ts} | 13 +- app/services/fetchMiddleware.ts | 50 +- app/services/user.service.ts | 14 +- app/store/receipts/receiptsSlice.ts | 8 +- package-lock.json | 449 +++++++++++++----- 20 files changed, 823 insertions(+), 645 deletions(-) create mode 100644 api/src/utils/db.ts delete mode 100644 api/src/utils/mongodb.ts create mode 100644 api/src/utils/schema.ts create mode 100644 app/.env.example rename app/services/{mongodb.service.ts => api.service.ts} (92%) diff --git a/api/package.json b/api/package.json index c0b7c3f..dca7dd2 100644 --- a/api/package.json +++ b/api/package.json @@ -35,12 +35,14 @@ "dependencies": { "@fastify/cors": "^10.0.1", "@fastify/helmet": "^12.0.1", + "@fastify/rate-limit": "^10.2.1", "@fastify/sensible": "^6.0.1", "@fastify/swagger": "^9.4.0", "@fastify/swagger-ui": "^5.1.0", + "@libsql/client": "^0.14.0", "dotenv": "^16.4.7", "fastify": "^5.2.2", - "mongodb": "^6.12.0" + "fastify-plugin": "^5.0.1" }, "devDependencies": { "@types/jest": "^29.5.14", diff --git a/api/src/index.ts b/api/src/index.ts index 771d9ba..6ea8a32 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -1,232 +1,227 @@ import Fastify from 'fastify'; import { FastifyInstance } from 'fastify'; import * as dotenv from 'dotenv'; -import { connectToMongoDB, closeMongoDB, isConnected } from './utils/mongodb'; +import { initializeDb, isConnected, closeDb } from './utils/db'; import authPlugin from './plugins/auth'; import usersRoutes from './routes/users'; import receiptsRoutes from './routes/receipts'; import { HealthCheckResponse } from './types'; +import type { IncomingMessage, ServerResponse } from 'http'; // Load environment variables dotenv.config(); -// Create Fastify instance -const fastify: FastifyInstance = Fastify({ - logger: { - level: process.env['NODE_ENV'] === 'production' ? 'info' : 'debug' - } -}); - // Environment configuration const config = { port: parseInt(process.env['PORT'] || '4000'), host: process.env['HOST'] || '0.0.0.0', - mongoUri: process.env['MONGODB_URI'] || '', - mongoDbName: process.env['MONGODB_DB_NAME'] || 'driversnote', - corsOrigin: process.env['CORS_ORIGIN'] || 'http://localhost:3000', + corsOrigin: process.env['CORS_ORIGIN'] || '*', rateLimitMax: parseInt(process.env['RATE_LIMIT_MAX'] || '100'), - rateLimitWindow: parseInt(process.env['RATE_LIMIT_WINDOW'] || '900000') // 15 minutes + rateLimitWindow: parseInt(process.env['RATE_LIMIT_WINDOW'] || '900000'), }; async function buildApp(): Promise { - try { - // Register security plugins - await fastify.register(import('@fastify/helmet'), { - contentSecurityPolicy: { - directives: { - defaultSrc: ["'self'"], - styleSrc: ["'self'", "'unsafe-inline'"], - scriptSrc: ["'self'"], - imgSrc: ["'self'", "data:", "https:"], - }, + const fastify: FastifyInstance = Fastify({ + logger: { + level: process.env['NODE_ENV'] === 'production' ? 'info' : 'debug' + } + }); + + // Initialize Turso database and schema + await initializeDb(); + + // Register security plugins + await fastify.register(import('@fastify/helmet'), { + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'"], + scriptSrc: ["'self'"], + imgSrc: ["'self'", "data:", "https:"], }, - }); - - // Register CORS - await fastify.register(import('@fastify/cors'), { - origin: config.corsOrigin, - credentials: true, - }); - - // Register rate limiting with stricter limits for production - await fastify.register(import('@fastify/rate-limit'), { - max: config.rateLimitMax, - timeWindow: config.rateLimitWindow, - skipOnError: false, - ban: process.env.NODE_ENV === 'production' ? 10 : undefined, // Ban after 10 violations in production - keyGenerator: (request) => { - // Use IP address for rate limiting - const forwarded = request.headers['x-forwarded-for'] as string; - const realIP = request.headers['x-real-ip'] as string; - - if (forwarded) { - return forwarded.split(',')[0].trim(); - } - - if (realIP) { - return realIP; - } - - return request.socket.remoteAddress || 'unknown'; + }, + }); + + // Register CORS + await fastify.register(import('@fastify/cors'), { + origin: config.corsOrigin, + credentials: true, + }); + + // Register rate limiting + await fastify.register(import('@fastify/rate-limit'), { + max: config.rateLimitMax, + timeWindow: config.rateLimitWindow, + skipOnError: false, + ban: process.env['NODE_ENV'] === 'production' ? 10 : undefined, + keyGenerator: (request) => { + const forwarded = request.headers['x-forwarded-for'] as string; + const realIP = request.headers['x-real-ip'] as string; + if (forwarded) return forwarded.split(',')[0].trim(); + if (realIP) return realIP; + return request.socket.remoteAddress || 'unknown'; + }, + errorResponseBuilder: (_request, context) => ({ + error: 'Rate limit exceeded', + message: `Too many requests. Limit: ${context.max} per ${Math.round(context.ttl / 1000)} seconds`, + retryAfter: Math.round(context.ttl / 1000), + }), + }); + + // Determine Swagger host dynamically + const vercelUrl = process.env['VERCEL_URL']; + const swaggerHost = vercelUrl || `localhost:${config.port}`; + const swaggerSchemes = vercelUrl ? ['https'] : ['http', 'https']; + + // Register Swagger + await fastify.register(import('@fastify/swagger'), { + swagger: { + info: { + title: 'Driversnote Assessment API', + description: 'API service for Driversnote assessment with Turso database', + version: '1.0.0', }, - errorResponseBuilder: (request, context) => { - return { - error: 'Rate limit exceeded', - message: `Too many requests. Limit: ${context.max} per ${Math.round(context.ttl / 1000)} seconds`, - retryAfter: Math.round(context.ttl / 1000) - }; - } - }); - - // Register Swagger - await fastify.register(import('@fastify/swagger'), { - swagger: { - info: { - title: 'Driversnote Assessment API', - description: 'API service for Driversnote assessment with MongoDB', - version: '1.0.0', + host: swaggerHost, + schemes: swaggerSchemes, + consumes: ['application/json'], + produces: ['application/json'], + securityDefinitions: { + apiKey: { + type: 'apiKey', + name: 'X-API-Key', + in: 'header', }, - host: `localhost:${config.port}`, - schemes: ['http', 'https'], - consumes: ['application/json'], - produces: ['application/json'], - securityDefinitions: { - apiKey: { - type: 'apiKey', - name: 'X-API-Key', - in: 'header', - }, - }, - security: [{ apiKey: [] }], }, - }); - - // Register Swagger UI - await fastify.register(import('@fastify/swagger-ui'), { - routePrefix: '/docs', - uiConfig: { - docExpansion: 'list', - deepLinking: false, - }, - staticCSP: true, - transformStaticCSP: (header) => header, - transformSpecification: (swaggerObject) => { - return swaggerObject; - }, - transformSpecificationClone: true, - }); - - // Register authentication plugin - await fastify.register(authPlugin); - - // Root endpoint - API information (no authentication required) - fastify.get('/', { - schema: { - tags: ['Info'], - description: 'API information and available endpoints', - response: { - 200: { - type: 'object', - properties: { - name: { type: 'string' }, - version: { type: 'string' }, - description: { type: 'string' }, - endpoints: { - type: 'object', - properties: { - health: { type: 'string' }, - users: { type: 'string' }, - receipts: { type: 'string' }, - documentation: { type: 'string' } - } - }, - documentation: { type: 'string' }, - status: { type: 'string' }, - authentication: { type: 'string' } - } + security: [{ apiKey: [] }], + }, + }); + + // Register Swagger UI + await fastify.register(import('@fastify/swagger-ui'), { + routePrefix: '/docs', + uiConfig: { + docExpansion: 'list', + deepLinking: false, + }, + staticCSP: true, + transformStaticCSP: (header) => header, + transformSpecification: (swaggerObject) => swaggerObject, + transformSpecificationClone: true, + }); + + // Register authentication plugin + await fastify.register(authPlugin); + + // Root endpoint + fastify.get('/', { + schema: { + tags: ['Info'], + description: 'API information and available endpoints', + response: { + 200: { + type: 'object', + properties: { + name: { type: 'string' }, + version: { type: 'string' }, + description: { type: 'string' }, + endpoints: { + type: 'object', + properties: { + health: { type: 'string' }, + users: { type: 'string' }, + receipts: { type: 'string' }, + documentation: { type: 'string' } + } + }, + documentation: { type: 'string' }, + status: { type: 'string' }, + authentication: { type: 'string' } } } } - }, async () => { - return { - name: 'Driversnote Assessment API', - version: '1.0.0', - description: 'A secure API service for managing users and receipts with MongoDB storage', - endpoints: { - health: '/api/health', - users: '/api/users', - receipts: '/api/receipts', - documentation: '/docs' - }, - documentation: '/docs', - status: 'running', - authentication: 'API Key required (X-API-Key header) for all endpoints except /api/health and this root endpoint' - }; - }); - - // Health check endpoint (no authentication required) - fastify.get('/api/health', { - schema: { - tags: ['Health'], - description: 'Health check endpoint', - response: { - 200: { - type: 'object', - properties: { - status: { type: 'string' }, - timestamp: { type: 'string' }, - database: { type: 'string' }, - version: { type: 'string' }, - }, + } + }, async () => ({ + name: 'Driversnote Assessment API', + version: '1.0.0', + description: 'A secure API service for managing users and receipts with Turso database', + endpoints: { + health: '/api/health', + users: '/api/users', + receipts: '/api/receipts', + documentation: '/docs' + }, + documentation: '/docs', + status: 'running', + authentication: 'API Key required (X-API-Key header) for all endpoints except /api/health and this root endpoint' + })); + + // Health check endpoint + fastify.get('/api/health', { + schema: { + tags: ['Health'], + description: 'Health check endpoint', + response: { + 200: { + type: 'object', + properties: { + status: { type: 'string' }, + timestamp: { type: 'string' }, + database: { type: 'string' }, + version: { type: 'string' }, }, }, }, - }, async () => { - const healthResponse: HealthCheckResponse = { - status: 'ok', - timestamp: new Date().toISOString(), - database: isConnected() ? 'connected' : 'disconnected', - version: process.env['npm_package_version'] || '1.0.0', - }; - - return healthResponse; - }); + }, + }, async () => { + const healthResponse: HealthCheckResponse = { + status: 'ok', + timestamp: new Date().toISOString(), + database: isConnected() ? 'connected' : 'disconnected', + version: process.env['npm_package_version'] || '1.0.0', + }; + return healthResponse; + }); + + // Register API routes + await fastify.register(usersRoutes, { prefix: '/api' }); + await fastify.register(receiptsRoutes, { prefix: '/api' }); + + return fastify; +} - // Register API routes - await fastify.register(usersRoutes, { prefix: '/api' }); - await fastify.register(receiptsRoutes, { prefix: '/api' }); +// --- Serverless handler for Vercel --- - return fastify; - } catch (error) { - console.error('Error building app:', error); - throw error; +let appPromise: Promise | null = null; + +function getApp(): Promise { + if (!appPromise) { + appPromise = buildApp().then(async (app) => { + await app.ready(); + return app; + }); } + return appPromise; +} + +// Default export for @vercel/node +export default async function handler(req: IncomingMessage, res: ServerResponse): Promise { + const app = await getApp(); + app.server.emit('request', req, res); } +// --- Local development server --- + async function start(): Promise { try { - // Connect to MongoDB - await connectToMongoDB({ - uri: config.mongoUri, - dbName: config.mongoDbName, - }); - - // Build the application const app = await buildApp(); - // Start the server await app.listen({ port: config.port, host: config.host, }); console.log(`🚀 Server running on http://${config.host}:${config.port}`); - console.log(`🏥 Health check: http://${config.host}:${config.port}/api/health`); console.log(`📚 API Documentation: http://${config.host}:${config.port}/docs`); - console.log(`👥 Users API: http://${config.host}:${config.port}/api/users`); - console.log(`🧾 Receipts API: http://${config.host}:${config.port}/api/receipts`); - console.log(`🔐 API Key required for all endpoints except /api/health`); - } catch (error) { console.error('❌ Failed to start server:', error); process.exit(1); @@ -236,10 +231,8 @@ async function start(): Promise { // Handle graceful shutdown async function gracefulShutdown(signal: string): Promise { console.log(`\n🔄 Received ${signal}, shutting down gracefully...`); - try { - await fastify.close(); - await closeMongoDB(); + await closeDb(); console.log('✅ Server shutdown complete'); process.exit(0); } catch (error) { @@ -248,20 +241,10 @@ async function gracefulShutdown(signal: string): Promise { } } -// Register shutdown handlers process.on('SIGINT', () => gracefulShutdown('SIGINT')); process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); -// Handle uncaught exceptions -process.on('uncaughtException', (error) => { - console.error('❌ Uncaught Exception:', error); - process.exit(1); -}); - -process.on('unhandledRejection', (reason, promise) => { - console.error('❌ Unhandled Rejection at:', promise, 'reason:', reason); - process.exit(1); -}); - -// Start the server -start().catch(console.error); +// Only start the server when not running on Vercel +if (!process.env['VERCEL']) { + start().catch(console.error); +} diff --git a/api/src/plugins/security.ts b/api/src/plugins/security.ts index 5294315..4645e79 100644 --- a/api/src/plugins/security.ts +++ b/api/src/plugins/security.ts @@ -54,7 +54,7 @@ const securityPlugin: FastifyPluginAsync = async ( ip: clientIP, userAgent: userAgent, statusCode: reply.statusCode, - responseTime: reply.getResponseTime() + responseTime: reply.elapsedTime }, 'Request processed'); } diff --git a/api/src/routes/receipts.ts b/api/src/routes/receipts.ts index 6eda942..4a19934 100644 --- a/api/src/routes/receipts.ts +++ b/api/src/routes/receipts.ts @@ -1,13 +1,26 @@ import { FastifyInstance, FastifyPluginAsync, FastifyRequest, FastifyReply } from 'fastify'; -import { getDatabase } from '../utils/mongodb'; -import { - Receipt, +import { getClient } from '../utils/db'; +import { + Receipt, CreateReceiptRequest, receiptSchema, createReceiptSchema } from '../types'; import { RouteSchema } from '../types/swagger'; +function rowToReceipt(row: Record): Receipt { + return { + id: row['id'] as string, + userId: row['user_id'] as number, + userName: row['user_name'] as string, + beaconQuantity: row['beacon_quantity'] as number, + discount: row['discount'] as number, + deliveryAddress: row['delivery_address'] as string, + totalPrice: row['total_price'] as number, + timestamp: row['timestamp'] as string, + }; +} + const receiptsRoutes: FastifyPluginAsync = async (fastify: FastifyInstance) => { // Get all receipts fastify.get('/receipts', { @@ -25,21 +38,13 @@ const receiptsRoutes: FastifyPluginAsync = async (fastify: FastifyInstance) => { } satisfies RouteSchema }, async (request: FastifyRequest, reply: FastifyReply): Promise => { try { - const db = getDatabase(); - const receipts = await db - .collection('receipts') - .find({}) - .sort({ timestamp: -1 }) - .toArray(); - - console.log(`📋 Fetched ${receipts.length} receipts from MongoDB`); + const db = getClient(); + const result = await db.execute('SELECT * FROM receipts ORDER BY timestamp DESC'); + const receipts = result.rows.map((row) => rowToReceipt(row as unknown as Record)); reply.send(receipts); } catch (error) { console.error('Error fetching receipts:', error); - reply.code(500).send({ - success: false, - error: 'Failed to fetch receipts' - }); + reply.code(500).send({ success: false, error: 'Failed to fetch receipts' }); } }); @@ -51,31 +56,41 @@ const receiptsRoutes: FastifyPluginAsync = async (fastify: FastifyInstance) => { description: 'Create a new receipt', security: [{ apiKey: [] }], body: createReceiptSchema, - response: { - 201: receiptSchema - } + response: { 201: receiptSchema } } satisfies RouteSchema }, async (request: FastifyRequest, reply: FastifyReply): Promise => { try { const receiptData = request.body as CreateReceiptRequest; - const db = getDatabase(); + const db = getClient(); + + const id = new Date().getTime().toString(); + const timestamp = new Date().toISOString(); + + await db.execute({ + sql: `INSERT INTO receipts (id, user_id, user_name, beacon_quantity, discount, delivery_address, total_price, timestamp) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + args: [ + id, + receiptData.userId, + receiptData.userName, + receiptData.beaconQuantity, + receiptData.discount, + receiptData.deliveryAddress, + receiptData.totalPrice, + timestamp, + ], + }); const newReceipt: Receipt = { ...receiptData, - id: new Date().getTime().toString(), // Simple ID generation - timestamp: new Date().toISOString(), + id, + timestamp, }; - await db.collection('receipts').insertOne(newReceipt); - console.log(`🧾 Created new receipt for user: ${newReceipt.userName}`); - reply.code(201).send(newReceipt); } catch (error) { console.error('Error creating receipt:', error); - reply.code(500).send({ - success: false, - error: 'Failed to create receipt' - }); + reply.code(500).send({ success: false, error: 'Failed to create receipt' }); } }); @@ -88,45 +103,32 @@ const receiptsRoutes: FastifyPluginAsync = async (fastify: FastifyInstance) => { security: [{ apiKey: [] }], params: { type: 'object', - properties: { - id: { type: 'string' } - }, + properties: { id: { type: 'string' } }, required: ['id'] }, response: { 200: { type: 'object', - properties: { - success: { type: 'boolean' } - } + properties: { success: { type: 'boolean' } } } } } satisfies RouteSchema }, async (request: FastifyRequest, reply: FastifyReply): Promise => { try { const receiptId = (request.params as { id: string }).id; - const db = getDatabase(); + const db = getClient(); - const result = await db - .collection('receipts') - .deleteOne({ id: receiptId }); + const result = await db.execute({ sql: 'DELETE FROM receipts WHERE id = ?', args: [receiptId] }); - if (result.deletedCount === 0) { - reply.code(404).send({ - success: false, - error: 'Receipt not found' - }); + if (result.rowsAffected === 0) { + reply.code(404).send({ success: false, error: 'Receipt not found' }); return; } - console.log(`🗑️ Deleted receipt with ID: ${receiptId}`); reply.send({ success: true }); } catch (error) { console.error('Error deleting receipt:', error); - reply.code(500).send({ - success: false, - error: 'Failed to delete receipt' - }); + reply.code(500).send({ success: false, error: 'Failed to delete receipt' }); } }); }; diff --git a/api/src/routes/users.ts b/api/src/routes/users.ts index 0ef3411..8a3640f 100644 --- a/api/src/routes/users.ts +++ b/api/src/routes/users.ts @@ -1,10 +1,10 @@ import { FastifyInstance, FastifyPluginAsync, FastifyRequest, FastifyReply } from 'fastify'; -import { getDatabase } from '../utils/mongodb'; -import { - User, - CreateUserRequest, - UpdateUserRequest, - InitializeUsersRequest, +import { getClient } from '../utils/db'; +import { + User, + CreateUserRequest, + UpdateUserRequest, + InitializeUsersRequest, userSchema, createUserSchema } from '../types'; @@ -13,7 +13,7 @@ import { RouteSchema } from '../types/swagger'; // Authentication function async function authenticateRequest(request: FastifyRequest, reply: FastifyReply): Promise { const apiKey = request.headers['x-api-key'] as string; - + if (!apiKey) { reply.code(401).send({ error: 'API key is required', @@ -23,7 +23,7 @@ async function authenticateRequest(request: FastifyRequest, reply: FastifyReply) } const expectedApiKey = process.env['API_KEY']; - + if (!expectedApiKey) { reply.code(500).send({ error: 'Server configuration error', @@ -43,8 +43,23 @@ async function authenticateRequest(request: FastifyRequest, reply: FastifyReply) return true; } +function rowToUser(row: Record): User { + return { + id: row['id'] as number, + full_name: row['full_name'] as string, + tag: (row['tag'] as string) ?? undefined, + address1: row['address1'] as string | null, + address2: row['address2'] as string | null, + postal_code: row['postal_code'] as string | null, + city: row['city'] as string | null, + country_name: (row['country_name'] as string) ?? undefined, + country_id: (row['country_id'] as string) ?? undefined, + organisation_id: row['organisation_id'] as number | null, + }; +} + const usersRoutes: FastifyPluginAsync = async (fastify: FastifyInstance) => { - // Get all users (requires authentication) + // Get all users fastify.get('/users', { schema: { tags: ['Users'], @@ -58,27 +73,21 @@ const usersRoutes: FastifyPluginAsync = async (fastify: FastifyInstance) => { } } satisfies RouteSchema }, async (request: FastifyRequest, reply: FastifyReply): Promise => { - // Check authentication const isAuthenticated = await authenticateRequest(request, reply); - if (!isAuthenticated) { - return; // Response already sent by authenticateRequest - } + if (!isAuthenticated) return; try { - const db = getDatabase(); - const users = await db.collection('users').find({}).toArray(); - console.log(`📋 Fetched ${users.length} users from MongoDB`); + const db = getClient(); + const result = await db.execute('SELECT * FROM users ORDER BY id'); + const users = result.rows.map((row) => rowToUser(row as unknown as Record)); reply.send(users); } catch (error) { console.error('Error fetching users:', error); - reply.code(500).send({ - success: false, - error: 'Failed to fetch users' - }); + reply.code(500).send({ success: false, error: 'Failed to fetch users' }); } }); - // Get user by ID (requires authentication) + // Get user by ID fastify.get('/users/:id', { schema: { tags: ['Users'], @@ -86,9 +95,7 @@ const usersRoutes: FastifyPluginAsync = async (fastify: FastifyInstance) => { security: [{ apiKey: [] }], params: { type: 'object', - properties: { - id: { type: 'string' } - }, + properties: { id: { type: 'string' } }, required: ['id'] }, response: { @@ -103,43 +110,28 @@ const usersRoutes: FastifyPluginAsync = async (fastify: FastifyInstance) => { } } satisfies RouteSchema }, async (request: FastifyRequest, reply: FastifyReply): Promise => { - // Check authentication const isAuthenticated = await authenticateRequest(request, reply); - if (!isAuthenticated) { - return; // Response already sent by authenticateRequest - } + if (!isAuthenticated) return; try { - const userIdParam = (request.params as { id: string }).id; - const userId = parseInt(userIdParam); - + const userId = parseInt((request.params as { id: string }).id); if (isNaN(userId)) { - reply.code(400).send({ - success: false, - error: 'Invalid user ID' - }); + reply.code(400).send({ success: false, error: 'Invalid user ID' }); return; } - const db = getDatabase(); - const user = await db.collection('users').findOne({ id: userId }); + const db = getClient(); + const result = await db.execute({ sql: 'SELECT * FROM users WHERE id = ?', args: [userId] }); - if (!user) { - reply.code(404).send({ - success: false, - error: 'User not found' - }); + if (result.rows.length === 0) { + reply.code(404).send({ success: false, error: 'User not found' }); return; } - console.log(`👤 Fetched user: ${user.full_name}`); - reply.send(user); + reply.send(rowToUser(result.rows[0] as unknown as Record)); } catch (error) { console.error('Error fetching user:', error); - reply.code(500).send({ - success: false, - error: 'Failed to fetch user' - }); + reply.code(500).send({ success: false, error: 'Failed to fetch user' }); } }); @@ -150,41 +142,38 @@ const usersRoutes: FastifyPluginAsync = async (fastify: FastifyInstance) => { description: 'Create a new user', security: [{ apiKey: [] }], body: createUserSchema, - response: { - 201: userSchema - } + response: { 201: userSchema } } }, async (request: FastifyRequest, reply: FastifyReply): Promise => { - // Check authentication const isAuthenticated = await authenticateRequest(request, reply); - if (!isAuthenticated) { - return; // Response already sent by authenticateRequest - } + if (!isAuthenticated) return; try { const userData = request.body as CreateUserRequest; - const db = getDatabase(); - - // Generate new ID - const maxUser = await db - .collection('users') - .findOne({}, { sort: { id: -1 } }); - - const newUser: User = { - ...userData, - id: (maxUser?.id || 0) + 1, - }; - - await db.collection('users').insertOne(newUser); - console.log(`➕ Created new user: ${newUser.full_name}`); - + const db = getClient(); + + const result = await db.execute({ + sql: `INSERT INTO users (full_name, tag, address1, address2, postal_code, city, country_name, country_id, organisation_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + args: [ + userData.full_name, + userData.tag ?? null, + userData.address1 ?? null, + userData.address2 ?? null, + userData.postal_code?.toString() ?? null, + userData.city ?? null, + userData.country_name ?? null, + userData.country_id ?? null, + userData.organisation_id ?? null, + ], + }); + + const newId = Number(result.lastInsertRowid); + const newUser: User = { ...userData, id: newId }; reply.code(201).send(newUser); } catch (error) { console.error('Error creating user:', error); - reply.code(500).send({ - success: false, - error: 'Failed to create user' - }); + reply.code(500).send({ success: false, error: 'Failed to create user' }); } }); @@ -196,9 +185,7 @@ const usersRoutes: FastifyPluginAsync = async (fastify: FastifyInstance) => { security: [{ apiKey: [] }], params: { type: 'object', - properties: { - id: { type: 'string' } - }, + properties: { id: { type: 'string' } }, required: ['id'] }, body: { @@ -227,51 +214,53 @@ const usersRoutes: FastifyPluginAsync = async (fastify: FastifyInstance) => { } } }, async (request: FastifyRequest, reply: FastifyReply): Promise => { - // Check authentication const isAuthenticated = await authenticateRequest(request, reply); - if (!isAuthenticated) { - return; // Response already sent by authenticateRequest - } + if (!isAuthenticated) return; try { - const userIdParam = (request.params as { id: string }).id; - const userId = parseInt(userIdParam); - + const userId = parseInt((request.params as { id: string }).id); if (isNaN(userId)) { - reply.code(400).send({ - success: false, - error: 'Invalid user ID' - }); + reply.code(400).send({ success: false, error: 'Invalid user ID' }); return; } const userData = request.body as UpdateUserRequest; - const db = getDatabase(); + const db = getClient(); + + const fields: string[] = []; + const values: (string | number | null)[] = []; + + if (userData.full_name !== undefined) { fields.push('full_name = ?'); values.push(userData.full_name); } + if (userData.tag !== undefined) { fields.push('tag = ?'); values.push(userData.tag); } + if (userData.address1 !== undefined) { fields.push('address1 = ?'); values.push(userData.address1 ?? null); } + if (userData.address2 !== undefined) { fields.push('address2 = ?'); values.push(userData.address2 ?? null); } + if (userData.postal_code !== undefined) { fields.push('postal_code = ?'); values.push(userData.postal_code?.toString() ?? null); } + if (userData.city !== undefined) { fields.push('city = ?'); values.push(userData.city ?? null); } + if (userData.country_name !== undefined) { fields.push('country_name = ?'); values.push(userData.country_name); } + if (userData.country_id !== undefined) { fields.push('country_id = ?'); values.push(userData.country_id); } + if (userData.organisation_id !== undefined) { fields.push('organisation_id = ?'); values.push(userData.organisation_id ?? null); } + + if (fields.length === 0) { + reply.code(400).send({ success: false, error: 'No fields to update' }); + return; + } - const result = await db - .collection('users') - .updateOne({ id: userId }, { $set: userData }); + values.push(userId); + const result = await db.execute({ + sql: `UPDATE users SET ${fields.join(', ')} WHERE id = ?`, + args: values, + }); - if (result.matchedCount === 0) { - reply.code(404).send({ - success: false, - error: 'User not found' - }); + if (result.rowsAffected === 0) { + reply.code(404).send({ success: false, error: 'User not found' }); return; } - const updatedUser = await db - .collection('users') - .findOne({ id: userId }); - - console.log(`✏️ Updated user: ${updatedUser?.full_name}`); - reply.send(updatedUser); + const updated = await db.execute({ sql: 'SELECT * FROM users WHERE id = ?', args: [userId] }); + reply.send(rowToUser(updated.rows[0] as unknown as Record)); } catch (error) { console.error('Error updating user:', error); - reply.code(500).send({ - success: false, - error: 'Failed to update user' - }); + reply.code(500).send({ success: false, error: 'Failed to update user' }); } }); @@ -283,60 +272,39 @@ const usersRoutes: FastifyPluginAsync = async (fastify: FastifyInstance) => { security: [{ apiKey: [] }], params: { type: 'object', - properties: { - id: { type: 'string' } - }, + properties: { id: { type: 'string' } }, required: ['id'] }, response: { 200: { type: 'object', - properties: { - success: { type: 'boolean' } - } + properties: { success: { type: 'boolean' } } } } } }, async (request: FastifyRequest, reply: FastifyReply): Promise => { - // Check authentication const isAuthenticated = await authenticateRequest(request, reply); - if (!isAuthenticated) { - return; // Response already sent by authenticateRequest - } + if (!isAuthenticated) return; try { - const userIdParam = (request.params as { id: string }).id; - const userId = parseInt(userIdParam); - + const userId = parseInt((request.params as { id: string }).id); if (isNaN(userId)) { - reply.code(400).send({ - success: false, - error: 'Invalid user ID' - }); + reply.code(400).send({ success: false, error: 'Invalid user ID' }); return; } - const db = getDatabase(); - const result = await db - .collection('users') - .deleteOne({ id: userId }); + const db = getClient(); + const result = await db.execute({ sql: 'DELETE FROM users WHERE id = ?', args: [userId] }); - if (result.deletedCount === 0) { - reply.code(404).send({ - success: false, - error: 'User not found' - }); + if (result.rowsAffected === 0) { + reply.code(404).send({ success: false, error: 'User not found' }); return; } - console.log(`🗑️ Deleted user with ID: ${userId}`); reply.send({ success: true }); } catch (error) { console.error('Error deleting user:', error); - reply.code(500).send({ - success: false, - error: 'Failed to delete user' - }); + reply.code(500).send({ success: false, error: 'Failed to delete user' }); } }); @@ -344,15 +312,12 @@ const usersRoutes: FastifyPluginAsync = async (fastify: FastifyInstance) => { fastify.post('/users/initialize', { schema: { tags: ['Users'], - description: 'Initialize users collection', + description: 'Initialize users table', security: [{ apiKey: [] }], body: { type: 'object', properties: { - users: { - type: 'array', - items: userSchema - } + users: { type: 'array', items: userSchema } }, required: ['users'] }, @@ -367,32 +332,38 @@ const usersRoutes: FastifyPluginAsync = async (fastify: FastifyInstance) => { } } }, async (request: FastifyRequest, reply: FastifyReply): Promise => { - // Check authentication const isAuthenticated = await authenticateRequest(request, reply); - if (!isAuthenticated) { - return; // Response already sent by authenticateRequest - } + if (!isAuthenticated) return; try { const { users } = request.body as InitializeUsersRequest; - const db = getDatabase(); + const db = getClient(); - // Check if users collection is empty - const existingCount = await db.collection('users').countDocuments(); + const countResult = await db.execute('SELECT COUNT(*) as count FROM users'); + const existingCount = (countResult.rows[0] as unknown as Record)['count'] as number; if (existingCount === 0) { - await db.collection('users').insertMany(users); - console.log(`🚀 Initialized ${users.length} users in MongoDB`); - reply.send({ - success: true, - message: `Initialized ${users.length} users`, - }); + for (const user of users) { + await db.execute({ + sql: `INSERT INTO users (id, full_name, tag, address1, address2, postal_code, city, country_name, country_id, organisation_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + args: [ + user.id, + user.full_name, + user.tag ?? null, + user.address1 ?? null, + user.address2 ?? null, + user.postal_code?.toString() ?? null, + user.city ?? null, + user.country_name ?? null, + user.country_id ?? null, + user.organisation_id ?? null, + ], + }); + } + reply.send({ success: true, message: `Initialized ${users.length} users` }); } else { - console.log(`📊 Users collection already has ${existingCount} documents`); - reply.send({ - success: true, - message: `Collection already has ${existingCount} users`, - }); + reply.send({ success: true, message: `Table already has ${existingCount} users` }); } } catch (error) { console.error('Error initializing users:', error); diff --git a/api/src/utils/db.ts b/api/src/utils/db.ts new file mode 100644 index 0000000..46c6fda --- /dev/null +++ b/api/src/utils/db.ts @@ -0,0 +1,47 @@ +import { createClient, Client } from '@libsql/client'; +import { initializeSchema } from './schema'; + +let client: Client | null = null; + +export interface DbConfig { + url: string; + authToken?: string; +} + +export function getClient(): Client { + if (client) { + return client; + } + + const url = process.env['TURSO_DATABASE_URL']; + const authToken = process.env['TURSO_AUTH_TOKEN']; + + if (!url) { + throw new Error('TURSO_DATABASE_URL is required'); + } + + client = createClient({ + url, + authToken, + }); + + return client; +} + +export async function initializeDb(): Promise { + const db = getClient(); + await initializeSchema(db); + console.log('✅ Connected to Turso database'); + return db; +} + +export function isConnected(): boolean { + return client !== null; +} + +export async function closeDb(): Promise { + if (client) { + client.close(); + client = null; + } +} diff --git a/api/src/utils/mongodb.ts b/api/src/utils/mongodb.ts deleted file mode 100644 index 065225e..0000000 --- a/api/src/utils/mongodb.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { MongoClient, Db } from 'mongodb'; - -let client: MongoClient; -let db: Db; - -export interface MongoConfig { - uri: string; - dbName: string; -} - -export async function connectToMongoDB(config: MongoConfig): Promise { - try { - if (!config.uri) { - throw new Error('MongoDB URI is required'); - } - - console.log('🔌 Connecting to MongoDB...'); - client = new MongoClient(config.uri); - await client.connect(); - db = client.db(config.dbName); - - console.log(`✅ Connected to MongoDB database: ${config.dbName}`); - return db; - } catch (error) { - console.error('❌ MongoDB connection error:', error); - throw error; - } -} - -export function getDatabase(): Db { - if (!db) { - throw new Error('Database not initialized. Call connectToMongoDB first.'); - } - return db; -} - -export async function closeMongoDB(): Promise { - if (client) { - console.log('🔌 Closing MongoDB connection...'); - await client.close(); - } -} - -export function isConnected(): boolean { - return !!db; -} diff --git a/api/src/utils/schema.ts b/api/src/utils/schema.ts new file mode 100644 index 0000000..89fb239 --- /dev/null +++ b/api/src/utils/schema.ts @@ -0,0 +1,29 @@ +import { Client } from '@libsql/client'; + +export async function initializeSchema(client: Client): Promise { + await client.executeMultiple(` + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + full_name TEXT NOT NULL, + tag TEXT, + address1 TEXT, + address2 TEXT, + postal_code TEXT, + city TEXT, + country_name TEXT, + country_id TEXT, + organisation_id INTEGER + ); + + CREATE TABLE IF NOT EXISTS receipts ( + id TEXT PRIMARY KEY, + user_id INTEGER NOT NULL, + user_name TEXT NOT NULL, + beacon_quantity INTEGER NOT NULL, + discount REAL NOT NULL, + delivery_address TEXT NOT NULL, + total_price REAL NOT NULL, + timestamp TEXT NOT NULL + ); + `); +} diff --git a/api/vercel.json b/api/vercel.json index ff22114..6bf627d 100644 --- a/api/vercel.json +++ b/api/vercel.json @@ -1,12 +1,9 @@ { - "version": "3.0.0", + "version": 2, "builds": [ { "src": "src/index.ts", - "use": "@vercel/node", - "config": { - "includeFiles": ["src/**"] - } + "use": "@vercel/node" } ], "routes": [ @@ -14,8 +11,5 @@ "src": "/(.*)", "dest": "src/index.ts" } - ], - "env": { - "NODE_ENV": "production" - } + ] } diff --git a/app/.env.example b/app/.env.example new file mode 100644 index 0000000..2a960c1 --- /dev/null +++ b/app/.env.example @@ -0,0 +1,2 @@ +API_BASE_URL=http://localhost:4000/api +API_KEY=your-api-key-here diff --git a/app/App.tsx b/app/App.tsx index 050548a..41f4dca 100644 --- a/app/App.tsx +++ b/app/App.tsx @@ -8,9 +8,9 @@ import { PersistGate } from 'redux-persist/integration/react'; import { ActivityIndicator, View, Platform } from 'react-native'; // Import our custom components -import { ThemeProvider, useAppTheme } from './app/context/ThemeContext'; -import TabNavigator from './app/navigation/TabNavigator'; -import { store, persistor } from './app/store/store'; +import { ThemeProvider, useAppTheme } from './context/ThemeContext'; +import TabNavigator from './navigation/TabNavigator'; +import { store, persistor } from './store/store'; function AppContent(): React.JSX.Element { const { theme, isDarkMode } = useAppTheme(); diff --git a/app/screens/BeaconsScreen.tsx b/app/screens/BeaconsScreen.tsx index faa9ffb..f5fa64a 100644 --- a/app/screens/BeaconsScreen.tsx +++ b/app/screens/BeaconsScreen.tsx @@ -116,7 +116,7 @@ const Beacons: React.FC = ({ route }) => { {user?.full_name} - {user?.email} + {user?.city ? `${user.city}, ${user.country_name || ''}` : user?.country_name || ''} {userDiscountPercent > 0 && ( {userDiscountPercent}% Discount diff --git a/app/screens/CreateUserScreen.tsx b/app/screens/CreateUserScreen.tsx index ac11589..c5afb9e 100644 --- a/app/screens/CreateUserScreen.tsx +++ b/app/screens/CreateUserScreen.tsx @@ -244,7 +244,7 @@ export default function CreateUserScreen() { value={discount} onValueChange={setDiscount} step={5} - thumbColor={theme.colors.primary} + thumbTintColor={theme.colors.primary} minimumTrackTintColor={theme.colors.primary} maximumTrackTintColor={theme.colors.outline} /> diff --git a/app/screens/StatsScreen.tsx b/app/screens/StatsScreen.tsx index 67efbb8..99b1404 100644 --- a/app/screens/StatsScreen.tsx +++ b/app/screens/StatsScreen.tsx @@ -95,7 +95,7 @@ export default function StatsScreen() { - + Loading statistics... diff --git a/app/screens/UsersScreen.tsx b/app/screens/UsersScreen.tsx index 9cad509..ff8fdfe 100644 --- a/app/screens/UsersScreen.tsx +++ b/app/screens/UsersScreen.tsx @@ -4,7 +4,7 @@ import { Text, Card, List, IconButton, ActivityIndicator, Dialog, TextInput, But import { useNavigation } from '@react-navigation/native'; import { useDispatch, useSelector } from 'react-redux'; import { RootState, AppDispatch } from '../store/store'; -import { fetchUsers, updateUser, deleteUser, selectUser, setSelectedUser } from '../store/user/userSlice'; +import { fetchUsers, updateUser, deleteUser, setSelectedUser } from '../store/user/userSlice'; import { User } from '../types/types'; import { StackNavigationProp } from '@react-navigation/stack'; import { commonStyles, userCardStyles, textStyles, createThemedStyles } from '../styles'; @@ -37,7 +37,7 @@ export default function UsersScreen() { const handleUserPress = (user: User) => { dispatch(setSelectedUser(user)); // Navigate to beacon selection screen - navigation.navigate('Beacons' as never, { userId: user.id } as never); + (navigation as any).navigate('Beacons', { userId: user.id }); }; const handleEditUser = (user: User) => { diff --git a/app/services/mongodb.service.ts b/app/services/api.service.ts similarity index 92% rename from app/services/mongodb.service.ts rename to app/services/api.service.ts index d058078..c40db0e 100644 --- a/app/services/mongodb.service.ts +++ b/app/services/api.service.ts @@ -1,17 +1,16 @@ import { API_BASE_URL, API_KEY } from '@env'; import { User, Receipt } from '../types/types'; -// API service for hosted database access -class MongoDBService { +// API service for backend access +class ApiService { private apiEndpoint: string; private apiKey: string; constructor() { - // Use the hosted API endpoint - this.apiEndpoint = API_BASE_URL || 'https://driversnote-assessment-api.onrender.com/api'; + this.apiEndpoint = API_BASE_URL || 'http://localhost:4000/api'; this.apiKey = API_KEY || ''; - console.log('MongoDB Service initialized with:', { + console.log('API Service initialized with:', { endpoint: this.apiEndpoint, hasApiKey: !!this.apiKey, }); @@ -32,7 +31,7 @@ class MongoDBService { ); const headers: Record = { 'Content-Type': 'application/json', - ...options?.headers, + ...(options?.headers as Record), }; // Only add API key header if we have one @@ -162,4 +161,4 @@ class MongoDBService { } // Export singleton instance -export const mongodbService = new MongoDBService(); +export const apiService = new ApiService(); diff --git a/app/services/fetchMiddleware.ts b/app/services/fetchMiddleware.ts index ba9ab3b..e27b5ea 100644 --- a/app/services/fetchMiddleware.ts +++ b/app/services/fetchMiddleware.ts @@ -1,5 +1,5 @@ import { User } from '../types/types'; -import { mongodbService } from './mongodb.service'; +import { apiService } from './api.service'; export class FetchMiddleware { private readonly COLLECTION_NAME = 'users'; @@ -9,9 +9,9 @@ export class FetchMiddleware { */ async initializeUsers(): Promise { try { - console.log('Initializing users in MongoDB...'); + console.log('Initializing users in API...'); // Try to initialize with sample data if collection is empty - await mongodbService.initializeUsers(SAMPLE_USERS); + await apiService.initializeUsers(SAMPLE_USERS); } catch (error) { console.warn( 'Failed to initialize users, falling back to sample data:', @@ -21,17 +21,17 @@ export class FetchMiddleware { } /** - * Fetch all users from MongoDB + * Fetch all users from API */ async fetchAllUsers(): Promise { try { - console.log('Fetching users from MongoDB...'); - const users = await mongodbService.getUsers(); - console.log('Successfully fetched', users.length, 'users from MongoDB'); + console.log('Fetching users from API...'); + const users = await apiService.getUsers(); + console.log('Successfully fetched', users.length, 'users from API'); return users; } catch (error) { console.warn( - 'Failed to fetch users from MongoDB, using sample data:', + 'Failed to fetch users from API, using sample data:', error, ); return SAMPLE_USERS; @@ -43,17 +43,17 @@ export class FetchMiddleware { */ async fetchUserById(id: number): Promise { try { - console.log(`Fetching user ${id} from MongoDB...`); - const user = await mongodbService.getUserById(id); + console.log(`Fetching user ${id} from API...`); + const user = await apiService.getUserById(id); if (user) { - console.log('Successfully fetched user from MongoDB:', user.full_name); + console.log('Successfully fetched user from API:', user.full_name); return user; } else { - console.log(`User ${id} not found in MongoDB, checking sample data...`); + console.log(`User ${id} not found in API, checking sample data...`); } } catch (error) { console.warn( - `Failed to fetch user ${id} from MongoDB, using sample data:`, + `Failed to fetch user ${id} from API, using sample data:`, error, ); } @@ -74,12 +74,12 @@ export class FetchMiddleware { */ async createUser(userData: Omit): Promise { try { - console.log('Creating user in MongoDB...', userData); - const newUser = await mongodbService.createUser(userData); - console.log('Successfully created user in MongoDB:', newUser); + console.log('Creating user in API...', userData); + const newUser = await apiService.createUser(userData); + console.log('Successfully created user in API:', newUser); return newUser; } catch (error) { - console.warn('Failed to create user in MongoDB:', error); + console.warn('Failed to create user in API:', error); throw error; } } @@ -89,12 +89,12 @@ export class FetchMiddleware { */ async updateUser(id: number, userData: Partial): Promise { try { - console.log(`Updating user ${id} in MongoDB...`, userData); - const updatedUser = await mongodbService.updateUser(id, userData); - console.log('Successfully updated user in MongoDB:', updatedUser); + console.log(`Updating user ${id} in API...`, userData); + const updatedUser = await apiService.updateUser(id, userData); + console.log('Successfully updated user in API:', updatedUser); return updatedUser; } catch (error) { - console.warn(`Failed to update user ${id} in MongoDB:`, error); + console.warn(`Failed to update user ${id} in API:`, error); throw error; } } @@ -104,12 +104,12 @@ export class FetchMiddleware { */ async deleteUser(id: number): Promise { try { - console.log(`Deleting user ${id} from MongoDB...`); - const result = await mongodbService.deleteUser(id); - console.log(`Successfully deleted user ${id} from MongoDB:`, result); + console.log(`Deleting user ${id} from API...`); + const result = await apiService.deleteUser(id); + console.log(`Successfully deleted user ${id} from API:`, result); return result.success; } catch (error) { - console.warn(`Failed to delete user ${id} from MongoDB:`, error); + console.warn(`Failed to delete user ${id} from API:`, error); throw error; } } diff --git a/app/services/user.service.ts b/app/services/user.service.ts index 31fe2ea..47626a4 100644 --- a/app/services/user.service.ts +++ b/app/services/user.service.ts @@ -1,4 +1,4 @@ -import { mongodbService } from '../services/mongodb.service'; +import { apiService } from './api.service'; import { User } from '../types/types'; export class UserService { @@ -11,7 +11,7 @@ export class UserService { const users = await this.getAllUsers(); if (users.length === 0) { // Initialize with sample users via API - await mongodbService.initializeUsers(SAMPLE_USERS); + await apiService.initializeUsers(SAMPLE_USERS); console.log('Sample users initialized via API'); } } catch (error) { @@ -25,7 +25,7 @@ export class UserService { */ async getAllUsers(): Promise { try { - return await mongodbService.getUsers(); + return await apiService.getUsers(); } catch (error) { console.error('Error getting users:', error); throw error; @@ -37,7 +37,7 @@ export class UserService { */ async getUserById(id: number): Promise { try { - return await mongodbService.getUserById(id); + return await apiService.getUserById(id); } catch (error) { console.error(`Error getting user with ID ${id}:`, error); throw error; @@ -49,7 +49,7 @@ export class UserService { */ async createUser(user: Omit): Promise { try { - return await mongodbService.createUser(user); + return await apiService.createUser(user); } catch (error) { console.error('Error creating user:', error); throw error; @@ -61,7 +61,7 @@ export class UserService { */ async updateUser(id: number, userData: Partial): Promise { try { - const updatedUser = await mongodbService.updateUser(id, userData); + const updatedUser = await apiService.updateUser(id, userData); return updatedUser; } catch (error) { console.error(`Error updating user with ID ${id}:`, error); @@ -74,7 +74,7 @@ export class UserService { */ async deleteUser(id: number): Promise { try { - const result = await mongodbService.deleteUser(id); + const result = await apiService.deleteUser(id); return result.success; } catch (error) { console.error(`Error deleting user with ID ${id}:`, error); diff --git a/app/store/receipts/receiptsSlice.ts b/app/store/receipts/receiptsSlice.ts index 353e620..0169a2d 100644 --- a/app/store/receipts/receiptsSlice.ts +++ b/app/store/receipts/receiptsSlice.ts @@ -1,6 +1,6 @@ import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; import { Receipt } from '../../types/types'; -import { mongodbService } from '../../services/mongodb.service'; +import { apiService } from '../../services/api.service'; // Define Receipt interface if not in types export interface ReceiptData { @@ -30,7 +30,7 @@ export const fetchReceipts = createAsyncThunk( 'receipts/fetchReceipts', async(_, { rejectWithValue }) => { try { - const receipts = await mongodbService.getReceipts(); + const receipts = await apiService.getReceipts(); return receipts; } catch (error) { return rejectWithValue( @@ -47,7 +47,7 @@ export const createReceipt = createAsyncThunk( { rejectWithValue }, ) => { try { - const newReceipt = await mongodbService.createReceipt(receiptData); + const newReceipt = await apiService.createReceipt(receiptData); return newReceipt; } catch (error) { return rejectWithValue( @@ -61,7 +61,7 @@ export const deleteReceipt = createAsyncThunk( 'receipts/deleteReceipt', async(receiptId: string, { rejectWithValue }) => { try { - await mongodbService.deleteReceipt(receiptId); + await apiService.deleteReceipt(receiptId); return receiptId; } catch (error) { return rejectWithValue( diff --git a/package-lock.json b/package-lock.json index 7c4ef0b..b9bd06d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,12 +19,14 @@ "dependencies": { "@fastify/cors": "^10.0.1", "@fastify/helmet": "^12.0.1", + "@fastify/rate-limit": "^10.2.1", "@fastify/sensible": "^6.0.1", "@fastify/swagger": "^9.4.0", "@fastify/swagger-ui": "^5.1.0", + "@libsql/client": "^0.14.0", "dotenv": "^16.4.7", "fastify": "^5.2.2", - "mongodb": "^6.12.0" + "fastify-plugin": "^5.0.1" }, "devDependencies": { "@types/jest": "^29.5.14", @@ -2844,6 +2846,27 @@ "ipaddr.js": "^2.1.0" } }, + "node_modules/@fastify/rate-limit": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@fastify/rate-limit/-/rate-limit-10.3.0.tgz", + "integrity": "sha512-eIGkG9XKQs0nyynatApA3EVrojHOuq4l6fhB4eeCk4PIOeadvOJz9/4w3vGI44Go17uaXOWEcPkaD8kuKm7g6Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@lukeed/ms": "^2.0.2", + "fastify-plugin": "^5.0.0", + "toad-cache": "^3.7.0" + } + }, "node_modules/@fastify/send": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@fastify/send/-/send-4.1.0.tgz", @@ -3651,6 +3674,150 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@libsql/client": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@libsql/client/-/client-0.14.0.tgz", + "integrity": "sha512-/9HEKfn6fwXB5aTEEoMeFh4CtG0ZzbncBb1e++OCdVpgKZ/xyMsIVYXm0w7Pv4RUel803vE6LwniB3PqD72R0Q==", + "license": "MIT", + "dependencies": { + "@libsql/core": "^0.14.0", + "@libsql/hrana-client": "^0.7.0", + "js-base64": "^3.7.5", + "libsql": "^0.4.4", + "promise-limit": "^2.7.0" + } + }, + "node_modules/@libsql/core": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@libsql/core/-/core-0.14.0.tgz", + "integrity": "sha512-nhbuXf7GP3PSZgdCY2Ecj8vz187ptHlZQ0VRc751oB2C1W8jQUXKKklvt7t1LJiUTQBVJuadF628eUk+3cRi4Q==", + "license": "MIT", + "dependencies": { + "js-base64": "^3.7.5" + } + }, + "node_modules/@libsql/darwin-arm64": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@libsql/darwin-arm64/-/darwin-arm64-0.4.7.tgz", + "integrity": "sha512-yOL742IfWUlUevnI5PdnIT4fryY3LYTdLm56bnY0wXBw7dhFcnjuA7jrH3oSVz2mjZTHujxoITgAE7V6Z+eAbg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@libsql/darwin-x64": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@libsql/darwin-x64/-/darwin-x64-0.4.7.tgz", + "integrity": "sha512-ezc7V75+eoyyH07BO9tIyJdqXXcRfZMbKcLCeF8+qWK5nP8wWuMcfOVywecsXGRbT99zc5eNra4NEx6z5PkSsA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@libsql/hrana-client": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@libsql/hrana-client/-/hrana-client-0.7.0.tgz", + "integrity": "sha512-OF8fFQSkbL7vJY9rfuegK1R7sPgQ6kFMkDamiEccNUvieQ+3urzfDFI616oPl8V7T9zRmnTkSjMOImYCAVRVuw==", + "license": "MIT", + "dependencies": { + "@libsql/isomorphic-fetch": "^0.3.1", + "@libsql/isomorphic-ws": "^0.1.5", + "js-base64": "^3.7.5", + "node-fetch": "^3.3.2" + } + }, + "node_modules/@libsql/isomorphic-fetch": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@libsql/isomorphic-fetch/-/isomorphic-fetch-0.3.1.tgz", + "integrity": "sha512-6kK3SUK5Uu56zPq/Las620n5aS9xJq+jMBcNSOmjhNf/MUvdyji4vrMTqD7ptY7/4/CAVEAYDeotUz60LNQHtw==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@libsql/isomorphic-ws": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@libsql/isomorphic-ws/-/isomorphic-ws-0.1.5.tgz", + "integrity": "sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==", + "license": "MIT", + "dependencies": { + "@types/ws": "^8.5.4", + "ws": "^8.13.0" + } + }, + "node_modules/@libsql/linux-arm64-gnu": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@libsql/linux-arm64-gnu/-/linux-arm64-gnu-0.4.7.tgz", + "integrity": "sha512-WlX2VYB5diM4kFfNaYcyhw5y+UJAI3xcMkEUJZPtRDEIu85SsSFrQ+gvoKfcVh76B//ztSeEX2wl9yrjF7BBCA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@libsql/linux-arm64-musl": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@libsql/linux-arm64-musl/-/linux-arm64-musl-0.4.7.tgz", + "integrity": "sha512-6kK9xAArVRlTCpWeqnNMCoXW1pe7WITI378n4NpvU5EJ0Ok3aNTIC2nRPRjhro90QcnmLL1jPcrVwO4WD1U0xw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@libsql/linux-x64-gnu": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@libsql/linux-x64-gnu/-/linux-x64-gnu-0.4.7.tgz", + "integrity": "sha512-CMnNRCmlWQqqzlTw6NeaZXzLWI8bydaXDke63JTUCvu8R+fj/ENsLrVBtPDlxQ0wGsYdXGlrUCH8Qi9gJep0yQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@libsql/linux-x64-musl": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@libsql/linux-x64-musl/-/linux-x64-musl-0.4.7.tgz", + "integrity": "sha512-nI6tpS1t6WzGAt1Kx1n1HsvtBbZ+jHn0m7ogNNT6pQHZQj7AFFTIMeDQw/i/Nt5H38np1GVRNsFe99eSIMs9XA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@libsql/win32-x64-msvc": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@libsql/win32-x64-msvc/-/win32-x64-msvc-0.4.7.tgz", + "integrity": "sha512-7pJzOWzPm6oJUxml+PCDRzYQ4A1hTMHAciTAHfFK4fkbDZX33nWPVG7Y3vqdKtslcwAzwmrNDc6sXy2nwWnbiw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@lukeed/ms": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", @@ -3660,14 +3827,11 @@ "node": ">=8" } }, - "node_modules/@mongodb-js/saslprep": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.5.tgz", - "integrity": "sha512-k64Lbyb7ycCSXHSLzxVdb2xsKGPMvYZfCICXvDsI8Z65CeWQzTEKS4YmGbnqw+U9RBvLPTsB6UCmwkgsDTGWIw==", - "license": "MIT", - "dependencies": { - "sparse-bitfield": "^3.0.3" - } + "node_modules/@neon-rs/load": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@neon-rs/load/-/load-0.0.4.tgz", + "integrity": "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==", + "license": "MIT" }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -4471,7 +4635,7 @@ "version": "19.0.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.14.tgz", "integrity": "sha512-ixLZ7zG7j1fM0DijL9hDArwhwcCb4vqmePgwtV0GfnkHRSCUEv4LvzarcTdhoqgyMznUx/EhoTUv31CKZzkQlw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -4527,19 +4691,13 @@ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "license": "MIT" }, - "node_modules/@types/webidl-conversions": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", - "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", - "license": "MIT" - }, - "node_modules/@types/whatwg-url": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", - "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "license": "MIT", "dependencies": { - "@types/webidl-conversions": "*" + "@types/node": "*" } }, "node_modules/@types/yargs": { @@ -5750,15 +5908,6 @@ "node-int64": "^0.4.0" } }, - "node_modules/bson": { - "version": "6.10.4", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", - "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", - "license": "Apache-2.0", - "engines": { - "node": ">=16.20.1" - } - }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -6474,9 +6623,18 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/data-urls": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", @@ -8277,6 +8435,29 @@ } } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -8469,6 +8650,18 @@ "node": ">= 6" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -10736,6 +10929,12 @@ "integrity": "sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww==", "license": "MIT" }, + "node_modules/js-base64": { + "version": "3.7.8", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz", + "integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==", + "license": "BSD-3-Clause" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -10967,6 +11166,44 @@ "node": ">= 0.8.0" } }, + "node_modules/libsql": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/libsql/-/libsql-0.4.7.tgz", + "integrity": "sha512-T9eIRCs6b0J1SHKYIvD8+KCJMcWZ900iZyxdnSCdqxN12Z1ijzT+jY5nrk72Jw4B0HGzms2NgpryArlJqvc3Lw==", + "cpu": [ + "x64", + "arm64", + "wasm32" + ], + "license": "MIT", + "os": [ + "darwin", + "linux", + "win32" + ], + "dependencies": { + "@neon-rs/load": "^0.0.4", + "detect-libc": "2.0.2" + }, + "optionalDependencies": { + "@libsql/darwin-arm64": "0.4.7", + "@libsql/darwin-x64": "0.4.7", + "@libsql/linux-arm64-gnu": "0.4.7", + "@libsql/linux-arm64-musl": "0.4.7", + "@libsql/linux-x64-gnu": "0.4.7", + "@libsql/linux-x64-musl": "0.4.7", + "@libsql/win32-x64-msvc": "0.4.7" + } + }, + "node_modules/libsql/node_modules/detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/light-my-request": { "version": "6.6.0", "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", @@ -11491,12 +11728,6 @@ "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", "license": "MIT" }, - "node_modules/memory-pager": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", - "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", - "license": "MIT" - }, "node_modules/merge-options": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", @@ -12040,87 +12271,6 @@ "obliterator": "^2.0.4" } }, - "node_modules/mongodb": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.21.0.tgz", - "integrity": "sha512-URyb/VXMjJ4da46OeSXg+puO39XH9DeQpWCslifrRn9JWugy0D+DvvBvkm2WxmHe61O/H19JM66p1z7RHVkZ6A==", - "license": "Apache-2.0", - "dependencies": { - "@mongodb-js/saslprep": "^1.3.0", - "bson": "^6.10.4", - "mongodb-connection-string-url": "^3.0.2" - }, - "engines": { - "node": ">=16.20.1" - }, - "peerDependencies": { - "@aws-sdk/credential-providers": "^3.188.0", - "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", - "gcp-metadata": "^5.2.0", - "kerberos": "^2.0.1", - "mongodb-client-encryption": ">=6.0.0 <7", - "snappy": "^7.3.2", - "socks": "^2.7.1" - }, - "peerDependenciesMeta": { - "@aws-sdk/credential-providers": { - "optional": true - }, - "@mongodb-js/zstd": { - "optional": true - }, - "gcp-metadata": { - "optional": true - }, - "kerberos": { - "optional": true - }, - "mongodb-client-encryption": { - "optional": true - }, - "snappy": { - "optional": true - }, - "socks": { - "optional": true - } - } - }, - "node_modules/mongodb-connection-string-url": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", - "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", - "license": "Apache-2.0", - "dependencies": { - "@types/whatwg-url": "^11.0.2", - "whatwg-url": "^14.1.0 || ^13.0.0" - } - }, - "node_modules/mongodb-connection-string-url/node_modules/tr46": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/mongodb-connection-string-url/node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", - "license": "MIT", - "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -12185,6 +12335,44 @@ "integrity": "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==", "license": "MIT" }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/node-forge": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", @@ -13155,6 +13343,12 @@ "asap": "~2.0.6" } }, + "node_modules/promise-limit": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/promise-limit/-/promise-limit-2.7.0.tgz", + "integrity": "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==", + "license": "ISC" + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -14790,15 +14984,6 @@ "source-map": "^0.6.0" } }, - "node_modules/sparse-bitfield": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", - "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", - "license": "MIT", - "dependencies": { - "memory-pager": "^1.0.2" - } - }, "node_modules/split-on-first": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", @@ -16262,10 +16447,20 @@ "defaults": "^1.0.3" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=12"