Compare commits
15 Commits
22205682a5
...
e0f924b320
Author | SHA1 | Date | |
---|---|---|---|
e0f924b320
|
|||
31114214ed | |||
ca42d0f66c | |||
10c9430488 | |||
f6538945c5 | |||
acb7aa2e16 | |||
e0faf5600c | |||
a7a474743c | |||
f65312209c | |||
038dec0197 | |||
bea517ebd0 | |||
f66ea8b544 | |||
5cff261d4b | |||
bb1ff0c281 | |||
d1d91581f5 |
@ -1,4 +1,4 @@
|
|||||||
[Coursework](https://gitea.linerds.us/0x1D8/repo) paper, declared in [typst](https://typst.app/) v0.13, using [this template](https://gitea.linerds.us/pencelheimer/typst_nure_template).
|
[Coursework](https://gitea.linerds.us/0x1D8/repo) paper, declared in [typst](https://typst.app/) v0.13, using [this template](https://gitea.linerds.us/pencelheimer/typst_nure_template).
|
||||||
|
|
||||||
> [!CAUTION]
|
> [!CAUTION]
|
||||||
> The [template.typ](template.typ) file was included for reproducibility, it has it's own [license terms](https://gitea.linerds.us/pencelheimer/typst_nure_template/src/branch/main/LICENSE).
|
> The [template.typ](template.typ) file was included for reproducibility, it has it's own [license terms](https://gitea.linerds.us/pencelheimer/typst_nure_template/src/branch/main/LICENSE) (GPL-3.0 at the time of writing).
|
||||||
|
2
src/.gitignore
vendored
Normal file
2
src/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/target/
|
||||||
|
/**/scrapyard/
|
5381
src/Cargo.lock
generated
Normal file
5381
src/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
src/Cargo.toml
Normal file
16
src/Cargo.toml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "repo"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
data = { path = "data" }
|
||||||
|
service = { path = "service" }
|
||||||
|
|
||||||
|
iced = { version = "0.13.1", features = ["tokio", "lazy"] }
|
||||||
|
strum = { version = "0.27.0", features = ["derive"] }
|
||||||
|
open = "5.3.2"
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
|
members = ["data", "service"]
|
20
src/README.md
Normal file
20
src/README.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Stuff that helped
|
||||||
|
|
||||||
|
* Architecture:
|
||||||
|
- [How to apply hexagonal architecture to Rust](https://www.barrage.net/blog/technology/how-to-apply-hexagonal-architecture-to-rust)
|
||||||
|
- [Implementing onion architecture using Rust](https://mathias-vandaele.dev/implementing-onion-architecture-using-rust)
|
||||||
|
* Design:
|
||||||
|
- [Rust Data Modelling Without Classes](https://www.youtube.com/watch?v=z-0-bbc80JM)
|
||||||
|
- ["Making Impossible States Impossible" by Richard Feldman](https://www.youtube.com/watch?v=IcgmSRJHu_8)
|
||||||
|
- [Pretty State Machine Patterns in Rust](https://hoverbear.org/blog/rust-state-machine-pattern/)
|
||||||
|
* How to Iced:
|
||||||
|
- [Building a simple text editor with iced, a cross-platform GUI library for Rust](https://www.youtube.com/watch?v=gcBJ7cPSALo)
|
||||||
|
- [Unofficial Iced Guide](https://jl710.github.io/iced-guide/)
|
||||||
|
- [icebreaker](https://github.com/hecrj/icebreaker)
|
||||||
|
- [Halloy](https://github.com/squidowl/halloy)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> _The purpose of abstraction is not to be vague, but to create a new semantic level in which one can be absolutely precise._
|
||||||
|
|
||||||
|
— _Edsger W. Dijkstra_
|
11
src/assets/compose.yaml
Normal file
11
src/assets/compose.yaml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
services:
|
||||||
|
database:
|
||||||
|
image: mysql:9.0
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:3306:3306"
|
||||||
|
volumes:
|
||||||
|
- ../../repo-database:/var/lib/mysql
|
||||||
|
- ./init:/docker-entrypoint-initdb.d/:ro
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: password # yes, I know
|
191
src/assets/init/0-init.sql
Normal file
191
src/assets/init/0-init.sql
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
-- DROP DATABASE IF EXISTS repository;
|
||||||
|
CREATE DATABASE repository;
|
||||||
|
USE repository;
|
||||||
|
|
||||||
|
-- Required info for an account
|
||||||
|
CREATE TABLE Users (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(31) UNIQUE NOT NULL,
|
||||||
|
email VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
|
||||||
|
last_used TIMESTAMP NULL,
|
||||||
|
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Enables multiple packages to have the same base yet different components
|
||||||
|
CREATE TABLE PackageBases (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(127) UNIQUE NOT NULL,
|
||||||
|
description VARCHAR(510) NULL,
|
||||||
|
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- User roles for working on packages: flagger, packager, submitter, maintainer, etc.
|
||||||
|
CREATE TABLE PackageBaseRoles (
|
||||||
|
id TINYINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(31) UNIQUE NOT NULL,
|
||||||
|
description VARCHAR(255) NULL
|
||||||
|
);
|
||||||
|
INSERT INTO PackageBaseRoles (id, name) VALUES
|
||||||
|
(1, 'submitter'),
|
||||||
|
(2, 'packager'),
|
||||||
|
(3, 'maintainer'),
|
||||||
|
(4, 'flagger');
|
||||||
|
|
||||||
|
-- Roles that a user has for a package
|
||||||
|
CREATE TABLE PackageBaseUserRoles (
|
||||||
|
base INT UNSIGNED,
|
||||||
|
user INT UNSIGNED,
|
||||||
|
role TINYINT UNSIGNED,
|
||||||
|
|
||||||
|
comment VARCHAR(255) NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (base, user, role), -- composite key
|
||||||
|
FOREIGN KEY (base) REFERENCES PackageBases(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (user) REFERENCES Users(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (role) REFERENCES PackageBaseRoles(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Information about the actual packages
|
||||||
|
CREATE TABLE Packages (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
base INT UNSIGNED NOT NULL,
|
||||||
|
name VARCHAR(127) UNIQUE NOT NULL,
|
||||||
|
version VARCHAR(127) NOT NULL,
|
||||||
|
description VARCHAR(255) NULL,
|
||||||
|
url VARCHAR(510) NULL,
|
||||||
|
|
||||||
|
flagged_at TIMESTAMP NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
FOREIGN KEY (base) REFERENCES PackageBases (id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- depends, makedepends, optdepends, etc.
|
||||||
|
CREATE TABLE DependencyTypes (
|
||||||
|
id TINYINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(31) UNIQUE NOT NULL
|
||||||
|
);
|
||||||
|
INSERT INTO DependencyTypes (id, name) VALUES
|
||||||
|
(1, 'depends'),
|
||||||
|
(2, 'makedepends'),
|
||||||
|
(3, 'checkdepends'),
|
||||||
|
(4, 'optdepends');
|
||||||
|
|
||||||
|
-- Track which dependencies a package has
|
||||||
|
CREATE TABLE PackageDependencies (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
arch VARCHAR(63) NULL,
|
||||||
|
requirement VARCHAR(255) NULL,
|
||||||
|
description VARCHAR(127) NULL,
|
||||||
|
|
||||||
|
package INT UNSIGNED NOT NULL,
|
||||||
|
dependency_type TINYINT UNSIGNED NOT NULL,
|
||||||
|
dependency_package_name VARCHAR(127) NOT NULL, -- Not an actual package, but an an alias. Allows for package substitution.
|
||||||
|
|
||||||
|
FOREIGN KEY (package) REFERENCES Packages (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (dependency_type) REFERENCES DependencyTypes (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- conflicts, provides, replaces, etc.
|
||||||
|
CREATE TABLE RelationTypes (
|
||||||
|
id TINYINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(31) UNIQUE NOT NULL
|
||||||
|
);
|
||||||
|
INSERT INTO RelationTypes (id, name) VALUES
|
||||||
|
(1, 'conflicts'),
|
||||||
|
(2, 'provides'),
|
||||||
|
(3, 'replaces');
|
||||||
|
|
||||||
|
-- Track which conflicts, provides and replaces a package has
|
||||||
|
CREATE TABLE PackageRelations (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
arch VARCHAR(63) NULL,
|
||||||
|
requirement VARCHAR(255) NULL,
|
||||||
|
|
||||||
|
package INT UNSIGNED NOT NULL,
|
||||||
|
relation_type TINYINT UNSIGNED NOT NULL,
|
||||||
|
relation_package_name VARCHAR(127) NOT NULL,
|
||||||
|
|
||||||
|
FOREIGN KEY (package) REFERENCES Packages (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (relation_type) REFERENCES RelationTypes (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Public user profile
|
||||||
|
/* CREATE TABLE UserProfiles ( user_id INT UNSIGNED PRIMARY KEY,
|
||||||
|
real_name VARCHAR(63) NULL,
|
||||||
|
homepage TEXT NULL, -- bio / description / whatever
|
||||||
|
irc_nick VARCHAR(31) NULL,
|
||||||
|
pgp_key CHAR(40) NULL,
|
||||||
|
language VARCHAR(31) NULL, -- only for display
|
||||||
|
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
|
||||||
|
FOREIGN KEY (user_id) REFERENCES Users(id) ON DELETE CASCADE
|
||||||
|
); */
|
||||||
|
|
||||||
|
-- Settings for the User
|
||||||
|
/* CREATE TABLE UserPreferences ( user_id INT UNSIGNED PRIMARY KEY,
|
||||||
|
inactive BOOLEAN DEFAULT 0 NOT NULL, -- user is no longer active
|
||||||
|
show_email BOOLEAN DEFAULT 0 NOT NULL, -- on public profile page
|
||||||
|
utc_timezone TINYINT DEFAULT 0 NOT NULL, -- adjust timestamps shown
|
||||||
|
backup_email VARCHAR(127) NULL, -- to restore the account
|
||||||
|
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
|
||||||
|
FOREIGN KEY (user_id) REFERENCES Users(id) ON DELETE CASCADE
|
||||||
|
); */
|
||||||
|
|
||||||
|
-- Levels of access to the repository
|
||||||
|
/* CREATE TABLE AccessRoles ( id TINYINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(31) UNIQUE NOT NULL,
|
||||||
|
description VARCHAR(255) NULL
|
||||||
|
); */
|
||||||
|
-- Roles that a user has
|
||||||
|
/* CREATE TABLE UserAccessRoles (
|
||||||
|
user_id INT UNSIGNED,
|
||||||
|
role_id TINYINT UNSIGNED,
|
||||||
|
|
||||||
|
PRIMARY KEY (user_id, role_id), -- composite key
|
||||||
|
FOREIGN KEY (user_id) REFERENCES Users(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (role_id) REFERENCES AccessRoles(id) ON DELETE CASCADE
|
||||||
|
); */
|
||||||
|
|
||||||
|
-- Votes
|
||||||
|
/* CREATE TABLE PackageBaseUserVotes (
|
||||||
|
package_base INT UNSIGNED,
|
||||||
|
user INT UNSIGNED,
|
||||||
|
score TINYINT UNSIGNED DEFAULT 0 NOT NULL CHECK (score <= 10),
|
||||||
|
|
||||||
|
comment VARCHAR(255) NULL,
|
||||||
|
log TEXT NULL, -- error logs, etc.
|
||||||
|
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (package_base, user), -- composite key
|
||||||
|
FOREIGN KEY (package_base) REFERENCES PackageBases (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (user) REFERENCES Users (id) ON DELETE CASCADE
|
||||||
|
); */
|
||||||
|
|
||||||
|
-- Information about licenses
|
||||||
|
/* CREATE TABLE Licenses ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(127) UNIQUE NOT NULL,
|
||||||
|
description TEXT NULL
|
||||||
|
); */
|
||||||
|
-- Information about licenses
|
||||||
|
/* CREATE TABLE PackageLicenses (
|
||||||
|
package INT UNSIGNED,
|
||||||
|
license INT UNSIGNED,
|
||||||
|
|
||||||
|
PRIMARY KEY (package, license), -- composite key
|
||||||
|
FOREIGN KEY (package) REFERENCES Packages (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (license) REFERENCES Licenses (id) ON DELETE CASCADE
|
||||||
|
); */
|
103
src/assets/init/1-data.sql
Normal file
103
src/assets/init/1-data.sql
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
-- Insert Users
|
||||||
|
INSERT INTO Users (name, email, password, last_used) VALUES
|
||||||
|
('alice', 'alice@example.com', 'password123', NOW()),
|
||||||
|
('bob', 'bob@example.com', 'securepass', NOW()),
|
||||||
|
('charlie', 'charlie@example.com', 'charliepwd', NOW()),
|
||||||
|
('dave', 'dave@example.com', 'davepass', NOW()),
|
||||||
|
('eve', 'eve@example.com', 'evepwd', NOW()),
|
||||||
|
('frank', 'frank@example.com', 'frankpass', NOW()),
|
||||||
|
('grace', 'grace@example.com', 'gracepwd', NOW()),
|
||||||
|
('heidi', 'heidi@example.com', 'heidipwd', NOW()),
|
||||||
|
('ivan', 'ivan@example.com', 'ivanpass', NOW()),
|
||||||
|
('judy', 'judy@example.com', 'judypass', NOW()),
|
||||||
|
('mallory', 'mallory@example.com', 'mallorypwd', NOW()),
|
||||||
|
('oscar', 'oscar@example.com', 'oscarpass', NOW()),
|
||||||
|
('peggy', 'peggy@example.com', 'peggypwd', NOW()),
|
||||||
|
('trent', 'trent@example.com', 'trentpass', NOW()),
|
||||||
|
('victor', 'victor@example.com', 'victorpwd', NOW());
|
||||||
|
|
||||||
|
-- Insert PackageBases
|
||||||
|
INSERT INTO PackageBases (name, description) VALUES
|
||||||
|
('libcore', 'Core system libraries'),
|
||||||
|
('webframework', 'A modern web framework'),
|
||||||
|
('dataproc', 'Data processing toolkit'),
|
||||||
|
('authmodule', 'Authentication and authorization module'),
|
||||||
|
('networkstack', 'Networking utilities and stack'),
|
||||||
|
('uikit', 'UI Kit for building interfaces'),
|
||||||
|
('cryptoengine', 'Cryptographic library'),
|
||||||
|
('dbconnector', 'Database connectivity drivers'),
|
||||||
|
('imageproc', 'Image processing library'),
|
||||||
|
('audiokit', 'Audio toolkit'),
|
||||||
|
('videokit', 'Video processing toolkit'),
|
||||||
|
('mlcore', 'Machine Learning core library'),
|
||||||
|
('analyticspro', 'Advanced analytics toolkit'),
|
||||||
|
('monitoragent', 'System monitoring agent'),
|
||||||
|
('filesystem', 'Filesystem utilities');
|
||||||
|
|
||||||
|
-- Assign Roles to Users for PackageBases
|
||||||
|
INSERT INTO PackageBaseUserRoles (base, user, role, comment) VALUES
|
||||||
|
(1, 1, 1, 'Original submitter'),
|
||||||
|
(1, 2, 2, 'Packager for latest release'),
|
||||||
|
(2, 3, 3, 'Maintains stability'),
|
||||||
|
(2, 4, 4, 'Flags issues'),
|
||||||
|
(3, 5, 1, 'Initial submission'),
|
||||||
|
(3, 6, 3, 'Lead maintainer'),
|
||||||
|
(4, 7, 2, 'Core packager'),
|
||||||
|
(5, 8, 1, 'Submitted new version'),
|
||||||
|
(6, 9, 4, 'Flagged for performance issues'),
|
||||||
|
(7, 10, 3, 'Maintainer for security fixes'),
|
||||||
|
(8, 11, 2, 'Driver package manager'),
|
||||||
|
(9, 12, 1, 'Original contributor'),
|
||||||
|
(10, 13, 3, 'Maintains core features'),
|
||||||
|
(11, 14, 4, 'Reported critical bug'),
|
||||||
|
(12, 15, 2, 'Optimized build process');
|
||||||
|
|
||||||
|
-- Insert Packages
|
||||||
|
INSERT INTO Packages (base, name, version, description, url) VALUES
|
||||||
|
(1, 'libcore-utils', '1.0.0', 'Utilities for libcore', 'http://example.com/libcore-utils'),
|
||||||
|
(1, 'libcore-extended', '1.1.0', 'Extended functionalities', 'http://example.com/libcore-extended'),
|
||||||
|
(2, 'webframework-api', '2.0.0', 'REST API module', 'http://example.com/webframework-api'),
|
||||||
|
(2, 'webframework-cli', '2.1.0', 'Command-line tools', 'http://example.com/webframework-cli'),
|
||||||
|
(3, 'dataproc-engine', '3.0.1', 'Data processing engine', 'http://example.com/dataproc-engine'),
|
||||||
|
(4, 'authmodule-oauth', '4.2.0', 'OAuth module', 'http://example.com/authmodule-oauth'),
|
||||||
|
(5, 'networkstack-core', '5.5.0', 'Core network stack', 'http://example.com/networkstack-core'),
|
||||||
|
(6, 'uikit-designer', '6.0.3', 'UI designer toolkit', 'http://example.com/uikit-designer'),
|
||||||
|
(7, 'cryptoengine-hash', '7.1.1', 'Hash algorithms', 'http://example.com/cryptoengine-hash'),
|
||||||
|
(8, 'dbconnector-mysql', '8.0.0', 'MySQL connector', 'http://example.com/dbconnector-mysql'),
|
||||||
|
(9, 'imageproc-filters', '9.3.0', 'Image filters library', 'http://example.com/imageproc-filters'),
|
||||||
|
(10, 'audiokit-mixer', '10.2.1', 'Audio mixing toolkit', 'http://example.com/audiokit-mixer'),
|
||||||
|
(11, 'videokit-stream', '11.4.0', 'Video streaming tools', 'http://example.com/videokit-stream'),
|
||||||
|
(12, 'mlcore-algo', '12.0.2', 'ML algorithms', 'http://example.com/mlcore-algo'),
|
||||||
|
(13, 'analyticspro-dashboard', '13.5.1', 'Analytics dashboard', 'http://example.com/analyticspro-dashboard');
|
||||||
|
|
||||||
|
-- Insert PackageDependencies
|
||||||
|
INSERT INTO PackageDependencies (arch, requirement, description, package, dependency_type, dependency_package_name) VALUES
|
||||||
|
('x86_64', '>=1.0.0', 'Core dependency', 3, 1, 'libcore-utils'),
|
||||||
|
('x86_64', '>=2.0.0', 'Required for API', 4, 2, 'webframework-api'),
|
||||||
|
('arm64', '>=3.0.1', 'Optional analytics', 5, 4, 'analyticspro-dashboard'),
|
||||||
|
('x86_64', '>=5.5.0', 'Network stack dependency', 6, 1, 'networkstack-core'),
|
||||||
|
('x86_64', '>=4.2.0', 'Authentication module', 7, 1, 'authmodule-oauth'),
|
||||||
|
('x86_64', NULL, 'Database driver', 8, 1, 'dbconnector-mysql'),
|
||||||
|
('arm64', NULL, 'Machine learning algorithms', 9, 3, 'mlcore-algo'),
|
||||||
|
('x86_64', '>=6.0.3', 'UI designer toolkit', 10, 1, 'uikit-designer'),
|
||||||
|
('x86_64', NULL, 'Audio toolkit dependency', 11, 2, 'audiokit-mixer'),
|
||||||
|
('x86_64', '>=7.1.1', 'Hash functions', 12, 1, 'cryptoengine-hash'),
|
||||||
|
('arm64', NULL, 'Video streaming tools', 13, 4, 'videokit-stream'),
|
||||||
|
('x86_64', '>=9.3.0', 'Image filters', 14, 1, 'imageproc-filters'),
|
||||||
|
('x86_64', NULL, 'System monitoring agent', 15, 2, 'monitoragent');
|
||||||
|
|
||||||
|
-- Insert PackageRelations
|
||||||
|
INSERT INTO PackageRelations (arch, requirement, package, relation_type, relation_package_name) VALUES
|
||||||
|
('x86_64', '>=1.0.0', 3, 1, 'legacy-web-api'), -- conflicts
|
||||||
|
('x86_64', NULL, 4, 2, 'web-cli-tools'), -- provides
|
||||||
|
('arm64', NULL, 5, 3, 'old-dataproc'), -- replaces
|
||||||
|
('x86_64', '>=5.0.0', 6, 1, 'net-tools-legacy'),
|
||||||
|
('x86_64', NULL, 7, 2, 'crypto-lib'),
|
||||||
|
('x86_64', '>=4.0.0', 8, 3, 'db-driver-old'),
|
||||||
|
('arm64', NULL, 9, 1, 'imgproc-v1'),
|
||||||
|
('x86_64', NULL, 10, 2, 'audio-tools'),
|
||||||
|
('x86_64', '>=7.0.0', 11, 3, 'video-kit-old'),
|
||||||
|
('x86_64', NULL, 12, 1, 'ml-core-legacy'),
|
||||||
|
('x86_64', '>=6.0.0', 13, 2, 'analytics-pro-tools'),
|
||||||
|
('x86_64', NULL, 14, 3, 'monitor-agent-v1'),
|
||||||
|
('x86_64', '>=9.0.0', 15, 1, 'filesystem-old');
|
12
src/data/.sqlx/query-014cf2ec55142a17047ad7c469685df75ae8e3c95a1a7c6c21be7b5624a82ae1.json
generated
Normal file
12
src/data/.sqlx/query-014cf2ec55142a17047ad7c469685df75ae8e3c95a1a7c6c21be7b5624a82ae1.json
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "MySQL",
|
||||||
|
"query": "INSERT INTO PackageBases (name, description, created_at, updated_at) VALUES (?, ?, ?, ?)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 4
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "014cf2ec55142a17047ad7c469685df75ae8e3c95a1a7c6c21be7b5624a82ae1"
|
||||||
|
}
|
12
src/data/.sqlx/query-063059de083c42956506d991bc04472929e6b2618ab13eb90e772ad9bd9c1984.json
generated
Normal file
12
src/data/.sqlx/query-063059de083c42956506d991bc04472929e6b2618ab13eb90e772ad9bd9c1984.json
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "MySQL",
|
||||||
|
"query": "UPDATE Packages SET created_at = ? WHERE id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "063059de083c42956506d991bc04472929e6b2618ab13eb90e772ad9bd9c1984"
|
||||||
|
}
|
12
src/data/.sqlx/query-0af939868e37bad5eb9097badeaefca62c247c4b2265a9667c4b33885126c771.json
generated
Normal file
12
src/data/.sqlx/query-0af939868e37bad5eb9097badeaefca62c247c4b2265a9667c4b33885126c771.json
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "MySQL",
|
||||||
|
"query": "UPDATE PackageBases SET name = ? WHERE id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "0af939868e37bad5eb9097badeaefca62c247c4b2265a9667c4b33885126c771"
|
||||||
|
}
|
84
src/data/.sqlx/query-0bb7353d64231dc12416f5504d94513493670e3f2ae017d87a2f0c3eca045f60.json
generated
Normal file
84
src/data/.sqlx/query-0bb7353d64231dc12416f5504d94513493670e3f2ae017d87a2f0c3eca045f60.json
generated
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
{
|
||||||
|
"db_name": "MySQL",
|
||||||
|
"query": "SELECT * FROM Users WHERE email = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "id",
|
||||||
|
"type_info": {
|
||||||
|
"type": "Long",
|
||||||
|
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
|
||||||
|
"max_size": 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "name",
|
||||||
|
"type_info": {
|
||||||
|
"type": "VarString",
|
||||||
|
"flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE",
|
||||||
|
"max_size": 124
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "email",
|
||||||
|
"type_info": {
|
||||||
|
"type": "VarString",
|
||||||
|
"flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE",
|
||||||
|
"max_size": 1020
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "password",
|
||||||
|
"type_info": {
|
||||||
|
"type": "VarString",
|
||||||
|
"flags": "NOT_NULL | NO_DEFAULT_VALUE",
|
||||||
|
"max_size": 1020
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
|
"name": "last_used",
|
||||||
|
"type_info": {
|
||||||
|
"type": "Timestamp",
|
||||||
|
"flags": "BINARY",
|
||||||
|
"max_size": 19
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 5,
|
||||||
|
"name": "created_at",
|
||||||
|
"type_info": {
|
||||||
|
"type": "Timestamp",
|
||||||
|
"flags": "NOT_NULL | BINARY | TIMESTAMP",
|
||||||
|
"max_size": 19
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 6,
|
||||||
|
"name": "updated_at",
|
||||||
|
"type_info": {
|
||||||
|
"type": "Timestamp",
|
||||||
|
"flags": "NOT_NULL | BINARY | TIMESTAMP",
|
||||||
|
"max_size": 19
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "0bb7353d64231dc12416f5504d94513493670e3f2ae017d87a2f0c3eca045f60"
|
||||||
|
}
|
12
src/data/.sqlx/query-346beb83d6351740a503b72133a190ac327ae79f6e555def8fec89fcc75fb015.json
generated
Normal file
12
src/data/.sqlx/query-346beb83d6351740a503b72133a190ac327ae79f6e555def8fec89fcc75fb015.json
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "MySQL",
|
||||||
|
"query": "UPDATE Packages SET version = ? WHERE id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "346beb83d6351740a503b72133a190ac327ae79f6e555def8fec89fcc75fb015"
|
||||||
|
}
|
12
src/data/.sqlx/query-389e38e7e0a0b7d9ba667ac148a0a468da889a3455c47325938b819ab41ef4c8.json
generated
Normal file
12
src/data/.sqlx/query-389e38e7e0a0b7d9ba667ac148a0a468da889a3455c47325938b819ab41ef4c8.json
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "MySQL",
|
||||||
|
"query": "DELETE FROM PackageBases WHERE id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "389e38e7e0a0b7d9ba667ac148a0a468da889a3455c47325938b819ab41ef4c8"
|
||||||
|
}
|
12
src/data/.sqlx/query-3ffe9168c1eb30bb54c65055314655c21f03d04973e4291e6d4b6c7847364659.json
generated
Normal file
12
src/data/.sqlx/query-3ffe9168c1eb30bb54c65055314655c21f03d04973e4291e6d4b6c7847364659.json
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "MySQL",
|
||||||
|
"query": "INSERT INTO Packages (base, name, version, description, url, flagged_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 8
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "3ffe9168c1eb30bb54c65055314655c21f03d04973e4291e6d4b6c7847364659"
|
||||||
|
}
|
12
src/data/.sqlx/query-404747d44ee859e8c967695c29963594bb8273e66c053934ec20d5fc3db9d41e.json
generated
Normal file
12
src/data/.sqlx/query-404747d44ee859e8c967695c29963594bb8273e66c053934ec20d5fc3db9d41e.json
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "MySQL",
|
||||||
|
"query": "DELETE FROM Users WHERE name = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "404747d44ee859e8c967695c29963594bb8273e66c053934ec20d5fc3db9d41e"
|
||||||
|
}
|
84
src/data/.sqlx/query-68ed36ae997fff190b4b15b80bf24b553d8ac922da251d9e8b8f4e897bab46b0.json
generated
Normal file
84
src/data/.sqlx/query-68ed36ae997fff190b4b15b80bf24b553d8ac922da251d9e8b8f4e897bab46b0.json
generated
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
{
|
||||||
|
"db_name": "MySQL",
|
||||||
|
"query": "SELECT * FROM Users WHERE name = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "id",
|
||||||
|
"type_info": {
|
||||||
|
"type": "Long",
|
||||||
|
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
|
||||||
|
"max_size": 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "name",
|
||||||
|
"type_info": {
|
||||||
|
"type": "VarString",
|
||||||
|
"flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE",
|
||||||
|
"max_size": 124
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "email",
|
||||||
|
"type_info": {
|
||||||
|
"type": "VarString",
|
||||||
|
"flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE",
|
||||||
|
"max_size": 1020
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "password",
|
||||||
|
"type_info": {
|
||||||
|
"type": "VarString",
|
||||||
|
"flags": "NOT_NULL | NO_DEFAULT_VALUE",
|
||||||
|
"max_size": 1020
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
|
"name": "last_used",
|
||||||
|
"type_info": {
|
||||||
|
"type": "Timestamp",
|
||||||
|
"flags": "BINARY",
|
||||||
|
"max_size": 19
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 5,
|
||||||
|
"name": "created_at",
|
||||||
|
"type_info": {
|
||||||
|
"type": "Timestamp",
|
||||||
|
"flags": "NOT_NULL | BINARY | TIMESTAMP",
|
||||||
|
"max_size": 19
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 6,
|
||||||
|
"name": "updated_at",
|
||||||
|
"type_info": {
|
||||||
|
"type": "Timestamp",
|
||||||
|
"flags": "NOT_NULL | BINARY | TIMESTAMP",
|
||||||
|
"max_size": 19
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "68ed36ae997fff190b4b15b80bf24b553d8ac922da251d9e8b8f4e897bab46b0"
|
||||||
|
}
|
104
src/data/.sqlx/query-695f4b0a4286cf625dc60dc3dfc4a9cd92aaea3ea58ef8702903983cfc32ab47.json
generated
Normal file
104
src/data/.sqlx/query-695f4b0a4286cf625dc60dc3dfc4a9cd92aaea3ea58ef8702903983cfc32ab47.json
generated
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
{
|
||||||
|
"db_name": "MySQL",
|
||||||
|
"query": "SELECT * FROM Packages WHERE id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "id",
|
||||||
|
"type_info": {
|
||||||
|
"type": "Long",
|
||||||
|
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
|
||||||
|
"max_size": 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "base",
|
||||||
|
"type_info": {
|
||||||
|
"type": "Long",
|
||||||
|
"flags": "NOT_NULL | MULTIPLE_KEY | UNSIGNED | NO_DEFAULT_VALUE",
|
||||||
|
"max_size": 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "name",
|
||||||
|
"type_info": {
|
||||||
|
"type": "VarString",
|
||||||
|
"flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE",
|
||||||
|
"max_size": 508
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "version",
|
||||||
|
"type_info": {
|
||||||
|
"type": "VarString",
|
||||||
|
"flags": "NOT_NULL | NO_DEFAULT_VALUE",
|
||||||
|
"max_size": 508
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
|
"name": "description",
|
||||||
|
"type_info": {
|
||||||
|
"type": "VarString",
|
||||||
|
"flags": "",
|
||||||
|
"max_size": 1020
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 5,
|
||||||
|
"name": "url",
|
||||||
|
"type_info": {
|
||||||
|
"type": "VarString",
|
||||||
|
"flags": "",
|
||||||
|
"max_size": 2040
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 6,
|
||||||
|
"name": "flagged_at",
|
||||||
|
"type_info": {
|
||||||
|
"type": "Timestamp",
|
||||||
|
"flags": "BINARY",
|
||||||
|
"max_size": 19
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 7,
|
||||||
|
"name": "created_at",
|
||||||
|
"type_info": {
|
||||||
|
"type": "Timestamp",
|
||||||
|
"flags": "NOT_NULL | BINARY | TIMESTAMP",
|
||||||
|
"max_size": 19
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 8,
|
||||||
|
"name": "updated_at",
|
||||||
|
"type_info": {
|
||||||
|
"type": "Timestamp",
|
||||||
|
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
|
||||||
|
"max_size": 19
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "695f4b0a4286cf625dc60dc3dfc4a9cd92aaea3ea58ef8702903983cfc32ab47"
|
||||||
|
}
|
12
src/data/.sqlx/query-6b6f842d169e2a9628474edcd6dad10878bb9aaa612f62080d40ba24682b0c3b.json
generated
Normal file
12
src/data/.sqlx/query-6b6f842d169e2a9628474edcd6dad10878bb9aaa612f62080d40ba24682b0c3b.json
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "MySQL",
|
||||||
|
"query": "UPDATE Packages SET base = ? WHERE id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "6b6f842d169e2a9628474edcd6dad10878bb9aaa612f62080d40ba24682b0c3b"
|
||||||
|
}
|
12
src/data/.sqlx/query-7cc4cf73572c0830d1da7b8e621a79a09f3e3d8cfd42d3946bd1fac93838b913.json
generated
Normal file
12
src/data/.sqlx/query-7cc4cf73572c0830d1da7b8e621a79a09f3e3d8cfd42d3946bd1fac93838b913.json
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "MySQL",
|
||||||
|
"query": "UPDATE Packages SET updated_at = ? WHERE id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "7cc4cf73572c0830d1da7b8e621a79a09f3e3d8cfd42d3946bd1fac93838b913"
|
||||||
|
}
|
12
src/data/.sqlx/query-7f06016e9892486c938a5e94c9e5f70903a38ed314235712c28ac5e14d9ac20f.json
generated
Normal file
12
src/data/.sqlx/query-7f06016e9892486c938a5e94c9e5f70903a38ed314235712c28ac5e14d9ac20f.json
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "MySQL",
|
||||||
|
"query": "UPDATE PackageBases SET description = ? WHERE id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "7f06016e9892486c938a5e94c9e5f70903a38ed314235712c28ac5e14d9ac20f"
|
||||||
|
}
|
64
src/data/.sqlx/query-839cea68f9de889f35a0d0ad0b48b4a0dc1af49f0f0e7bb12238d22a9c37fbbc.json
generated
Normal file
64
src/data/.sqlx/query-839cea68f9de889f35a0d0ad0b48b4a0dc1af49f0f0e7bb12238d22a9c37fbbc.json
generated
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"db_name": "MySQL",
|
||||||
|
"query": "SELECT * FROM PackageBases WHERE id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "id",
|
||||||
|
"type_info": {
|
||||||
|
"type": "Long",
|
||||||
|
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
|
||||||
|
"max_size": 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "name",
|
||||||
|
"type_info": {
|
||||||
|
"type": "VarString",
|
||||||
|
"flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE",
|
||||||
|
"max_size": 508
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "description",
|
||||||
|
"type_info": {
|
||||||
|
"type": "VarString",
|
||||||
|
"flags": "",
|
||||||
|
"max_size": 2040
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "created_at",
|
||||||
|
"type_info": {
|
||||||
|
"type": "Timestamp",
|
||||||
|
"flags": "NOT_NULL | BINARY | TIMESTAMP",
|
||||||
|
"max_size": 19
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
|
"name": "updated_at",
|
||||||
|
"type_info": {
|
||||||
|
"type": "Timestamp",
|
||||||
|
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
|
||||||
|
"max_size": 19
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "839cea68f9de889f35a0d0ad0b48b4a0dc1af49f0f0e7bb12238d22a9c37fbbc"
|
||||||
|
}
|
12
src/data/.sqlx/query-8af7a0169e934cb82997a1cab04e921f719ed9466c13713fda8736c540d0fa78.json
generated
Normal file
12
src/data/.sqlx/query-8af7a0169e934cb82997a1cab04e921f719ed9466c13713fda8736c540d0fa78.json
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "MySQL",
|
||||||
|
"query": "UPDATE Users SET updated_at = ? WHERE id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "8af7a0169e934cb82997a1cab04e921f719ed9466c13713fda8736c540d0fa78"
|
||||||
|
}
|
12
src/data/.sqlx/query-8be76176b46f645095dce3bcbed11134ec0f43504d3a820698282848fd67dbad.json
generated
Normal file
12
src/data/.sqlx/query-8be76176b46f645095dce3bcbed11134ec0f43504d3a820698282848fd67dbad.json
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "MySQL",
|
||||||
|
"query": "UPDATE Users SET password = ? WHERE id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "8be76176b46f645095dce3bcbed11134ec0f43504d3a820698282848fd67dbad"
|
||||||
|
}
|
12
src/data/.sqlx/query-8bfaca937858ed1060da5a650f749849d29af3d6345d0e02474abf4d8c78b89d.json
generated
Normal file
12
src/data/.sqlx/query-8bfaca937858ed1060da5a650f749849d29af3d6345d0e02474abf4d8c78b89d.json
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "MySQL",
|
||||||
|
"query": "UPDATE Packages SET flagged_at = ? WHERE id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "8bfaca937858ed1060da5a650f749849d29af3d6345d0e02474abf4d8c78b89d"
|
||||||
|
}
|
84
src/data/.sqlx/query-8e3ffe0d11d3eb38cd805771cd133588c0679404a68a8041f414553226abeeb2.json
generated
Normal file
84
src/data/.sqlx/query-8e3ffe0d11d3eb38cd805771cd133588c0679404a68a8041f414553226abeeb2.json
generated
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
{
|
||||||
|
"db_name": "MySQL",
|
||||||
|
"query": "SELECT * FROM Users WHERE id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "id",
|
||||||
|
"type_info": {
|
||||||
|
"type": "Long",
|
||||||
|
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
|
||||||
|
"max_size": 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "name",
|
||||||
|
"type_info": {
|
||||||
|
"type": "VarString",
|
||||||
|
"flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE",
|
||||||
|
"max_size": 124
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "email",
|
||||||
|
"type_info": {
|
||||||
|
"type": "VarString",
|
||||||
|
"flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE",
|
||||||
|
"max_size": 1020
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "password",
|
||||||
|
"type_info": {
|
||||||
|
"type": "VarString",
|
||||||
|
"flags": "NOT_NULL | NO_DEFAULT_VALUE",
|
||||||
|
"max_size": 1020
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
|
"name": "last_used",
|
||||||
|
"type_info": {
|
||||||
|
"type": "Timestamp",
|
||||||
|
"flags": "BINARY",
|
||||||
|
"max_size": 19
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 5,
|
||||||
|
"name": "created_at",
|
||||||
|
"type_info": {
|
||||||
|
"type": "Timestamp",
|
||||||
|
"flags": "NOT_NULL | BINARY | TIMESTAMP",
|
||||||
|
"max_size": 19
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 6,
|
||||||
|
"name": "updated_at",
|
||||||
|
"type_info": {
|
||||||
|
"type": "Timestamp",
|
||||||
|
"flags": "NOT_NULL | BINARY | TIMESTAMP",
|
||||||
|
"max_size": 19
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "8e3ffe0d11d3eb38cd805771cd133588c0679404a68a8041f414553226abeeb2"
|
||||||
|
}
|
12
src/data/.sqlx/query-93ec7d124c9bfa7329478d975614db874788ed297fe2d95275592becb186f942.json
generated
Normal file
12
src/data/.sqlx/query-93ec7d124c9bfa7329478d975614db874788ed297fe2d95275592becb186f942.json
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "MySQL",
|
||||||
|
"query": "UPDATE PackageBases SET created_at = ? WHERE id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "93ec7d124c9bfa7329478d975614db874788ed297fe2d95275592becb186f942"
|
||||||
|
}
|
104
src/data/.sqlx/query-944eb40633e943a75244dee639fe6efb16919aff7172189c81240cb12462ae58.json
generated
Normal file
104
src/data/.sqlx/query-944eb40633e943a75244dee639fe6efb16919aff7172189c81240cb12462ae58.json
generated
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
{
|
||||||
|
"db_name": "MySQL",
|
||||||
|
"query": "SELECT * FROM Packages WHERE name = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "id",
|
||||||
|
"type_info": {
|
||||||
|
"type": "Long",
|
||||||
|
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
|
||||||
|
"max_size": 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "base",
|
||||||
|
"type_info": {
|
||||||
|
"type": "Long",
|
||||||
|
"flags": "NOT_NULL | MULTIPLE_KEY | UNSIGNED | NO_DEFAULT_VALUE",
|
||||||
|
"max_size": 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "name",
|
||||||
|
"type_info": {
|
||||||
|
"type": "VarString",
|
||||||
|
"flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE",
|
||||||
|
"max_size": 508
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "version",
|
||||||
|
"type_info": {
|
||||||
|
"type": "VarString",
|
||||||
|
"flags": "NOT_NULL | NO_DEFAULT_VALUE",
|
||||||
|
"max_size": 508
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
|
"name": "description",
|
||||||
|
"type_info": {
|
||||||
|
"type": "VarString",
|
||||||
|
"flags": "",
|
||||||
|
"max_size": 1020
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 5,
|
||||||
|
"name": "url",
|
||||||
|
"type_info": {
|
||||||
|
"type": "VarString",
|
||||||
|
"flags": "",
|
||||||
|
"max_size": 2040
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 6,
|
||||||
|
"name": "flagged_at",
|
||||||
|
"type_info": {
|
||||||
|
"type": "Timestamp",
|
||||||
|
"flags": "BINARY",
|
||||||
|
"max_size": 19
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 7,
|
||||||
|
"name": "created_at",
|
||||||
|
"type_info": {
|
||||||
|
"type": "Timestamp",
|
||||||
|
"flags": "NOT_NULL | BINARY | TIMESTAMP",
|
||||||
|
"max_size": 19
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 8,
|
||||||
|
"name": "updated_at",
|
||||||
|
"type_info": {
|
||||||
|
"type": "Timestamp",
|
||||||
|
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
|
||||||
|
"max_size": 19
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "944eb40633e943a75244dee639fe6efb16919aff7172189c81240cb12462ae58"
|
||||||
|
}
|
12
src/data/.sqlx/query-9be7f66630e64787e55946dff428d28035747b66e57260bc9cd4634a71a037a6.json
generated
Normal file
12
src/data/.sqlx/query-9be7f66630e64787e55946dff428d28035747b66e57260bc9cd4634a71a037a6.json
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "MySQL",
|
||||||
|
"query": "UPDATE Users SET email = ? WHERE id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "9be7f66630e64787e55946dff428d28035747b66e57260bc9cd4634a71a037a6"
|
||||||
|
}
|
12
src/data/.sqlx/query-9fa86328c40ce0469f755efb4876010092b7bc9f240a5d43dc69f9d0b1b5b7ce.json
generated
Normal file
12
src/data/.sqlx/query-9fa86328c40ce0469f755efb4876010092b7bc9f240a5d43dc69f9d0b1b5b7ce.json
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "MySQL",
|
||||||
|
"query": "DELETE FROM Users WHERE id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "9fa86328c40ce0469f755efb4876010092b7bc9f240a5d43dc69f9d0b1b5b7ce"
|
||||||
|
}
|
12
src/data/.sqlx/query-b5814b93236d587957a103e61726b0b9ae811ba6bff0617871e76de3ef0ff662.json
generated
Normal file
12
src/data/.sqlx/query-b5814b93236d587957a103e61726b0b9ae811ba6bff0617871e76de3ef0ff662.json
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "MySQL",
|
||||||
|
"query": "UPDATE Packages SET url = ? WHERE id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "b5814b93236d587957a103e61726b0b9ae811ba6bff0617871e76de3ef0ff662"
|
||||||
|
}
|
12
src/data/.sqlx/query-c1abf048d65d421717f20343bb0ef4fcd78f8571cfe2347c147124763bd17491.json
generated
Normal file
12
src/data/.sqlx/query-c1abf048d65d421717f20343bb0ef4fcd78f8571cfe2347c147124763bd17491.json
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "MySQL",
|
||||||
|
"query": "UPDATE Packages SET description = ? WHERE id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "c1abf048d65d421717f20343bb0ef4fcd78f8571cfe2347c147124763bd17491"
|
||||||
|
}
|
12
src/data/.sqlx/query-c2b00adbcb3c35a6ffa6b2bce08a738a9b3cd1ca4aa4c843909c7e14f7ef3e06.json
generated
Normal file
12
src/data/.sqlx/query-c2b00adbcb3c35a6ffa6b2bce08a738a9b3cd1ca4aa4c843909c7e14f7ef3e06.json
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "MySQL",
|
||||||
|
"query": "DELETE FROM Users WHERE email = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "c2b00adbcb3c35a6ffa6b2bce08a738a9b3cd1ca4aa4c843909c7e14f7ef3e06"
|
||||||
|
}
|
12
src/data/.sqlx/query-cc8f7e13c6aedf6aa4d6d4fc39db7aa98b84baf911e7f779641c1dc514c676cd.json
generated
Normal file
12
src/data/.sqlx/query-cc8f7e13c6aedf6aa4d6d4fc39db7aa98b84baf911e7f779641c1dc514c676cd.json
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "MySQL",
|
||||||
|
"query": "UPDATE Users SET last_used = ? WHERE id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "cc8f7e13c6aedf6aa4d6d4fc39db7aa98b84baf911e7f779641c1dc514c676cd"
|
||||||
|
}
|
12
src/data/.sqlx/query-cf79e2f6038dddd055d535d2c41dd8dccd1a4e6a763963590c904c25abf33137.json
generated
Normal file
12
src/data/.sqlx/query-cf79e2f6038dddd055d535d2c41dd8dccd1a4e6a763963590c904c25abf33137.json
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "MySQL",
|
||||||
|
"query": "UPDATE Users SET created_at = ? WHERE id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "cf79e2f6038dddd055d535d2c41dd8dccd1a4e6a763963590c904c25abf33137"
|
||||||
|
}
|
12
src/data/.sqlx/query-d289747c7c7fba86e2b66174e2d1546f10e8213d36b6b3cd25016f829e9d731b.json
generated
Normal file
12
src/data/.sqlx/query-d289747c7c7fba86e2b66174e2d1546f10e8213d36b6b3cd25016f829e9d731b.json
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "MySQL",
|
||||||
|
"query": "UPDATE PackageBases SET updated_at = ? WHERE id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "d289747c7c7fba86e2b66174e2d1546f10e8213d36b6b3cd25016f829e9d731b"
|
||||||
|
}
|
12
src/data/.sqlx/query-d474dd848d0ef8832afd4d1302fa562a3c4a4569032e8636d664043b5dc96661.json
generated
Normal file
12
src/data/.sqlx/query-d474dd848d0ef8832afd4d1302fa562a3c4a4569032e8636d664043b5dc96661.json
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "MySQL",
|
||||||
|
"query": "DELETE FROM Packages WHERE id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "d474dd848d0ef8832afd4d1302fa562a3c4a4569032e8636d664043b5dc96661"
|
||||||
|
}
|
12
src/data/.sqlx/query-daf98e6f1013c4993f7329f6fa690e92bccd89d1ff90131719c40626088dabd1.json
generated
Normal file
12
src/data/.sqlx/query-daf98e6f1013c4993f7329f6fa690e92bccd89d1ff90131719c40626088dabd1.json
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "MySQL",
|
||||||
|
"query": "INSERT INTO Users (name, email, password, last_used, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 6
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "daf98e6f1013c4993f7329f6fa690e92bccd89d1ff90131719c40626088dabd1"
|
||||||
|
}
|
12
src/data/.sqlx/query-e8ee44281a87c6e7147332dd5548971cb804a1ab1edcdae8bf009ac39059c2bb.json
generated
Normal file
12
src/data/.sqlx/query-e8ee44281a87c6e7147332dd5548971cb804a1ab1edcdae8bf009ac39059c2bb.json
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "MySQL",
|
||||||
|
"query": "UPDATE Packages SET name = ? WHERE id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "e8ee44281a87c6e7147332dd5548971cb804a1ab1edcdae8bf009ac39059c2bb"
|
||||||
|
}
|
12
src/data/.sqlx/query-f4963ad77bcbc0af4fc929f1f66b7ee842c26c44da32ae9bbbc06c466a908ccf.json
generated
Normal file
12
src/data/.sqlx/query-f4963ad77bcbc0af4fc929f1f66b7ee842c26c44da32ae9bbbc06c466a908ccf.json
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "MySQL",
|
||||||
|
"query": "DELETE FROM Packages WHERE name = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "f4963ad77bcbc0af4fc929f1f66b7ee842c26c44da32ae9bbbc06c466a908ccf"
|
||||||
|
}
|
12
src/data/.sqlx/query-f656bd1abb82c10af4e0e21b4a04a364988f5329356282f2ae0098dbfcaec671.json
generated
Normal file
12
src/data/.sqlx/query-f656bd1abb82c10af4e0e21b4a04a364988f5329356282f2ae0098dbfcaec671.json
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "MySQL",
|
||||||
|
"query": "UPDATE Users SET name = ? WHERE id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "f656bd1abb82c10af4e0e21b4a04a364988f5329356282f2ae0098dbfcaec671"
|
||||||
|
}
|
17
src/data/Cargo.toml
Normal file
17
src/data/Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "data"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
derive_more = { version = "2.0.1", features = ["deref", "into"] }
|
||||||
|
|
||||||
|
futures = "0.3.31"
|
||||||
|
chrono = { version = "0.4.39", default-features = false, features = [
|
||||||
|
"std",
|
||||||
|
"now",
|
||||||
|
] }
|
||||||
|
sqlx = { version = "0.8.3", default-features = false, features = ["mysql", "macros", "chrono", "runtime-tokio"] }
|
||||||
|
|
||||||
|
# thiserror = "2.0.11"
|
||||||
|
# garde = { version = "0.22.0", features = ["email", "url", "derive"] }
|
2
src/data/src/adapter.rs
Normal file
2
src/data/src/adapter.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
//! Specific implementations of [`crate::port`]s to plug into other parts of the application.
|
||||||
|
pub mod mysql;
|
5
src/data/src/adapter/mysql.rs
Normal file
5
src/data/src/adapter/mysql.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
//! `MySQL` adapters.
|
||||||
|
pub mod base;
|
||||||
|
pub mod package;
|
||||||
|
pub mod search;
|
||||||
|
pub mod user;
|
105
src/data/src/adapter/mysql/base.rs
Normal file
105
src/data/src/adapter/mysql/base.rs
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
use crate::Result;
|
||||||
|
use crate::port::base::{Base, BaseRepository, Field, New};
|
||||||
|
|
||||||
|
use chrono::Utc;
|
||||||
|
use sqlx::{Executor, MySql};
|
||||||
|
|
||||||
|
pub struct BaseAdapter;
|
||||||
|
|
||||||
|
impl<E> BaseRepository<E> for BaseAdapter
|
||||||
|
where
|
||||||
|
E: Send,
|
||||||
|
for<'a> &'a E: Executor<'a, Database = MySql>,
|
||||||
|
{
|
||||||
|
}
|
||||||
|
impl<E> crate::port::Crud<E> for BaseAdapter
|
||||||
|
where
|
||||||
|
E: Send,
|
||||||
|
for<'a> &'a E: Executor<'a, Database = MySql>,
|
||||||
|
{
|
||||||
|
type New = New;
|
||||||
|
type Unique = u64;
|
||||||
|
type Update = Field;
|
||||||
|
type Existing = Base;
|
||||||
|
|
||||||
|
async fn create(connection: &mut E, data: Self::New) -> Result<Self::Existing> {
|
||||||
|
let created_at = Utc::now();
|
||||||
|
let id = sqlx::query!(
|
||||||
|
"INSERT INTO PackageBases (name, description, created_at, updated_at) VALUES (?, ?, ?, ?)",
|
||||||
|
data.name.as_str(),
|
||||||
|
data.description.as_ref(),
|
||||||
|
created_at, created_at,
|
||||||
|
)
|
||||||
|
.execute(&*connection)
|
||||||
|
.await?
|
||||||
|
.last_insert_id();
|
||||||
|
|
||||||
|
Ok(Self::Existing {
|
||||||
|
id,
|
||||||
|
name: data.name.into(),
|
||||||
|
description: data.description.into(),
|
||||||
|
created_at,
|
||||||
|
updated_at: created_at,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read(connection: &E, data: Self::Unique) -> Result<Option<Self::Existing>> {
|
||||||
|
Ok(
|
||||||
|
sqlx::query_as!(Base, "SELECT * FROM PackageBases WHERE id = ?", data)
|
||||||
|
.fetch_optional(connection)
|
||||||
|
.await?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update(
|
||||||
|
connection: &mut E,
|
||||||
|
existing: &mut Self::Existing,
|
||||||
|
data: Self::Update,
|
||||||
|
) -> Result {
|
||||||
|
match &data {
|
||||||
|
Field::Name(name) => {
|
||||||
|
sqlx::query!(
|
||||||
|
"UPDATE PackageBases SET name = ? WHERE id = ?",
|
||||||
|
name.as_str(),
|
||||||
|
existing.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Field::Description(description) => {
|
||||||
|
sqlx::query!(
|
||||||
|
"UPDATE PackageBases SET description = ? WHERE id = ?",
|
||||||
|
description.as_ref(),
|
||||||
|
existing.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Field::CreatedAt(date_time) => sqlx::query!(
|
||||||
|
"UPDATE PackageBases SET created_at = ? WHERE id = ?",
|
||||||
|
date_time,
|
||||||
|
existing.id
|
||||||
|
),
|
||||||
|
Field::UpdatedAt(date_time) => sqlx::query!(
|
||||||
|
"UPDATE PackageBases SET updated_at = ? WHERE id = ?",
|
||||||
|
date_time,
|
||||||
|
existing.id
|
||||||
|
),
|
||||||
|
}
|
||||||
|
.execute(&*connection)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match data {
|
||||||
|
Field::Name(s) => existing.name = s.into(),
|
||||||
|
Field::Description(o) => existing.description = o.into(),
|
||||||
|
Field::CreatedAt(date_time) => existing.created_at = date_time,
|
||||||
|
Field::UpdatedAt(date_time) => existing.updated_at = date_time,
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(connection: &mut E, data: Self::Unique) -> Result {
|
||||||
|
sqlx::query!("DELETE FROM PackageBases WHERE id = ?", data)
|
||||||
|
.execute(&*connection)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
162
src/data/src/adapter/mysql/package.rs
Normal file
162
src/data/src/adapter/mysql/package.rs
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
use crate::Result;
|
||||||
|
use crate::port::package::{Field, New, Package, PackageRepository, Unique};
|
||||||
|
|
||||||
|
use chrono::Utc;
|
||||||
|
use sqlx::{Executor, MySql};
|
||||||
|
|
||||||
|
pub struct PackageAdapter;
|
||||||
|
|
||||||
|
impl<E> PackageRepository<E> for PackageAdapter
|
||||||
|
where
|
||||||
|
E: Send,
|
||||||
|
for<'a> &'a E: Executor<'a, Database = MySql>,
|
||||||
|
{
|
||||||
|
}
|
||||||
|
impl<E> crate::port::Crud<E> for PackageAdapter
|
||||||
|
where
|
||||||
|
E: Send,
|
||||||
|
for<'a> &'a E: Executor<'a, Database = MySql>,
|
||||||
|
{
|
||||||
|
type New = New;
|
||||||
|
type Update = Field;
|
||||||
|
type Unique = Unique;
|
||||||
|
type Existing = Package;
|
||||||
|
|
||||||
|
async fn create(connection: &mut E, data: Self::New) -> Result<Self::Existing> {
|
||||||
|
let created_at = Utc::now();
|
||||||
|
let id = sqlx::query!(
|
||||||
|
"INSERT INTO Packages \
|
||||||
|
(base, name, version, description, url, flagged_at, created_at, updated_at) \
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
data.package_base.id,
|
||||||
|
data.name.as_str(),
|
||||||
|
data.version.as_str(),
|
||||||
|
data.description.as_ref(),
|
||||||
|
data.url.as_ref(),
|
||||||
|
data.flagged_at,
|
||||||
|
created_at,
|
||||||
|
created_at,
|
||||||
|
)
|
||||||
|
.execute(&*connection)
|
||||||
|
.await?
|
||||||
|
.last_insert_id();
|
||||||
|
|
||||||
|
Ok(Self::Existing {
|
||||||
|
id,
|
||||||
|
base: data.package_base.id,
|
||||||
|
name: data.name.into(),
|
||||||
|
version: data.version.into(),
|
||||||
|
description: data.description.into(),
|
||||||
|
url: data.url.into(),
|
||||||
|
flagged_at: data.flagged_at,
|
||||||
|
created_at,
|
||||||
|
updated_at: created_at,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read(connection: &E, data: Self::Unique) -> Result<Option<Self::Existing>> {
|
||||||
|
Ok(match data {
|
||||||
|
Unique::Id(id) => {
|
||||||
|
sqlx::query_as!(Package, "SELECT * FROM Packages WHERE id = ?", id)
|
||||||
|
.fetch_optional(connection)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
Unique::Name(name) => {
|
||||||
|
sqlx::query_as!(
|
||||||
|
Package,
|
||||||
|
"SELECT * FROM Packages WHERE name = ?",
|
||||||
|
name.as_str()
|
||||||
|
)
|
||||||
|
.fetch_optional(connection)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}?)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update(
|
||||||
|
connection: &mut E,
|
||||||
|
existing: &mut Self::Existing,
|
||||||
|
data: Self::Update,
|
||||||
|
) -> Result {
|
||||||
|
match &data {
|
||||||
|
Field::Name(name) => {
|
||||||
|
sqlx::query!(
|
||||||
|
"UPDATE Packages SET name = ? WHERE id = ?",
|
||||||
|
name.as_str(),
|
||||||
|
existing.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Field::PackageBase(package_base) => {
|
||||||
|
sqlx::query!(
|
||||||
|
"UPDATE Packages SET base = ? WHERE id = ?",
|
||||||
|
package_base.id,
|
||||||
|
existing.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Field::Version(version) => {
|
||||||
|
sqlx::query!(
|
||||||
|
"UPDATE Packages SET version = ? WHERE id = ?",
|
||||||
|
version.as_str(),
|
||||||
|
existing.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Field::Description(description) => {
|
||||||
|
sqlx::query!(
|
||||||
|
"UPDATE Packages SET description = ? WHERE id = ?",
|
||||||
|
description.as_ref(),
|
||||||
|
existing.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Field::Url(url) => {
|
||||||
|
sqlx::query!(
|
||||||
|
"UPDATE Packages SET url = ? WHERE id = ?",
|
||||||
|
url.as_ref(),
|
||||||
|
existing.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Field::FlaggedAt(date_time) => sqlx::query!(
|
||||||
|
"UPDATE Packages SET flagged_at = ? WHERE id = ?",
|
||||||
|
date_time,
|
||||||
|
existing.id
|
||||||
|
),
|
||||||
|
Field::CreatedAt(date_time) => sqlx::query!(
|
||||||
|
"UPDATE Packages SET created_at = ? WHERE id = ?",
|
||||||
|
date_time,
|
||||||
|
existing.id
|
||||||
|
),
|
||||||
|
Field::UpdatedAt(date_time) => sqlx::query!(
|
||||||
|
"UPDATE Packages SET updated_at = ? WHERE id = ?",
|
||||||
|
date_time,
|
||||||
|
existing.id
|
||||||
|
),
|
||||||
|
}
|
||||||
|
.execute(&*connection)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match data {
|
||||||
|
Field::Name(s) => existing.name = s.into(),
|
||||||
|
Field::PackageBase(s) => existing.base = s.id,
|
||||||
|
Field::Version(s) => existing.version = s.into(),
|
||||||
|
Field::Description(o) => existing.description = o.into(),
|
||||||
|
Field::Url(o) => existing.url = o.into(),
|
||||||
|
Field::FlaggedAt(date_time) => existing.flagged_at = date_time,
|
||||||
|
Field::CreatedAt(date_time) => existing.created_at = date_time,
|
||||||
|
Field::UpdatedAt(date_time) => existing.updated_at = date_time,
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(connection: &mut E, data: Self::Unique) -> Result {
|
||||||
|
match data {
|
||||||
|
Unique::Id(id) => sqlx::query!("DELETE FROM Packages WHERE id = ?", id),
|
||||||
|
Unique::Name(name) => {
|
||||||
|
sqlx::query!("DELETE FROM Packages WHERE name = ?", name.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.execute(&*connection)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
152
src/data/src/adapter/mysql/search.rs
Normal file
152
src/data/src/adapter/mysql/search.rs
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
use crate::Result;
|
||||||
|
use crate::port::search::{Data, Entry, Mode, Order, SearchRepository};
|
||||||
|
|
||||||
|
// use chrono::Utc;
|
||||||
|
use futures::TryStreamExt;
|
||||||
|
use sqlx::{Executor, MySql, QueryBuilder, Row};
|
||||||
|
|
||||||
|
pub struct SearchAdapter;
|
||||||
|
|
||||||
|
impl<E> SearchRepository<E> for SearchAdapter
|
||||||
|
where
|
||||||
|
E: Send,
|
||||||
|
for<'a> &'a E: Executor<'a, Database = MySql>,
|
||||||
|
{
|
||||||
|
async fn search(connection: &E, data: Data) -> Result<Vec<Entry>> {
|
||||||
|
let mut builder = QueryBuilder::new(
|
||||||
|
"SELECT \
|
||||||
|
p.id, p.name, p.version, p.url, p.description, \
|
||||||
|
p.updated_at, p.created_at, \
|
||||||
|
pb.id AS base_id, pb.name AS base_name, \
|
||||||
|
( \
|
||||||
|
SELECT COUNT(DISTINCT pbur.user) \
|
||||||
|
FROM PackageBaseUserRoles pbur \
|
||||||
|
WHERE pbur.base = pb.id AND pbur.role = 3 \
|
||||||
|
) AS maintainers_num \
|
||||||
|
FROM \
|
||||||
|
Packages p \
|
||||||
|
JOIN \
|
||||||
|
PackageBases pb ON p.base = pb.id ",
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut push_search = |cond, param| {
|
||||||
|
builder.push(format_args!(
|
||||||
|
" {cond} {param} {} ",
|
||||||
|
if data.exact { "=" } else { "LIKE" }
|
||||||
|
));
|
||||||
|
builder.push_bind(if data.exact {
|
||||||
|
data.search.to_string()
|
||||||
|
} else {
|
||||||
|
format!("%{}%", data.search.as_str())
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let join_user = " JOIN PackageBaseUserRoles pbur ON pb.id = pbur.base \
|
||||||
|
JOIN Users u ON pbur.user = u.id WHERE ";
|
||||||
|
|
||||||
|
match data.mode {
|
||||||
|
Mode::Url => push_search("WHERE", "p.url"),
|
||||||
|
Mode::Name => push_search("WHERE", "p.name"),
|
||||||
|
Mode::PackageBase => push_search("WHERE", "pb.name"),
|
||||||
|
Mode::Description => push_search("WHERE", "p.description"),
|
||||||
|
Mode::BaseDescription => push_search("WHERE", "pb.description"),
|
||||||
|
Mode::NameAndDescription => {
|
||||||
|
// WHERE (p.name LIKE '%search_term%' OR p.description LIKE '%search_term%')
|
||||||
|
builder.push(" WHERE p.name LIKE ");
|
||||||
|
builder.push_bind(format!("%{}%", data.search.as_str()));
|
||||||
|
builder.push(" OR p.description LIKE ");
|
||||||
|
builder.push_bind(format!("%{}%", data.search.as_str()));
|
||||||
|
}
|
||||||
|
Mode::User => {
|
||||||
|
push_search(
|
||||||
|
"WHERE EXISTS ( \
|
||||||
|
SELECT 1 \
|
||||||
|
FROM PackageBaseUserRoles pbur \
|
||||||
|
JOIN Users u ON pbur.user = u.id \
|
||||||
|
WHERE pbur.base = pb.id AND",
|
||||||
|
"u.name",
|
||||||
|
);
|
||||||
|
builder.push(" ) ");
|
||||||
|
}
|
||||||
|
Mode::Flagger => {
|
||||||
|
push_search(join_user, "u.name");
|
||||||
|
builder.push(" AND pbur.role = 4 ");
|
||||||
|
} // 4
|
||||||
|
Mode::Packager => {
|
||||||
|
push_search(join_user, "u.name");
|
||||||
|
builder.push(" AND pbur.role = 2 ");
|
||||||
|
} // 2
|
||||||
|
Mode::Submitter => {
|
||||||
|
push_search(join_user, "u.name");
|
||||||
|
builder.push(" AND pbur.role = 1 ");
|
||||||
|
} // 1
|
||||||
|
Mode::Maintainer => {
|
||||||
|
push_search(join_user, "u.name");
|
||||||
|
builder.push(" AND pbur.role = 3 ");
|
||||||
|
} // 3
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.push(format_args!(
|
||||||
|
" ORDER BY {} {} LIMIT {};",
|
||||||
|
match data.order {
|
||||||
|
Order::Name => "p.name",
|
||||||
|
Order::Version => "p.version",
|
||||||
|
Order::BaseName => "pb.name",
|
||||||
|
Order::UpdatedAt => "p.updated_at",
|
||||||
|
Order::CreatedAt => "p.created_at",
|
||||||
|
},
|
||||||
|
if data.ascending { "ASC" } else { "DESC" },
|
||||||
|
data.limit
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
|
||||||
|
let mut rows = builder.build().fetch(connection);
|
||||||
|
while let Some(row) = rows.try_next().await? {
|
||||||
|
entries.push(Entry {
|
||||||
|
id: row.try_get("id")?,
|
||||||
|
name: row.try_get("name")?,
|
||||||
|
version: row.try_get("version")?,
|
||||||
|
base_id: row.try_get("base_id")?,
|
||||||
|
base_name: row.try_get("base_name")?,
|
||||||
|
url: row.try_get("url")?,
|
||||||
|
description: row.try_get("description")?,
|
||||||
|
// submitter_id: row.try_get("submitter_id")?,
|
||||||
|
// submitter_name: row.try_get("submitter_name")?,
|
||||||
|
updated_at: row.try_get("updated_at")?,
|
||||||
|
created_at: row.try_get("created_at")?,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(entries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::Validation;
|
||||||
|
use crate::port::search::Search;
|
||||||
|
use sqlx::MySqlPool;
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
|
async fn search() -> crate::Result {
|
||||||
|
let pool = MySqlPool::connect_lazy(
|
||||||
|
&std::env::var("DATABASE_URL")
|
||||||
|
.expect("environment variable `DATABASE_URL` should be set"),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let data = Data {
|
||||||
|
mode: Mode::NameAndDescription,
|
||||||
|
order: Order::UpdatedAt,
|
||||||
|
search: Search::new("f").map_err(|e| e.1)?,
|
||||||
|
limit: 50,
|
||||||
|
exact: true,
|
||||||
|
ascending: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
SearchAdapter::search(&pool, data).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
146
src/data/src/adapter/mysql/user.rs
Normal file
146
src/data/src/adapter/mysql/user.rs
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
use crate::Result;
|
||||||
|
use crate::port::user::{Field, New, Unique, User, UserRepository};
|
||||||
|
|
||||||
|
use chrono::Utc;
|
||||||
|
use sqlx::{Executor, MySql};
|
||||||
|
|
||||||
|
pub struct UserAdapter;
|
||||||
|
|
||||||
|
impl<E> UserRepository<E> for UserAdapter
|
||||||
|
where
|
||||||
|
E: Send,
|
||||||
|
for<'a> &'a E: Executor<'a, Database = MySql>,
|
||||||
|
{
|
||||||
|
}
|
||||||
|
impl<E> crate::port::Crud<E> for UserAdapter
|
||||||
|
where
|
||||||
|
E: Send,
|
||||||
|
for<'a> &'a E: Executor<'a, Database = MySql>,
|
||||||
|
{
|
||||||
|
type New = New;
|
||||||
|
type Update = Field;
|
||||||
|
type Unique = Unique;
|
||||||
|
type Existing = User;
|
||||||
|
|
||||||
|
async fn create(connection: &mut E, data: Self::New) -> Result<Self::Existing> {
|
||||||
|
let created_at = Utc::now();
|
||||||
|
let id = sqlx::query!(
|
||||||
|
"INSERT INTO Users (name, email, password, last_used, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
|
data.name.as_str(),
|
||||||
|
data.email.as_str(),
|
||||||
|
data.password.as_str(),
|
||||||
|
data.last_used,
|
||||||
|
created_at,
|
||||||
|
created_at,
|
||||||
|
)
|
||||||
|
.execute(&*connection)
|
||||||
|
.await?
|
||||||
|
.last_insert_id();
|
||||||
|
|
||||||
|
Ok(Self::Existing {
|
||||||
|
id,
|
||||||
|
name: data.name.into(),
|
||||||
|
email: data.email.into(),
|
||||||
|
password: data.password.into(),
|
||||||
|
last_used: data.last_used,
|
||||||
|
created_at,
|
||||||
|
updated_at: created_at,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read(connection: &E, data: Self::Unique) -> Result<Option<Self::Existing>> {
|
||||||
|
Ok(match data {
|
||||||
|
Unique::Id(id) => {
|
||||||
|
sqlx::query_as!(User, "SELECT * FROM Users WHERE id = ?", id)
|
||||||
|
.fetch_optional(connection)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
Unique::Name(name) => {
|
||||||
|
sqlx::query_as!(User, "SELECT * FROM Users WHERE name = ?", name.as_str())
|
||||||
|
.fetch_optional(connection)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
Unique::Email(email) => {
|
||||||
|
sqlx::query_as!(User, "SELECT * FROM Users WHERE email = ?", email.as_str())
|
||||||
|
.fetch_optional(connection)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}?)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update(
|
||||||
|
connection: &mut E,
|
||||||
|
existing: &mut Self::Existing,
|
||||||
|
data: Self::Update,
|
||||||
|
) -> Result {
|
||||||
|
match &data {
|
||||||
|
Field::Name(name) => {
|
||||||
|
sqlx::query!(
|
||||||
|
"UPDATE Users SET name = ? WHERE id = ?",
|
||||||
|
name.as_str(),
|
||||||
|
existing.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Field::Email(email) => {
|
||||||
|
sqlx::query!(
|
||||||
|
"UPDATE Users SET email = ? WHERE id = ?",
|
||||||
|
email.as_str(),
|
||||||
|
existing.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Field::Password(password) => {
|
||||||
|
sqlx::query!(
|
||||||
|
"UPDATE Users SET password = ? WHERE id = ?",
|
||||||
|
password.as_str(),
|
||||||
|
existing.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Field::LastUsed(date_time) => {
|
||||||
|
sqlx::query!(
|
||||||
|
"UPDATE Users SET last_used = ? WHERE id = ?",
|
||||||
|
date_time,
|
||||||
|
existing.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Field::CreatedAt(date_time) => sqlx::query!(
|
||||||
|
"UPDATE Users SET created_at = ? WHERE id = ?",
|
||||||
|
date_time,
|
||||||
|
existing.id
|
||||||
|
),
|
||||||
|
Field::UpdatedAt(date_time) => sqlx::query!(
|
||||||
|
"UPDATE Users SET updated_at = ? WHERE id = ?",
|
||||||
|
date_time,
|
||||||
|
existing.id
|
||||||
|
),
|
||||||
|
}
|
||||||
|
.execute(&*connection)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match data {
|
||||||
|
Field::Name(valid) => existing.name = valid.into(),
|
||||||
|
Field::Email(valid) => existing.email = valid.into(),
|
||||||
|
Field::Password(valid) => existing.password = valid.into(),
|
||||||
|
Field::LastUsed(date_time) => existing.last_used = date_time,
|
||||||
|
Field::CreatedAt(date_time) => existing.created_at = date_time,
|
||||||
|
Field::UpdatedAt(date_time) => existing.updated_at = date_time,
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(connection: &mut E, data: Self::Unique) -> Result {
|
||||||
|
match data {
|
||||||
|
Unique::Id(id) => sqlx::query!("DELETE FROM Users WHERE id = ?", id),
|
||||||
|
Unique::Name(name) => {
|
||||||
|
sqlx::query!("DELETE FROM Users WHERE name = ?", name.as_str())
|
||||||
|
}
|
||||||
|
Unique::Email(email) => {
|
||||||
|
sqlx::query!("DELETE FROM Users WHERE email = ?", email.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.execute(&*connection)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
43
src/data/src/atomic.rs
Normal file
43
src/data/src/atomic.rs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
//! Unify transaction management for established connections.
|
||||||
|
use crate::Result;
|
||||||
|
|
||||||
|
pub trait Atomic {
|
||||||
|
type Transaction<'a>;
|
||||||
|
|
||||||
|
fn start_transaction(&mut self) -> impl Future<Output = Result<Self::Transaction<'_>>> + Send;
|
||||||
|
fn abort_transaction(transaction: Self::Transaction<'_>)
|
||||||
|
-> impl Future<Output = Result> + Send;
|
||||||
|
fn commit_transaction(
|
||||||
|
transaction: Self::Transaction<'_>,
|
||||||
|
) -> impl Future<Output = Result> + Send;
|
||||||
|
}
|
||||||
|
|
||||||
|
use sqlx::Connection;
|
||||||
|
|
||||||
|
impl Atomic for sqlx::MySqlPool {
|
||||||
|
type Transaction<'a> = sqlx::MySqlTransaction<'a>;
|
||||||
|
|
||||||
|
async fn start_transaction(&mut self) -> Result<Self::Transaction<'_>> {
|
||||||
|
self.begin().await.map_err(Box::from)
|
||||||
|
}
|
||||||
|
async fn abort_transaction(transaction: Self::Transaction<'_>) -> Result {
|
||||||
|
transaction.rollback().await.map_err(Box::from)
|
||||||
|
}
|
||||||
|
async fn commit_transaction(transaction: Self::Transaction<'_>) -> Result {
|
||||||
|
transaction.commit().await.map_err(Box::from)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Atomic for sqlx::MySqlConnection {
|
||||||
|
type Transaction<'a> = sqlx::MySqlTransaction<'a>;
|
||||||
|
|
||||||
|
async fn start_transaction(&mut self) -> Result<Self::Transaction<'_>> {
|
||||||
|
self.begin().await.map_err(Box::from)
|
||||||
|
}
|
||||||
|
async fn abort_transaction(transaction: Self::Transaction<'_>) -> Result {
|
||||||
|
transaction.rollback().await.map_err(Box::from)
|
||||||
|
}
|
||||||
|
async fn commit_transaction(transaction: Self::Transaction<'_>) -> Result {
|
||||||
|
transaction.commit().await.map_err(Box::from)
|
||||||
|
}
|
||||||
|
}
|
53
src/data/src/connect.rs
Normal file
53
src/data/src/connect.rs
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
//! Driver to manage a connection which is passed to adapters.
|
||||||
|
use crate::Result;
|
||||||
|
|
||||||
|
pub trait Connect {
|
||||||
|
type Connection;
|
||||||
|
|
||||||
|
fn open_connection(&self) -> impl Future<Output = Result<Self::Connection>> + Send;
|
||||||
|
fn close_connection(connection: Self::Connection) -> impl Future<Output = Result> + Send;
|
||||||
|
}
|
||||||
|
|
||||||
|
use sqlx::Connection;
|
||||||
|
pub use sqlx::MySqlConnection as SqlxConnection;
|
||||||
|
pub use sqlx::MySqlPool as SqlxPool;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct MySqlPool {
|
||||||
|
pool: SqlxPool,
|
||||||
|
}
|
||||||
|
impl MySqlPool {
|
||||||
|
pub const fn new(pool: SqlxPool) -> Self {
|
||||||
|
Self { pool }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Connect for MySqlPool {
|
||||||
|
type Connection = SqlxPool;
|
||||||
|
|
||||||
|
async fn open_connection(&self) -> Result<Self::Connection> {
|
||||||
|
Ok(self.pool.clone())
|
||||||
|
}
|
||||||
|
async fn close_connection(_: Self::Connection) -> Result {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MySqlConnection {
|
||||||
|
link: String,
|
||||||
|
}
|
||||||
|
impl MySqlConnection {
|
||||||
|
pub const fn new(link: String) -> Self {
|
||||||
|
Self { link }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Connect for MySqlConnection {
|
||||||
|
type Connection = SqlxConnection;
|
||||||
|
|
||||||
|
async fn open_connection(&self) -> Result<Self::Connection> {
|
||||||
|
SqlxConnection::connect(&self.link).await.map_err(Box::from)
|
||||||
|
}
|
||||||
|
async fn close_connection(connection: Self::Connection) -> Result {
|
||||||
|
connection.close().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
23
src/data/src/lib.rs
Normal file
23
src/data/src/lib.rs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
//! Data access for the application.
|
||||||
|
pub mod adapter;
|
||||||
|
pub mod atomic;
|
||||||
|
pub mod connect;
|
||||||
|
pub mod port;
|
||||||
|
|
||||||
|
// Don't want to handle errors for dynamic mess.
|
||||||
|
pub type BoxDynError = Box<dyn std::error::Error + Send + Sync + 'static>;
|
||||||
|
pub type Result<T = (), E = BoxDynError> = std::result::Result<T, E>;
|
||||||
|
|
||||||
|
pub use chrono::Utc;
|
||||||
|
|
||||||
|
pub use adapter::mysql::base::BaseAdapter as MySqlBaseAdapter;
|
||||||
|
pub use adapter::mysql::package::PackageAdapter as MySqlPackageAdapter;
|
||||||
|
pub use adapter::mysql::user::UserAdapter as MySqlUserAdapter;
|
||||||
|
pub use adapter::mysql::search::SearchAdapter as MySqlSearchAdapter;
|
||||||
|
pub use atomic::Atomic;
|
||||||
|
pub use connect::*;
|
||||||
|
pub use port::base::{Base, BaseRepository};
|
||||||
|
pub use port::package::{Package, PackageRepository};
|
||||||
|
pub use port::search::{Search, SearchRepository};
|
||||||
|
pub use port::user::{User, UserRepository};
|
||||||
|
pub use port::*;
|
91
src/data/src/port.rs
Normal file
91
src/data/src/port.rs
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
//! Low-level repository traits for unified data access.
|
||||||
|
//!
|
||||||
|
//! Very mild argument validation.
|
||||||
|
use crate::{BoxDynError, Result};
|
||||||
|
|
||||||
|
pub mod base;
|
||||||
|
pub mod package;
|
||||||
|
pub mod search;
|
||||||
|
pub mod user;
|
||||||
|
|
||||||
|
pub trait Crud<C> {
|
||||||
|
type New;
|
||||||
|
type Unique;
|
||||||
|
type Update;
|
||||||
|
type Existing;
|
||||||
|
|
||||||
|
fn create(
|
||||||
|
connection: &mut C,
|
||||||
|
data: Self::New,
|
||||||
|
) -> impl Future<Output = Result<Self::Existing>> + Send;
|
||||||
|
fn read(
|
||||||
|
connection: &C,
|
||||||
|
data: Self::Unique,
|
||||||
|
) -> impl Future<Output = Result<Option<Self::Existing>>> + Send;
|
||||||
|
fn update(
|
||||||
|
connection: &mut C,
|
||||||
|
existing: &mut Self::Existing,
|
||||||
|
data: Self::Update,
|
||||||
|
) -> impl Future<Output = Result> + Send;
|
||||||
|
fn delete(connection: &mut C, data: Self::Unique) -> impl Future<Output = Result> + Send;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait CharLength {
|
||||||
|
fn length(&self) -> usize;
|
||||||
|
}
|
||||||
|
impl CharLength for &str {
|
||||||
|
fn length(&self) -> usize {
|
||||||
|
self.chars().count()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl CharLength for String {
|
||||||
|
fn length(&self) -> usize {
|
||||||
|
self.chars().count()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl CharLength for Option<String> {
|
||||||
|
fn length(&self) -> usize {
|
||||||
|
self.as_ref().map_or(0, CharLength::length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait Validatable {
|
||||||
|
type Inner: CharLength;
|
||||||
|
const MAX_LENGTH: usize;
|
||||||
|
fn encapsulate(value: Self::Inner) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(private_bounds)] // don't expose the impl details
|
||||||
|
pub trait Validation<T>: Validatable
|
||||||
|
where
|
||||||
|
T: CharLength + Into<Self::Inner>,
|
||||||
|
{
|
||||||
|
fn valid(value: &T) -> Result<(), String> {
|
||||||
|
if value.length() > Self::MAX_LENGTH {
|
||||||
|
Err(format!(
|
||||||
|
"too long (length: {}, max length: {})",
|
||||||
|
value.length(),
|
||||||
|
Self::MAX_LENGTH
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new(value: T) -> Result<Self, (T, BoxDynError)>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
match Self::valid(&value) {
|
||||||
|
Ok(()) => Ok(Self::encapsulate(value.into())),
|
||||||
|
Err(e) => Err((value, e.into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, U> Validation<U> for T
|
||||||
|
where
|
||||||
|
T: Validatable,
|
||||||
|
U: CharLength + Into<T::Inner>,
|
||||||
|
{
|
||||||
|
}
|
70
src/data/src/port/base.rs
Normal file
70
src/data/src/port/base.rs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
use super::Validatable;
|
||||||
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use derive_more::{Deref, Into};
|
||||||
|
|
||||||
|
pub trait BaseRepository<C>:
|
||||||
|
super::Crud<C, New = New, Unique = u64, Update = Field, Existing = Base>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[derive(Deref, Into, Clone, Copy)]
|
||||||
|
// pub struct Id(pub(crate) u64);
|
||||||
|
|
||||||
|
#[derive(Clone, Deref, Into)]
|
||||||
|
pub struct Name(String);
|
||||||
|
impl Validatable for Name {
|
||||||
|
type Inner = String;
|
||||||
|
const MAX_LENGTH: usize = 127;
|
||||||
|
fn encapsulate(value: Self::Inner) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deref, Into)]
|
||||||
|
pub struct Description(Option<String>);
|
||||||
|
impl Validatable for Description {
|
||||||
|
type Inner = Option<String>;
|
||||||
|
const MAX_LENGTH: usize = 510;
|
||||||
|
fn encapsulate(value: Self::Inner) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Field {
|
||||||
|
Name(Name),
|
||||||
|
Description(Description),
|
||||||
|
CreatedAt(DateTime<Utc>),
|
||||||
|
UpdatedAt(DateTime<Utc>),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct New {
|
||||||
|
pub name: Name,
|
||||||
|
pub description: Description,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Base {
|
||||||
|
pub(crate) id: u64,
|
||||||
|
pub(crate) name: String,
|
||||||
|
pub(crate) description: Option<String>,
|
||||||
|
pub(crate) created_at: DateTime<Utc>,
|
||||||
|
pub(crate) updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Base {
|
||||||
|
pub const fn id(&self) -> u64 {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
pub const fn name(&self) -> &String {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
pub const fn description(&self) -> Option<&String> {
|
||||||
|
self.description.as_ref()
|
||||||
|
}
|
||||||
|
pub const fn created_at(&self) -> DateTime<Utc> {
|
||||||
|
self.created_at
|
||||||
|
}
|
||||||
|
pub const fn updated_at(&self) -> DateTime<Utc> {
|
||||||
|
self.updated_at
|
||||||
|
}
|
||||||
|
}
|
117
src/data/src/port/package.rs
Normal file
117
src/data/src/port/package.rs
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
use super::Validatable;
|
||||||
|
use crate::Base;
|
||||||
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use derive_more::{Deref, Into};
|
||||||
|
|
||||||
|
pub trait PackageRepository<C>:
|
||||||
|
super::Crud<C, New = New, Update = Field, Unique = Unique, Existing = Package>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deref, Into)]
|
||||||
|
pub struct Name(String);
|
||||||
|
impl Validatable for Name {
|
||||||
|
type Inner = String;
|
||||||
|
const MAX_LENGTH: usize = 127;
|
||||||
|
fn encapsulate(value: Self::Inner) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deref, Into)]
|
||||||
|
pub struct Version(String);
|
||||||
|
impl Validatable for Version {
|
||||||
|
type Inner = String;
|
||||||
|
const MAX_LENGTH: usize = 127;
|
||||||
|
fn encapsulate(value: Self::Inner) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deref, Into)]
|
||||||
|
pub struct Description(Option<String>);
|
||||||
|
impl Validatable for Description {
|
||||||
|
type Inner = Option<String>;
|
||||||
|
const MAX_LENGTH: usize = 255;
|
||||||
|
fn encapsulate(value: Self::Inner) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deref, Into)]
|
||||||
|
pub struct Url(Option<String>);
|
||||||
|
impl Validatable for Url {
|
||||||
|
type Inner = Option<String>;
|
||||||
|
const MAX_LENGTH: usize = 510;
|
||||||
|
fn encapsulate(value: Self::Inner) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Unique {
|
||||||
|
Id(u64),
|
||||||
|
Name(Name),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Field {
|
||||||
|
PackageBase(Base),
|
||||||
|
Name(Name),
|
||||||
|
Version(Version),
|
||||||
|
Description(Description),
|
||||||
|
Url(Url),
|
||||||
|
FlaggedAt(Option<DateTime<Utc>>),
|
||||||
|
CreatedAt(DateTime<Utc>),
|
||||||
|
UpdatedAt(DateTime<Utc>),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct New {
|
||||||
|
pub package_base: Base,
|
||||||
|
pub name: Name,
|
||||||
|
pub version: Version,
|
||||||
|
pub description: Description,
|
||||||
|
pub url: Url,
|
||||||
|
pub flagged_at: Option<DateTime<Utc>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Package {
|
||||||
|
pub(crate) id: u64,
|
||||||
|
pub(crate) base: u64,
|
||||||
|
pub(crate) name: String,
|
||||||
|
pub(crate) version: String,
|
||||||
|
pub(crate) description: Option<String>,
|
||||||
|
pub(crate) url: Option<String>,
|
||||||
|
pub(crate) flagged_at: Option<DateTime<Utc>>,
|
||||||
|
pub(crate) created_at: DateTime<Utc>,
|
||||||
|
pub(crate) updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Package {
|
||||||
|
pub const fn id(&self) -> u64 {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
pub const fn package_base(&self) -> u64 {
|
||||||
|
self.base
|
||||||
|
}
|
||||||
|
pub const fn name(&self) -> &String {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
pub const fn version(&self) -> &String {
|
||||||
|
&self.version
|
||||||
|
}
|
||||||
|
pub const fn description(&self) -> Option<&String> {
|
||||||
|
self.description.as_ref()
|
||||||
|
}
|
||||||
|
pub const fn url(&self) -> Option<&String> {
|
||||||
|
self.url.as_ref()
|
||||||
|
}
|
||||||
|
pub const fn flagged_at(&self) -> Option<DateTime<Utc>> {
|
||||||
|
self.flagged_at
|
||||||
|
}
|
||||||
|
pub const fn created_at(&self) -> DateTime<Utc> {
|
||||||
|
self.created_at
|
||||||
|
}
|
||||||
|
pub const fn updated_at(&self) -> DateTime<Utc> {
|
||||||
|
self.updated_at
|
||||||
|
}
|
||||||
|
}
|
69
src/data/src/port/search.rs
Normal file
69
src/data/src/port/search.rs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
use super::Validatable;
|
||||||
|
use crate::Result;
|
||||||
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use derive_more::{Deref, Into};
|
||||||
|
|
||||||
|
pub trait SearchRepository<C> {
|
||||||
|
fn search(connection: &C, data: Data) -> impl Future<Output = Result<Vec<Entry>>> + Send;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deref, Into)]
|
||||||
|
pub struct Search(String);
|
||||||
|
impl Validatable for Search {
|
||||||
|
type Inner = String;
|
||||||
|
const MAX_LENGTH: usize = 255;
|
||||||
|
fn encapsulate(value: Self::Inner) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Data {
|
||||||
|
pub mode: Mode,
|
||||||
|
pub order: Order,
|
||||||
|
pub search: Search,
|
||||||
|
|
||||||
|
pub limit: u16,
|
||||||
|
pub exact: bool,
|
||||||
|
pub ascending: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||||
|
pub struct Entry {
|
||||||
|
pub id: u64,
|
||||||
|
pub name: Box<str>,
|
||||||
|
pub version: Box<str>,
|
||||||
|
pub base_id: u64,
|
||||||
|
pub base_name: Box<str>,
|
||||||
|
pub url: Option<Box<str>>,
|
||||||
|
pub description: Box<str>,
|
||||||
|
// pub submitter_id: u64,
|
||||||
|
// pub submitter_name: Box<str>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Mode {
|
||||||
|
Url,
|
||||||
|
Name,
|
||||||
|
PackageBase,
|
||||||
|
Description,
|
||||||
|
BaseDescription,
|
||||||
|
NameAndDescription,
|
||||||
|
User,
|
||||||
|
Flagger,
|
||||||
|
Packager,
|
||||||
|
Submitter,
|
||||||
|
Maintainer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Order {
|
||||||
|
Name,
|
||||||
|
Version,
|
||||||
|
BaseName,
|
||||||
|
// Submitter,
|
||||||
|
UpdatedAt,
|
||||||
|
CreatedAt,
|
||||||
|
}
|
96
src/data/src/port/user.rs
Normal file
96
src/data/src/port/user.rs
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
use super::Validatable;
|
||||||
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use derive_more::{Deref, Into};
|
||||||
|
|
||||||
|
pub trait UserRepository<C>:
|
||||||
|
super::Crud<C, New = New, Update = Field, Unique = Unique, Existing = User>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deref, Into)]
|
||||||
|
pub struct Name(String);
|
||||||
|
impl Validatable for Name {
|
||||||
|
type Inner = String;
|
||||||
|
const MAX_LENGTH: usize = 31;
|
||||||
|
fn encapsulate(value: Self::Inner) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deref, Into)]
|
||||||
|
pub struct Email(String);
|
||||||
|
impl Validatable for Email {
|
||||||
|
type Inner = String;
|
||||||
|
const MAX_LENGTH: usize = 255;
|
||||||
|
fn encapsulate(value: Self::Inner) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deref, Into)]
|
||||||
|
pub struct Password(String);
|
||||||
|
impl Validatable for Password {
|
||||||
|
type Inner = String;
|
||||||
|
const MAX_LENGTH: usize = 255;
|
||||||
|
fn encapsulate(value: Self::Inner) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Unique {
|
||||||
|
Id(u64),
|
||||||
|
Name(Name),
|
||||||
|
Email(Email),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Field {
|
||||||
|
Name(Name),
|
||||||
|
Email(Email),
|
||||||
|
Password(Password),
|
||||||
|
LastUsed(Option<DateTime<Utc>>),
|
||||||
|
CreatedAt(DateTime<Utc>),
|
||||||
|
UpdatedAt(DateTime<Utc>),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct New {
|
||||||
|
pub name: Name,
|
||||||
|
pub email: Email,
|
||||||
|
pub password: Password,
|
||||||
|
pub last_used: Option<DateTime<Utc>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct User {
|
||||||
|
pub(crate) id: u64,
|
||||||
|
pub(crate) name: String,
|
||||||
|
pub(crate) email: String,
|
||||||
|
pub(crate) password: String,
|
||||||
|
pub(crate) last_used: Option<DateTime<Utc>>,
|
||||||
|
pub(crate) created_at: DateTime<Utc>,
|
||||||
|
pub(crate) updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
pub const fn id(&self) -> u64 {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
pub const fn name(&self) -> &String {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
pub const fn email(&self) -> &String {
|
||||||
|
&self.email
|
||||||
|
}
|
||||||
|
pub const fn password(&self) -> &String {
|
||||||
|
&self.password
|
||||||
|
}
|
||||||
|
pub const fn last_used(&self) -> Option<DateTime<Utc>> {
|
||||||
|
self.last_used
|
||||||
|
}
|
||||||
|
pub const fn created_at(&self) -> DateTime<Utc> {
|
||||||
|
self.created_at
|
||||||
|
}
|
||||||
|
pub const fn updated_at(&self) -> DateTime<Utc> {
|
||||||
|
self.updated_at
|
||||||
|
}
|
||||||
|
}
|
13
src/service/Cargo.toml
Normal file
13
src/service/Cargo.toml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "service"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
thiserror = "2.0.11"
|
||||||
|
argon2 = { version = "0.5.3", features = ["std"] }
|
||||||
|
garde = { version = "0.22.0", features = ["email", "url", "derive"] }
|
||||||
|
derive_more = { version = "2.0.1", features = ["deref", "deref_mut", "into"] }
|
||||||
|
|
||||||
|
[dependencies.data]
|
||||||
|
path = "../data"
|
9
src/service/src/authentication.rs
Normal file
9
src/service/src/authentication.rs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
pub mod adapter;
|
||||||
|
pub mod contract;
|
||||||
|
pub mod repository;
|
||||||
|
pub mod service;
|
||||||
|
|
||||||
|
pub use adapter::*;
|
||||||
|
pub use contract::*;
|
||||||
|
pub use repository::*;
|
||||||
|
pub use service::*;
|
71
src/service/src/authentication/adapter.rs
Normal file
71
src/service/src/authentication/adapter.rs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
use super::{Authenticated, AuthenticationRepository, Get};
|
||||||
|
use data::user::{Field, New, User, UserRepository};
|
||||||
|
use data::{Connect, Result};
|
||||||
|
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
|
pub struct AuthenticationAdapter<D, C, UR>
|
||||||
|
where
|
||||||
|
C: Send,
|
||||||
|
D: Connect<Connection = C> + Sync,
|
||||||
|
UR: UserRepository<C> + Sync,
|
||||||
|
{
|
||||||
|
driver: D,
|
||||||
|
// connection: Option<C>,
|
||||||
|
_user_repository: PhantomData<UR>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D, C, UR> AuthenticationAdapter<D, C, UR>
|
||||||
|
where
|
||||||
|
C: Send,
|
||||||
|
D: Connect<Connection = C> + Sync,
|
||||||
|
UR: UserRepository<C> + Sync,
|
||||||
|
{
|
||||||
|
pub const fn new(driver: D) -> Self {
|
||||||
|
Self {
|
||||||
|
driver,
|
||||||
|
// connection: None,
|
||||||
|
_user_repository: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// async fn connection(&mut self) -> Result<&C> {
|
||||||
|
// if let Some(ref c) = self.connection {
|
||||||
|
// Ok(c)
|
||||||
|
// } else {
|
||||||
|
// self.connection = Some(self.driver.open_connection().await?);
|
||||||
|
// self.connection().await
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D, C, UR> AuthenticationRepository for AuthenticationAdapter<D, C, UR>
|
||||||
|
where
|
||||||
|
C: Send, //+ Sync,
|
||||||
|
D: Connect<Connection = C> + Sync,
|
||||||
|
UR: UserRepository<C> + Sync,
|
||||||
|
{
|
||||||
|
async fn get_user(&self, get: Get) -> Result<Option<User>> {
|
||||||
|
let c = self.driver.open_connection().await?;
|
||||||
|
let user = UR::read(&c, get.into()).await?;
|
||||||
|
D::close_connection(c).await?;
|
||||||
|
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_user(&self, new: New) -> Result<User> {
|
||||||
|
let mut c = self.driver.open_connection().await?;
|
||||||
|
let user = UR::create(&mut c, new).await?;
|
||||||
|
D::close_connection(c).await?;
|
||||||
|
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn start_session(&self, mut user: User) -> Result<Authenticated> {
|
||||||
|
let mut c = self.driver.open_connection().await?;
|
||||||
|
UR::update(&mut c, &mut user, Field::LastUsed(Some(data::Utc::now()))).await?;
|
||||||
|
D::close_connection(c).await?;
|
||||||
|
|
||||||
|
Ok(Authenticated(user))
|
||||||
|
}
|
||||||
|
}
|
155
src/service/src/authentication/contract.rs
Normal file
155
src/service/src/authentication/contract.rs
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
use super::Authenticated;
|
||||||
|
pub use data::Validation;
|
||||||
|
use data::{BoxDynError, user};
|
||||||
|
|
||||||
|
use derive_more::{Deref, Into};
|
||||||
|
use garde::Validate;
|
||||||
|
|
||||||
|
pub type Result<T = (), E = Error> = std::result::Result<T, E>;
|
||||||
|
|
||||||
|
pub trait AuthenticationContract: Send {
|
||||||
|
fn name_available(&self, name: Name) -> impl Future<Output = Result> + Send;
|
||||||
|
fn email_available(&self, email: Email) -> impl Future<Output = Result> + Send;
|
||||||
|
|
||||||
|
fn login(&self, data: LoginData) -> impl Future<Output = Result<Authenticated>> + Send;
|
||||||
|
fn register(
|
||||||
|
&mut self,
|
||||||
|
data: RegisterData,
|
||||||
|
) -> impl Future<Output = Result<Authenticated>> + Send;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LoginData {
|
||||||
|
pub login: Login,
|
||||||
|
pub password: Password,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RegisterData {
|
||||||
|
pub name: Name,
|
||||||
|
pub email: Email,
|
||||||
|
pub password: Password,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
// Login
|
||||||
|
#[error("login was not found")]
|
||||||
|
LoginNotFound,
|
||||||
|
#[error("incorrect password")]
|
||||||
|
IncorrectPassword,
|
||||||
|
// Register
|
||||||
|
#[error("username is taken")]
|
||||||
|
NameExists,
|
||||||
|
#[error("email is already in use")]
|
||||||
|
EmailExists,
|
||||||
|
// Shared
|
||||||
|
#[error("invalid password: {0}")]
|
||||||
|
InvalidPassword(data::BoxDynError),
|
||||||
|
#[error("data source error: {0}")]
|
||||||
|
Repository(data::BoxDynError),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Other(data::BoxDynError),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type ReturnError<T = String> = (T, BoxDynError);
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum Login {
|
||||||
|
Name(Name),
|
||||||
|
Email(Email),
|
||||||
|
}
|
||||||
|
impl AsRef<str> for Login {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::Name(name) => name.as_ref(),
|
||||||
|
Self::Email(email) => email.as_ref(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl TryFrom<String> for Login {
|
||||||
|
type Error = ReturnError;
|
||||||
|
|
||||||
|
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||||
|
let value = match Email::try_from(value) {
|
||||||
|
Ok(x) => return Ok(Self::Email(x)),
|
||||||
|
Err((v, _)) => v,
|
||||||
|
};
|
||||||
|
match Name::try_from(value) {
|
||||||
|
Ok(x) => Ok(Self::Name(x)),
|
||||||
|
Err((v, _)) => Err((v, "login is invalid".into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<Login> for String {
|
||||||
|
fn from(val: Login) -> Self {
|
||||||
|
match val {
|
||||||
|
Login::Name(name) => name.0.into(),
|
||||||
|
Login::Email(email) => email.0.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deref, Into)]
|
||||||
|
pub struct Name(user::Name);
|
||||||
|
impl AsRef<str> for Name {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
self.0.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl TryFrom<String> for Name {
|
||||||
|
type Error = ReturnError;
|
||||||
|
|
||||||
|
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||||
|
#[derive(Validate)]
|
||||||
|
#[garde(transparent)]
|
||||||
|
struct Username<'a>(#[garde(alphanumeric, length(chars, min = 2, max = 31))] &'a str);
|
||||||
|
|
||||||
|
match Username(value.as_str()).validate() {
|
||||||
|
Ok(()) => (),
|
||||||
|
Err(e) => return Err((value, e.into())),
|
||||||
|
}
|
||||||
|
Ok(Self(user::Name::new(value)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deref, Into)]
|
||||||
|
pub struct Email(user::Email);
|
||||||
|
impl AsRef<str> for Email {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
self.0.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl TryFrom<String> for Email {
|
||||||
|
type Error = ReturnError;
|
||||||
|
|
||||||
|
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||||
|
#[derive(Validate)]
|
||||||
|
#[garde(transparent)]
|
||||||
|
pub struct Email<'a>(#[garde(email, length(chars, max = 255))] &'a str);
|
||||||
|
|
||||||
|
match Email(value.as_str()).validate() {
|
||||||
|
Ok(()) => (),
|
||||||
|
Err(e) => return Err((value, e.into())),
|
||||||
|
}
|
||||||
|
Ok(Self(user::Email::new(value)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deref, Into)]
|
||||||
|
pub struct Password(String);
|
||||||
|
impl AsRef<str> for Password {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
self.0.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl TryFrom<String> for Password {
|
||||||
|
type Error = ReturnError;
|
||||||
|
|
||||||
|
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||||
|
if value.chars().count() > 7 {
|
||||||
|
Ok(Self(value))
|
||||||
|
} else {
|
||||||
|
Err((value, "password must be longer than 7 characters".into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
src/service/src/authentication/repository.rs
Normal file
26
src/service/src/authentication/repository.rs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
use data::Result;
|
||||||
|
use data::user::{Email, Name, New, Unique, User};
|
||||||
|
|
||||||
|
use derive_more::{Deref, DerefMut};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deref, DerefMut)]
|
||||||
|
pub struct Authenticated(pub(super) User);
|
||||||
|
|
||||||
|
pub trait AuthenticationRepository {
|
||||||
|
fn get_user(&self, get: Get) -> impl Future<Output = Result<Option<User>>> + Send;
|
||||||
|
fn create_user(&self, new: New) -> impl Future<Output = Result<User>> + Send;
|
||||||
|
fn start_session(&self, user: User) -> impl Future<Output = Result<Authenticated>> + Send;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Get {
|
||||||
|
Name(Name),
|
||||||
|
Email(Email),
|
||||||
|
}
|
||||||
|
impl From<Get> for Unique {
|
||||||
|
fn from(value: Get) -> Self {
|
||||||
|
match value {
|
||||||
|
Get::Name(s) => Self::Name(s),
|
||||||
|
Get::Email(s) => Self::Email(s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
114
src/service/src/authentication/service.rs
Normal file
114
src/service/src/authentication/service.rs
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
use super::{
|
||||||
|
Authenticated, AuthenticationContract, AuthenticationRepository, Email, Error, Get, Login,
|
||||||
|
LoginData, Name, RegisterData, Result, Validation,
|
||||||
|
};
|
||||||
|
|
||||||
|
use argon2::{
|
||||||
|
Argon2,
|
||||||
|
password_hash::{
|
||||||
|
self, PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
impl From<password_hash::Error> for Error {
|
||||||
|
fn from(error: password_hash::Error) -> Self {
|
||||||
|
match error {
|
||||||
|
password_hash::Error::Password => Self::IncorrectPassword,
|
||||||
|
_ => Self::InvalidPassword(error.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AuthenticationService<R>
|
||||||
|
where
|
||||||
|
R: AuthenticationRepository,
|
||||||
|
{
|
||||||
|
pub(crate) repository: R,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R> AuthenticationService<R>
|
||||||
|
where
|
||||||
|
R: AuthenticationRepository,
|
||||||
|
{
|
||||||
|
pub const fn new(repository: R) -> Self {
|
||||||
|
Self { repository }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R> AuthenticationContract for AuthenticationService<R>
|
||||||
|
where
|
||||||
|
R: AuthenticationRepository + Send + Sync,
|
||||||
|
{
|
||||||
|
async fn name_available(&self, name: Name) -> Result {
|
||||||
|
if self
|
||||||
|
.repository
|
||||||
|
.get_user(Get::Name(name.into()))
|
||||||
|
.await
|
||||||
|
.map_err(Error::Repository)?
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
return Err(Error::NameExists);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn email_available(&self, email: Email) -> Result {
|
||||||
|
if self
|
||||||
|
.repository
|
||||||
|
.get_user(Get::Email(email.into()))
|
||||||
|
.await
|
||||||
|
.map_err(Error::Repository)?
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
return Err(Error::EmailExists);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn login(&self, data: LoginData) -> Result<Authenticated> {
|
||||||
|
let user = match data.login {
|
||||||
|
Login::Name(name) => self.repository.get_user(Get::Name(name.into())),
|
||||||
|
Login::Email(email) => self.repository.get_user(Get::Email(email.into())),
|
||||||
|
}
|
||||||
|
.await
|
||||||
|
.map_err(Error::Repository)?
|
||||||
|
.ok_or(Error::LoginNotFound)?;
|
||||||
|
|
||||||
|
Argon2::default().verify_password(
|
||||||
|
data.password.as_bytes(),
|
||||||
|
&PasswordHash::new(user.password())?,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
self.repository
|
||||||
|
.start_session(user)
|
||||||
|
.await
|
||||||
|
.map_err(Error::Repository)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn register(&mut self, data: RegisterData) -> Result<Authenticated> {
|
||||||
|
self.name_available(data.name.clone()).await?;
|
||||||
|
self.email_available(data.email.clone()).await?;
|
||||||
|
|
||||||
|
// Get PHC string ($argon2id$v=19$...)
|
||||||
|
let phc = Argon2::default()
|
||||||
|
.hash_password(data.password.as_bytes(), &SaltString::generate(&mut OsRng))?
|
||||||
|
.to_string();
|
||||||
|
let password = data::user::Password::new(phc)
|
||||||
|
.map_err(|(_, e)| Error::InvalidPassword(e))?;
|
||||||
|
|
||||||
|
let user = self
|
||||||
|
.repository
|
||||||
|
.create_user(data::user::New {
|
||||||
|
name: data.name.into(),
|
||||||
|
email: data.email.into(),
|
||||||
|
password,
|
||||||
|
last_used: None,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(Error::Repository)?;
|
||||||
|
|
||||||
|
self.repository
|
||||||
|
.start_session(user)
|
||||||
|
.await
|
||||||
|
.map_err(Error::Repository)
|
||||||
|
}
|
||||||
|
}
|
8
src/service/src/lib.rs
Normal file
8
src/service/src/lib.rs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
pub mod authentication;
|
||||||
|
pub mod search;
|
||||||
|
|
||||||
|
pub use authentication::{
|
||||||
|
Authenticated, AuthenticationAdapter, AuthenticationContract, AuthenticationRepository,
|
||||||
|
AuthenticationService,
|
||||||
|
};
|
||||||
|
pub use search::{Search, SearchAdapter, SearchContract, SearchRepository, SearchService};
|
9
src/service/src/search.rs
Normal file
9
src/service/src/search.rs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
pub mod adapter;
|
||||||
|
pub mod contract;
|
||||||
|
pub mod repository;
|
||||||
|
pub mod service;
|
||||||
|
|
||||||
|
pub use adapter::*;
|
||||||
|
pub use contract::*;
|
||||||
|
pub use repository::*;
|
||||||
|
pub use service::*;
|
43
src/service/src/search/adapter.rs
Normal file
43
src/service/src/search/adapter.rs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
use data::search::*;
|
||||||
|
use data::{Connect, Result};
|
||||||
|
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
|
pub struct SearchAdapter<D, C, UR>
|
||||||
|
where
|
||||||
|
C: Send,
|
||||||
|
D: Connect<Connection = C> + Sync,
|
||||||
|
UR: SearchRepository<C> + Sync,
|
||||||
|
{
|
||||||
|
driver: D,
|
||||||
|
_search_repository: PhantomData<UR>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D, C, UR> SearchAdapter<D, C, UR>
|
||||||
|
where
|
||||||
|
C: Send,
|
||||||
|
D: Connect<Connection = C> + Sync,
|
||||||
|
UR: SearchRepository<C> + Sync,
|
||||||
|
{
|
||||||
|
pub const fn new(driver: D) -> Self {
|
||||||
|
Self {
|
||||||
|
driver,
|
||||||
|
_search_repository: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D, C, SR> super::SearchRepository for SearchAdapter<D, C, SR>
|
||||||
|
where
|
||||||
|
C: Send, //+ Sync,
|
||||||
|
D: Connect<Connection = C> + Sync,
|
||||||
|
SR: SearchRepository<C> + Sync,
|
||||||
|
{
|
||||||
|
async fn search(&self, data: Data) -> Result<Vec<Entry>> {
|
||||||
|
let c = self.driver.open_connection().await?;
|
||||||
|
let result = SR::search(&c, data).await?;
|
||||||
|
D::close_connection(c).await?;
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
60
src/service/src/search/contract.rs
Normal file
60
src/service/src/search/contract.rs
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
use data::{BoxDynError, search};
|
||||||
|
pub use data::{
|
||||||
|
Result, Validation,
|
||||||
|
search::{Mode, Order, Entry},
|
||||||
|
};
|
||||||
|
|
||||||
|
use derive_more::{Deref, Into};
|
||||||
|
use garde::Validate;
|
||||||
|
|
||||||
|
pub trait SearchContract: Send {
|
||||||
|
fn search(&self, data: Data) -> impl Future<Output = Result<Vec<Entry>>> + Send;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Data {
|
||||||
|
pub mode: Mode,
|
||||||
|
pub order: Order,
|
||||||
|
pub search: Search,
|
||||||
|
|
||||||
|
pub limit: u16,
|
||||||
|
pub exact: bool,
|
||||||
|
pub ascending: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Data> for search::Data {
|
||||||
|
fn from(value: Data) -> Self {
|
||||||
|
Self {
|
||||||
|
mode: value.mode,
|
||||||
|
order: value.order,
|
||||||
|
search: value.search.into(),
|
||||||
|
limit: value.limit,
|
||||||
|
exact: value.exact,
|
||||||
|
ascending: value.ascending,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type ReturnError<T = String> = (T, BoxDynError);
|
||||||
|
|
||||||
|
#[derive(Clone, Deref, Into)]
|
||||||
|
pub struct Search(search::Search);
|
||||||
|
impl AsRef<str> for Search {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
self.0.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl TryFrom<String> for Search {
|
||||||
|
type Error = ReturnError;
|
||||||
|
|
||||||
|
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||||
|
#[derive(Validate)]
|
||||||
|
#[garde(transparent)]
|
||||||
|
struct Check<'a>(#[garde(ascii, length(chars, min = 1, max = 255))] &'a str);
|
||||||
|
|
||||||
|
match Check(value.as_str()).validate() {
|
||||||
|
Ok(()) => (),
|
||||||
|
Err(e) => return Err((value, e.into())),
|
||||||
|
}
|
||||||
|
Ok(Self(search::Search::new(value)?))
|
||||||
|
}
|
||||||
|
}
|
6
src/service/src/search/repository.rs
Normal file
6
src/service/src/search/repository.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
use data::Result;
|
||||||
|
use data::search::{Data, Entry};
|
||||||
|
|
||||||
|
pub trait SearchRepository {
|
||||||
|
fn search(&self, data: Data) -> impl Future<Output = Result<Vec<Entry>>> + Send;
|
||||||
|
}
|
27
src/service/src/search/service.rs
Normal file
27
src/service/src/search/service.rs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
use super::{Data, Result, SearchContract, SearchRepository};
|
||||||
|
use data::search;
|
||||||
|
|
||||||
|
pub struct SearchService<R>
|
||||||
|
where
|
||||||
|
R: SearchRepository,
|
||||||
|
{
|
||||||
|
pub(crate) repository: R,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R> SearchService<R>
|
||||||
|
where
|
||||||
|
R: SearchRepository,
|
||||||
|
{
|
||||||
|
pub const fn new(repository: R) -> Self {
|
||||||
|
Self { repository }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R> SearchContract for SearchService<R>
|
||||||
|
where
|
||||||
|
R: SearchRepository + Send + Sync,
|
||||||
|
{
|
||||||
|
async fn search(&self, data: Data) -> Result<Vec<search::Entry>> {
|
||||||
|
self.repository.search(data.into()).await
|
||||||
|
}
|
||||||
|
}
|
83
src/src/authentication.rs
Normal file
83
src/src/authentication.rs
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
mod login;
|
||||||
|
mod register;
|
||||||
|
|
||||||
|
use login::Login;
|
||||||
|
use register::Register;
|
||||||
|
|
||||||
|
use service::{Authenticated, AuthenticationContract};
|
||||||
|
|
||||||
|
use iced::{Element, Task, futures::lock::Mutex};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub struct Authentication<S> {
|
||||||
|
login: Login<S>,
|
||||||
|
register: Register<S>,
|
||||||
|
screen: Screen,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Screen {
|
||||||
|
Login,
|
||||||
|
Register,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Message {
|
||||||
|
Login(login::Message),
|
||||||
|
Register(register::Message),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Event {
|
||||||
|
Task(Task<Message>),
|
||||||
|
Authenticated(Authenticated),
|
||||||
|
}
|
||||||
|
impl From<Task<Message>> for Event {
|
||||||
|
fn from(value: Task<Message>) -> Self {
|
||||||
|
Self::Task(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: AuthenticationContract + 'static> Authentication<S> {
|
||||||
|
pub fn new(service: Arc<Mutex<S>>) -> Self {
|
||||||
|
Self {
|
||||||
|
login: Login::new(service.clone()),
|
||||||
|
register: Register::new(service),
|
||||||
|
screen: Screen::Login,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, message: Message) -> Option<Event> {
|
||||||
|
Some(match message {
|
||||||
|
Message::Login(message) => match self.login.update(message)? {
|
||||||
|
login::Event::SwitchToRegister => {
|
||||||
|
self.screen = Screen::Register;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
login::Event::Task(task) => task.map(Message::Login).into(),
|
||||||
|
login::Event::Authenticated(x) => Event::Authenticated(x),
|
||||||
|
},
|
||||||
|
Message::Register(message) => match self.register.update(message)? {
|
||||||
|
register::Event::SwitchToLogin => {
|
||||||
|
self.screen = Screen::Login;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
register::Event::Task(task) => task.map(Message::Register).into(),
|
||||||
|
register::Event::Authenticated(x) => Event::Authenticated(x),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view(&self) -> Element<Message> {
|
||||||
|
match self.screen {
|
||||||
|
Screen::Login => self.login.view().map(Message::Login),
|
||||||
|
Screen::Register => self.register.view().map(Message::Register),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn title(&self) -> String {
|
||||||
|
match self.screen {
|
||||||
|
Screen::Login => self.login.title(),
|
||||||
|
Screen::Register => self.register.title(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
181
src/src/authentication/login.rs
Normal file
181
src/src/authentication/login.rs
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
use crate::input::Input;
|
||||||
|
use crate::widget::centerbox;
|
||||||
|
use service::{
|
||||||
|
Authenticated, AuthenticationContract,
|
||||||
|
authentication::{self, Error, LoginData, Result},
|
||||||
|
};
|
||||||
|
|
||||||
|
use iced::futures::lock::Mutex;
|
||||||
|
use iced::widget::{Space, button, checkbox, column, container, row, text};
|
||||||
|
use iced::{Length, Task, padding};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub struct Login<S> {
|
||||||
|
login: Input<authentication::Login>,
|
||||||
|
password: Input<authentication::Password>,
|
||||||
|
show_password: bool,
|
||||||
|
|
||||||
|
state: State,
|
||||||
|
service: Arc<Mutex<S>>,
|
||||||
|
}
|
||||||
|
enum State {
|
||||||
|
None,
|
||||||
|
Requesting,
|
||||||
|
Success,
|
||||||
|
Error(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Event {
|
||||||
|
SwitchToRegister,
|
||||||
|
Task(Task<Message>),
|
||||||
|
Authenticated(Authenticated),
|
||||||
|
}
|
||||||
|
impl From<Task<Message>> for Event {
|
||||||
|
fn from(value: Task<Message>) -> Self {
|
||||||
|
Self::Task(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Message {
|
||||||
|
LoginChanged(String),
|
||||||
|
PasswordChanged(String),
|
||||||
|
ShowPasswordToggled(bool),
|
||||||
|
|
||||||
|
LoginSubmitted,
|
||||||
|
PasswordSubmitted,
|
||||||
|
|
||||||
|
LoginPressed,
|
||||||
|
RegisterPressed,
|
||||||
|
|
||||||
|
RequestResult(Arc<Result<Authenticated>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: AuthenticationContract + 'static> Login<S> {
|
||||||
|
pub fn new(service: Arc<Mutex<S>>) -> Self {
|
||||||
|
Self {
|
||||||
|
login: Input::new("login_name"),
|
||||||
|
password: Input::new("login_password"),
|
||||||
|
show_password: false,
|
||||||
|
state: State::None,
|
||||||
|
service,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, message: Message) -> Option<Event> {
|
||||||
|
match message {
|
||||||
|
Message::LoginChanged(s) => self.login.update(s),
|
||||||
|
Message::PasswordChanged(s) => self.password.update(s),
|
||||||
|
Message::ShowPasswordToggled(b) => self.show_password = b,
|
||||||
|
|
||||||
|
Message::LoginSubmitted if self.login.critical() => (),
|
||||||
|
Message::LoginSubmitted => return Some(self.password.focus().into()),
|
||||||
|
|
||||||
|
Message::RegisterPressed => return Some(Event::SwitchToRegister),
|
||||||
|
|
||||||
|
Message::LoginPressed | Message::PasswordSubmitted => {
|
||||||
|
let login_data = LoginData {
|
||||||
|
login: match self.login.submit() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(t) => return Some(t.into()),
|
||||||
|
},
|
||||||
|
password: match self.password.submit() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(t) => return Some(t.into()),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
self.state = State::Requesting;
|
||||||
|
let arc = self.service.clone();
|
||||||
|
|
||||||
|
return Some(
|
||||||
|
Task::perform(
|
||||||
|
async move {
|
||||||
|
let Some(service) = arc.try_lock() else {
|
||||||
|
return Err(Error::Other(
|
||||||
|
"other authentication request is being performed".into(),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
service.login(login_data).await
|
||||||
|
},
|
||||||
|
|r| Message::RequestResult(Arc::new(r)),
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Message::RequestResult(r) => match &*r {
|
||||||
|
Ok(a) => {
|
||||||
|
self.state = State::Success;
|
||||||
|
return Some(Event::Authenticated(a.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(e) => {
|
||||||
|
self.state = State::None;
|
||||||
|
match e {
|
||||||
|
Error::LoginNotFound => self.login.set_warning(e),
|
||||||
|
Error::IncorrectPassword => self.password.set_warning(e),
|
||||||
|
Error::InvalidPassword(_) => self.password.set_error(e),
|
||||||
|
|
||||||
|
_ => self.state = State::Error(e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view(&self) -> iced::Element<Message> {
|
||||||
|
centerbox(
|
||||||
|
column![
|
||||||
|
container(text(self.title()).size(20))
|
||||||
|
.center_x(Length::Fill)
|
||||||
|
.padding(padding::bottom(10)),
|
||||||
|
self.login
|
||||||
|
.view("Email or Username")
|
||||||
|
.on_input(Message::LoginChanged)
|
||||||
|
.on_submit(Message::LoginSubmitted),
|
||||||
|
self.password
|
||||||
|
.view("Password")
|
||||||
|
.on_input(Message::PasswordChanged)
|
||||||
|
.on_submit(Message::PasswordSubmitted)
|
||||||
|
.secure(!self.show_password),
|
||||||
|
checkbox("Show password", self.show_password)
|
||||||
|
.on_toggle(Message::ShowPasswordToggled),
|
||||||
|
row![
|
||||||
|
button(text("Register").center().size(18))
|
||||||
|
.on_press(Message::RegisterPressed)
|
||||||
|
.style(button::secondary)
|
||||||
|
.width(Length::FillPortion(3))
|
||||||
|
.padding(10),
|
||||||
|
Space::with_width(Length::FillPortion(2)),
|
||||||
|
button(text("Login").center().size(18))
|
||||||
|
.on_press(Message::LoginPressed)
|
||||||
|
.style(button::primary)
|
||||||
|
.width(Length::FillPortion(3))
|
||||||
|
.padding(10)
|
||||||
|
]
|
||||||
|
.padding(padding::top(15)),
|
||||||
|
]
|
||||||
|
.width(Length::Fixed(250.))
|
||||||
|
.spacing(20),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn title(&self) -> String {
|
||||||
|
let errors = [
|
||||||
|
self.login.error(),
|
||||||
|
self.password.error(),
|
||||||
|
self.login.warning(),
|
||||||
|
self.password.warning(),
|
||||||
|
];
|
||||||
|
let error = errors.into_iter().flatten().next();
|
||||||
|
|
||||||
|
match &self.state {
|
||||||
|
State::None => error.map_or_else(|| "Login".into(), Into::into),
|
||||||
|
State::Success => "Success".into(),
|
||||||
|
State::Requesting => "Requesting...".into(),
|
||||||
|
State::Error(e) => e.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
234
src/src/authentication/register.rs
Normal file
234
src/src/authentication/register.rs
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
use crate::input::{self, Input, Value};
|
||||||
|
use crate::widget::centerbox;
|
||||||
|
use service::authentication::{self, Email, Name, Password, RegisterData};
|
||||||
|
use service::{
|
||||||
|
Authenticated, AuthenticationContract,
|
||||||
|
authentication::{Error, LoginData, Result},
|
||||||
|
};
|
||||||
|
|
||||||
|
use iced::futures::lock::Mutex;
|
||||||
|
use iced::widget::{Space, button, checkbox, column, container, row, text};
|
||||||
|
use iced::{Length, Task, padding};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub struct Register<S> {
|
||||||
|
name: Input<Name>,
|
||||||
|
email: Input<Email>,
|
||||||
|
password: Input<Password>,
|
||||||
|
repeat: Input<String>,
|
||||||
|
show_password: bool,
|
||||||
|
|
||||||
|
state: State,
|
||||||
|
service: Arc<Mutex<S>>,
|
||||||
|
}
|
||||||
|
enum State {
|
||||||
|
None,
|
||||||
|
Success,
|
||||||
|
Requesting,
|
||||||
|
Error(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Event {
|
||||||
|
SwitchToLogin,
|
||||||
|
Task(Task<Message>),
|
||||||
|
Authenticated(Authenticated),
|
||||||
|
}
|
||||||
|
impl From<Task<Message>> for Event {
|
||||||
|
fn from(value: Task<Message>) -> Self {
|
||||||
|
Self::Task(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Message {
|
||||||
|
NameChanged(String),
|
||||||
|
EmailChanged(String),
|
||||||
|
PasswordChanged(String),
|
||||||
|
RepeatChanged(String),
|
||||||
|
ShowPasswordToggled(bool),
|
||||||
|
|
||||||
|
EmailSubmitted,
|
||||||
|
NameSubmitted,
|
||||||
|
PasswordSubmitted,
|
||||||
|
RepeatSubmitted,
|
||||||
|
|
||||||
|
RegisterPressed,
|
||||||
|
LoginPressed,
|
||||||
|
|
||||||
|
RequestResult(Arc<Result<Authenticated>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: AuthenticationContract + 'static> Register<S> {
|
||||||
|
pub fn new(service: Arc<Mutex<S>>) -> Self {
|
||||||
|
Self {
|
||||||
|
name: Input::new("register_name"),
|
||||||
|
email: Input::new("register_email"),
|
||||||
|
password: Input::new("register_password"),
|
||||||
|
repeat: Input::new("register_repeat"),
|
||||||
|
show_password: false,
|
||||||
|
|
||||||
|
state: State::None,
|
||||||
|
service,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_passwords(&mut self) {
|
||||||
|
if self.password.as_ref() == self.repeat.as_ref() {
|
||||||
|
self.repeat
|
||||||
|
.set_value(Value::Valid(self.repeat.as_ref().to_string()));
|
||||||
|
} else {
|
||||||
|
self.repeat.set_error(&"passwords are different");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, message: Message) -> Option<Event> {
|
||||||
|
match message {
|
||||||
|
Message::NameChanged(s) => self.name.update(s),
|
||||||
|
Message::EmailChanged(s) => self.email.update(s),
|
||||||
|
Message::PasswordChanged(s) => {
|
||||||
|
self.password.update(s);
|
||||||
|
self.check_passwords();
|
||||||
|
}
|
||||||
|
Message::RepeatChanged(s) => {
|
||||||
|
self.repeat.set_value(Value::Valid(s));
|
||||||
|
self.check_passwords();
|
||||||
|
}
|
||||||
|
Message::ShowPasswordToggled(b) => self.show_password = b,
|
||||||
|
|
||||||
|
Message::NameSubmitted if self.name.critical() => (),
|
||||||
|
Message::NameSubmitted => return Some(self.email.focus().into()),
|
||||||
|
Message::EmailSubmitted if self.email.critical() => (),
|
||||||
|
Message::EmailSubmitted => return Some(self.password.focus().into()),
|
||||||
|
Message::PasswordSubmitted if self.password.critical() => (),
|
||||||
|
Message::PasswordSubmitted => return Some(self.repeat.focus().into()),
|
||||||
|
Message::RegisterPressed | Message::RepeatSubmitted
|
||||||
|
if self.repeat.error().is_some() =>
|
||||||
|
{
|
||||||
|
return Some(self.repeat.focus().into());
|
||||||
|
}
|
||||||
|
Message::RegisterPressed | Message::RepeatSubmitted => {
|
||||||
|
let register_data = RegisterData {
|
||||||
|
name: match self.name.submit() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(t) => return Some(t.into()),
|
||||||
|
},
|
||||||
|
email: match self.email.submit() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(t) => return Some(t.into()),
|
||||||
|
},
|
||||||
|
password: match self.password.submit() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(t) => return Some(t.into()),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
self.state = State::Requesting;
|
||||||
|
let arc = self.service.clone();
|
||||||
|
|
||||||
|
return Some(
|
||||||
|
Task::perform(
|
||||||
|
async move {
|
||||||
|
let Some(mut service) = arc.try_lock() else {
|
||||||
|
return Err(Error::Other(
|
||||||
|
"other authentication request is being performed".into(),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
service.register(register_data).await
|
||||||
|
},
|
||||||
|
|r| Message::RequestResult(Arc::new(r)),
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::LoginPressed => return Some(Event::SwitchToLogin),
|
||||||
|
Message::RequestResult(r) => match &*r {
|
||||||
|
Ok(a) => {
|
||||||
|
self.state = State::Success;
|
||||||
|
return Some(Event::Authenticated(a.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(e) => {
|
||||||
|
self.state = State::None;
|
||||||
|
match e {
|
||||||
|
Error::NameExists => self.name.set_warning(e),
|
||||||
|
Error::EmailExists => self.email.set_warning(e),
|
||||||
|
Error::IncorrectPassword => self.password.set_warning(e),
|
||||||
|
Error::InvalidPassword(_) => self.password.set_error(e),
|
||||||
|
|
||||||
|
_ => self.state = State::Error(e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view(&self) -> iced::Element<Message> {
|
||||||
|
centerbox(
|
||||||
|
column![
|
||||||
|
container(text(self.title()).size(20))
|
||||||
|
.center_x(Length::Fill)
|
||||||
|
.padding(padding::bottom(10)),
|
||||||
|
self.name
|
||||||
|
.view("Username")
|
||||||
|
.on_input(Message::NameChanged)
|
||||||
|
.on_submit(Message::NameSubmitted),
|
||||||
|
self.email
|
||||||
|
.view("Email")
|
||||||
|
.on_input(Message::EmailChanged)
|
||||||
|
.on_submit(Message::EmailSubmitted),
|
||||||
|
self.password
|
||||||
|
.view("Password")
|
||||||
|
.on_input(Message::PasswordChanged)
|
||||||
|
.on_submit(Message::PasswordSubmitted)
|
||||||
|
.secure(!self.show_password),
|
||||||
|
self.repeat
|
||||||
|
.view("Repeat Password")
|
||||||
|
.on_input(Message::RepeatChanged)
|
||||||
|
.on_submit(Message::RepeatSubmitted)
|
||||||
|
.secure(!self.show_password),
|
||||||
|
checkbox("Show password", self.show_password)
|
||||||
|
.on_toggle(Message::ShowPasswordToggled),
|
||||||
|
row![
|
||||||
|
button(text("Login").center().size(18))
|
||||||
|
.on_press(Message::LoginPressed)
|
||||||
|
.style(button::secondary)
|
||||||
|
.width(Length::FillPortion(3))
|
||||||
|
.padding(10),
|
||||||
|
Space::with_width(Length::FillPortion(2)),
|
||||||
|
button(text("Register").center().size(18))
|
||||||
|
.on_press(Message::RegisterPressed)
|
||||||
|
.style(button::primary)
|
||||||
|
.width(Length::FillPortion(3))
|
||||||
|
.padding(10),
|
||||||
|
]
|
||||||
|
.padding(padding::top(15)),
|
||||||
|
]
|
||||||
|
.width(Length::Fixed(250.))
|
||||||
|
.spacing(20),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn title(&self) -> String {
|
||||||
|
let errors = [
|
||||||
|
self.name.error(),
|
||||||
|
self.email.error(),
|
||||||
|
self.password.error(),
|
||||||
|
self.repeat.error(),
|
||||||
|
self.name.warning(),
|
||||||
|
self.email.warning(),
|
||||||
|
self.password.warning(),
|
||||||
|
self.repeat.warning(),
|
||||||
|
];
|
||||||
|
let error = errors.into_iter().flatten().next();
|
||||||
|
|
||||||
|
match &self.state {
|
||||||
|
State::None => error.map_or_else(|| "Register".into(), Into::into),
|
||||||
|
State::Success => "Success".into(),
|
||||||
|
State::Requesting => "Requesting...".into(),
|
||||||
|
State::Error(e) => e.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
171
src/src/input.rs
Normal file
171
src/src/input.rs
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
use crate::widget::text_input::{error, success, warning};
|
||||||
|
|
||||||
|
use iced::widget::{TextInput, text_input, text_input::default};
|
||||||
|
|
||||||
|
/// A smarter [`text_input`].to avoid boilerplate.
|
||||||
|
pub struct Input<T> {
|
||||||
|
id: &'static str,
|
||||||
|
value: Value<T>,
|
||||||
|
warning: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// use std::ops::Deref;
|
||||||
|
// impl<T> Deref for Input<T> {
|
||||||
|
// type Target = Value<T>;
|
||||||
|
// fn deref(&self) -> &Self::Target {
|
||||||
|
// &self.value
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
impl<T: AsRef<str>> AsRef<str> for Input<T> {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
self.value.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Input<T> {
|
||||||
|
pub const fn new(id: &'static str) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
value: Value::None,
|
||||||
|
warning: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focus<Message>(&self) -> iced::Task<Message> {
|
||||||
|
iced::widget::text_input::focus(self.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn warning(&self) -> Option<&str> {
|
||||||
|
self.warning.as_ref().map(AsRef::as_ref)
|
||||||
|
}
|
||||||
|
pub fn error(&self) -> Option<&str> {
|
||||||
|
match &self.value {
|
||||||
|
Value::Invalid { error, .. } => Some(error.as_ref()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_value(&mut self, value: Value<T>) {
|
||||||
|
self.value = value;
|
||||||
|
}
|
||||||
|
pub fn set_warning(&mut self, value: &impl ToString) {
|
||||||
|
self.warning = Some(value.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: AsRef<str>> Input<T> {
|
||||||
|
pub fn set_error(&mut self, value: &impl ToString) {
|
||||||
|
self.value.set_error(value.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view<Message: Clone>(&self, placeholder: &str) -> TextInput<Message> {
|
||||||
|
text_input(placeholder, self.value.as_ref())
|
||||||
|
.id(self.id)
|
||||||
|
.padding(12)
|
||||||
|
.style(match self.value {
|
||||||
|
Value::Invalid { .. } => error,
|
||||||
|
Value::None | Value::Valid(_) if self.warning.is_some() => warning,
|
||||||
|
Value::Valid(_) => success,
|
||||||
|
Value::None => default,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, E> Input<T>
|
||||||
|
where
|
||||||
|
E: ToString,
|
||||||
|
T: TryFrom<String, Error = (String, E)>,
|
||||||
|
{
|
||||||
|
pub fn update(&mut self, value: String) {
|
||||||
|
self.value.update(value);
|
||||||
|
self.warning = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn value(&mut self) -> Result<&T, &str> {
|
||||||
|
match self.value {
|
||||||
|
Value::None => {
|
||||||
|
self.value.update(String::new());
|
||||||
|
self.value()
|
||||||
|
}
|
||||||
|
Value::Valid(ref x) => Ok(x),
|
||||||
|
Value::Invalid { ref error, .. } => Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn submittable(&mut self) -> bool {
|
||||||
|
self.value().is_ok()
|
||||||
|
}
|
||||||
|
pub fn critical(&mut self) -> bool {
|
||||||
|
self.value().is_err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, E> Input<T>
|
||||||
|
where
|
||||||
|
E: ToString,
|
||||||
|
T: TryFrom<String, Error = (String, E)> + Clone,
|
||||||
|
{
|
||||||
|
pub fn submit<Message>(&mut self) -> Result<T, iced::Task<Message>> {
|
||||||
|
match self.value() {
|
||||||
|
Ok(x) => Ok(x.clone()),
|
||||||
|
Err(_) => Err(self.focus()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub enum Value<T> {
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
|
Valid(T),
|
||||||
|
Invalid {
|
||||||
|
value: String,
|
||||||
|
error: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: AsRef<str>> AsRef<str> for Value<T> {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::None => "",
|
||||||
|
Self::Valid(x) => x.as_ref(),
|
||||||
|
Self::Invalid { value, .. } => value.as_ref(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: AsRef<str>> Value<T> {
|
||||||
|
fn set_error(&mut self, error: String) {
|
||||||
|
match self {
|
||||||
|
Self::None => {
|
||||||
|
*self = Self::Invalid {
|
||||||
|
value: String::new(),
|
||||||
|
error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Valid(x) => {
|
||||||
|
*self = Self::Invalid {
|
||||||
|
value: x.as_ref().to_string(),
|
||||||
|
error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Invalid { error: e, .. } => *e = error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, E> Value<T>
|
||||||
|
where
|
||||||
|
E: ToString,
|
||||||
|
T: TryFrom<String, Error = (String, E)>,
|
||||||
|
{
|
||||||
|
fn update(&mut self, value: String) {
|
||||||
|
*self = match T::try_from(value) {
|
||||||
|
Ok(x) => Self::Valid(x),
|
||||||
|
Err((s, e)) => Self::Invalid {
|
||||||
|
value: s,
|
||||||
|
error: e.to_string(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
208
src/src/main.rs
Normal file
208
src/src/main.rs
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
mod authentication;
|
||||||
|
mod input;
|
||||||
|
mod search;
|
||||||
|
//mod statistics;
|
||||||
|
mod widget;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::authentication::Authentication;
|
||||||
|
use crate::search::Search;
|
||||||
|
//use crate::statistics::Statistics;
|
||||||
|
|
||||||
|
use data::{MySqlPool, MySqlSearchAdapter, MySqlUserAdapter, SqlxPool};
|
||||||
|
use iced::{
|
||||||
|
Element, Subscription, Task, Theme,
|
||||||
|
futures::lock::Mutex,
|
||||||
|
widget::{center, row},
|
||||||
|
window,
|
||||||
|
};
|
||||||
|
use service::{
|
||||||
|
Authenticated, AuthenticationAdapter, AuthenticationService, SearchAdapter, SearchService,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Repository {
|
||||||
|
scale_factor: f64,
|
||||||
|
main_id: window::Id,
|
||||||
|
screen: Screen,
|
||||||
|
|
||||||
|
authenticated: Option<Authenticated>,
|
||||||
|
|
||||||
|
search: Search<SearchService<SearchAdapter<MySqlPool, SqlxPool, MySqlSearchAdapter>>>,
|
||||||
|
authentication: Authentication<
|
||||||
|
AuthenticationService<AuthenticationAdapter<MySqlPool, SqlxPool, MySqlUserAdapter>>,
|
||||||
|
>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
enum Screen {
|
||||||
|
Search,
|
||||||
|
// Statistics,
|
||||||
|
#[default]
|
||||||
|
Authentication,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum Message {
|
||||||
|
ScaleUp,
|
||||||
|
ScaleDown,
|
||||||
|
WindowOpened(window::Id),
|
||||||
|
WindowClosed(window::Id),
|
||||||
|
|
||||||
|
Search(search::Message),
|
||||||
|
Authentecation(authentication::Message),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Repository {
|
||||||
|
fn new() -> (Self, Task<Message>) {
|
||||||
|
let (main_id, open_task) = window::open(window::Settings::default());
|
||||||
|
// let (main_window, main_window_task) = MainWindow::new();
|
||||||
|
|
||||||
|
let pool = MySqlPool::new(
|
||||||
|
SqlxPool::connect_lazy(
|
||||||
|
&std::env::var("DATABASE_URL")
|
||||||
|
.expect("environment variable `DATABASE_URL` should be set"),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let auth_service = Arc::new(Mutex::new(AuthenticationService::new(
|
||||||
|
AuthenticationAdapter::new(pool.clone()),
|
||||||
|
)));
|
||||||
|
|
||||||
|
let search_service = Arc::new(Mutex::new(SearchService::new(SearchAdapter::new(
|
||||||
|
pool.clone(),
|
||||||
|
))));
|
||||||
|
|
||||||
|
(
|
||||||
|
Self {
|
||||||
|
main_id,
|
||||||
|
scale_factor: 1.4,
|
||||||
|
screen: Screen::default(),
|
||||||
|
|
||||||
|
authenticated: None,
|
||||||
|
|
||||||
|
search: Search::new(search_service),
|
||||||
|
authentication: Authentication::new(auth_service),
|
||||||
|
},
|
||||||
|
Task::batch([
|
||||||
|
open_task.map(Message::WindowOpened),
|
||||||
|
// main_window_task.map(Message::MainWindow),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, message: Message) -> Task<Message> {
|
||||||
|
match message {
|
||||||
|
Message::ScaleUp => self.scale_factor = (self.scale_factor + 0.2).min(5.0),
|
||||||
|
Message::ScaleDown => self.scale_factor = (self.scale_factor - 0.2).max(0.2),
|
||||||
|
Message::WindowOpened(id) => {
|
||||||
|
log!("Window opened: {id}");
|
||||||
|
return iced::widget::focus_next();
|
||||||
|
}
|
||||||
|
Message::WindowClosed(id) => {
|
||||||
|
log!("Window closed: {id}");
|
||||||
|
if id == self.main_id {
|
||||||
|
return iced::exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::Authentecation(message) => {
|
||||||
|
if let Some(event) = self.authentication.update(message) {
|
||||||
|
match event {
|
||||||
|
authentication::Event::Task(task) => {
|
||||||
|
return task.map(Message::Authentecation);
|
||||||
|
}
|
||||||
|
authentication::Event::Authenticated(authenticated) => {
|
||||||
|
log!("authenticated as {:#?}", *authenticated);
|
||||||
|
self.authenticated = Some(authenticated);
|
||||||
|
self.screen = Screen::Search;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::Search(m) => {
|
||||||
|
if let Some(event) = self.search.update(m) {
|
||||||
|
match event {
|
||||||
|
search::Event::Task(task) => {
|
||||||
|
return task.map(Message::Search);
|
||||||
|
}
|
||||||
|
search::Event::OpenPackage(id) => {
|
||||||
|
log!("opening package {id}");
|
||||||
|
}
|
||||||
|
search::Event::OpenBase(id) => {
|
||||||
|
log!("opening package base {id}");
|
||||||
|
}
|
||||||
|
search::Event::OpenURL(url) => {
|
||||||
|
log!("opening url {url}");
|
||||||
|
match open::that(url.as_ref()) {
|
||||||
|
Ok(()) => {
|
||||||
|
log!("opened url {url}");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log!("can't open url \"{url}\": {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self, id: window::Id) -> Element<Message> {
|
||||||
|
if self.main_id == id {
|
||||||
|
match self.screen {
|
||||||
|
Screen::Search => self.search.view().map(Message::Search),
|
||||||
|
Screen::Authentication => self.authentication.view().map(Message::Authentecation),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
center(row!["This window is unknown.", "It may be closed."]).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title(&self, _: window::Id) -> String {
|
||||||
|
// "Repository".into()
|
||||||
|
match self.screen {
|
||||||
|
Screen::Search => self.search.title(),
|
||||||
|
Screen::Authentication => self.authentication.title(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn subscription(&self) -> Subscription<Message> {
|
||||||
|
use iced::keyboard::{self, Key, Modifiers};
|
||||||
|
|
||||||
|
let hotkeys = keyboard::on_key_press(|key, modifiers| match (modifiers, key) {
|
||||||
|
(Modifiers::CTRL, Key::Character(c)) if c == "-" => Some(Message::ScaleDown),
|
||||||
|
(Modifiers::CTRL, Key::Character(c)) if c == "=" || c == "+" => Some(Message::ScaleUp),
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
|
||||||
|
Subscription::batch([hotkeys, window::close_events().map(Message::WindowClosed)])
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn scale_factor(&self, _: window::Id) -> f64 {
|
||||||
|
self.scale_factor
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn theme(_: &Self, _: window::Id) -> Theme {
|
||||||
|
Theme::TokyoNight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! log {
|
||||||
|
($($arg:tt)*) => {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
println!($($arg)*)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> iced::Result {
|
||||||
|
iced::daemon(Repository::title, Repository::update, Repository::view)
|
||||||
|
.subscription(Repository::subscription)
|
||||||
|
.scale_factor(Repository::scale_factor)
|
||||||
|
.theme(Repository::theme)
|
||||||
|
.run_with(Repository::new)
|
||||||
|
}
|
341
src/src/search.rs
Normal file
341
src/src/search.rs
Normal file
@ -0,0 +1,341 @@
|
|||||||
|
use crate::input::Input;
|
||||||
|
use crate::widget::{scroll, tip, url};
|
||||||
|
|
||||||
|
use iced::Length::Shrink;
|
||||||
|
use service::search::Data;
|
||||||
|
use service::{
|
||||||
|
SearchContract,
|
||||||
|
search::{self, Entry, Result},
|
||||||
|
};
|
||||||
|
|
||||||
|
use iced::widget::{Column, button, checkbox, column, container, lazy, pick_list, row, text};
|
||||||
|
use iced::{Element, Length::Fill, Task, futures::lock::Mutex};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use strum::{Display, VariantArray};
|
||||||
|
|
||||||
|
pub struct Search<S> {
|
||||||
|
input: Input<search::Search>,
|
||||||
|
mode: Mode,
|
||||||
|
order: Order,
|
||||||
|
ascending: bool,
|
||||||
|
exact: bool,
|
||||||
|
limit: u8,
|
||||||
|
|
||||||
|
state: State,
|
||||||
|
service: Arc<Mutex<S>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
enum State {
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
|
Searching,
|
||||||
|
// Aborted,
|
||||||
|
Table(Table),
|
||||||
|
Error(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Display, VariantArray)]
|
||||||
|
pub enum Mode {
|
||||||
|
Url,
|
||||||
|
Name,
|
||||||
|
#[strum(to_string = "Package Base")]
|
||||||
|
PackageBase,
|
||||||
|
Description,
|
||||||
|
#[strum(to_string = "Base description")]
|
||||||
|
BaseDescription,
|
||||||
|
#[default]
|
||||||
|
#[strum(to_string = "Name and Description")]
|
||||||
|
NameAndDescription,
|
||||||
|
User,
|
||||||
|
Flagger,
|
||||||
|
Packager,
|
||||||
|
Submitter,
|
||||||
|
Maintainer,
|
||||||
|
}
|
||||||
|
impl From<Mode> for search::Mode {
|
||||||
|
fn from(value: Mode) -> Self {
|
||||||
|
match value {
|
||||||
|
Mode::Url => Self::Url,
|
||||||
|
Mode::Name => Self::Name,
|
||||||
|
Mode::PackageBase => Self::PackageBase,
|
||||||
|
Mode::Description => Self::Description,
|
||||||
|
Mode::BaseDescription => Self::BaseDescription,
|
||||||
|
Mode::NameAndDescription => Self::NameAndDescription,
|
||||||
|
Mode::User => Self::User,
|
||||||
|
Mode::Flagger => Self::Flagger,
|
||||||
|
Mode::Packager => Self::Packager,
|
||||||
|
Mode::Submitter => Self::Submitter,
|
||||||
|
Mode::Maintainer => Self::Maintainer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Display, VariantArray)]
|
||||||
|
pub enum Order {
|
||||||
|
Name,
|
||||||
|
Version,
|
||||||
|
#[strum(to_string = "Base Name")]
|
||||||
|
BaseName,
|
||||||
|
// Submitter,
|
||||||
|
#[default]
|
||||||
|
#[strum(to_string = "Last update")]
|
||||||
|
UpdatedAt,
|
||||||
|
#[strum(to_string = "Created time")]
|
||||||
|
CreatedAt,
|
||||||
|
}
|
||||||
|
impl From<Order> for search::Order {
|
||||||
|
fn from(value: Order) -> Self {
|
||||||
|
match value {
|
||||||
|
Order::Name => Self::Name,
|
||||||
|
Order::Version => Self::Version,
|
||||||
|
Order::BaseName => Self::BaseName,
|
||||||
|
Order::UpdatedAt => Self::UpdatedAt,
|
||||||
|
Order::CreatedAt => Self::CreatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Message {
|
||||||
|
// Search bar
|
||||||
|
Reset,
|
||||||
|
Search,
|
||||||
|
SearchChanged(String),
|
||||||
|
ModePicked(Mode),
|
||||||
|
OrderPicked(Order),
|
||||||
|
AscendingToggled(bool),
|
||||||
|
ExactToggled(bool),
|
||||||
|
ShowEntriesPicked(u8),
|
||||||
|
// Table
|
||||||
|
PackagePressed(u64),
|
||||||
|
BasePressed(u64),
|
||||||
|
URLPressed(Box<str>),
|
||||||
|
|
||||||
|
RequestResult(Arc<Result<Vec<Entry>>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Event {
|
||||||
|
Task(Task<Message>),
|
||||||
|
OpenPackage(u64),
|
||||||
|
OpenBase(u64),
|
||||||
|
OpenURL(Box<str>),
|
||||||
|
}
|
||||||
|
impl From<Task<Message>> for Event {
|
||||||
|
fn from(value: Task<Message>) -> Self {
|
||||||
|
Self::Task(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Hash)]
|
||||||
|
struct Table(Vec<Entry>);
|
||||||
|
|
||||||
|
impl Table {
|
||||||
|
pub fn view(&self) -> Element<'static, Message> {
|
||||||
|
let mut table: Vec<_> = [
|
||||||
|
"Package", // 0
|
||||||
|
"Version", // 1
|
||||||
|
"Base", // 2
|
||||||
|
"URL", // 3
|
||||||
|
"Description", // 4
|
||||||
|
"Last Updated", // 5
|
||||||
|
"Created", // 6
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| {
|
||||||
|
let mut v = Vec::with_capacity(self.0.len());
|
||||||
|
v.push(s.into());
|
||||||
|
v.push("".into());
|
||||||
|
v
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for entry in &self.0 {
|
||||||
|
table[0].push(url(&entry.name, Message::PackagePressed(entry.id)));
|
||||||
|
table[1].push(text(entry.version.to_string()).into());
|
||||||
|
table[2].push(url(&entry.base_name, Message::BasePressed(entry.base_id)));
|
||||||
|
table[3].push(
|
||||||
|
entry
|
||||||
|
.url
|
||||||
|
.as_ref()
|
||||||
|
.map_or("-".into(), |s|
|
||||||
|
|
||||||
|
tip(
|
||||||
|
url(&"link", Message::URLPressed(s.clone())),
|
||||||
|
s.clone(),
|
||||||
|
tip::Position::Bottom,
|
||||||
|
),
|
||||||
|
|
||||||
|
),
|
||||||
|
);
|
||||||
|
table[4].push(text(entry.description.to_string()).into());
|
||||||
|
table[5].push(text(entry.updated_at.to_string()).into());
|
||||||
|
table[6].push(text(entry.created_at.to_string()).into());
|
||||||
|
// table[5].push(Element::from(column( entry .maintainers .iter() .map(|(id, s)| url(s, Message::UserPressed(*id))),)));
|
||||||
|
}
|
||||||
|
|
||||||
|
scroll(
|
||||||
|
row(table
|
||||||
|
.into_iter()
|
||||||
|
.map(|v| Column::from_vec(v).spacing(5).into()))
|
||||||
|
.spacing(20)
|
||||||
|
.padding(30),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: SearchContract + 'static> Search<S> {
|
||||||
|
pub fn new(service: Arc<Mutex<S>>) -> Self {
|
||||||
|
Self {
|
||||||
|
input: Input::new("search_input"),
|
||||||
|
mode: Mode::NameAndDescription,
|
||||||
|
order: Order::UpdatedAt,
|
||||||
|
ascending: false,
|
||||||
|
exact: false,
|
||||||
|
limit: 25,
|
||||||
|
state: State::default(),
|
||||||
|
service,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view(&self) -> Element<Message> {
|
||||||
|
let search_bar = container(scroll(
|
||||||
|
column![
|
||||||
|
row![
|
||||||
|
self.input
|
||||||
|
.view("Search")
|
||||||
|
.on_input(Message::SearchChanged)
|
||||||
|
.on_submit(Message::Search),
|
||||||
|
tip(
|
||||||
|
button("Go").on_press(Message::Search),
|
||||||
|
"Perform the search",
|
||||||
|
tip::Position::Bottom,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.spacing(10),
|
||||||
|
row![
|
||||||
|
tip(
|
||||||
|
button("Reset").on_press(Message::Reset),
|
||||||
|
"Reset the search bar",
|
||||||
|
tip::Position::Bottom,
|
||||||
|
),
|
||||||
|
tip(
|
||||||
|
pick_list(Mode::VARIANTS, Some(&self.mode), Message::ModePicked),
|
||||||
|
"Search mode",
|
||||||
|
tip::Position::Bottom,
|
||||||
|
),
|
||||||
|
tip(
|
||||||
|
pick_list(Order::VARIANTS, Some(&self.order), Message::OrderPicked),
|
||||||
|
"Field used to sort the results",
|
||||||
|
tip::Position::Bottom,
|
||||||
|
),
|
||||||
|
tip(
|
||||||
|
checkbox("Exact", self.exact).on_toggle(Message::ExactToggled),
|
||||||
|
"Exact search",
|
||||||
|
tip::Position::Bottom,
|
||||||
|
),
|
||||||
|
tip(
|
||||||
|
checkbox("Ascending", self.ascending).on_toggle(Message::AscendingToggled),
|
||||||
|
"Sort order of results",
|
||||||
|
tip::Position::Bottom,
|
||||||
|
),
|
||||||
|
tip(
|
||||||
|
pick_list(
|
||||||
|
[25, 50, 75, 100],
|
||||||
|
Some(self.limit),
|
||||||
|
Message::ShowEntriesPicked
|
||||||
|
),
|
||||||
|
"Number of results to show",
|
||||||
|
tip::Position::Bottom,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
]
|
||||||
|
.padding(20)
|
||||||
|
.width(750)
|
||||||
|
.spacing(10),
|
||||||
|
))
|
||||||
|
.center_x(Fill);
|
||||||
|
|
||||||
|
column![
|
||||||
|
search_bar,
|
||||||
|
match &self.state {
|
||||||
|
State::None => Element::from(""),
|
||||||
|
State::Searching => "Searching...".into(),
|
||||||
|
// State::Aborted => "Aborted".into(),
|
||||||
|
State::Error(e) => text(e).into(),
|
||||||
|
State::Table(table) => container(lazy(table, |t| t.view())).center_x(Fill).into(),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, message: Message) -> Option<Event> {
|
||||||
|
match message {
|
||||||
|
Message::SearchChanged(s) => self.input.update(s),
|
||||||
|
Message::ModePicked(mode) => self.mode = mode,
|
||||||
|
Message::OrderPicked(order) => self.order = order,
|
||||||
|
Message::AscendingToggled(b) => self.ascending = b,
|
||||||
|
Message::ExactToggled(b) => self.exact = b,
|
||||||
|
Message::ShowEntriesPicked(x) => self.limit = x,
|
||||||
|
Message::Reset => {
|
||||||
|
let state = std::mem::take(&mut self.state);
|
||||||
|
*self = Self::new(self.service.clone());
|
||||||
|
self.state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::PackagePressed(id) => return Some(Event::OpenPackage(id)),
|
||||||
|
Message::BasePressed(id) => return Some(Event::OpenBase(id)),
|
||||||
|
Message::URLPressed(url) => return Some(Event::OpenURL(url)),
|
||||||
|
|
||||||
|
Message::Search => {
|
||||||
|
let search_data = Data {
|
||||||
|
mode: self.mode.into(),
|
||||||
|
order: self.order.into(),
|
||||||
|
search: match self.input.submit() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(t) => return Some(t.into()),
|
||||||
|
},
|
||||||
|
limit: self.limit.into(),
|
||||||
|
exact: self.exact,
|
||||||
|
ascending: self.ascending,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.state = State::Searching;
|
||||||
|
let arc = self.service.clone();
|
||||||
|
|
||||||
|
return Some(
|
||||||
|
Task::perform(
|
||||||
|
async move {
|
||||||
|
let Some(service) = arc.try_lock() else {
|
||||||
|
return Err("other search request is being performed".into());
|
||||||
|
};
|
||||||
|
service.search(search_data).await
|
||||||
|
},
|
||||||
|
|r| Message::RequestResult(Arc::new(r)),
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::RequestResult(r) => match &*r {
|
||||||
|
Ok(v) => self.state = State::Table(Table(v.clone())),
|
||||||
|
Err(e) => self.state = State::Error(e.to_string()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn title(&self) -> String {
|
||||||
|
let errors = [self.input.error(), self.input.warning()];
|
||||||
|
let error = errors.into_iter().flatten().next();
|
||||||
|
|
||||||
|
match &self.state {
|
||||||
|
State::None => error.map_or_else(|| "Search".into(), Into::into),
|
||||||
|
State::Searching => "Searching...".into(),
|
||||||
|
State::Table(_) => "Displaying search results".into(),
|
||||||
|
State::Error(e) => e.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
74
src/src/widget.rs
Normal file
74
src/src/widget.rs
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
use iced::widget::{Scrollable, center, container, mouse_area, scrollable, text, tooltip};
|
||||||
|
use iced::{Element, color};
|
||||||
|
|
||||||
|
/// Put content into a dark container at the center of the screen
|
||||||
|
/// which can be scrolled in multiple dirrections
|
||||||
|
pub fn centerbox<'a, Message: 'a>(
|
||||||
|
content: impl Into<Element<'a, Message>>,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
|
center(scroll(
|
||||||
|
container(content).style(container::dark).padding(20),
|
||||||
|
))
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scrollable but in both vertical and horizontal directions
|
||||||
|
pub fn scroll<'a, Message: 'a>(content: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
|
||||||
|
Scrollable::with_direction(
|
||||||
|
content,
|
||||||
|
scrollable::Direction::Both {
|
||||||
|
vertical: scrollable::Scrollbar::default(),
|
||||||
|
horizontal: scrollable::Scrollbar::default(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clickable url
|
||||||
|
pub fn url<'a, Message: Clone + 'a>(txt: &impl ToString, msg: Message) -> Element<'a, Message> {
|
||||||
|
Element::from(mouse_area(text(txt.to_string()).color(color!(0x00_BB_FF))).on_press(msg))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod tip {
|
||||||
|
pub use iced::widget::tooltip::Position;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tooltip with some styling applied
|
||||||
|
pub fn tip<'a, Message: 'a>(
|
||||||
|
content: impl Into<Element<'a, Message>>,
|
||||||
|
tip: impl ToString,
|
||||||
|
position: tip::Position,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
|
tooltip(
|
||||||
|
content,
|
||||||
|
container(text(tip.to_string()).size(14))
|
||||||
|
.padding(5)
|
||||||
|
.style(container::dark),
|
||||||
|
position,
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod text_input {
|
||||||
|
use iced::widget::text_input::{Status, Style, default};
|
||||||
|
use iced::{Theme, color};
|
||||||
|
|
||||||
|
pub fn success(theme: &Theme, status: Status) -> Style {
|
||||||
|
Style {
|
||||||
|
background: color!(0x00_33_00).into(),
|
||||||
|
..default(theme, status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn warning(theme: &Theme, status: Status) -> Style {
|
||||||
|
Style {
|
||||||
|
background: color!(0x33_33_00).into(),
|
||||||
|
..default(theme, status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn error(theme: &Theme, status: Status) -> Style {
|
||||||
|
Style {
|
||||||
|
background: color!(0x33_00_00).into(),
|
||||||
|
..default(theme, status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user