Add unit tests for domain and application layers, and update licenses to correct license

This commit is contained in:
continuist 2025-06-22 16:19:38 -04:00
parent 0e03712e61
commit 3e7f6fea8e
29 changed files with 1631 additions and 26 deletions

91
LICENSE_NOTICE.md Normal file
View file

@ -0,0 +1,91 @@
# License Notice
## Sharenet License Information
This project, Sharenet, is licensed under the **Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License** (CC-BY-NC-SA-4.0).
## License Details
- **Full License Text**: See [LICENSE.md](LICENSE.md) for the complete license text
- **License URL**: https://creativecommons.org/licenses/by-nc-sa/4.0/
- **SPDX Identifier**: CC-BY-NC-SA-4.0
## What This License Means
### You are free to:
- **Share**: Copy and redistribute the material in any medium or format
- **Adapt**: Remix, transform, and build upon the material
- **Attribution**: You must give appropriate credit, provide a link to the license, and indicate if changes were made
### Under the following terms:
- **NonCommercial**: You may not use the material for commercial purposes
- **ShareAlike**: If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original
## Copyright Information
- **Copyright Holder**: Continuist <continuist02@gmail.com>
- **Year**: 2024
- **Project**: Sharenet
## License Headers
All source files in this project include license headers that reference this license. The headers follow this format:
### For Rust files:
```rust
/*
* This file is part of Sharenet.
*
* Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
*
* You may obtain a copy of the license at:
* https://creativecommons.org/licenses/by-nc-sa/4.0/
*
* Copyright (c) 2024 Continuist <continuist02@gmail.com>
*/
```
### For TypeScript/JavaScript files:
```typescript
/**
* This file is part of Sharenet.
*
* Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
*
* You may obtain a copy of the license at:
* https://creativecommons.org/licenses/by-nc-sa/4.0/
*
* Copyright (c) 2024 Continuist <continuist02@gmail.com>
*/
```
## Package Configuration
### Backend (Rust)
- **Cargo.toml**: `license = "CC-BY-NC-SA-4.0"`
### Frontend (Node.js)
- **package.json**: `"license": "CC-BY-NC-SA-4.0"`
## Compliance
To comply with this license when using, modifying, or distributing this software:
1. **Attribution**: Always include the copyright notice and license reference
2. **Non-Commercial Use**: Do not use for commercial purposes without permission
3. **Share Alike**: If you create derivatives, license them under the same terms
4. **License Link**: Include a link to the full license text
## Questions
If you have questions about the license or need permission for commercial use, please contact:
- **Email**: continuist02@gmail.com
## Third-Party Dependencies
This project includes third-party dependencies that are licensed under their own terms. Please refer to the respective license files for each dependency:
- **Backend Dependencies**: See `backend/Cargo.lock` for dependency licenses
- **Frontend Dependencies**: See `frontend/package-lock.json` for dependency licenses
The CC-BY-NC-SA-4.0 license applies only to the original Sharenet codebase, not to third-party dependencies.

415
PROJECT_INDEX.md Normal file
View file

