Add unit tests for domain and application layers, and update licenses to correct license
This commit is contained in:
parent
0e03712e61
commit
3e7f6fea8e
29 changed files with 1631 additions and 26 deletions
91
LICENSE_NOTICE.md
Normal file
91
LICENSE_NOTICE.md
Normal 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
415
PROJECT_INDEX.md
Normal 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).
|
|
@ -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).
|
|
@ -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"] }
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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(_)))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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};
|
||||||
|
|
|
@ -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"] }
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
Loading…
Add table
Reference in a new issue