@ -0,0 +1,415 @@
# Sharenet Project Index
## Project Overview
Sharenet is a full-stack application for managing users and products, built with Rust (backend) and Next.js (frontend). It demonstrates clean architecture principles with multiple interface options and configurable storage backends.
## Architecture
### Backend Architecture (Clean Architecture)
The backend follows clean architecture principles with clear separation of concerns:
```
┌─────────────────────────────────────────────────────────────┐
│ Interface Layer │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ API │ │ CLI │ │ TUI │ │
│ │ (Axum) │ │ (Clap) │ │ (Ratatui) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Application Layer │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Service/UseCase Layer │ │
│ │ - UserService │ │
│ │ - ProductService │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Domain Layer │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ - User Entity │ │
│ │ - Product Entity │ │
│ │ - Repository Traits │ │
│ │ - Domain Errors │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Infrastructure Layer │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Memory │ │ PostgreSQL │ │
│ │ Repository │ │ Repository │ │
│ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
## Project Structure
```
sharenet/
├── backend/ # Rust backend
│ ├── Cargo.toml # Workspace configuration
│ ├── README.md # Backend documentation
│ ├── crates/ # Backend modules
│ │ ├── domain/ # Domain entities and traits
│ │ │ ├── Cargo.toml
│ │ │ └── src/lib.rs # Core domain logic
│ │ ├── application/ # Application services
│ │ │ ├── Cargo.toml
│ │ │ └── src/lib.rs # Use case implementations
│ │ ├── api/ # HTTP API (Axum)
│ │ │ ├── Cargo.toml
│ │ │ └── src/lib.rs # REST API endpoints
│ │ ├── cli/ # CLI interface (Clap)
│ │ │ ├── Cargo.toml
│ │ │ └── src/lib.rs # Command-line interface
│ │ ├── tui/ # TUI interface (Ratatui)
│ │ │ ├── Cargo.toml
│ │ │ └── src/lib.rs # Text user interface
│ │ ├── memory/ # In-memory storage
│ │ │ ├── Cargo.toml
│ │ │ └── src/lib.rs # Memory repository implementation
│ │ ├── postgres/ # PostgreSQL storage
│ │ │ ├── Cargo.toml
│ │ │ └── src/lib.rs # PostgreSQL repository implementation
│ │ ├── sharenet-api-memory/ # API binary (memory)
│ │ │ ├── Cargo.toml
│ │ │ └── src/main.rs # API entry point
│ │ ├── sharenet-api-postgres/ # API binary (PostgreSQL)
│ │ │ ├── Cargo.toml
│ │ │ └── src/main.rs # API entry point
│ │ ├── sharenet-cli-memory/ # CLI binary (memory)
│ │ │ ├── Cargo.toml
│ │ │ └── src/main.rs # CLI entry point
│ │ ├── sharenet-cli-postgres/ # CLI binary (PostgreSQL)
│ │ │ ├── Cargo.toml
│ │ │ └── src/main.rs # CLI entry point
│ │ ├── sharenet-tui-memory/ # TUI binary (memory)
│ │ │ ├── Cargo.toml
│ │ │ └── src/main.rs # TUI entry point
│ │ └── sharenet-tui-postgres/ # TUI binary (PostgreSQL)
│ │ ├── Cargo.toml
│ │ └── src/main.rs # TUI entry point
│ ├── migrations/ # Database migrations
│ │ └── 20240101000000_create_tables.sql
│ └── config/ # Configuration files
│ ├── api-memory.env # API with memory storage
│ ├── api-postgres.env # API with PostgreSQL
│ ├── cli-memory.env # CLI with memory storage
│ ├── cli-postgres.env # CLI with PostgreSQL
│ ├── tui-memory.env # TUI with memory storage
│ └── tui-postgres.env # TUI with PostgreSQL
├── frontend/ # Next.js frontend
│ ├── package.json # Frontend dependencies
│ ├── README.md # Frontend documentation
│ ├── src/ # Source code
│ │ ├── app/ # Next.js app directory
│ │ │ ├── layout.tsx # Root layout
│ │ │ ├── page.tsx # Dashboard page
│ │ │ ├── users/ # Users pages
│ │ │ │ └── page.tsx # Users management
│ │ │ └── products/ # Products pages
│ │ │ └── page.tsx # Products management
│ │ ├── components/ # React components
│ │ │ └── ui/ # shadcn/ui components
│ │ │ ├── button.tsx # Button component
│ │ │ ├── card.tsx # Card component
│ │ │ ├── dialog.tsx # Dialog component
│ │ │ ├── form.tsx # Form component
│ │ │ ├── input.tsx # Input component
│ │ │ ├── label.tsx # Label component
│ │ │ └── table.tsx # Table component
│ │ └── lib/ # Utility functions
│ │ ├── api.ts # API client
│ │ └── utils.ts # Utility functions
│ ├── public/ # Static assets
│ │ ├── file.svg # File icon
│ │ ├── globe.svg # Globe icon
│ │ ├── next.svg # Next.js logo
│ │ ├── vercel.svg # Vercel logo
│ │ └── window.svg # Window icon
│ ├── components.json # shadcn/ui configuration
│ ├── next.config.ts # Next.js configuration
│ ├── postcss.config.mjs # PostCSS configuration
│ ├── tailwind.config.js # Tailwind CSS configuration
│ └── tsconfig.json # TypeScript configuration
├── README.md # Project documentation
├── LICENSE.md # Project license
└── .gitignore # Git ignore rules
```
## Key Components
### Domain Layer (`backend/crates/domain/`)
**Core Entities:**
- `User`: User entity with id, username, email, timestamps
- `Product`: Product entity with id, name, description, timestamps
- `CreateUser`/`UpdateUser`: DTOs for user operations
- `CreateProduct`/`UpdateProduct`: DTOs for product operations
**Traits:**
- `Entity`: Base trait for domain entities
- `Repository<T>`: Generic repository trait for CRUD operations
**Error Handling:**
- `DomainError`: Domain-specific error types (NotFound, InvalidInput, Internal)
### Application Layer (`backend/crates/application/`)
**Services:**
- `Service<T, R>`: Generic service implementation
- `UseCase<T>`: Use case trait for business operations
- `Repository<T>`: Repository trait for data access
**Features:**
- Generic implementations for easy extension
- Static dispatch with generic traits
- Comprehensive test coverage with mock repositories
### Infrastructure Layer
#### Memory Repository (`backend/crates/memory/`)
- In-memory storage using `HashMap` and `RwLock`
- Suitable for development and testing
- No external dependencies
#### PostgreSQL Repository (`backend/crates/postgres/`)
- SQLx-based PostgreSQL implementation
- Connection pooling with PgPool
- Prepared query caching for performance
- Migration support
### Interface Layer
#### HTTP API (`backend/crates/api/`)
- **Framework**: Axum
- **Features**:
- RESTful endpoints for users and products
- CORS support
- Request/response logging
- JSON serialization/deserialization
- **Endpoints**:
- `POST /users` - Create user
- `GET /users/:id` - Get user by ID
- `GET /users` - List all users
- `PUT /users/:id` - Update user
- `DELETE /users/:id` - Delete user
- `POST /products` - Create product
- `GET /products/:id` - Get product by ID
- `GET /products` - List all products
- `PUT /products/:id` - Update product
- `DELETE /products/:id` - Delete product
#### CLI (`backend/crates/cli/`)
- **Framework**: Clap
- **Features**:
- Subcommand-based interface
- User and product management commands
- JSON output support
- **Commands**:
- `user create --username <name> --email <email>`
- `user get --id <id>`
- `user list`
- `user update --id <id> [--username <name>] [--email <email>]`
- `user delete --id <id>`
- `product create --name <name> --description <desc>`
- `product get --id <id>`
- `product list`
- `product update --id <id> [--name <name>] [--description <desc>]`
- `product delete --id <id>`
#### TUI (`backend/crates/tui/`)
- **Framework**: Ratatui
- **Features**:
- Interactive terminal interface
- Table-based data display
- Form-based data entry
- Navigation between users and products
### Frontend (`frontend/`)
#### Technology Stack
- **Framework**: Next.js 15.3.3
- **Language**: TypeScript
- **UI Library**: React 19
- **Styling**: Tailwind CSS
- **Components**: shadcn/ui
- **Form Handling**: React Hook Form with Zod validation
- **HTTP Client**: Axios
#### Key Features
- **Dashboard**: Overview page with navigation to users and products
- **Users Management**:
- View all users in a table
- Create new users via dialog
- Edit existing users
- Delete users with confirmation
- **Products Management**:
- View all products in a table
- Create new products via dialog
- Edit existing products
- Delete products with confirmation
#### API Integration
- **Base URL**: `http://127.0.0.1:3000`
- **Client**: Axios-based API client in `src/lib/api.ts`
- **Type Safety**: Full TypeScript interfaces for API responses
## Database Schema
### Users Table
```sql
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username TEXT NOT NULL,
email TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
```
### Products Table
```sql
CREATE TABLE products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
description TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
```
## Configuration
### Backend Configuration
Configuration files are located in `backend/config/`:
- `api-memory.env` - API server with in-memory storage
- `api-postgres.env` - API server with PostgreSQL storage
- `cli-memory.env` - CLI with in-memory storage
- `cli-postgres.env` - CLI with PostgreSQL storage
- `tui-memory.env` - TUI with in-memory storage
- `tui-postgres.env` - TUI with PostgreSQL storage
### Environment Variables
- `SERVER_ADDR` - Server address (default: 127.0.0.1:3000)
- `DATABASE_URL` - PostgreSQL connection string
- `RUST_LOG` - Logging level (default: info)
## Development Workflow
### Backend Development
1. **Setup**: Install Rust and SQLx CLI
2. **Database**: Create database and run migrations
3. **Build**: `cargo build`
4. **Run**: Choose appropriate binary for your use case
5. **Test**: `cargo test`
### Frontend Development
1. **Setup**: Install Node.js and npm
2. **Install**: `npm install`
3. **Run**: `npm run dev`
4. **Build**: `npm run build`
### Running the Full Stack
1. Start backend: `cargo run --bin sharenet-api-postgres`
2. Start frontend: `cd frontend && npm run dev`
3. Access web interface at `http://localhost:3000`
## Testing
### Backend Testing
- **Unit Tests**: Each crate has comprehensive unit tests
- **Integration Tests**: Repository implementations are tested
- **Mock Testing**: Mock repositories for isolated testing
### Frontend Testing
- **Component Testing**: Individual React components
- **Integration Testing**: API integration and user flows
- **E2E Testing**: Full application workflows
## Deployment
### Backend Deployment
- **Docker**: Containerized deployment
- **Binary**: Standalone executables
- **Database**: PostgreSQL with connection pooling
### Frontend Deployment
- **Vercel**: Optimized for Next.js
- **Static Export**: Can be deployed to any static hosting
- **Docker**: Containerized deployment
## Performance Considerations
### Backend
- **Connection Pooling**: PostgreSQL connection pooling
- **Query Caching**: SQLx prepared query caching
- **Async/Await**: Non-blocking I/O operations
- **Generic Implementations**: Zero-cost abstractions
### Frontend
- **Code Splitting**: Next.js automatic code splitting
- **Static Generation**: Pre-rendered pages where possible
- **Optimized Images**: Next.js image optimization
- **Bundle Analysis**: Webpack bundle analysis
## Security
### Backend Security
- **Input Validation**: Domain-level validation
- **Error Handling**: Secure error messages
- **CORS**: Configurable CORS policies
- **SQL Injection**: SQLx prevents SQL injection
### Frontend Security
- **Type Safety**: TypeScript prevents type-related errors
- **Input Validation**: Client-side validation with Zod
- **XSS Prevention**: React automatic escaping
- **CSRF Protection**: Built-in Next.js protection
## Future Enhancements
### Planned Features
- **Authentication**: User authentication and authorization
- **Role-Based Access Control**: User roles and permissions
- **Product Categories**: Product categorization system
- **Inventory Tracking**: Stock management
- **Audit Logging**: Operation logging and history
- **API Documentation**: OpenAPI/Swagger documentation
- **Real-time Updates**: WebSocket support
- **File Uploads**: Product image uploads
- **Search and Filtering**: Advanced data filtering
- **Pagination**: Large dataset handling
### Technical Improvements
- **Caching**: Redis integration for caching
- **Monitoring**: Application monitoring and metrics
- **Logging**: Structured logging with correlation IDs
- **Health Checks**: Application health endpoints
- **Rate Limiting**: API rate limiting
- **Compression**: Response compression
- **CDN**: Static asset delivery optimization
## Contributing
### Development Guidelines
- **Code Style**: Follow Rust and TypeScript conventions
- **Testing**: Maintain high test coverage
- **Documentation**: Keep documentation up to date
- **Architecture**: Follow clean architecture principles
- **Error Handling**: Comprehensive error handling
- **Performance**: Consider performance implications
### Code Review Process
- **Pull Requests**: All changes via pull requests
- **Code Review**: Mandatory code review
- **Testing**: Automated testing on all changes
- **Documentation**: Update documentation as needed
## License
This project is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License - see the [LICENSE.md](LICENSE.md) file for details.
For more information about the license and compliance requirements, see [LICENSE_NOTICE.md](LICENSE_NOTICE.md).

View file

@ -144,4 +144,6 @@ The frontend is configured to connect to the backend at `http://127.0.0.1:3000`
## License ## License
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details. This project is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License - see the [LICENSE.md](LICENSE.md) file for details.
For more information about the license and compliance requirements, see [LICENSE_NOTICE.md](LICENSE_NOTICE.md).

View file

@ -8,7 +8,7 @@ resolver = "2"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
authors = ["Continuist <continuist02@gmail.com>"] authors = ["Continuist <continuist02@gmail.com>"]
license = "CC-BY-NC-4.0" license = "CC-BY-NC-SA-4.0"
[workspace.dependencies] [workspace.dependencies]
tokio = { version = "1.36", features = ["full"] } tokio = { version = "1.36", features = ["full"] }

View file

@ -1,3 +1,14 @@
/*
* This file is part of Sharenet.
*
* Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
*
* You may obtain a copy of the license at:
* https://creativecommons.org/licenses/by-nc-sa/4.0/
*
* Copyright (c) 2024 Continuist <continuist02@gmail.com>
*/
use std::net::SocketAddr; use std::net::SocketAddr;
use std::sync::Arc; use std::sync::Arc;

View file

@ -9,3 +9,7 @@ license.workspace = true
domain = { path = "../domain" } domain = { path = "../domain" }
thiserror = { workspace = true } thiserror = { workspace = true }
uuid = { workspace = true } uuid = { workspace = true }
[dev-dependencies]
tokio = { workspace = true, features = ["full", "macros", "rt-multi-thread"] }
chrono = { workspace = true }

View file

@ -1,3 +1,14 @@
/*
* This file is part of Sharenet.
*
* Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
*
* You may obtain a copy of the license at:
* https://creativecommons.org/licenses/by-nc-sa/4.0/
*
* Copyright (c) 2024 Continuist <continuist02@gmail.com>
*/
use domain::{Result, Entity}; use domain::{Result, Entity};
use thiserror::Error; use thiserror::Error;
use uuid::Uuid; use uuid::Uuid;
@ -76,3 +87,361 @@ impl<T: Entity, R: Repository<T> + Clone> UseCase<T> for Service<T, R> {
} }
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use domain::{User, CreateUser, UpdateUser, Product, CreateProduct, UpdateProduct};
use std::sync::Arc;
use tokio::sync::RwLock;
use std::collections::HashMap;
use chrono::Utc;
// Mock repository for testing
#[derive(Clone)]
struct MockRepository<T: Entity> {
data: Arc<RwLock<HashMap<Uuid, T>>>,
}
impl<T: Entity + Clone + Send + Sync> MockRepository<T> {
fn new() -> Self {
Self {
data: Arc::new(RwLock::new(HashMap::new())),
}
}
}
impl Repository<User> for MockRepository<User> {
fn create(&self, data: CreateUser) -> impl Future<Output = Result<User>> + Send {
async move {
let mut guard = self.data.write().await;
let id = Uuid::new_v4();
let user = User {
id,
username: data.username,
email: data.email,
created_at: Utc::now(),
updated_at: Utc::now(),
};
guard.insert(id, user.clone());
Ok(user)
}
}
fn find_by_id(&self, id: Uuid) -> impl Future<Output = Result<User>> + Send {
async move {
let guard = self.data.read().await;
guard.get(&id)
.cloned()
.ok_or_else(|| domain::DomainError::NotFound(format!("Entity not found: {}", id)))
}
}
fn find_all(&self) -> impl Future<Output = Result<Vec<User>>> + Send {
async move {
let guard = self.data.read().await;
Ok(guard.values().cloned().collect())
}
}
fn update(&self, id: Uuid, data: UpdateUser) -> impl Future<Output = Result<User>> + Send {
async move {
let mut guard = self.data.write().await;
let user = guard.get_mut(&id)
.ok_or_else(|| domain::DomainError::NotFound(format!("Entity not found: {}", id)))?;
if let Some(username) = data.username {
user.username = username;
}
if let Some(email) = data.email {
user.email = email;
}
user.updated_at = Utc::now();
Ok(user.clone())
}
}
fn delete(&self, id: Uuid) -> impl Future<Output = Result<()>> + Send {
async move {
let mut guard = self.data.write().await;
guard.remove(&id)
.ok_or_else(|| domain::DomainError::NotFound(format!("Entity not found: {}", id)))?;
Ok(())
}
}
}
impl Repository<Product> for MockRepository<Product> {
fn create(&self, data: CreateProduct) -> impl Future<Output = Result<Product>> + Send {
async move {
let mut guard = self.data.write().await;
let id = Uuid::new_v4();
let product = Product {
id,
name: data.name,
description: data.description,
created_at: Utc::now(),
updated_at: Utc::now(),
};
guard.insert(id, product.clone());
Ok(product)
}
}
fn find_by_id(&self, id: Uuid) -> impl Future<Output = Result<Product>> + Send {
async move {
let guard = self.data.read().await;
guard.get(&id)
.cloned()
.ok_or_else(|| domain::DomainError::NotFound(format!("Entity not found: {}", id)))
}
}
fn find_all(&self) -> impl Future<Output = Result<Vec<Product>>> + Send {
async move {
let guard = self.data.read().await;
Ok(guard.values().cloned().collect())
}
}
fn update(&self, id: Uuid, data: UpdateProduct) -> impl Future<Output = Result<Product>> + Send {
async move {
let mut guard = self.data.write().await;
let product = guard.get_mut(&id)
.ok_or_else(|| domain::DomainError::NotFound(format!("Entity not found: {}", id)))?;
if let Some(name) = data.name {
product.name = name;
}
if let Some(description) = data.description {
product.description = description;
}
product.updated_at = Utc::now();
Ok(product.clone())
}
}
fn delete(&self, id: Uuid) -> impl Future<Output = Result<()>> + Send {
async move {
let mut guard = self.data.write().await;
guard.remove(&id)
.ok_or_else(|| domain::DomainError::NotFound(format!("Entity not found: {}", id)))?;
Ok(())
}
}
}
mod service_tests {
use super::*;
#[tokio::test]
async fn test_user_service_create() {
let repo = MockRepository::<User>::new();
let service = Service::new(repo);
let create_user = CreateUser {
username: "test_user".to_string(),
email: "test@example.com".to_string(),
};
let user = service.create(create_user).await.unwrap();
assert_eq!(user.username, "test_user");
assert_eq!(user.email, "test@example.com");
}
#[tokio::test]
async fn test_user_service_get() {
let repo = MockRepository::<User>::new();
let service = Service::new(repo);
let create_user = CreateUser {
username: "test_user".to_string(),
email: "test@example.com".to_string(),
};
let created = service.create(create_user).await.unwrap();
let found = service.get(created.id).await.unwrap();
assert_eq!(found.id, created.id);
}
#[tokio::test]
async fn test_user_service_list() {
let repo = MockRepository::<User>::new();
let service = Service::new(repo);
let user1 = CreateUser {
username: "user1".to_string(),
email: "user1@example.com".to_string(),
};
let user2 = CreateUser {
username: "user2".to_string(),
email: "user2@example.com".to_string(),
};
service.create(user1).await.unwrap();
service.create(user2).await.unwrap();
let users = service.list().await.unwrap();
assert_eq!(users.len(), 2);
}
#[tokio::test]
async fn test_user_service_update() {
let repo = MockRepository::<User>::new();
let service = Service::new(repo);
let create_user = CreateUser {
username: "test_user".to_string(),
email: "test@example.com".to_string(),
};
let created = service.create(create_user).await.unwrap();
let update = UpdateUser {
username: Some("updated_user".to_string()),
email: None,
};
let updated = service.update(created.id, update).await.unwrap();
assert_eq!(updated.username, "updated_user");
assert_eq!(updated.email, "test@example.com");
}
#[tokio::test]
async fn test_user_service_delete() {
let repo = MockRepository::<User>::new();
let service = Service::new(repo);
let create_user = CreateUser {
username: "test_user".to_string(),
email: "test@example.com".to_string(),
};
let created = service.create(create_user).await.unwrap();
service.delete(created.id).await.unwrap();
assert!(service.get(created.id).await.is_err());
}
#[tokio::test]
async fn test_product_service_create() {
let repo = MockRepository::<Product>::new();
let service = Service::new(repo);
let create_product = CreateProduct {
name: "Test Product".to_string(),
description: "Test Description".to_string(),
};
let product = service.create(create_product).await.unwrap();
assert_eq!(product.name, "Test Product");
assert_eq!(product.description, "Test Description");
}
#[tokio::test]
async fn test_product_service_get() {
let repo = MockRepository::<Product>::new();
let service = Service::new(repo);
let create_product = CreateProduct {
name: "Test Product".to_string(),
description: "Test Description".to_string(),
};
let created = service.create(create_product).await.unwrap();
let found = service.get(created.id).await.unwrap();
assert_eq!(found.id, created.id);
}
#[tokio::test]
async fn test_product_service_list() {
let repo = MockRepository::<Product>::new();
let service = Service::new(repo);
let product1 = CreateProduct {
name: "Product 1".to_string(),
description: "Description 1".to_string(),
};
let product2 = CreateProduct {
name: "Product 2".to_string(),
description: "Description 2".to_string(),
};
service.create(product1).await.unwrap();
service.create(product2).await.unwrap();
let products = service.list().await.unwrap();
assert_eq!(products.len(), 2);
}
#[tokio::test]
async fn test_product_service_update() {
let repo = MockRepository::<Product>::new();
let service = Service::new(repo);
let create_product = CreateProduct {
name: "Test Product".to_string(),
description: "Test Description".to_string(),
};
let created = service.create(create_product).await.unwrap();
let update = UpdateProduct {
name: Some("Updated Product".to_string()),
description: None,
};
let updated = service.update(created.id, update).await.unwrap();
assert_eq!(updated.name, "Updated Product");
assert_eq!(updated.description, "Test Description");
}
#[tokio::test]
async fn test_product_service_delete() {
let repo = MockRepository::<Product>::new();
let service = Service::new(repo);
let create_product = CreateProduct {
name: "Test Product".to_string(),
description: "Test Description".to_string(),
};
let created = service.create(create_product).await.unwrap();
service.delete(created.id).await.unwrap();
assert!(service.get(created.id).await.is_err());
}
}
mod error_tests {
use super::*;
#[tokio::test]
async fn test_not_found_error() {
let repo = MockRepository::<User>::new();
let service = Service::new(repo);
let result = service.get(Uuid::new_v4()).await;
assert!(matches!(result, Err(ApplicationError::Domain(domain::DomainError::NotFound(_)))));
}
#[tokio::test]
async fn test_update_nonexistent() {
let repo = MockRepository::<User>::new();
let service = Service::new(repo);
let update = UpdateUser {
username: Some("new_username".to_string()),
email: None,
};
let result = service.update(Uuid::new_v4(), update).await;
assert!(matches!(result, Err(ApplicationError::Domain(domain::DomainError::NotFound(_)))));
}
#[tokio::test]
async fn test_delete_nonexistent() {
let repo = MockRepository::<User>::new();
let service = Service::new(repo);
let result = service.delete(Uuid::new_v4()).await;
assert!(matches!(result, Err(ApplicationError::Domain(domain::DomainError::NotFound(_)))));
}
}
}

View file

@ -1,3 +1,14 @@
/*
* This file is part of Sharenet.
*
* Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
*
* You may obtain a copy of the license at:
* https://creativecommons.org/licenses/by-nc-sa/4.0/
*
* Copyright (c) 2024 Continuist <continuist02@gmail.com>
*/
use anyhow::Result; use anyhow::Result;
use clap::Parser; use clap::Parser;
use domain::{CreateProduct, CreateUser, Product, User, UpdateProduct, UpdateUser}; use domain::{CreateProduct, CreateUser, Product, User, UpdateProduct, UpdateUser};

View file

@ -10,3 +10,6 @@ serde = { workspace = true }
uuid = { workspace = true } uuid = { workspace = true }
chrono = { workspace = true } chrono = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
[dev-dependencies]
tokio = { workspace = true, features = ["full", "macros", "rt-multi-thread"] }

View file

@ -1,3 +1,14 @@
/*
* This file is part of Sharenet.
*
* Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
*
* You may obtain a copy of the license at:
* https://creativecommons.org/licenses/by-nc-sa/4.0/
*
* Copyright (c) 2024 Continuist <continuist02@gmail.com>
*/
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
@ -89,17 +100,233 @@ where
fn delete(&self, id: Uuid) -> impl Future<Output = Result<()>> + Send; fn delete(&self, id: Uuid) -> impl Future<Output = Result<()>> + Send;
} }
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use chrono::Utc;
#[test] mod user_tests {
fn it_works() { use super::*;
let result = add(2, 2);
assert_eq!(result, 4); #[test]
fn test_user_entity_impl() {
let user = User {
id: Uuid::new_v4(),
username: "test_user".to_string(),
email: "test@example.com".to_string(),
created_at: Utc::now(),
updated_at: Utc::now(),
};
assert_eq!(user.username, "test_user");
assert_eq!(user.email, "test@example.com");
}
#[test]
fn test_create_user_validation() {
let create_user = CreateUser {
username: "test_user".to_string(),
email: "test@example.com".to_string(),
};
assert_eq!(create_user.username, "test_user");
assert_eq!(create_user.email, "test@example.com");
}
#[test]
fn test_update_user_partial() {
let update_user = UpdateUser {
username: Some("new_username".to_string()),
email: None,
};
assert_eq!(update_user.username, Some("new_username".to_string()));
assert_eq!(update_user.email, None);
}
}
mod product_tests {
use super::*;
#[test]
fn test_product_entity_impl() {
let product = Product {
id: Uuid::new_v4(),
name: "Test Product".to_string(),
description: "Test Description".to_string(),
created_at: Utc::now(),
updated_at: Utc::now(),
};
assert_eq!(product.name, "Test Product");
assert_eq!(product.description, "Test Description");
}
#[test]
fn test_create_product_validation() {
let create_product = CreateProduct {
name: "Test Product".to_string(),
description: "Test Description".to_string(),
};
assert_eq!(create_product.name, "Test Product");
assert_eq!(create_product.description, "Test Description");
}
#[test]
fn test_update_product_partial() {
let update_product = UpdateProduct {
name: Some("New Product Name".to_string()),
description: None,
};
assert_eq!(update_product.name, Some("New Product Name".to_string()));
assert_eq!(update_product.description, None);
}
}
mod domain_error_tests {
use super::*;
#[test]
fn test_not_found_error() {
let error = DomainError::NotFound("User not found".to_string());
assert_eq!(error.to_string(), "Entity not found: User not found");
}
#[test]
fn test_invalid_input_error() {
let error = DomainError::InvalidInput("Invalid email format".to_string());
assert_eq!(error.to_string(), "Invalid input: Invalid email format");
}
#[test]
fn test_internal_error() {
let error = DomainError::Internal("Database connection failed".to_string());
assert_eq!(error.to_string(), "Internal error: Database connection failed");
}
}
mod repository_trait_tests {
use super::*;
use std::sync::Arc;
use tokio::sync::RwLock;
use std::collections::HashMap;
// Mock implementation of Repository for testing
struct MockRepository<T> {
data: Arc<RwLock<HashMap<Uuid, T>>>,
}
impl<T: Clone + Send + Sync> MockRepository<T> {
fn new() -> Self {
Self {
data: Arc::new(RwLock::new(HashMap::new())),
}
}
}
#[tokio::test]
async fn test_repository_create() {
let repo = MockRepository::<User>::new();
let create_user = CreateUser {
username: "test_user".to_string(),
email: "test@example.com".to_string(),
};
let user = User {
id: Uuid::new_v4(),
username: create_user.username.clone(),
email: create_user.email.clone(),
created_at: Utc::now(),
updated_at: Utc::now(),
};
repo.data.write().await.insert(user.id, user.clone());
let guard = repo.data.read().await;
let stored = guard.get(&user.id).unwrap();
assert_eq!(stored.username, create_user.username);
assert_eq!(stored.email, create_user.email);
}
#[tokio::test]
async fn test_repository_find_by_id() {
let repo = MockRepository::<User>::new();
let user = User {
id: Uuid::new_v4(),
username: "test_user".to_string(),
email: "test@example.com".to_string(),
created_at: Utc::now(),
updated_at: Utc::now(),
};
repo.data.write().await.insert(user.id, user.clone());
let guard = repo.data.read().await;
let found = guard.get(&user.id).unwrap();
assert_eq!(found.id, user.id);
}
#[tokio::test]
async fn test_repository_find_all() {
let repo = MockRepository::<User>::new();
let user1 = User {
id: Uuid::new_v4(),
username: "user1".to_string(),
email: "user1@example.com".to_string(),
created_at: Utc::now(),
updated_at: Utc::now(),
};
let user2 = User {
id: Uuid::new_v4(),
username: "user2".to_string(),
email: "user2@example.com".to_string(),
created_at: Utc::now(),
updated_at: Utc::now(),
};
repo.data.write().await.insert(user1.id, user1.clone());
repo.data.write().await.insert(user2.id, user2.clone());
let all = repo.data.read().await;
assert_eq!(all.len(), 2);
}
#[tokio::test]
async fn test_repository_update() {
let repo = MockRepository::<User>::new();
let user = User {
id: Uuid::new_v4(),
username: "test_user".to_string(),
email: "test@example.com".to_string(),
created_at: Utc::now(),
updated_at: Utc::now(),
};
repo.data.write().await.insert(user.id, user.clone());
let mut guard = repo.data.write().await;
let stored = guard.get_mut(&user.id).unwrap();
stored.username = "updated_user".to_string();
drop(guard);
let read_guard = repo.data.read().await;
let updated = read_guard.get(&user.id).unwrap();
assert_eq!(updated.username, "updated_user");
}
#[tokio::test]
async fn test_repository_delete() {
let repo = MockRepository::<User>::new();
let user = User {
id: Uuid::new_v4(),
username: "test_user".to_string(),
email: "test@example.com".to_string(),
created_at: Utc::now(),
updated_at: Utc::now(),
};
repo.data.write().await.insert(user.id, user.clone());
repo.data.write().await.remove(&user.id);
assert!(repo.data.read().await.get(&user.id).is_none());
}
} }
} }

View file

@ -1,3 +1,14 @@
/*
* This file is part of Sharenet.
*
* Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
*
* You may obtain a copy of the license at:
* https://creativecommons.org/licenses/by-nc-sa/4.0/
*
* Copyright (c) 2024 Continuist <continuist02@gmail.com>
*/
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::RwLock; use tokio::sync::RwLock;
@ -142,20 +153,5 @@ impl Repository<Product> for InMemoryProductRepository {
} }
} }
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
pub type MemoryUserService = Service<User, InMemoryUserRepository>; pub type MemoryUserService = Service<User, InMemoryUserRepository>;
pub type MemoryProductService = Service<Product, InMemoryProductRepository>; pub type MemoryProductService = Service<Product, InMemoryProductRepository>;

View file

@ -0,0 +1,48 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE users\n SET\n username = COALESCE($1, username),\n email = COALESCE($2, email),\n updated_at = CURRENT_TIMESTAMP\n WHERE id = $3\n RETURNING id, username, email, created_at, updated_at\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "username",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "email",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "updated_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text",
"Text",
"Uuid"
]
},
"nullable": [
false,
false,
false,
false,
false
]
},
"hash": "508d8e56fc561537060a4bf378dc057cc55999d2115521419d00ca57843af046"
}

View file

@ -0,0 +1,44 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, username, email, created_at, updated_at\n FROM users\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "username",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "email",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "updated_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false,
false,
false,
false
]
},
"hash": "54394890f1b4efd6487c5147e9a1942a86d7b2edfeea1cfb432758acbfbe6da6"
}

View file

@ -0,0 +1,46 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, username, email, created_at, updated_at\n FROM users\n WHERE id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "username",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "email",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "updated_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
false,
false,
false
]
},
"hash": "5bb95e6a9bea1ac6d4aab08eb63b1f9127a3a6b95046bb91bea60a2d6ee6dfc2"
}

View file

@ -0,0 +1,44 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, name, description, created_at, updated_at\n FROM products\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "updated_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false,
false,
false,
false
]
},
"hash": "62874c01dcd85e441608d6d92661277e3b3638412204a2aa5fad7a51c3ce855d"
}

View file

@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM products\n WHERE id = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "82746e921edfb33ae6c0f434d96ce74e0de9dc8e24ae1d9fa945b9924498a652"
}

View file

@ -0,0 +1,47 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO products (name, description)\n VALUES ($1, $2)\n RETURNING id, name, description, created_at, updated_at\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "updated_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false
]
},
"hash": "882fbe26ce3c2f8baf74b89805fc4be0d99874bee7ffed9d948fc57759f03d61"
}

View file

@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM users\n WHERE id = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "b69a6f42965b3e7103fcbf46e39528466926789ff31e9ed2591bb175527ec169"
}

View file

@ -0,0 +1,48 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE products\n SET\n name = COALESCE($1, name),\n description = COALESCE($2, description),\n updated_at = CURRENT_TIMESTAMP\n WHERE id = $3\n RETURNING id, name, description, created_at, updated_at\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "updated_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text",
"Text",
"Uuid"
]
},
"nullable": [
false,
false,
false,
false,
false
]
},
"hash": "c424e1240a945e7dea975c325a1a7db21d5503ccfb9f9ad788f3897fb8216eee"
}

View file

@ -0,0 +1,46 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, name, description, created_at, updated_at\n FROM products\n WHERE id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "updated_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
false,
false,
false
]
},
"hash": "ed0fa11d45b99c47dcbf75aa0646cf91516f91388d484e398273be3532d8e027"
}

View file

@ -0,0 +1,47 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO users (username, email)\n VALUES ($1, $2)\n RETURNING id, username, email, created_at, updated_at\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "username",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "email",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "updated_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false
]
},
"hash": "ffa2228196655e9f2b8152fc4b79d720cadc68f2d12bed4f46aeb87401ce2de9"
}

View file

@ -1,3 +1,14 @@
/*
* This file is part of Sharenet.
*
* Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
*
* You may obtain a copy of the license at:
* https://creativecommons.org/licenses/by-nc-sa/4.0/
*
* Copyright (c) 2024 Continuist <continuist02@gmail.com>
*/
use sqlx::PgPool; use sqlx::PgPool;
use uuid::Uuid; use uuid::Uuid;

View file

@ -1,3 +1,14 @@
/*
* This file is part of Sharenet.
*
* Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
*
* You may obtain a copy of the license at:
* https://creativecommons.org/licenses/by-nc-sa/4.0/
*
* Copyright (c) 2024 Continuist <continuist02@gmail.com>
*/
use std::io; use std::io;
use std::sync::mpsc; use std::sync::mpsc;
use std::thread; use std::thread;

View file

@ -2,6 +2,7 @@
"name": "frontend", "name": "frontend",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"license": "CC-BY-NC-SA-4.0",
"scripts": { "scripts": {
"dev": "next dev --turbopack", "dev": "next dev --turbopack",
"build": "next build", "build": "next build",

View file

@ -1,3 +1,14 @@
/**
* This file is part of Sharenet.
*
* Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
*
* You may obtain a copy of the license at:
* https://creativecommons.org/licenses/by-nc-sa/4.0/
*
* Copyright (c) 2024 Continuist <continuist02@gmail.com>
*/
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Inter } from "next/font/google"; import { Inter } from "next/font/google";
import "./globals.css"; import "./globals.css";

View file

@ -1,3 +1,14 @@
/**
* This file is part of Sharenet.
*
* Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
*
* You may obtain a copy of the license at:
* https://creativecommons.org/licenses/by-nc-sa/4.0/
*
* Copyright (c) 2024 Continuist <continuist02@gmail.com>
*/
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import Link from "next/link"; import Link from "next/link";

View file

@ -1,3 +1,14 @@
/**
* This file is part of Sharenet.
*
* Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
*
* You may obtain a copy of the license at:
* https://creativecommons.org/licenses/by-nc-sa/4.0/
*
* Copyright (c) 2024 Continuist <continuist02@gmail.com>
*/
'use client'; 'use client';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
@ -69,7 +80,7 @@ export default function ProductsPage() {
setIsDialogOpen(true); setIsDialogOpen(true);
}; };
const handleDelete = async (id: number) => { const handleDelete = async (id: string) => {
if (confirm('Are you sure you want to delete this product?')) { if (confirm('Are you sure you want to delete this product?')) {
try { try {
await productApi.delete(id); await productApi.delete(id);

View file

@ -1,3 +1,14 @@
/**
* This file is part of Sharenet.
*
* Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
*
* You may obtain a copy of the license at:
* https://creativecommons.org/licenses/by-nc-sa/4.0/
*
* Copyright (c) 2024 Continuist <continuist02@gmail.com>
*/
'use client'; 'use client';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';

View file

@ -1,3 +1,14 @@
/**
* This file is part of Sharenet.
*
* Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
*
* You may obtain a copy of the license at:
* https://creativecommons.org/licenses/by-nc-sa/4.0/
*
* Copyright (c) 2024 Continuist <continuist02@gmail.com>
*/
import axios from 'axios'; import axios from 'axios';
const API_BASE_URL = 'http://127.0.0.1:3000'; const API_BASE_URL = 'http://127.0.0.1:3000';