merge main into update-rspc-and-more
20
.github/workflows/ci.yml
vendored
@@ -50,6 +50,22 @@ jobs:
|
||||
|
||||
- name: Perform typechecks
|
||||
run: pnpm typecheck
|
||||
|
||||
rustfmt:
|
||||
name: rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
components: rustfmt
|
||||
|
||||
- name: Run rustfmt
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
build-core:
|
||||
name: Build Core (${{ matrix.platform }})
|
||||
@@ -121,7 +137,7 @@ jobs:
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ./core/src/prisma.rs
|
||||
key: prisma-${{ runner.os }}-${{ hashFiles('./core/prisma/Cargo.toml', './core/prisma/schema.prisma', './core/prisma/src/main.rs') }}
|
||||
key: prisma-${{ runner.os }}-${{ hashFiles('./core/prisma/*', './Cargo.toml') }}
|
||||
|
||||
- name: Generate Prisma client
|
||||
working-directory: core
|
||||
@@ -132,7 +148,7 @@ jobs:
|
||||
run: cargo fetch
|
||||
|
||||
- name: Check Core
|
||||
run: cargo check -p sdcore --release
|
||||
run: cargo check -p sd-core --release
|
||||
|
||||
- name: Bundle Desktop
|
||||
run: pnpm desktop tauri build
|
||||
|
||||
3
.gitignore
vendored
@@ -65,4 +65,5 @@ examples/*/*.lock
|
||||
/core/src/prisma.rs
|
||||
|
||||
/sdserver_data
|
||||
.spacedrive
|
||||
.spacedrive
|
||||
dev.db-journal
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
{
|
||||
"pluginSearchDirs": ["."],
|
||||
"useTabs": true,
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"bracketSameLine": false,
|
||||
"semi": true,
|
||||
"quoteProps": "consistent",
|
||||
"importOrder": ["^@sd/interface/(.*)$", "^@sd/client/(.*)$", "^@sd/ui/(.*)$", "^[./]"],
|
||||
"importOrderSeparation": true,
|
||||
"importOrderSortSpecifiers": true
|
||||
"pluginSearchDirs": [
|
||||
"."
|
||||
],
|
||||
"useTabs": true,
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"bracketSameLine": false,
|
||||
"semi": true,
|
||||
"quoteProps": "consistent",
|
||||
"importOrder": [
|
||||
"^[./]",
|
||||
"^@sd/ui/(.*)$",
|
||||
"^@sd/client/(.*)$",
|
||||
"^@sd/interface/(.*)$"
|
||||
],
|
||||
"importOrderSeparation": true,
|
||||
"importOrderSortSpecifiers": true
|
||||
}
|
||||
|
||||
BIN
Cargo.lock
generated
20
Cargo.toml
@@ -2,16 +2,28 @@
|
||||
members = [
|
||||
"core",
|
||||
"crates/*",
|
||||
"crates/sync/example/src-tauri",
|
||||
"apps/desktop/src-tauri",
|
||||
"apps/mobile/rust",
|
||||
"apps/server",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
prisma-client-rust = { git = "https://github.com/Brendonovich/prisma-client-rust.git", rev = "f5d4a952d8e8bc799a0006dc5f375a22fd64ac27", features = [
|
||||
"rspc",
|
||||
"sqlite-create-many",
|
||||
"migrations",
|
||||
"sqlite",
|
||||
], default-features = false }
|
||||
prisma-client-rust-cli = { git = "https://github.com/Brendonovich/prisma-client-rust.git", rev = "f5d4a952d8e8bc799a0006dc5f375a22fd64ac27", features = [
|
||||
"rspc",
|
||||
"sqlite-create-many",
|
||||
"migrations",
|
||||
"sqlite",
|
||||
], default-features = false }
|
||||
rspc = { version = "0.1.2" }
|
||||
|
||||
[patch.crates-io]
|
||||
# We use this patch so we can compile for the IOS simulator on M1
|
||||
openssl-sys = { git = "https://github.com/spacedriveapp/rust-openssl" }
|
||||
|
||||
[patch."https://github.com/Brendonovich/prisma-client-rust.git"]
|
||||
prisma-client-rust ={ git = "https://github.com//Brendonovich/prisma-client-rust.git", branch = "rspc-0.1.1" }
|
||||
prisma-client-rust-cli = { git = "https://github.com//Brendonovich/prisma-client-rust.git", branch = "rspc-0.1.1" }
|
||||
|
||||
@@ -51,7 +51,9 @@ For independent creatives, hoarders and those that want to own their digital foo
|
||||
<a href="https://instagram.com/spacedriveapp">
|
||||
<img src="https://img.shields.io/badge/Instagram-E4405F?logo=instagram&logoColor=white" />
|
||||
</a>
|
||||
<img src="https://img.shields.io/static/v1?label=Licence&message=GNU%20v3&color=000" />
|
||||
<a href="https://www.gnu.org/licenses/agpl-3.0">
|
||||
<img src="https://img.shields.io/static/v1?label=Licence&message=AGPL%20v3&color=000" />
|
||||
</a>
|
||||
<img src="https://img.shields.io/static/v1?label=Bundled%20Size&message=16.3MB&color=0974B4" />
|
||||
<img src="https://img.shields.io/static/v1?label=Stage&message=Alpha&color=2BB4AB" />
|
||||
<br />
|
||||
|
||||
@@ -11,8 +11,8 @@ build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "1.1.1", features = ["api-all", "macos-private-api"] }
|
||||
rspc = { version = "0.1.2", features = ["tauri"] }
|
||||
sdcore = { path = "../../../core", features = ["ffmpeg"] }
|
||||
rspc = { workspace = true, features = ["tauri"] }
|
||||
sd-core = { path = "../../../core", features = ["ffmpeg"] }
|
||||
tokio = { version = "1.21.2", features = ["sync"] }
|
||||
window-shadows = "0.2.0"
|
||||
tracing = "0.1.36"
|
||||
@@ -25,8 +25,10 @@ swift-rs = { git = "https://github.com/Brendonovich/swift-rs.git", branch = "aut
|
||||
tauri-build = { version = "1.1.1", features = [] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.build-dependencies]
|
||||
swift-rs = { git = "https://github.com/Brendonovich/swift-rs.git", branch = "autorelease", features = ["build"] }
|
||||
swift-rs = { git = "https://github.com/Brendonovich/swift-rs.git", branch = "autorelease", features = [
|
||||
"build",
|
||||
] }
|
||||
|
||||
[features]
|
||||
default = [ "custom-protocol" ]
|
||||
custom-protocol = [ "tauri/custom-protocol" ]
|
||||
default = ["custom-protocol"]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
use std::error::Error;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use sdcore::Node;
|
||||
use sd_core::Node;
|
||||
use tauri::async_runtime::block_on;
|
||||
use tauri::{
|
||||
api::path,
|
||||
|
||||
@@ -56,7 +56,7 @@ export function Footer() {
|
||||
<h3 className="mb-1 text-xs font-bold uppercase ">About</h3>
|
||||
|
||||
<FooterLink link="/team">Team</FooterLink>
|
||||
<FooterLink link="/faq">FAQ</FooterLink>
|
||||
<FooterLink link="/docs/product/resources/faq">FAQ</FooterLink>
|
||||
<FooterLink link="/careers">Careers</FooterLink>
|
||||
<FooterLink link="/changelog">Changelog</FooterLink>
|
||||
<FooterLink link="/blog">Blog</FooterLink>
|
||||
|
||||
@@ -226,7 +226,7 @@ function Page() {
|
||||
lives, at unlimited scale.
|
||||
</p>
|
||||
<a
|
||||
href="/faq"
|
||||
href="/docs/product/resources/faq"
|
||||
className="flex flex-row items-center text-gray-400 duration-150 animation-delay-3 fade-in-heading hover:text-white text-underline underline-offset-4"
|
||||
>
|
||||
<ArrowRight className="mr-2" />
|
||||
|
||||
@@ -10,11 +10,11 @@ crate-type = ["staticlib", "cdylib"] # staticlib for IOS and cdylib for Android
|
||||
|
||||
[dependencies]
|
||||
once_cell = "1.15.0"
|
||||
sdcore = { path = "../../../core", features = [
|
||||
sd-core = { path = "../../../core", features = [
|
||||
"mobile",
|
||||
"p2p",
|
||||
], default-features = false }
|
||||
rspc = { version = "0.1.2", features = [] }
|
||||
rspc = { workspace = true }
|
||||
serde_json = "1.0.85"
|
||||
tokio = "1.21.2"
|
||||
openssl = { version = "0.10.42", features = [
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{EVENT_SENDER, NODE, RUNTIME, SUBSCRIPTIONS};
|
||||
use jni::objects::{JClass, JObject, JString};
|
||||
use jni::JNIEnv;
|
||||
use rspc::internal::jsonrpc::{handle_json_rpc, Request, Sender, SubscriptionMap};
|
||||
use sdcore::Node;
|
||||
use sd_core::Node;
|
||||
use tokio::sync::mpsc::unbounded_channel;
|
||||
|
||||
#[no_mangle]
|
||||
|
||||
@@ -9,7 +9,7 @@ use objc::{class, msg_send, runtime::Object, sel, sel_impl};
|
||||
use objc_foundation::{INSString, NSString};
|
||||
use objc_id::Id;
|
||||
use rspc::Request;
|
||||
use sdcore::Node;
|
||||
use sd_core::Node;
|
||||
|
||||
extern "C" {
|
||||
fn get_data_directory() -> *const c_char;
|
||||
@@ -45,7 +45,7 @@ pub unsafe extern "C" fn register_core_event_listener(id: *mut Object) {
|
||||
Err(err) => {
|
||||
println!("Failed to serialize event: {}", err);
|
||||
continue;
|
||||
},
|
||||
}
|
||||
};
|
||||
let data = NSString::from_str(&data);
|
||||
let _: () = msg_send![id, sendCoreEvent: data];
|
||||
@@ -73,7 +73,7 @@ pub unsafe extern "C" fn sd_core_msg(query: *const c_char, resolve: *const c_voi
|
||||
let new_node = Node::new(doc_dir).await;
|
||||
node.replace(new_node.clone());
|
||||
new_node
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
resolve.resolve(
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use once_cell::sync::{Lazy, OnceCell};
|
||||
use rspc::internal::jsonrpc::{RequestId, Response};
|
||||
use sdcore::{api::Router, Node};
|
||||
use sd_core::{api::Router, Node};
|
||||
use tokio::{
|
||||
runtime::Runtime,
|
||||
sync::{mpsc::UnboundedSender, oneshot, Mutex},
|
||||
|
||||
@@ -14,7 +14,7 @@ const placeholderFileItems: FilePath[] = [
|
||||
date_indexed: '2020-01-01T00:00:00.000Z',
|
||||
date_modified: '2020-01-01T00:00:00.000Z',
|
||||
extension: '',
|
||||
file_id: 1,
|
||||
object_id: 1,
|
||||
id: 1,
|
||||
location_id: 1,
|
||||
materialized_path: '',
|
||||
@@ -28,7 +28,7 @@ const placeholderFileItems: FilePath[] = [
|
||||
date_indexed: '2020-01-01T00:00:00.000Z',
|
||||
date_modified: '2020-01-01T00:00:00.000Z',
|
||||
extension: '',
|
||||
file_id: 2,
|
||||
object_id: 2,
|
||||
id: 2,
|
||||
location_id: 2,
|
||||
materialized_path: '',
|
||||
@@ -42,7 +42,7 @@ const placeholderFileItems: FilePath[] = [
|
||||
date_indexed: '2020-01-01T00:00:00.000Z',
|
||||
date_modified: '2020-01-01T00:00:00.000Z',
|
||||
extension: 'tsx',
|
||||
file_id: 3,
|
||||
object_id: 3,
|
||||
id: 3,
|
||||
location_id: 3,
|
||||
materialized_path: '',
|
||||
@@ -56,7 +56,7 @@ const placeholderFileItems: FilePath[] = [
|
||||
date_indexed: '2020-01-01T00:00:00.000Z',
|
||||
date_modified: '2020-01-01T00:00:00.000Z',
|
||||
extension: 'vite',
|
||||
file_id: 4,
|
||||
object_id: 4,
|
||||
id: 4,
|
||||
location_id: 4,
|
||||
materialized_path: '',
|
||||
@@ -70,7 +70,7 @@ const placeholderFileItems: FilePath[] = [
|
||||
date_indexed: '2020-01-01T00:00:00.000Z',
|
||||
date_modified: '2020-01-01T00:00:00.000Z',
|
||||
extension: 'docker',
|
||||
file_id: 5,
|
||||
object_id: 5,
|
||||
id: 5,
|
||||
location_id: 5,
|
||||
materialized_path: '',
|
||||
@@ -90,7 +90,7 @@ export interface DeviceProps {
|
||||
|
||||
const Device = ({ name, locations, size, type }: DeviceProps) => {
|
||||
return (
|
||||
<View style={tw`bg-gray-600 border rounded-md border-gray-550 my-2`}>
|
||||
<View style={tw`my-2 bg-gray-600 border rounded-md border-gray-550`}>
|
||||
<View style={tw`flex flex-row items-center px-3.5 pt-3 pb-2`}>
|
||||
<View style={tw`flex flex-row items-center`}>
|
||||
{type === 'phone' && (
|
||||
@@ -107,7 +107,7 @@ const Device = ({ name, locations, size, type }: DeviceProps) => {
|
||||
</View>
|
||||
</View>
|
||||
{/* Size */}
|
||||
<Text style={tw`font-semibold text-sm ml-2 text-gray-400`}>{size}</Text>
|
||||
<Text style={tw`ml-2 text-sm font-semibold text-gray-400`}>{size}</Text>
|
||||
</View>
|
||||
{/* Locations/Files TODO: Maybe use FlashList? */}
|
||||
<FlatList
|
||||
|
||||
@@ -16,7 +16,7 @@ export type Procedures = {
|
||||
{ key: "locations.list", input: LibraryArgs<null>, result: Array<{ id: number, pub_id: Array<number>, node_id: number, name: string | null, local_path: string | null, total_capacity: number | null, available_capacity: number | null, filesystem: string | null, disk_type: number | null, is_removable: boolean | null, is_online: boolean, is_archived: boolean, date_created: string, node: Node }> } |
|
||||
{ key: "tags.get", input: LibraryArgs<number>, result: Tag | null } |
|
||||
{ key: "tags.getExplorerData", input: LibraryArgs<number>, result: ExplorerData } |
|
||||
{ key: "tags.getForFile", input: LibraryArgs<number>, result: Array<Tag> } |
|
||||
{ key: "tags.getForObject", input: LibraryArgs<number>, result: Array<Tag> } |
|
||||
{ key: "tags.list", input: LibraryArgs<null>, result: Array<Tag> } |
|
||||
{ key: "version", input: never, result: string } |
|
||||
{ key: "volumes.list", input: never, result: Array<Volume> },
|
||||
@@ -53,11 +53,9 @@ export type ExplorerContext = { type: "Location" } & Location | { type: "Tag" }
|
||||
|
||||
export interface ExplorerData { context: ExplorerContext, items: Array<ExplorerItem> }
|
||||
|
||||
export type ExplorerItem = { type: "Path" } & { id: number, is_dir: boolean, location_id: number, materialized_path: string, name: string, extension: string | null, file_id: number | null, parent_id: number | null, key_id: number | null, date_created: string, date_modified: string, date_indexed: string, file: File | null } | { type: "Object" } & { id: number, cas_id: string, integrity_checksum: string | null, name: string | null, extension: string | null, kind: number, size_in_bytes: string, key_id: number | null, hidden: boolean, favorite: boolean, important: boolean, has_thumbnail: boolean, has_thumbstrip: boolean, has_video_preview: boolean, ipfs_id: string | null, note: string | null, date_created: string, date_modified: string, date_indexed: string, paths: Array<FilePath> }
|
||||
export type ExplorerItem = { type: "Path" } & { id: number, is_dir: boolean, location_id: number, materialized_path: string, name: string, extension: string | null, object_id: number | null, parent_id: number | null, key_id: number | null, date_created: string, date_modified: string, date_indexed: string, object: Object | null } | { type: "Object" } & { id: number, cas_id: string, integrity_checksum: string | null, name: string | null, extension: string | null, kind: number, size_in_bytes: string, key_id: number | null, hidden: boolean, favorite: boolean, important: boolean, has_thumbnail: boolean, has_thumbstrip: boolean, has_video_preview: boolean, ipfs_id: string | null, note: string | null, date_created: string, date_modified: string, date_indexed: string, file_paths: Array<FilePath> }
|
||||
|
||||
export interface File { id: number, cas_id: string, integrity_checksum: string | null, name: string | null, extension: string | null, kind: number, size_in_bytes: string, key_id: number | null, hidden: boolean, favorite: boolean, important: boolean, has_thumbnail: boolean, has_thumbstrip: boolean, has_video_preview: boolean, ipfs_id: string | null, note: string | null, date_created: string, date_modified: string, date_indexed: string }
|
||||
|
||||
export interface FilePath { id: number, is_dir: boolean, location_id: number, materialized_path: string, name: string, extension: string | null, file_id: number | null, parent_id: number | null, key_id: number | null, date_created: string, date_modified: string, date_indexed: string }
|
||||
export interface FilePath { id: number, is_dir: boolean, location_id: number, materialized_path: string, name: string, extension: string | null, object_id: number | null, parent_id: number | null, key_id: number | null, date_created: string, date_modified: string, date_indexed: string }
|
||||
|
||||
export interface GenerateThumbsForLocationArgs { id: number, path: string }
|
||||
|
||||
@@ -93,17 +91,19 @@ export interface NodeConfig { version: string | null, id: string, name: string,
|
||||
|
||||
export interface NodeState { version: string | null, id: string, name: string, p2p_port: number | null, data_path: string }
|
||||
|
||||
export interface Object { id: number, cas_id: string, integrity_checksum: string | null, name: string | null, extension: string | null, kind: number, size_in_bytes: string, key_id: number | null, hidden: boolean, favorite: boolean, important: boolean, has_thumbnail: boolean, has_thumbstrip: boolean, has_video_preview: boolean, ipfs_id: string | null, note: string | null, date_created: string, date_modified: string, date_indexed: string }
|
||||
|
||||
export type RuleKind = "AcceptFilesByGlob" | "RejectFilesByGlob" | "AcceptIfChildrenDirectoriesArePresent" | "RejectIfChildrenDirectoriesArePresent"
|
||||
|
||||
export interface SetFavoriteArgs { id: number, favorite: boolean }
|
||||
|
||||
export interface SetNoteArgs { id: number, note: string | null }
|
||||
|
||||
export interface Statistics { id: number, date_captured: string, total_file_count: number, library_db_size: string, total_bytes_used: string, total_bytes_capacity: string, total_unique_bytes: string, total_bytes_free: string, preview_media_bytes: string }
|
||||
export interface Statistics { id: number, date_captured: string, total_object_count: number, library_db_size: string, total_bytes_used: string, total_bytes_capacity: string, total_unique_bytes: string, total_bytes_free: string, preview_media_bytes: string }
|
||||
|
||||
export interface Tag { id: number, pub_id: Array<number>, name: string | null, color: string | null, total_files: number | null, redundancy_goal: number | null, date_created: string, date_modified: string }
|
||||
export interface Tag { id: number, pub_id: Array<number>, name: string | null, color: string | null, total_objects: number | null, redundancy_goal: number | null, date_created: string, date_modified: string }
|
||||
|
||||
export interface TagAssignArgs { file_id: number, tag_id: number, unassign: boolean }
|
||||
export interface TagAssignArgs { object_id: number, tag_id: number, unassign: boolean }
|
||||
|
||||
export interface TagCreateArgs { name: string, color: string }
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
sdcore = { path = "../../core", features = [] }
|
||||
rspc = { version = "0.1.2", features = ["axum"] }
|
||||
sd-core = { path = "../../core", features = [] }
|
||||
rspc = { workspace = true, features = ["axum"] }
|
||||
axum = "0.5.16"
|
||||
tokio = { version = "1.21.2", features = ["sync", "rt-multi-thread", "signal"] }
|
||||
tracing = "0.1.36"
|
||||
|
||||
@@ -6,7 +6,7 @@ use axum::{
|
||||
http::{header::CONTENT_TYPE, HeaderMap, StatusCode},
|
||||
routing::get,
|
||||
};
|
||||
use sdcore::Node;
|
||||
use sd_core::Node;
|
||||
use tracing::info;
|
||||
|
||||
mod utils;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use sdcore::Node;
|
||||
use sd_core::Node;
|
||||
use tokio::signal;
|
||||
|
||||
/// shutdown_signal will inform axum to gracefully shutdown when the process is asked to shutdown.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "sdcore"
|
||||
name = "sd-core"
|
||||
version = "0.1.0"
|
||||
description = "Virtual distributed filesystem engine that powers Spacedrive."
|
||||
authors = ["Spacedrive Technology Inc."]
|
||||
@@ -16,7 +16,7 @@ mobile = [
|
||||
] # This feature allows features to be disabled when the Core is running on mobile.
|
||||
ffmpeg = [
|
||||
"dep:ffmpeg-next",
|
||||
"dep:sd_ffmpeg",
|
||||
"dep:sd-ffmpeg",
|
||||
] # This feature controls whether the Spacedrive Core contains functionality which requires FFmpeg.
|
||||
|
||||
[dependencies]
|
||||
@@ -35,13 +35,8 @@ rmp = "^0.8.11"
|
||||
rmp-serde = "^1.1.1"
|
||||
|
||||
# Project dependencies
|
||||
prisma-client-rust = { git = "https://github.com/Brendonovich/prisma-client-rust.git", tag = "0.6.0", features = [
|
||||
"rspc",
|
||||
"sqlite-create-many",
|
||||
"migrations",
|
||||
"sqlite",
|
||||
], default-features = false }
|
||||
rspc = { version = "0.1.2", features = ["uuid", "chrono", "tracing"] }
|
||||
rspc = { workspace = true, features = ["uuid", "chrono", "tracing"] }
|
||||
prisma-client-rust = { workspace = true }
|
||||
uuid = { version = "1.1.2", features = ["v4", "serde"] }
|
||||
sysinfo = "0.26.4"
|
||||
thiserror = "1.0.37"
|
||||
@@ -56,7 +51,7 @@ async-trait = "^0.1.57"
|
||||
image = "0.24.4"
|
||||
webp = "0.2.2"
|
||||
ffmpeg-next = { version = "5.1.1", optional = true, features = [] }
|
||||
sd_ffmpeg = { path = "../crates/ffmpeg", optional = true }
|
||||
sd-ffmpeg = { path = "../crates/ffmpeg", optional = true }
|
||||
fs_extra = "1.2.0"
|
||||
tracing = "0.1.36"
|
||||
tracing-subscriber = { version = "0.3.15", features = ["env-filter"] }
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "_migrations" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"name" TEXT NOT NULL,
|
||||
"checksum" TEXT NOT NULL,
|
||||
"steps_applied" INTEGER NOT NULL DEFAULT 0,
|
||||
"applied_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "sync_events" (
|
||||
CREATE TABLE "sync_event" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"node_id" INTEGER NOT NULL,
|
||||
"timestamp" TEXT NOT NULL,
|
||||
@@ -16,14 +7,14 @@ CREATE TABLE "sync_events" (
|
||||
"kind" INTEGER NOT NULL,
|
||||
"column" TEXT,
|
||||
"value" TEXT NOT NULL,
|
||||
CONSTRAINT "sync_events_node_id_fkey" FOREIGN KEY ("node_id") REFERENCES "nodes" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
CONSTRAINT "sync_event_node_id_fkey" FOREIGN KEY ("node_id") REFERENCES "node" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "statistics" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"date_captured" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"total_file_count" INTEGER NOT NULL DEFAULT 0,
|
||||
"total_object_count" INTEGER NOT NULL DEFAULT 0,
|
||||
"library_db_size" TEXT NOT NULL DEFAULT '0',
|
||||
"total_bytes_used" TEXT NOT NULL DEFAULT '0',
|
||||
"total_bytes_capacity" TEXT NOT NULL DEFAULT '0',
|
||||
@@ -33,7 +24,7 @@ CREATE TABLE "statistics" (
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "nodes" (
|
||||
CREATE TABLE "node" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"pub_id" BLOB NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
@@ -45,7 +36,7 @@ CREATE TABLE "nodes" (
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "volumes" (
|
||||
CREATE TABLE "volume" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"node_id" INTEGER NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
@@ -59,7 +50,7 @@ CREATE TABLE "volumes" (
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "locations" (
|
||||
CREATE TABLE "location" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"pub_id" BLOB NOT NULL,
|
||||
"node_id" INTEGER NOT NULL,
|
||||
@@ -73,11 +64,11 @@ CREATE TABLE "locations" (
|
||||
"is_online" BOOLEAN NOT NULL DEFAULT true,
|
||||
"is_archived" BOOLEAN NOT NULL DEFAULT false,
|
||||
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "locations_node_id_fkey" FOREIGN KEY ("node_id") REFERENCES "nodes" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
CONSTRAINT "location_node_id_fkey" FOREIGN KEY ("node_id") REFERENCES "node" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "files" (
|
||||
CREATE TABLE "object" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"cas_id" TEXT NOT NULL,
|
||||
"integrity_checksum" TEXT,
|
||||
@@ -97,18 +88,18 @@ CREATE TABLE "files" (
|
||||
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"date_modified" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"date_indexed" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "files_key_id_fkey" FOREIGN KEY ("key_id") REFERENCES "keys" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
CONSTRAINT "object_key_id_fkey" FOREIGN KEY ("key_id") REFERENCES "key" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "file_paths" (
|
||||
CREATE TABLE "file_path" (
|
||||
"id" INTEGER NOT NULL,
|
||||
"is_dir" BOOLEAN NOT NULL DEFAULT false,
|
||||
"location_id" INTEGER NOT NULL,
|
||||
"materialized_path" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"extension" TEXT,
|
||||
"file_id" INTEGER,
|
||||
"object_id" INTEGER,
|
||||
"parent_id" INTEGER,
|
||||
"key_id" INTEGER,
|
||||
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
@@ -116,19 +107,19 @@ CREATE TABLE "file_paths" (
|
||||
"date_indexed" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
PRIMARY KEY ("location_id", "id"),
|
||||
CONSTRAINT "file_paths_file_id_fkey" FOREIGN KEY ("file_id") REFERENCES "files" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "file_paths_location_id_fkey" FOREIGN KEY ("location_id") REFERENCES "locations" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "file_paths_key_id_fkey" FOREIGN KEY ("key_id") REFERENCES "keys" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
CONSTRAINT "file_path_object_id_fkey" FOREIGN KEY ("object_id") REFERENCES "object" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "file_path_location_id_fkey" FOREIGN KEY ("location_id") REFERENCES "location" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "file_path_key_id_fkey" FOREIGN KEY ("key_id") REFERENCES "key" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "file_conflicts" (
|
||||
"original_file_id" INTEGER NOT NULL,
|
||||
"detactched_file_id" INTEGER NOT NULL
|
||||
CREATE TABLE "file_conflict" (
|
||||
"original_object_id" INTEGER NOT NULL,
|
||||
"detactched_object_id" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "keys" (
|
||||
CREATE TABLE "key" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"checksum" TEXT NOT NULL,
|
||||
"name" TEXT,
|
||||
@@ -150,34 +141,34 @@ CREATE TABLE "media_data" (
|
||||
"duration_seconds" INTEGER,
|
||||
"codecs" TEXT,
|
||||
"streams" INTEGER,
|
||||
CONSTRAINT "media_data_id_fkey" FOREIGN KEY ("id") REFERENCES "files" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
CONSTRAINT "media_data_id_fkey" FOREIGN KEY ("id") REFERENCES "object" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "tags" (
|
||||
CREATE TABLE "tag" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"pub_id" BLOB NOT NULL,
|
||||
"name" TEXT,
|
||||
"color" TEXT,
|
||||
"total_files" INTEGER DEFAULT 0,
|
||||
"total_objects" INTEGER DEFAULT 0,
|
||||
"redundancy_goal" INTEGER DEFAULT 1,
|
||||
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"date_modified" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "tags_on_file" (
|
||||
CREATE TABLE "tag_on_object" (
|
||||
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"tag_id" INTEGER NOT NULL,
|
||||
"file_id" INTEGER NOT NULL,
|
||||
"object_id" INTEGER NOT NULL,
|
||||
|
||||
PRIMARY KEY ("tag_id", "file_id"),
|
||||
CONSTRAINT "tags_on_file_tag_id_fkey" FOREIGN KEY ("tag_id") REFERENCES "tags" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||
CONSTRAINT "tags_on_file_file_id_fkey" FOREIGN KEY ("file_id") REFERENCES "files" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION
|
||||
PRIMARY KEY ("tag_id", "object_id"),
|
||||
CONSTRAINT "tag_on_object_tag_id_fkey" FOREIGN KEY ("tag_id") REFERENCES "tag" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||
CONSTRAINT "tag_on_object_object_id_fkey" FOREIGN KEY ("object_id") REFERENCES "object" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "labels" (
|
||||
CREATE TABLE "label" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"pub_id" BLOB NOT NULL,
|
||||
"name" TEXT,
|
||||
@@ -186,18 +177,18 @@ CREATE TABLE "labels" (
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "label_on_file" (
|
||||
CREATE TABLE "label_on_object" (
|
||||
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"label_id" INTEGER NOT NULL,
|
||||
"file_id" INTEGER NOT NULL,
|
||||
"object_id" INTEGER NOT NULL,
|
||||
|
||||
PRIMARY KEY ("label_id", "file_id"),
|
||||
CONSTRAINT "label_on_file_label_id_fkey" FOREIGN KEY ("label_id") REFERENCES "labels" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||
CONSTRAINT "label_on_file_file_id_fkey" FOREIGN KEY ("file_id") REFERENCES "files" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION
|
||||
PRIMARY KEY ("label_id", "object_id"),
|
||||
CONSTRAINT "label_on_object_label_id_fkey" FOREIGN KEY ("label_id") REFERENCES "label" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||
CONSTRAINT "label_on_object_object_id_fkey" FOREIGN KEY ("object_id") REFERENCES "object" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "spaces" (
|
||||
CREATE TABLE "space" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"pub_id" BLOB NOT NULL,
|
||||
"name" TEXT,
|
||||
@@ -207,18 +198,18 @@ CREATE TABLE "spaces" (
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "file_in_space" (
|
||||
CREATE TABLE "object_in_space" (
|
||||
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"space_id" INTEGER NOT NULL,
|
||||
"file_id" INTEGER NOT NULL,
|
||||
"object_id" INTEGER NOT NULL,
|
||||
|
||||
PRIMARY KEY ("space_id", "file_id"),
|
||||
CONSTRAINT "file_in_space_space_id_fkey" FOREIGN KEY ("space_id") REFERENCES "spaces" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||
CONSTRAINT "file_in_space_file_id_fkey" FOREIGN KEY ("file_id") REFERENCES "files" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION
|
||||
PRIMARY KEY ("space_id", "object_id"),
|
||||
CONSTRAINT "object_in_space_space_id_fkey" FOREIGN KEY ("space_id") REFERENCES "space" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||
CONSTRAINT "object_in_space_object_id_fkey" FOREIGN KEY ("object_id") REFERENCES "object" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "jobs" (
|
||||
CREATE TABLE "job" (
|
||||
"id" BLOB NOT NULL PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
"node_id" INTEGER NOT NULL,
|
||||
@@ -231,11 +222,11 @@ CREATE TABLE "jobs" (
|
||||
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"date_modified" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"seconds_elapsed" INTEGER NOT NULL DEFAULT 0,
|
||||
CONSTRAINT "jobs_node_id_fkey" FOREIGN KEY ("node_id") REFERENCES "nodes" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
CONSTRAINT "job_node_id_fkey" FOREIGN KEY ("node_id") REFERENCES "node" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "albums" (
|
||||
CREATE TABLE "album" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"pub_id" BLOB NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
@@ -245,29 +236,29 @@ CREATE TABLE "albums" (
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "files_in_albums" (
|
||||
CREATE TABLE "object_in_album" (
|
||||
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"album_id" INTEGER NOT NULL,
|
||||
"file_id" INTEGER NOT NULL,
|
||||
"object_id" INTEGER NOT NULL,
|
||||
|
||||
PRIMARY KEY ("album_id", "file_id"),
|
||||
CONSTRAINT "files_in_albums_album_id_fkey" FOREIGN KEY ("album_id") REFERENCES "albums" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||
CONSTRAINT "files_in_albums_file_id_fkey" FOREIGN KEY ("file_id") REFERENCES "files" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION
|
||||
PRIMARY KEY ("album_id", "object_id"),
|
||||
CONSTRAINT "object_in_album_album_id_fkey" FOREIGN KEY ("album_id") REFERENCES "album" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||
CONSTRAINT "object_in_album_object_id_fkey" FOREIGN KEY ("object_id") REFERENCES "object" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "comments" (
|
||||
CREATE TABLE "comment" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"pub_id" BLOB NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"date_modified" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"file_id" INTEGER,
|
||||
CONSTRAINT "comments_file_id_fkey" FOREIGN KEY ("file_id") REFERENCES "files" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
"object_id" INTEGER,
|
||||
CONSTRAINT "comment_object_id_fkey" FOREIGN KEY ("object_id") REFERENCES "object" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "indexer_rules" (
|
||||
CREATE TABLE "indexer_rule" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"kind" INTEGER NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
@@ -277,60 +268,57 @@ CREATE TABLE "indexer_rules" (
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "indexer_rules_in_location" (
|
||||
CREATE TABLE "indexer_rule_in_location" (
|
||||
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"location_id" INTEGER NOT NULL,
|
||||
"indexer_rule_id" INTEGER NOT NULL,
|
||||
|
||||
PRIMARY KEY ("location_id", "indexer_rule_id"),
|
||||
CONSTRAINT "indexer_rules_in_location_location_id_fkey" FOREIGN KEY ("location_id") REFERENCES "locations" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||
CONSTRAINT "indexer_rules_in_location_indexer_rule_id_fkey" FOREIGN KEY ("indexer_rule_id") REFERENCES "indexer_rules" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION
|
||||
CONSTRAINT "indexer_rule_in_location_location_id_fkey" FOREIGN KEY ("location_id") REFERENCES "location" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||
CONSTRAINT "indexer_rule_in_location_indexer_rule_id_fkey" FOREIGN KEY ("indexer_rule_id") REFERENCES "indexer_rule" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_migrations_checksum_key" ON "_migrations"("checksum");
|
||||
CREATE UNIQUE INDEX "node_pub_id_key" ON "node"("pub_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "nodes_pub_id_key" ON "nodes"("pub_id");
|
||||
CREATE UNIQUE INDEX "volume_node_id_mount_point_name_key" ON "volume"("node_id", "mount_point", "name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "volumes_node_id_mount_point_name_key" ON "volumes"("node_id", "mount_point", "name");
|
||||
CREATE UNIQUE INDEX "location_pub_id_key" ON "location"("pub_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "locations_pub_id_key" ON "locations"("pub_id");
|
||||
CREATE UNIQUE INDEX "object_cas_id_key" ON "object"("cas_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "files_cas_id_key" ON "files"("cas_id");
|
||||
CREATE UNIQUE INDEX "object_integrity_checksum_key" ON "object"("integrity_checksum");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "files_integrity_checksum_key" ON "files"("integrity_checksum");
|
||||
CREATE INDEX "file_path_location_id_idx" ON "file_path"("location_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "file_paths_location_id_idx" ON "file_paths"("location_id");
|
||||
CREATE UNIQUE INDEX "file_path_location_id_materialized_path_name_extension_key" ON "file_path"("location_id", "materialized_path", "name", "extension");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "file_paths_location_id_materialized_path_name_extension_key" ON "file_paths"("location_id", "materialized_path", "name", "extension");
|
||||
CREATE UNIQUE INDEX "file_conflict_original_object_id_key" ON "file_conflict"("original_object_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "file_conflicts_original_file_id_key" ON "file_conflicts"("original_file_id");
|
||||
CREATE UNIQUE INDEX "file_conflict_detactched_object_id_key" ON "file_conflict"("detactched_object_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "file_conflicts_detactched_file_id_key" ON "file_conflicts"("detactched_file_id");
|
||||
CREATE UNIQUE INDEX "key_checksum_key" ON "key"("checksum");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "keys_checksum_key" ON "keys"("checksum");
|
||||
CREATE UNIQUE INDEX "tag_pub_id_key" ON "tag"("pub_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "tags_pub_id_key" ON "tags"("pub_id");
|
||||
CREATE UNIQUE INDEX "label_pub_id_key" ON "label"("pub_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "labels_pub_id_key" ON "labels"("pub_id");
|
||||
CREATE UNIQUE INDEX "space_pub_id_key" ON "space"("pub_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "spaces_pub_id_key" ON "spaces"("pub_id");
|
||||
CREATE UNIQUE INDEX "album_pub_id_key" ON "album"("pub_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "albums_pub_id_key" ON "albums"("pub_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "comments_pub_id_key" ON "comments"("pub_id");
|
||||
CREATE UNIQUE INDEX "comment_pub_id_key" ON "comment"("pub_id");
|
||||
@@ -5,17 +5,7 @@ datasource db {
|
||||
|
||||
generator client {
|
||||
provider = "cargo prisma"
|
||||
output = "../../core/src/prisma.rs"
|
||||
}
|
||||
|
||||
model Migration {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
checksum String @unique
|
||||
steps_applied Int @default(0)
|
||||
applied_at DateTime @default(now())
|
||||
|
||||
@@map("_migrations")
|
||||
output = "../src/prisma.rs"
|
||||
}
|
||||
|
||||
model SyncEvent {
|
||||
@@ -33,13 +23,13 @@ model SyncEvent {
|
||||
|
||||
node Node @relation(fields: [node_id], references: [id])
|
||||
|
||||
@@map("sync_events")
|
||||
@@map("sync_event")
|
||||
}
|
||||
|
||||
model Statistics {
|
||||
id Int @id @default(autoincrement())
|
||||
date_captured DateTime @default(now())
|
||||
total_file_count Int @default(0)
|
||||
total_object_count Int @default(0)
|
||||
library_db_size String @default("0")
|
||||
total_bytes_used String @default("0")
|
||||
total_bytes_capacity String @default("0")
|
||||
@@ -65,7 +55,7 @@ model Node {
|
||||
|
||||
Location Location[]
|
||||
|
||||
@@map("nodes")
|
||||
@@map("node")
|
||||
}
|
||||
|
||||
model Volume {
|
||||
@@ -81,7 +71,7 @@ model Volume {
|
||||
date_modified DateTime @default(now())
|
||||
|
||||
@@unique([node_id, mount_point, name])
|
||||
@@map("volumes")
|
||||
@@map("volume")
|
||||
}
|
||||
|
||||
model Location {
|
||||
@@ -103,10 +93,10 @@ model Location {
|
||||
file_paths FilePath[]
|
||||
indexer_rules IndexerRulesInLocation[]
|
||||
|
||||
@@map("locations")
|
||||
@@map("location")
|
||||
}
|
||||
|
||||
model File {
|
||||
model Object {
|
||||
id Int @id @default(autoincrement())
|
||||
// content addressable storage id - sha256 sampled checksum
|
||||
cas_id String @unique
|
||||
@@ -118,11 +108,11 @@ model File {
|
||||
kind Int @default(0)
|
||||
size_in_bytes String
|
||||
key_id Int?
|
||||
// handy ways to mark a file
|
||||
// handy ways to mark an object
|
||||
hidden Boolean @default(false)
|
||||
favorite Boolean @default(false)
|
||||
important Boolean @default(false)
|
||||
// if we have generated preview media for this file
|
||||
// if we have generated preview media for this object
|
||||
has_thumbnail Boolean @default(false)
|
||||
has_thumbstrip Boolean @default(false)
|
||||
has_video_preview Boolean @default(false)
|
||||
@@ -130,24 +120,24 @@ model File {
|
||||
ipfs_id String?
|
||||
// plain text note
|
||||
note String?
|
||||
// the original known creation date of this file
|
||||
// the original known creation date of this object
|
||||
date_created DateTime @default(now())
|
||||
// the last time this file was modified
|
||||
// the last time this object was modified
|
||||
date_modified DateTime @default(now())
|
||||
// when this file was first indexed
|
||||
// when this object was first indexed
|
||||
date_indexed DateTime @default(now())
|
||||
|
||||
tags TagOnFile[]
|
||||
labels LabelOnFile[]
|
||||
albums FileInAlbum[]
|
||||
spaces FileInSpace[]
|
||||
paths FilePath[]
|
||||
tags TagOnObject[]
|
||||
labels LabelOnObject[]
|
||||
albums ObjectInAlbum[]
|
||||
spaces ObjectInSpace[]
|
||||
file_paths FilePath[]
|
||||
comments Comment[]
|
||||
media_data MediaData?
|
||||
|
||||
key Key? @relation(fields: [key_id], references: [id])
|
||||
|
||||
@@map("files")
|
||||
@@map("object")
|
||||
}
|
||||
|
||||
model FilePath {
|
||||
@@ -160,8 +150,8 @@ model FilePath {
|
||||
// the name and extension
|
||||
name String
|
||||
extension String?
|
||||
// the unique File for this file path
|
||||
file_id Int?
|
||||
// the unique Object for this file path
|
||||
object_id Int?
|
||||
// the parent in the file tree
|
||||
parent_id Int?
|
||||
key_id Int? // replacement for encryption
|
||||
@@ -172,7 +162,7 @@ model FilePath {
|
||||
date_modified DateTime @default(now())
|
||||
date_indexed DateTime @default(now())
|
||||
|
||||
file File? @relation(fields: [file_id], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
object Object? @relation(fields: [object_id], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
location Location? @relation(fields: [location_id], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
|
||||
// NOTE: this self relation for the file tree was causing SQLite to go to forever bed, disabling until workaround
|
||||
@@ -184,15 +174,15 @@ model FilePath {
|
||||
@@id([location_id, id])
|
||||
@@unique([location_id, materialized_path, name, extension])
|
||||
@@index([location_id])
|
||||
@@map("file_paths")
|
||||
@@map("file_path")
|
||||
}
|
||||
|
||||
// if there is a conflicting cas_id, the conficting file should be updated to have a larger cas_id as the field is unique, however this record is kept to tell the indexer (upon discovering this CAS) that there is alternate versions of the file and to check by a full integrity hash to define for which to associate with.
|
||||
model FileConflict {
|
||||
original_file_id Int @unique
|
||||
detactched_file_id Int @unique
|
||||
original_object_id Int @unique
|
||||
detactched_object_id Int @unique
|
||||
|
||||
@@map("file_conflicts")
|
||||
@@map("file_conflict")
|
||||
}
|
||||
|
||||
// keys allow us to know exactly which files can be decrypted with a given key
|
||||
@@ -207,10 +197,10 @@ model Key {
|
||||
// so we know which algorithm to use, can be null if user must select
|
||||
algorithm Int? @default(0)
|
||||
|
||||
files File[]
|
||||
objects Object[]
|
||||
file_paths FilePath[]
|
||||
|
||||
@@map("keys")
|
||||
@@map("key")
|
||||
}
|
||||
|
||||
model MediaData {
|
||||
@@ -227,8 +217,8 @@ model MediaData {
|
||||
codecs String? // eg: "h264,acc"
|
||||
streams Int?
|
||||
|
||||
// change this relation to File after testing
|
||||
files File? @relation(fields: [id], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
// change this relation to Object after testing
|
||||
objects Object? @relation(fields: [id], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
|
||||
@@map("media_data")
|
||||
}
|
||||
@@ -238,27 +228,27 @@ model Tag {
|
||||
pub_id Bytes @unique
|
||||
name String?
|
||||
color String?
|
||||
total_files Int? @default(0)
|
||||
total_objects Int? @default(0)
|
||||
redundancy_goal Int? @default(1)
|
||||
date_created DateTime @default(now())
|
||||
date_modified DateTime @default(now())
|
||||
|
||||
tag_files TagOnFile[]
|
||||
tag_objects TagOnObject[]
|
||||
|
||||
@@map("tags")
|
||||
@@map("tag")
|
||||
}
|
||||
|
||||
model TagOnFile {
|
||||
model TagOnObject {
|
||||
date_created DateTime @default(now())
|
||||
|
||||
tag_id Int
|
||||
tag Tag @relation(fields: [tag_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
|
||||
|
||||
file_id Int
|
||||
file File @relation(fields: [file_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
|
||||
object_id Int
|
||||
object Object @relation(fields: [object_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
|
||||
|
||||
@@id([tag_id, file_id])
|
||||
@@map("tags_on_file")
|
||||
@@id([tag_id, object_id])
|
||||
@@map("tag_on_object")
|
||||
}
|
||||
|
||||
model Label {
|
||||
@@ -268,22 +258,22 @@ model Label {
|
||||
date_created DateTime @default(now())
|
||||
date_modified DateTime @default(now())
|
||||
|
||||
label_files LabelOnFile[]
|
||||
label_objects LabelOnObject[]
|
||||
|
||||
@@map("labels")
|
||||
@@map("label")
|
||||
}
|
||||
|
||||
model LabelOnFile {
|
||||
model LabelOnObject {
|
||||
date_created DateTime @default(now())
|
||||
|
||||
label_id Int
|
||||
label Label @relation(fields: [label_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
|
||||
|
||||
file_id Int
|
||||
file File @relation(fields: [file_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
|
||||
object_id Int
|
||||
object Object @relation(fields: [object_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
|
||||
|
||||
@@id([label_id, file_id])
|
||||
@@map("label_on_file")
|
||||
@@id([label_id, object_id])
|
||||
@@map("label_on_object")
|
||||
}
|
||||
|
||||
model Space {
|
||||
@@ -294,22 +284,22 @@ model Space {
|
||||
date_created DateTime @default(now())
|
||||
date_modified DateTime @default(now())
|
||||
|
||||
files FileInSpace[]
|
||||
objects ObjectInSpace[]
|
||||
|
||||
@@map("spaces")
|
||||
@@map("space")
|
||||
}
|
||||
|
||||
model FileInSpace {
|
||||
model ObjectInSpace {
|
||||
date_created DateTime @default(now())
|
||||
|
||||
space_id Int
|
||||
space Space @relation(fields: [space_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
|
||||
|
||||
file_id Int
|
||||
file File @relation(fields: [file_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
|
||||
object_id Int
|
||||
object Object @relation(fields: [object_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
|
||||
|
||||
@@id([space_id, file_id])
|
||||
@@map("file_in_space")
|
||||
@@id([space_id, object_id])
|
||||
@@map("object_in_space")
|
||||
}
|
||||
|
||||
model Job {
|
||||
@@ -329,7 +319,7 @@ model Job {
|
||||
|
||||
nodes Node @relation(fields: [node_id], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
|
||||
@@map("jobs")
|
||||
@@map("job")
|
||||
}
|
||||
|
||||
model Album {
|
||||
@@ -341,22 +331,22 @@ model Album {
|
||||
date_created DateTime @default(now())
|
||||
date_modified DateTime @default(now())
|
||||
|
||||
files FileInAlbum[]
|
||||
objects ObjectInAlbum[]
|
||||
|
||||
@@map("albums")
|
||||
@@map("album")
|
||||
}
|
||||
|
||||
model FileInAlbum {
|
||||
model ObjectInAlbum {
|
||||
date_created DateTime @default(now())
|
||||
|
||||
album_id Int
|
||||
album Album @relation(fields: [album_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
|
||||
|
||||
file_id Int
|
||||
file File @relation(fields: [file_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
|
||||
object_id Int
|
||||
object Object @relation(fields: [object_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
|
||||
|
||||
@@id([album_id, file_id])
|
||||
@@map("files_in_albums")
|
||||
@@id([album_id, object_id])
|
||||
@@map("object_in_album")
|
||||
}
|
||||
|
||||
model Comment {
|
||||
@@ -365,10 +355,10 @@ model Comment {
|
||||
content String
|
||||
date_created DateTime @default(now())
|
||||
date_modified DateTime @default(now())
|
||||
file_id Int?
|
||||
file File? @relation(fields: [file_id], references: [id])
|
||||
object_id Int?
|
||||
object Object? @relation(fields: [object_id], references: [id])
|
||||
|
||||
@@map("comments")
|
||||
@@map("comment")
|
||||
}
|
||||
|
||||
model IndexerRule {
|
||||
@@ -381,7 +371,7 @@ model IndexerRule {
|
||||
|
||||
locations IndexerRulesInLocation[]
|
||||
|
||||
@@map("indexer_rules")
|
||||
@@map("indexer_rule")
|
||||
}
|
||||
|
||||
model IndexerRulesInLocation {
|
||||
@@ -394,5 +384,5 @@ model IndexerRulesInLocation {
|
||||
indexer_rule IndexerRule @relation(fields: [indexer_rule_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
|
||||
|
||||
@@id([location_id, indexer_rule_id])
|
||||
@@map("indexer_rules_in_location")
|
||||
@@map("indexer_rule_in_location")
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{invalidate_query, prisma::file};
|
||||
use crate::{invalidate_query, prisma::object};
|
||||
|
||||
use rspc::Type;
|
||||
use serde::Deserialize;
|
||||
@@ -29,8 +29,11 @@ pub(crate) fn mount() -> RouterBuilder {
|
||||
t(|_, args: SetNoteArgs, library| async move {
|
||||
library
|
||||
.db
|
||||
.file()
|
||||
.update(file::id::equals(args.id), vec![file::note::set(args.note)])
|
||||
.object()
|
||||
.update(
|
||||
object::id::equals(args.id),
|
||||
vec![object::note::set(args.note)],
|
||||
)
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
@@ -43,10 +46,10 @@ pub(crate) fn mount() -> RouterBuilder {
|
||||
t(|_, args: SetFavoriteArgs, library| async move {
|
||||
library
|
||||
.db
|
||||
.file()
|
||||
.object()
|
||||
.update(
|
||||
file::id::equals(args.id),
|
||||
vec![file::favorite::set(args.favorite)],
|
||||
object::id::equals(args.id),
|
||||
vec![object::favorite::set(args.favorite)],
|
||||
)
|
||||
.exec()
|
||||
.await?;
|
||||
@@ -60,8 +63,8 @@ pub(crate) fn mount() -> RouterBuilder {
|
||||
t(|_, id: i32, library| async move {
|
||||
library
|
||||
.db
|
||||
.file()
|
||||
.delete(file::id::equals(id))
|
||||
.object()
|
||||
.delete(object::id::equals(id))
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ pub(crate) fn mount() -> RouterBuilder {
|
||||
let params = vec![
|
||||
id::set(1), // Each library is a database so only one of these ever exists
|
||||
date_captured::set(Utc::now().into()),
|
||||
total_file_count::set(0),
|
||||
total_object_count::set(0),
|
||||
library_db_size::set(library_db_size.to_string()),
|
||||
total_bytes_used::set(0.to_string()),
|
||||
total_bytes_capacity::set(total_capacity.to_string()),
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
scan_location, LocationCreateArgs, LocationError, LocationUpdateArgs,
|
||||
},
|
||||
object::preview::THUMBNAIL_CACHE_DIR_NAME,
|
||||
prisma::{file, file_path, indexer_rule, indexer_rules_in_location, location, tag},
|
||||
prisma::{file_path, indexer_rule, indexer_rules_in_location, location, object, tag},
|
||||
};
|
||||
|
||||
use rspc::{self, internal::MiddlewareBuilderLike, ErrorCode, Type};
|
||||
@@ -29,14 +29,14 @@ pub enum ExplorerContext {
|
||||
// Space(object_in_space::Data),
|
||||
}
|
||||
|
||||
file_path::include!(file_path_with_file { file });
|
||||
file::include!(file_with_paths { paths });
|
||||
file_path::include!(file_path_with_object { object });
|
||||
object::include!(object_with_file_paths { file_paths });
|
||||
|
||||
#[derive(Serialize, Deserialize, Type, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ExplorerItem {
|
||||
Path(Box<file_path_with_file::Data>),
|
||||
Object(Box<file_with_paths::Data>),
|
||||
Path(Box<file_path_with_object::Data>),
|
||||
Object(Box<object_with_file_paths::Data>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Type, Debug)]
|
||||
@@ -108,7 +108,7 @@ pub(crate) fn mount() -> rspc::RouterBuilder<
|
||||
file_path::location_id::equals(location.id),
|
||||
file_path::parent_id::equals(Some(directory.id)),
|
||||
])
|
||||
.include(file_path_with_file::include())
|
||||
.include(file_path_with_object::include())
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
@@ -117,16 +117,16 @@ pub(crate) fn mount() -> rspc::RouterBuilder<
|
||||
items: file_paths
|
||||
.into_iter()
|
||||
.map(|mut file_path| {
|
||||
if let Some(file) = &mut file_path.file.as_mut() {
|
||||
if let Some(object) = &mut file_path.object.as_mut() {
|
||||
// TODO: Use helper function to build this url as as the Rust file loading layer
|
||||
let thumb_path = library
|
||||
.config()
|
||||
.data_directory()
|
||||
.join(THUMBNAIL_CACHE_DIR_NAME)
|
||||
.join(&file.cas_id)
|
||||
.join(&object.cas_id)
|
||||
.with_extension("webp");
|
||||
|
||||
file.has_thumbnail = thumb_path.exists();
|
||||
object.has_thumbnail = thumb_path.exists();
|
||||
}
|
||||
ExplorerItem::Path(Box::new(file_path))
|
||||
})
|
||||
|
||||
@@ -122,6 +122,6 @@ mod tests {
|
||||
/// This test will ensure the rspc router and all calls to `invalidate_query` are valid and also export an updated version of the Typescript bindings.
|
||||
#[test]
|
||||
fn test_and_export_rspc_bindings() {
|
||||
super::export_ts_bindings(super::mount());
|
||||
super::export_ts_bindings(&super::mount());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ use tracing::info;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
api::locations::{file_with_paths, ExplorerContext, ExplorerData, ExplorerItem},
|
||||
api::locations::{object_with_file_paths, ExplorerContext, ExplorerData, ExplorerItem},
|
||||
invalidate_query,
|
||||
object::preview::THUMBNAIL_CACHE_DIR_NAME,
|
||||
prisma::{file, tag, tag_on_file},
|
||||
prisma::{object, tag, tag_on_object},
|
||||
};
|
||||
|
||||
use super::{utils::LibraryRequest, RouterBuilder};
|
||||
@@ -20,7 +20,7 @@ pub struct TagCreateArgs {
|
||||
|
||||
#[derive(Debug, Type, Deserialize)]
|
||||
pub struct TagAssignArgs {
|
||||
pub file_id: i32,
|
||||
pub object_id: i32,
|
||||
pub tag_id: i32,
|
||||
pub unassign: bool,
|
||||
}
|
||||
@@ -56,52 +56,52 @@ pub(crate) fn mount() -> RouterBuilder {
|
||||
)
|
||||
})?;
|
||||
|
||||
let files: Vec<ExplorerItem> = library
|
||||
let objects: Vec<ExplorerItem> = library
|
||||
.db
|
||||
.file()
|
||||
.find_many(vec![file::tags::some(vec![tag_on_file::tag_id::equals(
|
||||
tag_id,
|
||||
)])])
|
||||
.include(file_with_paths::include())
|
||||
.object()
|
||||
.find_many(vec![object::tags::some(vec![
|
||||
tag_on_object::tag_id::equals(tag_id),
|
||||
])])
|
||||
.include(object_with_file_paths::include())
|
||||
.exec()
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|mut file| {
|
||||
.map(|mut object| {
|
||||
// sorry brendan
|
||||
// grab the first path and tac on the name
|
||||
let oldest_path = &file.paths[0];
|
||||
file.name = Some(oldest_path.name.clone());
|
||||
file.extension = oldest_path.extension.clone();
|
||||
let oldest_path = &object.file_paths[0];
|
||||
object.name = Some(oldest_path.name.clone());
|
||||
object.extension = oldest_path.extension.clone();
|
||||
// a long term fix for this would be to have the indexer give the Object a name and extension, sacrificing its own and only store newly found Path names that differ from the Object name
|
||||
|
||||
let thumb_path = library
|
||||
.config()
|
||||
.data_directory()
|
||||
.join(THUMBNAIL_CACHE_DIR_NAME)
|
||||
.join(&file.cas_id)
|
||||
.join(&object.cas_id)
|
||||
.with_extension("webp");
|
||||
|
||||
file.has_thumbnail = thumb_path.exists();
|
||||
object.has_thumbnail = thumb_path.exists();
|
||||
|
||||
ExplorerItem::Object(Box::new(file))
|
||||
ExplorerItem::Object(Box::new(object))
|
||||
})
|
||||
.collect();
|
||||
|
||||
info!("Got files {}", files.len());
|
||||
info!("Got objects {}", objects.len());
|
||||
|
||||
Ok(ExplorerData {
|
||||
context: ExplorerContext::Tag(tag),
|
||||
items: files,
|
||||
items: objects,
|
||||
})
|
||||
})
|
||||
})
|
||||
.library_query("getForFile", |t| {
|
||||
t(|_, file_id: i32, library| async move {
|
||||
.library_query("getForObject", |t| {
|
||||
t(|_, object_id: i32, library| async move {
|
||||
Ok(library
|
||||
.db
|
||||
.tag()
|
||||
.find_many(vec![tag::tag_files::some(vec![
|
||||
tag_on_file::file_id::equals(file_id),
|
||||
.find_many(vec![tag::tag_objects::some(vec![
|
||||
tag_on_object::object_id::equals(object_id),
|
||||
])])
|
||||
.exec()
|
||||
.await?)
|
||||
@@ -142,24 +142,24 @@ pub(crate) fn mount() -> RouterBuilder {
|
||||
if args.unassign {
|
||||
library
|
||||
.db
|
||||
.tag_on_file()
|
||||
.delete(tag_on_file::tag_id_file_id(args.tag_id, args.file_id))
|
||||
.tag_on_object()
|
||||
.delete(tag_on_object::tag_id_object_id(args.tag_id, args.object_id))
|
||||
.exec()
|
||||
.await?;
|
||||
} else {
|
||||
library
|
||||
.db
|
||||
.tag_on_file()
|
||||
.tag_on_object()
|
||||
.create(
|
||||
tag::id::equals(args.tag_id),
|
||||
file::id::equals(args.file_id),
|
||||
object::id::equals(args.object_id),
|
||||
vec![],
|
||||
)
|
||||
.exec()
|
||||
.await?;
|
||||
}
|
||||
|
||||
invalidate_query!(library, "tags.getForFile");
|
||||
invalidate_query!(library, "tags.getForObject");
|
||||
|
||||
Ok(())
|
||||
})
|
||||
|
||||
@@ -92,7 +92,7 @@ impl InvalidRequests {
|
||||
#[allow(clippy::crate_in_macro_def)]
|
||||
macro_rules! invalidate_query {
|
||||
($ctx:expr, $key:literal) => {{
|
||||
let _: &crate::library::LibraryContext = &$ctx; // Assert the context is the correct type
|
||||
let ctx: &crate::library::LibraryContext = &$ctx; // Assert the context is the correct type
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
@@ -105,13 +105,15 @@ macro_rules! invalidate_query {
|
||||
.push(crate::api::utils::InvalidationRequest {
|
||||
key: $key,
|
||||
arg_ty: None,
|
||||
macro_src: concat!(file!(), ":", line!()),
|
||||
macro_src: concat!(file!(), ":", line!()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The error are ignored here because they aren't mission critical. If they fail the UI might be outdated for a bit.
|
||||
crate::api::utils::InvalidateOperationEvent::dangerously_create($key, serde_json::Value::Null)
|
||||
ctx.emit(crate::api::CoreEvent::InvalidateOperation(
|
||||
crate::api::utils::InvalidateOperationEvent::dangerously_create($key, serde_json::Value::Null)
|
||||
))
|
||||
}};
|
||||
($ctx:expr, $key:literal: $arg_ty:ty, $arg:expr $(,)?) => {{
|
||||
let _: $arg_ty = $arg; // Assert the type the user provided is correct
|
||||
|
||||
@@ -262,7 +262,7 @@ impl StatefulJob for IndexerJob {
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
file_path::create(
|
||||
file_path::create_unchecked(
|
||||
entry.file_id,
|
||||
location_id,
|
||||
materialized_path,
|
||||
|
||||
@@ -224,7 +224,7 @@ async fn link_location_and_indexer_rules(
|
||||
.create_many(
|
||||
rules_ids
|
||||
.iter()
|
||||
.map(|id| indexer_rules_in_location::create(location_id, *id, vec![]))
|
||||
.map(|id| indexer_rules_in_location::create_unchecked(location_id, *id, vec![]))
|
||||
.collect(),
|
||||
)
|
||||
.exec()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
job::{JobError, JobReportUpdate, JobResult, JobState, StatefulJob, WorkerContext},
|
||||
library::LibraryContext,
|
||||
prisma::{file, file_path, location},
|
||||
prisma::{file_path, location, object},
|
||||
};
|
||||
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
@@ -162,10 +162,10 @@ impl StatefulJob for FileIdentifierJob {
|
||||
for file_path in &file_paths {
|
||||
// get the cas_id and extract metadata
|
||||
match assemble_object_metadata(&data.location_path, file_path).await {
|
||||
Ok(file) => {
|
||||
let cas_id = file.cas_id.clone();
|
||||
Ok(object) => {
|
||||
let cas_id = object.cas_id.clone();
|
||||
// create entry into chunks for created file data
|
||||
chunk.insert(file_path.id, file);
|
||||
chunk.insert(file_path.id, object);
|
||||
cas_lookup.insert(cas_id, file_path.id);
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -177,23 +177,23 @@ impl StatefulJob for FileIdentifierJob {
|
||||
|
||||
// find all existing files by cas id
|
||||
let generated_cas_ids = chunk.values().map(|c| c.cas_id.clone()).collect();
|
||||
let existing_files = db
|
||||
.file()
|
||||
.find_many(vec![file::cas_id::in_vec(generated_cas_ids)])
|
||||
let existing_objects = db
|
||||
.object()
|
||||
.find_many(vec![object::cas_id::in_vec(generated_cas_ids)])
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
info!("Found {} existing files", existing_files.len());
|
||||
info!("Found {} existing files", existing_objects.len());
|
||||
|
||||
for existing_file in &existing_files {
|
||||
for existing_object in &existing_objects {
|
||||
if let Err(e) = db
|
||||
.file_path()
|
||||
.update(
|
||||
file_path::location_id_id(
|
||||
state.init.location_id,
|
||||
*cas_lookup.get(&existing_file.cas_id).unwrap(),
|
||||
*cas_lookup.get(&existing_object.cas_id).unwrap(),
|
||||
),
|
||||
vec![file_path::file_id::set(Some(existing_file.id))],
|
||||
vec![file_path::object_id::set(Some(existing_object.id))],
|
||||
)
|
||||
.exec()
|
||||
.await
|
||||
@@ -202,26 +202,26 @@ impl StatefulJob for FileIdentifierJob {
|
||||
}
|
||||
}
|
||||
|
||||
let existing_files_cas_ids = existing_files
|
||||
let existing_object_cas_ids = existing_objects
|
||||
.iter()
|
||||
.map(|file| file.cas_id.clone())
|
||||
.map(|object| object.cas_id.clone())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
// extract files that don't already exist in the database
|
||||
let new_files = chunk
|
||||
// extract objects that don't already exist in the database
|
||||
let new_objects = chunk
|
||||
.iter()
|
||||
.map(|(_id, create_file)| create_file)
|
||||
.filter(|create_file| !existing_files_cas_ids.contains(&create_file.cas_id))
|
||||
.filter(|create_file| !existing_object_cas_ids.contains(&create_file.cas_id))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !new_files.is_empty() {
|
||||
if !new_objects.is_empty() {
|
||||
// assemble prisma values for new unique files
|
||||
let mut values = Vec::with_capacity(new_files.len() * 3);
|
||||
for file in &new_files {
|
||||
let mut values = Vec::with_capacity(new_objects.len() * 3);
|
||||
for object in &new_objects {
|
||||
values.extend([
|
||||
PrismaValue::String(file.cas_id.clone()),
|
||||
PrismaValue::Int(file.size_in_bytes),
|
||||
PrismaValue::DateTime(file.date_created),
|
||||
PrismaValue::String(object.cas_id.clone()),
|
||||
PrismaValue::Int(object.size_in_bytes),
|
||||
PrismaValue::DateTime(object.date_created),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -230,9 +230,9 @@ impl StatefulJob for FileIdentifierJob {
|
||||
let created_files: Vec<FileCreated> = db
|
||||
._query_raw(Raw::new(
|
||||
&format!(
|
||||
"INSERT INTO files (cas_id, size_in_bytes, date_created) VALUES {}
|
||||
"INSERT INTO object (cas_id, size_in_bytes, date_created) VALUES {}
|
||||
ON CONFLICT (cas_id) DO NOTHING RETURNING id, cas_id",
|
||||
vec!["({}, {}, {})"; new_files.len()].join(",")
|
||||
vec!["({}, {}, {})"; new_objects.len()].join(",")
|
||||
),
|
||||
values,
|
||||
))
|
||||
@@ -256,7 +256,7 @@ impl StatefulJob for FileIdentifierJob {
|
||||
state.init.location_id,
|
||||
*cas_lookup.get(&created_file.cas_id).unwrap(),
|
||||
),
|
||||
vec![file_path::file_id::set(Some(created_file.id))],
|
||||
vec![file_path::object_id::set(Some(created_file.id))],
|
||||
)
|
||||
.exec()
|
||||
.await
|
||||
@@ -305,7 +305,7 @@ impl StatefulJob for FileIdentifierJob {
|
||||
|
||||
fn orphan_path_filters(location_id: i32, file_path_id: Option<i32>) -> Vec<file_path::WhereParam> {
|
||||
let mut params = vec![
|
||||
file_path::file_id::equals(None),
|
||||
file_path::object_id::equals(None),
|
||||
file_path::is_dir::equals(false),
|
||||
file_path::location_id::equals(location_id),
|
||||
];
|
||||
@@ -329,7 +329,7 @@ async fn count_orphan_file_paths(
|
||||
.db
|
||||
.file_path()
|
||||
.count(vec![
|
||||
file_path::file_id::equals(None),
|
||||
file_path::object_id::equals(None),
|
||||
file_path::is_dir::equals(false),
|
||||
file_path::location_id::equals(location_id),
|
||||
])
|
||||
|
||||
@@ -28,7 +28,7 @@ pub struct ObjectsForExplorer {
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Type)]
|
||||
pub enum ObjectData {
|
||||
Object(Box<prisma::file::Data>),
|
||||
Object(Box<prisma::object::Data>),
|
||||
Path(Box<prisma::file_path::Data>),
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ pub struct ThumbnailJobState {
|
||||
root_path: PathBuf,
|
||||
}
|
||||
|
||||
file_path::include!(file_path_with_file { file });
|
||||
file_path::include!(file_path_with_object { object });
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
|
||||
enum ThumbnailJobStepKind {
|
||||
@@ -49,7 +49,7 @@ enum ThumbnailJobStepKind {
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ThumbnailJobStep {
|
||||
file: file_path_with_file::Data,
|
||||
file_path: file_path_with_object::Data,
|
||||
kind: ThumbnailJobStepKind,
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ impl StatefulJob for ThumbnailJob {
|
||||
let step = &state.steps[0];
|
||||
ctx.progress(vec![JobReportUpdate::Message(format!(
|
||||
"Processing {}",
|
||||
step.file.materialized_path
|
||||
step.file_path.materialized_path
|
||||
))]);
|
||||
|
||||
let data = state
|
||||
@@ -188,16 +188,16 @@ impl StatefulJob for ThumbnailJob {
|
||||
.expect("critical error: missing data on job state");
|
||||
|
||||
// assemble the file path
|
||||
let path = data.root_path.join(&step.file.materialized_path);
|
||||
let path = data.root_path.join(&step.file_path.materialized_path);
|
||||
trace!("image_file {:?}", step);
|
||||
|
||||
// get cas_id, if none found skip
|
||||
let cas_id = match &step.file.file {
|
||||
let cas_id = match &step.file_path.object {
|
||||
Some(f) => f.cas_id.clone(),
|
||||
_ => {
|
||||
warn!(
|
||||
"skipping thumbnail generation for {}",
|
||||
step.file.materialized_path
|
||||
step.file_path.materialized_path
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
@@ -327,10 +327,10 @@ async fn get_files_by_extension(
|
||||
.db
|
||||
.file_path()
|
||||
.find_many(params)
|
||||
.include(file_path_with_file::include())
|
||||
.include(file_path_with_object::include())
|
||||
.exec()
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|file| ThumbnailJobStep { file, kind })
|
||||
.map(|file_path| ThumbnailJobStep { file_path, kind })
|
||||
.collect())
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ pub enum MigrationError {
|
||||
#[error("An error occurred while initialising a new database connection: {0}")]
|
||||
NewClient(#[from] Box<NewClientError>),
|
||||
#[cfg(debug_assertions)]
|
||||
#[error("An error occurred during migartion: {0}")]
|
||||
#[error("An error occurred during migration: {0}")]
|
||||
MigrateFailed(#[from] DbPushError),
|
||||
#[cfg(not(debug_assertions))]
|
||||
#[error("An error occurred during migration: {0}")]
|
||||
@@ -29,7 +29,7 @@ pub async fn load_and_migrate(db_url: &str) -> Result<PrismaClient, MigrationErr
|
||||
.map(|v| v == "true")
|
||||
.unwrap_or(false)
|
||||
{
|
||||
builder = builder.force_reset();
|
||||
builder = builder.accept_data_loss().force_reset();
|
||||
}
|
||||
|
||||
builder.await?;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "sd_ffmpeg"
|
||||
name = "sd-ffmpeg"
|
||||
version = "0.1.0"
|
||||
authors = ["Ericson Soares <ericson.ds999@gmail.com>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "p2p"
|
||||
name = "sd-p2p"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
|
||||
7
crates/prisma-cli/Cargo.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "prisma-cli"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
prisma-client-rust-cli = { workspace = true }
|
||||
@@ -1,12 +0,0 @@
|
||||
[package]
|
||||
name = "prisma-cli"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
prisma-client-rust-cli = { git = "https://github.com/Brendonovich/prisma-client-rust.git", tag = "0.6.0", features = [
|
||||
"rspc",
|
||||
"sqlite-create-many",
|
||||
"migrations",
|
||||
"sqlite",
|
||||
], default-features = false }
|
||||
@@ -1,8 +1,12 @@
|
||||
[package]
|
||||
name = "sync"
|
||||
name = "sd-sync"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
rspc = { workspace = true, features = ["uuid"] }
|
||||
serde = "1.0.145"
|
||||
serde_json = "1.0.85"
|
||||
uhlc = "0.5.1"
|
||||
uuid = { version = "1.1.2", features = ["serde", "v4"] }
|
||||
|
||||
4
crates/sync/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# crdt-rs
|
||||
|
||||
Just @brendonovich experimenting with CRDT stuff.
|
||||
|
||||
79
crates/sync/docs/HLC.md
Normal file
@@ -0,0 +1,79 @@
|
||||
|
||||
|
||||
```rust
|
||||
pub fn update_with_timestamp(&self, timestamp: &Timestamp) -> Result<(), String> {
|
||||
let mut now = (self.clock)();
|
||||
now.0 &= LMASK;
|
||||
let msg_time = timestamp.get_time();
|
||||
if *msg_time > now && *msg_time - now > self.delta {
|
||||
let err_msg = format!(
|
||||
"incoming timestamp from {} exceeding delta {}ms is rejected: {} vs. now: {}",
|
||||
timestamp.get_id(),
|
||||
self.delta.to_duration().as_millis(),
|
||||
msg_time,
|
||||
now
|
||||
);
|
||||
warn!("{}", err_msg);
|
||||
Err(err_msg)
|
||||
} else {
|
||||
let mut last_time = lock!(self.last_time);
|
||||
let max_time = cmp::max(cmp::max(now, *msg_time), *last_time);
|
||||
if max_time == now {
|
||||
*last_time = now;
|
||||
} else if max_time == *msg_time {
|
||||
*last_time = *msg_time + 1;
|
||||
} else {
|
||||
*last_time += 1;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
Timestamp.recv = function(msg) {
|
||||
if (!clock) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var now = Date.now();
|
||||
|
||||
var msg_time = msg.millis();
|
||||
var msg_time = msg.counter();
|
||||
|
||||
if (msg_time - now > config.maxDrift) {
|
||||
throw new Timestamp.ClockDriftError();
|
||||
}
|
||||
|
||||
var last_time = clock.timestamp.millis();
|
||||
var last_time = clock.timestamp.counter();
|
||||
|
||||
var max_time = Math.max(Math.max(last_time, now), msg_time);
|
||||
|
||||
var last_time =
|
||||
max_time === last_time && lNew === msg_time
|
||||
? Math.max(last_time, msg_time) + 1
|
||||
: max_time === last_time
|
||||
? last_time + 1
|
||||
: max_time === msg_time
|
||||
? msg_time + 1
|
||||
: 0;
|
||||
|
||||
// 3.
|
||||
if (max_time - phys > config.maxDrift) {
|
||||
throw new Timestamp.ClockDriftError();
|
||||
}
|
||||
if (last_time > MAX_COUNTER) {
|
||||
throw new Timestamp.OverflowError();
|
||||
}
|
||||
|
||||
clock.timestamp.setMillis(max_time);
|
||||
clock.timestamp.setCounter(last_time);
|
||||
|
||||
return new Timestamp(
|
||||
clock.timestamp.millis(),
|
||||
clock.timestamp.counter(),
|
||||
clock.timestamp.node()
|
||||
);
|
||||
};
|
||||
```
|
||||
115
crates/sync/docs/index.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Owned Records
|
||||
|
||||
Node which owns the record is the sole source of truth for that record's state
|
||||
|
||||
# Shared Records
|
||||
|
||||
This includes Shared Record O-M Relations, since the foreign key is stored on the Many record.
|
||||
|
||||
## Create
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "CREATE",
|
||||
"recordId": "{uuid}",
|
||||
"model": "{model}",
|
||||
"data": {
|
||||
"key": "value"
|
||||
},
|
||||
"node": "{uuid}",
|
||||
"timestamp": {
|
||||
"hybrid": "logical clock"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Update
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "UPDATE",
|
||||
"recordId": "{uuid}",
|
||||
"field": "{field}",
|
||||
"value": "{value}",
|
||||
"node": "{uuid}",
|
||||
"timestamp": {
|
||||
"hybrid": "logical clock"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Delete
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "DELETE",
|
||||
"recordId": "{uuid}",
|
||||
"node": "{uuid}",
|
||||
"timestamp": {
|
||||
"hybrid": "logical clock"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Shared Record M-M Relations
|
||||
|
||||
x-M relations usually signify an item belonging to a group. 1-M relations are handled normally by Shared Records since the ID of the record is just the ID of the M record. M-M relations require custom handling since they are identified by the two records they join, so 2 create instructions
|
||||
|
||||
UNANSWERED: M-M relations that _can_ be duplicated. In this case, a single ID for the relation would suffice, in the same way that 1-M relations do.
|
||||
|
||||
NOTE: Ordering is very important when syncing relations. If a target of a relation doesn't exist, what should happen? This presents two options:
|
||||
|
||||
1. Don't use a foreign key, just join/fetch separately on a possibly non-existent foreign id. This is pretty cringe since Prisma only affords the niceties of relations if foreign keys are actually used.
|
||||
2. Require that all operations are synced in order, independent of which node they were created on. This is nicer since it means that in order for a node to create a relation in the first place it must possess a message indicating creation of the relation target, but it is much more difficult to coordinate deletion of messages this way. Probably still doable though.
|
||||
|
||||
Option 2 is probably the best way to go, since having to do annoying joins and losing database ergonomics is not great. Additionally, option 2 would result in the ability to sync shared data between any two nodes, even if the node being synced from didn't create the operation in the first place.
|
||||
|
||||
## Create
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "CREATE",
|
||||
// Record that is being assigned to a group eg. a file
|
||||
"relationItem": "{uuid}",
|
||||
// Group that the record is being assigned to eg. a photo album
|
||||
"relationGroup": "{uuid}",
|
||||
// Name of the model which represents the relation
|
||||
"relation": "model",
|
||||
"node": "{uuid}",
|
||||
"timestamp": {
|
||||
"hybrid": "logical clock"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Update
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "UPDATE",
|
||||
"relationItem": "{uuid}",
|
||||
"relationGroup": "{uuid}",
|
||||
"relation": "model",
|
||||
"field": "field",
|
||||
"value": "{value}",
|
||||
"node": "{uuid}",
|
||||
"timestamp": {
|
||||
"hybrid": "logical clock"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Delete
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "DELETE",
|
||||
"relationItem": "{uuid}",
|
||||
"relationGroup": "{uuid}",
|
||||
"relation": "relation",
|
||||
"node": "{uuid}",
|
||||
"timestamp": {
|
||||
"hybrid": "logical clock"
|
||||
}
|
||||
}
|
||||
```
|
||||
52
crates/sync/docs/prisma-schema.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Prisma Schema
|
||||
|
||||
`prisma-crdt` introduces new attributes that must be applied to fields and models via triple slash documentation comments.
|
||||
|
||||
_Sync ID_: As well as having a primary key - denoted in Prisma with the `@id` field attribute - `prisma-crdt` introduces another ID - the _Sync ID_.
|
||||
A model's Sync ID defaults to its regular ID, and is what identifies a model inside a sync operation.
|
||||
Regular IDs are often not suitable for use inside a sync operation, as they may not be unique when sent to other nodes - eg. autoincrementing IDs - so something more unique can used, like a UUID.
|
||||
|
||||
## Model Attributes
|
||||
|
||||
#### `@local`
|
||||
|
||||
Model that is entirely disconnected from sync.
|
||||
|
||||
_Arguments_
|
||||
|
||||
- `id` (optional): Scalar field to override the default Sync ID.
|
||||
|
||||
#### `@owned`
|
||||
|
||||
Model that is synced via replicating from its owner to all other nodes, with the other nodes treating the model's owner as its single source of truth.
|
||||
|
||||
_Arguments_
|
||||
|
||||
- `owner`: Field that identifies the owner of this model. If a scalar, will directly use that value in sync operations. If a relation, the Sync ID of the related model will be resolved for sync operations.
|
||||
- `id` (optional): Scalar field to override the default Sync ID.
|
||||
|
||||
#### `@shared`
|
||||
|
||||
Model that is synced via updates on a per-field basis.
|
||||
|
||||
_Arguments_
|
||||
|
||||
- `id` (optional): Scalar field to override the default Sync ID.
|
||||
- `create` (optional): How the model should be created.
|
||||
- `Unique` (default): Model can be created with many required arguemnts, but ID provided _must_ be unique across all nodes. Useful for Tags since their IDs are non-deterministic.
|
||||
- `Atomic`: Require the model to have no required arguments apart from ID and apply all create arguments as atomic updates. Necessary for models with the same ID that can be created on multiple nodes. Useful for Files since their ID is dependent on their content, and could be the same across nodes.
|
||||
|
||||
#### `@relation`
|
||||
|
||||
Similar to `@shared`, but identified by the two records that it relates. Sync ID is always the combination of `item` and `group`.
|
||||
|
||||
_Arguments_
|
||||
|
||||
- `item`: Field that identifies the item that the relation links to. Operates like the `owner` argument of `@owned`.
|
||||
- `group`: Field that identifies the group that the item should be related to. Operates like the `owner` argument of `@owned`.
|
||||
|
||||
## Field Attributes
|
||||
|
||||
#### `@node`
|
||||
|
||||
A relation whose value is automatically set to the current node. This could be done manually, but `@node` allows `node_id` fields to not be stored in `OwnedOperationData`, but rather be resolved from the `node_id` field of a `CRDTOperation`, saving on bandwidth.
|
||||
2
crates/sync/example/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
dist
|
||||
3
crates/sync/example/.prettierrc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"printWidth": 80
|
||||
}
|
||||
3
crates/sync/example/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
|
||||
}
|
||||
7
crates/sync/example/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Tauri + Solid + Typescript
|
||||
|
||||
This template should help get you started developing with Tauri, Solid and Typescript in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
||||
5
crates/sync/example/dist/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# Ignore everything in this directory
|
||||
*
|
||||
# Except this file
|
||||
!.gitignore
|
||||
# This is done so that Tauri never complains that '../dist does not exist'
|
||||
17
crates/sync/example/index.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<link rel="icon" type="image/svg+xml" href="/src/assets/logo.svg" />
|
||||
<title>Tauri + Solid + Typescript App</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
|
||||
<script src="/src/index.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
32
crates/sync/example/package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@sd/sync-example",
|
||||
"version": "0.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^1.1.0",
|
||||
"@types/babel__core": "^7.1.19",
|
||||
"@types/node": "^18.7.10",
|
||||
"autoprefixer": "^10.4.12",
|
||||
"postcss": "^8.4.16",
|
||||
"tailwindcss": "^3.1.8",
|
||||
"typescript": "^4.7.4",
|
||||
"vite": "^3.0.0",
|
||||
"vite-plugin-solid": "^2.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rspc/client": "~0.1.2",
|
||||
"@rspc/solid": "~0.1.2",
|
||||
"@rspc/tauri": "~0.1.2",
|
||||
"@tanstack/solid-query": "4.7.1",
|
||||
"clsx": "^1.2.1",
|
||||
"solid-js": "^1.4.7"
|
||||
}
|
||||
}
|
||||
BIN
crates/sync/example/pnpm-lock.yaml
generated
Normal file
6
crates/sync/example/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
6
crates/sync/example/public/tauri.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
|
||||
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
1
crates/sync/example/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
4
crates/sync/example/src-tauri/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
0
crates/sync/example/src-tauri/.taurignore
Normal file
BIN
crates/sync/example/src-tauri/Cargo.lock
generated
Normal file
31
crates/sync/example/src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,31 @@
|
||||
[package]
|
||||
name = "example"
|
||||
version = "0.0.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
license = ""
|
||||
repository = ""
|
||||
edition = "2021"
|
||||
rust-version = "1.57"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.1", features = [] }
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "1.1", features = ["api-all"] }
|
||||
rspc = { workspace = true, features = ["tauri", "uuid"] }
|
||||
sd-sync = { path = "../.." }
|
||||
tokio = { version = "1.21.2", features = ["macros"] }
|
||||
uuid = { version = "1.1.2", features = ["v4"] }
|
||||
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
|
||||
default = ["custom-protocol"]
|
||||
# this feature is used used for production builds where `devPath` points to the filesystem
|
||||
# DO NOT remove this
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
3
crates/sync/example/src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
BIN
crates/sync/example/src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
crates/sync/example/src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
crates/sync/example/src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 974 B |
BIN
crates/sync/example/src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
crates/sync/example/src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
crates/sync/example/src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
crates/sync/example/src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
crates/sync/example/src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 903 B |
BIN
crates/sync/example/src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
crates/sync/example/src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
crates/sync/example/src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
crates/sync/example/src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
crates/sync/example/src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
crates/sync/example/src-tauri/icons/icon.icns
Normal file
BIN
crates/sync/example/src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
crates/sync/example/src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
77
crates/sync/example/src-tauri/src/main.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
#![cfg_attr(
|
||||
all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
use std::{collections::HashMap, path::PathBuf, sync::Arc};
|
||||
use tokio::sync::Mutex;
|
||||
use uuid::Uuid;
|
||||
|
||||
use rspc::*;
|
||||
use sd_sync::*;
|
||||
|
||||
#[derive(Default)]
|
||||
struct Ctx {
|
||||
pub dbs: HashMap<Uuid, Db>,
|
||||
}
|
||||
|
||||
type Router = rspc::Router<Arc<Mutex<Ctx>>>;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let router = Arc::new(
|
||||
<Router>::new()
|
||||
.config(Config::new().export_ts_bindings(
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../src/bindings.ts"),
|
||||
))
|
||||
.mutation("createDatabase", |r| {
|
||||
r(|ctx, _: ()| async move {
|
||||
let dbs = &mut ctx.lock().await.dbs;
|
||||
let uuid = Uuid::new_v4();
|
||||
|
||||
dbs.insert(uuid, Db::new(uuid));
|
||||
|
||||
println!("{:?}", dbs);
|
||||
|
||||
Ok(uuid)
|
||||
})
|
||||
})
|
||||
.mutation("removeDatabases", |r| {
|
||||
r(|ctx, _: ()| async move {
|
||||
let dbs = &mut ctx.lock().await.dbs;
|
||||
|
||||
dbs.drain();
|
||||
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.query("dbs", |r| {
|
||||
r(|ctx, _: ()| async move {
|
||||
let dbs = &mut ctx.lock().await.dbs;
|
||||
|
||||
Ok(dbs.iter().map(|(id, _)| *id).collect::<Vec<_>>())
|
||||
})
|
||||
})
|
||||
.query("db.tags", |r| {
|
||||
r(|ctx, id: String| async move {
|
||||
let dbs = &mut ctx.lock().await.dbs;
|
||||
|
||||
let id = id.parse().unwrap();
|
||||
|
||||
println!("{:?}", &dbs);
|
||||
|
||||
Ok(dbs.get(&id).unwrap().tags.clone())
|
||||
})
|
||||
})
|
||||
.build(),
|
||||
);
|
||||
|
||||
let ctx = Arc::new(Mutex::new(Default::default()));
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(rspc::integrations::tauri::plugin(router, move || {
|
||||
ctx.clone()
|
||||
}))
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
66
crates/sync/example/src-tauri/tauri.conf.json
Normal file
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
"beforeBuildCommand": "pnpm build",
|
||||
"devPath": "http://localhost:1420",
|
||||
"distDir": "../dist",
|
||||
"withGlobalTauri": false
|
||||
},
|
||||
"package": {
|
||||
"productName": "example",
|
||||
"version": "0.0.0"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
"all": true
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"category": "DeveloperTool",
|
||||
"copyright": "",
|
||||
"deb": {
|
||||
"depends": []
|
||||
},
|
||||
"externalBin": [],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"identifier": "com.tauri.dev",
|
||||
"longDescription": "",
|
||||
"macOS": {
|
||||
"entitlements": null,
|
||||
"exceptionDomain": "",
|
||||
"frameworks": [],
|
||||
"providerShortName": null,
|
||||
"signingIdentity": null
|
||||
},
|
||||
"resources": [],
|
||||
"shortDescription": "",
|
||||
"targets": "all",
|
||||
"windows": {
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": ""
|
||||
}
|
||||
},
|
||||
"security": {
|
||||
"csp": null
|
||||
},
|
||||
"updater": {
|
||||
"active": false
|
||||
},
|
||||
"windows": [
|
||||
{
|
||||
"fullscreen": false,
|
||||
"height": 600,
|
||||
"resizable": true,
|
||||
"title": "example",
|
||||
"width": 800
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
75
crates/sync/example/src/App.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import clsx from "clsx";
|
||||
import { createSignal, For, JSX, Suspense } from "solid-js";
|
||||
import { queryClient, rspc } from "./rspc";
|
||||
|
||||
export function App() {
|
||||
const dbs = rspc.createQuery(() => ["dbs"]);
|
||||
|
||||
const createDb = rspc.createMutation("createDatabase", {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries();
|
||||
},
|
||||
});
|
||||
const removeDbs = rspc.createMutation("removeDatabases", {
|
||||
onSuccess: () => queryClient.invalidateQueries(),
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="p-4 space-y-4">
|
||||
<div class="space-x-4">
|
||||
<Button onClick={() => createDb.mutate(undefined)}>Add Database</Button>
|
||||
<Button onClick={() => removeDbs.mutate(undefined)}>
|
||||
Remove Databases
|
||||
</Button>
|
||||
</div>
|
||||
<ul class="gap-2 flex flex-row flex-wrap">
|
||||
<For each={dbs.data}>
|
||||
{(id) => (
|
||||
<Suspense fallback={null}>
|
||||
<DatabaseView id={id} />
|
||||
</Suspense>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface DatabaseViewProps {
|
||||
id: string;
|
||||
}
|
||||
const TABS = ["Tags", "Files", "File Paths", "Messages"];
|
||||
|
||||
function DatabaseView(props: DatabaseViewProps) {
|
||||
const [currentTab, setCurrentTab] = createSignal<typeof TABS[number]>("Tags");
|
||||
|
||||
return (
|
||||
<div class="bg-indigo-300 rounded-md min-w-[40rem] flex-1 overflow-hidden">
|
||||
<h1 class="p-2 text-xl font-medium">{props.id}</h1>
|
||||
<div>
|
||||
<nav class="space-x-2">
|
||||
<For each={TABS}>
|
||||
{(tab) => (
|
||||
<button
|
||||
class={clsx(
|
||||
"px-2 py-1",
|
||||
tab === currentTab() && "bg-indigo-400"
|
||||
)}
|
||||
onClick={() => setCurrentTab(tab)}
|
||||
>
|
||||
{tab}
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</nav>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Button(props: JSX.ButtonHTMLAttributes<HTMLButtonElement>) {
|
||||
return (
|
||||
<button {...props} class="bg-blue-500 text-white px-2 py-1 rounded-md" />
|
||||
);
|
||||
}
|
||||
15
crates/sync/example/src/bindings.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// This file was generated by [rspc](https://github.com/oscartbeaumont/rspc). Do not edit this file manually.
|
||||
|
||||
export type Procedures = {
|
||||
queries:
|
||||
{ key: "db.tags", input: string, result: Record<string, Tag> } |
|
||||
{ key: "dbs", input: never, result: Array<string> },
|
||||
mutations:
|
||||
{ key: "createDatabase", input: never, result: string } |
|
||||
{ key: "removeDatabases", input: never, result: null },
|
||||
subscriptions: never
|
||||
};
|
||||
|
||||
export interface Color { red: number, green: number, blue: number }
|
||||
|
||||
export interface Tag { color: Color, name: string }
|
||||
3
crates/sync/example/src/index.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
17
crates/sync/example/src/index.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
/* @refresh reload */
|
||||
import { render, Suspense } from "solid-js/web";
|
||||
|
||||
import "./index.css";
|
||||
import { App } from "./App";
|
||||
import { queryClient, rspc, rspcClient } from "./rspc";
|
||||
|
||||
render(
|
||||
() => (
|
||||
<rspc.Provider client={rspcClient} queryClient={queryClient}>
|
||||
<Suspense fallback={null}>
|
||||
<App />
|
||||
</Suspense>
|
||||
</rspc.Provider>
|
||||
),
|
||||
document.getElementById("root") as HTMLElement
|
||||
);
|
||||
22
crates/sync/example/src/rspc.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { QueryClient } from "@tanstack/solid-query";
|
||||
import { createSolidQueryHooks } from "@rspc/solid";
|
||||
import { createClient } from "@rspc/client";
|
||||
import { TauriTransport } from "@rspc/tauri";
|
||||
|
||||
import type { Procedures } from "./bindings"; // These were the bindings exported from your Rust code!
|
||||
|
||||
// You must provide the generated types as a generic and create a transport (in this example we are using HTTP Fetch) so that the client knows how to communicate with your API.
|
||||
export const rspcClient = createClient<Procedures>({
|
||||
// Refer to the integration your using for the correct transport.
|
||||
transport: new TauriTransport(),
|
||||
});
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
suspense: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const rspc = createSolidQueryHooks<Procedures>();
|
||||
11
crates/sync/example/tailwind.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
15
crates/sync/example/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js",
|
||||
"types": ["vite/client"],
|
||||
"noEmit": true,
|
||||
"isolatedModules": true
|
||||
}
|
||||
}
|
||||
25
crates/sync/example/vite.config.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { defineConfig } from "vite";
|
||||
import solidPlugin from "vite-plugin-solid";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [solidPlugin()],
|
||||
// Vite optons tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||
// prevent vite from obscuring rust errors
|
||||
clearScreen: false,
|
||||
// tauri expects a fixed port, fail if that port is not available
|
||||
server: {
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
},
|
||||
// to make use of `TAURI_DEBUG` and other env variables
|
||||
// https://tauri.studio/v1/api/config#buildconfig.beforedevcommand
|
||||
envPrefix: ["VITE_", "TAURI_"],
|
||||
build: {
|
||||
// Tauri supports es2021
|
||||
target: ["es2021", "chrome100", "safari13"],
|
||||
// don't minify for debug builds
|
||||
minify: !process.env.TAURI_DEBUG ? "esbuild" : false,
|
||||
// produce sourcemaps for debug builds
|
||||
sourcemap: !!process.env.TAURI_DEBUG,
|
||||
},
|
||||
});
|
||||
41
crates/sync/src/attribute/mod.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
mod parser;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AttributeFieldValue<'a> {
|
||||
Single(&'a str),
|
||||
List(Vec<&'a str>),
|
||||
}
|
||||
|
||||
impl AttributeFieldValue<'_> {
|
||||
pub fn as_single(&self) -> Option<&str> {
|
||||
match self {
|
||||
AttributeFieldValue::Single(field) => Some(field),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn as_list(&self) -> Option<&Vec<&str>> {
|
||||
// match self {
|
||||
// AttributeFieldValue::List(fields) => Some(fields),
|
||||
// _ => None,
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Attribute<'a> {
|
||||
pub name: &'a str,
|
||||
pub fields: Vec<(&'a str, AttributeFieldValue<'a>)>,
|
||||
}
|
||||
|
||||
impl<'a> Attribute<'a> {
|
||||
pub fn parse(input: &'a impl AsRef<str>) -> Result<Self, ()> {
|
||||
parser::parse(input.as_ref())
|
||||
.map(|(_, a)| a)
|
||||
.map_err(|_| ())
|
||||
}
|
||||
|
||||
pub fn field(&self, name: &str) -> Option<&AttributeFieldValue> {
|
||||
self.fields.iter().find(|(n, _)| *n == name).map(|(_, v)| v)
|
||||
}
|
||||
}
|
||||
155
crates/sync/src/attribute/parser.rs
Normal file
@@ -0,0 +1,155 @@
|
||||
use nom::{
|
||||
branch::alt,
|
||||
bytes::complete::*,
|
||||
character::complete::*,
|
||||
combinator::*,
|
||||
error::{ErrorKind, ParseError},
|
||||
multi::*,
|
||||
sequence::*,
|
||||
AsChar, IResult, InputTakeAtPosition,
|
||||
};
|
||||
|
||||
use super::{Attribute, AttributeFieldValue};
|
||||
|
||||
fn remove_ws<'a, O, E: ParseError<&'a str>, F>(
|
||||
wrapped: F,
|
||||
) -> impl FnMut(&'a str) -> IResult<&'a str, O, E>
|
||||
where
|
||||
F: FnMut(&'a str) -> IResult<&'a str, O, E>,
|
||||
{
|
||||
delimited(multispace0, wrapped, multispace0)
|
||||
}
|
||||
|
||||
fn parens(input: &str) -> IResult<&str, &str> {
|
||||
delimited(char('('), is_not(")"), char(')'))(input)
|
||||
}
|
||||
|
||||
pub fn single_value<T, E: ParseError<T>>(i: T) -> IResult<T, T, E>
|
||||
where
|
||||
T: InputTakeAtPosition,
|
||||
<T as InputTakeAtPosition>::Item: AsChar,
|
||||
{
|
||||
i.split_at_position1_complete(
|
||||
|item| {
|
||||
let char_item = item.as_char();
|
||||
char_item != '_' && !char_item.is_alphanum()
|
||||
},
|
||||
ErrorKind::AlphaNumeric,
|
||||
)
|
||||
}
|
||||
|
||||
fn list_value(input: &str) -> IResult<&str, Vec<&str>> {
|
||||
delimited(
|
||||
char('['),
|
||||
separated_list1(char(','), remove_ws(single_value)),
|
||||
char(']'),
|
||||
)(input)
|
||||
}
|
||||
|
||||
fn attribute_field_value(input: &str) -> IResult<&str, AttributeFieldValue> {
|
||||
remove_ws(alt((
|
||||
map(list_value, AttributeFieldValue::List),
|
||||
map(single_value, AttributeFieldValue::Single),
|
||||
)))(input)
|
||||
}
|
||||
|
||||
fn attribute_field(input: &str) -> IResult<&str, (&str, AttributeFieldValue)> {
|
||||
remove_ws(separated_pair(
|
||||
remove_ws(is_not(":")),
|
||||
char(':'),
|
||||
remove_ws(attribute_field_value),
|
||||
))(input)
|
||||
}
|
||||
|
||||
fn attribute_fields(input: &str) -> IResult<&str, Vec<(&str, AttributeFieldValue)>> {
|
||||
separated_list1(char(','), attribute_field)(input)
|
||||
}
|
||||
|
||||
pub fn parse(input: &str) -> IResult<&str, Attribute> {
|
||||
let (input, _) = remove_ws(tag("@"))(input)?;
|
||||
let (input, name) = alpha1(input)?;
|
||||
let (input, values_str) = opt(remove_ws(parens))(input)?;
|
||||
|
||||
let fields = match values_str {
|
||||
Some(values_str) => attribute_fields(values_str)?.1,
|
||||
None => vec![],
|
||||
};
|
||||
|
||||
Ok((input, Attribute { name, fields }))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn marker() {
|
||||
let s = "@local";
|
||||
|
||||
let (remaining, attribute) = super::parse(s).unwrap();
|
||||
|
||||
assert_eq!(remaining, "");
|
||||
assert_eq!(attribute.name, "local");
|
||||
assert_eq!(attribute.fields.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single() {
|
||||
let s = "@local(foo: bar)";
|
||||
|
||||
let (remaining, attribute) = super::parse(s).unwrap();
|
||||
|
||||
assert_eq!(remaining, "");
|
||||
assert_eq!(attribute.name, "local");
|
||||
assert_eq!(attribute.fields.len(), 1);
|
||||
assert_eq!(attribute.fields[0].0, "foo");
|
||||
assert!(matches!(
|
||||
attribute.fields[0].1,
|
||||
AttributeFieldValue::Single("bar")
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list() {
|
||||
let s = "@local(foo: [bar, baz])";
|
||||
|
||||
let (remaining, attribute) = match super::parse(s) {
|
||||
Ok(v) => v,
|
||||
Err(e) => panic!("{}", e),
|
||||
};
|
||||
|
||||
assert_eq!(remaining, "");
|
||||
assert_eq!(attribute.name, "local");
|
||||
assert_eq!(attribute.fields.len(), 1);
|
||||
assert_eq!(attribute.fields[0].0, "foo");
|
||||
|
||||
if let AttributeFieldValue::List(list) = &attribute.fields[0].1 {
|
||||
assert_eq!(list.len(), 2);
|
||||
assert_eq!(list[0], "bar");
|
||||
assert_eq!(list[1], "baz");
|
||||
} else {
|
||||
panic!("Expected list, got {:?}", attribute.fields[0].1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple() {
|
||||
let s = "@local(foo: bar, baz: qux)";
|
||||
|
||||
let (remaining, attribute) = super::parse(s).unwrap();
|
||||
|
||||
assert_eq!(remaining, "");
|
||||
assert_eq!(attribute.name, "local");
|
||||
assert_eq!(attribute.fields.len(), 2);
|
||||
assert_eq!(attribute.fields[0].0, "foo");
|
||||
assert!(matches!(
|
||||
attribute.fields[0].1,
|
||||
AttributeFieldValue::Single("bar")
|
||||
));
|
||||
assert_eq!(attribute.fields[1].0, "baz");
|
||||
assert!(matches!(
|
||||
attribute.fields[1].1,
|
||||
AttributeFieldValue::Single("qux")
|
||||
));
|
||||
}
|
||||
}
|
||||
72
crates/sync/src/crdt.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Value};
|
||||
use uhlc::NTP64;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub enum SharedOperationCreateData {
|
||||
Unique(Map<String, Value>),
|
||||
Atomic,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub enum SharedOperationData {
|
||||
Create(SharedOperationCreateData),
|
||||
Update { field: String, value: Value },
|
||||
Delete,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct SharedOperation {
|
||||
pub record_id: Value, // Uuid,
|
||||
pub model: String,
|
||||
pub data: SharedOperationData,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub enum OwnedOperationData {
|
||||
Create(Map<String, Value>),
|
||||
Update(Map<String, Value>),
|
||||
Delete,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct OwnedOperationItem {
|
||||
pub id: Value,
|
||||
pub data: OwnedOperationData,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct OwnedOperation {
|
||||
pub model: String,
|
||||
pub items: Vec<OwnedOperationItem>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum CRDTOperationType {
|
||||
Shared(SharedOperation),
|
||||
// Relation(RelationOperation),
|
||||
Owned(OwnedOperation),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct CRDTOperation {
|
||||
pub node: Uuid,
|
||||
pub timestamp: NTP64,
|
||||
pub id: Uuid,
|
||||
#[serde(flatten)]
|
||||
pub typ: CRDTOperationType,
|
||||
}
|
||||
|
||||
impl Debug for CRDTOperation {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("CRDTOperation")
|
||||
.field("node", &self.node.to_string())
|
||||
.field("timestamp", &self.timestamp.to_string())
|
||||
.field("typ", &self.typ)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
186
crates/sync/src/datamodel/field.rs
Normal file
@@ -0,0 +1,186 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Field<'a> {
|
||||
pub prisma: &'a dml::Field,
|
||||
pub model: &'a str,
|
||||
pub typ: FieldType<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Field<'a> {
|
||||
pub fn new(field: &'a dml::Field, model: &'a str, datamodel: &'a dml::Datamodel) -> Field<'a> {
|
||||
let typ = FieldType::new(field, model, datamodel);
|
||||
|
||||
Self {
|
||||
prisma: field,
|
||||
model,
|
||||
typ,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the token representation of the field's type,
|
||||
/// accounting for a sync ID reference if it is a field
|
||||
/// of a relation
|
||||
pub fn crdt_type_tokens(&self, datamodel: &Datamodel) -> TokenStream {
|
||||
let relation_field_info = match &self.typ {
|
||||
FieldType::Scalar {
|
||||
relation_field_info,
|
||||
} => relation_field_info,
|
||||
_ => unreachable!("Cannot get CRDT type for non-scalar field"),
|
||||
};
|
||||
|
||||
match relation_field_info.as_ref() {
|
||||
Some(relation_field_info) => {
|
||||
let relation_model = datamodel
|
||||
.model(relation_field_info.referenced_model)
|
||||
.unwrap();
|
||||
|
||||
let sync_id_field =
|
||||
relation_model.sync_id_for_pk(relation_field_info.referenced_field);
|
||||
|
||||
match sync_id_field {
|
||||
Some(field) => {
|
||||
let relation_field_type = field.field_type().to_tokens();
|
||||
|
||||
match self.arity() {
|
||||
dml::FieldArity::Required => relation_field_type,
|
||||
dml::FieldArity::Optional => quote!(Option<#relation_field_type>),
|
||||
dml::FieldArity::List => quote!(Vec<#relation_field_type>),
|
||||
}
|
||||
}
|
||||
None => self.type_tokens(),
|
||||
}
|
||||
}
|
||||
None => datamodel
|
||||
.model(self.model)
|
||||
.unwrap()
|
||||
.sync_id_for_pk(self.name())
|
||||
.unwrap_or(self)
|
||||
.type_tokens(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for Field<'a> {
|
||||
type Target = dml::Field;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.prisma
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum FieldType<'a> {
|
||||
Scalar {
|
||||
/// The relation field that this scalar field is a part of.
|
||||
relation_field_info: Option<RelationFieldInfo<'a>>,
|
||||
},
|
||||
Relation {
|
||||
relation_info: RelationInfo<'a>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'a> FieldType<'a> {
|
||||
fn new(field: &'a dml::Field, model: &str, datamodel: &'a dml::Datamodel) -> Self {
|
||||
match field.field_type() {
|
||||
dml::FieldType::Scalar(_, _, _) => FieldType::Scalar {
|
||||
relation_field_info: {
|
||||
datamodel
|
||||
.find_model(model)
|
||||
.unwrap()
|
||||
.fields()
|
||||
.find_map(|relation_field| {
|
||||
relation_field
|
||||
.as_relation_field()
|
||||
.and_then(|relation_field_data| {
|
||||
relation_field_data
|
||||
.relation_info
|
||||
.fields
|
||||
.iter()
|
||||
.position(|rf_name| rf_name == field.name())
|
||||
.map(|pos| (relation_field_data, pos))
|
||||
})
|
||||
.and_then(|(relation_field_data, i)| {
|
||||
datamodel
|
||||
.models()
|
||||
.find(|relation_model| {
|
||||
relation_model.name
|
||||
== relation_field_data.relation_info.to
|
||||
})
|
||||
.and_then(|relation_model| {
|
||||
relation_model
|
||||
.fields()
|
||||
.find(|referenced_field| {
|
||||
referenced_field.name()
|
||||
== relation_field_data
|
||||
.relation_info
|
||||
.references[i]
|
||||
})
|
||||
.map(|f| (relation_model, f))
|
||||
})
|
||||
})
|
||||
.map(|(ref_model, ref_field)| {
|
||||
(relation_field.name(), &ref_model.name, ref_field.name())
|
||||
})
|
||||
})
|
||||
.map(|(rel, ref_model, ref_field)| {
|
||||
RelationFieldInfo::new(rel, ref_model, ref_field)
|
||||
})
|
||||
},
|
||||
},
|
||||
dml::FieldType::Relation(_) => FieldType::Relation {
|
||||
relation_info: {
|
||||
field
|
||||
.as_relation_field()
|
||||
.filter(|rf| !OPERATION_MODELS.contains(&rf.relation_info.to.as_str()))
|
||||
.map(|rf| {
|
||||
RelationInfo::new(
|
||||
&rf.relation_info.to,
|
||||
&rf.relation_info.fields,
|
||||
&rf.relation_info.references,
|
||||
)
|
||||
})
|
||||
.unwrap()
|
||||
},
|
||||
},
|
||||
t => unimplemented!("Unsupported field type: {:?}", t),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RelationFieldInfo<'a> {
|
||||
/// Field on the same model that represents the relation
|
||||
pub relation: &'a str,
|
||||
pub referenced_model: &'a str,
|
||||
/// Scalar field on the referenced model that matches the scalar on the same model
|
||||
pub referenced_field: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> RelationFieldInfo<'a> {
|
||||
pub fn new(relation: &'a str, referenced_model: &'a str, referenced_field: &'a str) -> Self {
|
||||
Self {
|
||||
relation,
|
||||
referenced_model,
|
||||
referenced_field,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RelationInfo<'a> {
|
||||
pub to: &'a str,
|
||||
pub fields: &'a Vec<String>,
|
||||
pub references: &'a Vec<String>,
|
||||
}
|
||||
|
||||
impl<'a> RelationInfo<'a> {
|
||||
pub fn new(to: &'a str, fields: &'a Vec<String>, references: &'a Vec<String>) -> Self {
|
||||
Self {
|
||||
to,
|
||||
fields,
|
||||
references,
|
||||
}
|
||||
}
|
||||
}
|
||||
39
crates/sync/src/datamodel/mod.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
mod field;
|
||||
mod model;
|
||||
|
||||
pub use field::*;
|
||||
pub use model::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
pub const OPERATION_MODELS: &[&str] = &["OwnedOperation", "SharedOperation", "RelationOperation"];
|
||||
|
||||
pub struct Datamodel<'a> {
|
||||
pub prisma: &'a dml::Datamodel,
|
||||
pub models: Vec<Model<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Datamodel<'a> {
|
||||
pub fn model(&self, name: &str) -> Option<&'a Model> {
|
||||
self.models.iter().find(|m| m.name == name)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a dml::Datamodel> for Datamodel<'a> {
|
||||
type Error = String;
|
||||
fn try_from(datamodel: &'a dml::Datamodel) -> Result<Self, Self::Error> {
|
||||
let models = datamodel
|
||||
.models
|
||||
.iter()
|
||||
.filter(|m| !OPERATION_MODELS.contains(&m.name.as_str()))
|
||||
.map(|m| Model::new(m, datamodel))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let datamodel = Self {
|
||||
prisma: datamodel,
|
||||
models,
|
||||
};
|
||||
|
||||
Ok(datamodel)
|
||||
}
|
||||
}
|
||||
314
crates/sync/src/datamodel/model.rs
Normal file
@@ -0,0 +1,314 @@
|
||||
use std::{ops::Deref, str::FromStr};
|
||||
|
||||
use crate::attribute::{Attribute, AttributeFieldValue};
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Model<'a> {
|
||||
pub prisma: &'a dml::Model,
|
||||
pub typ: ModelType,
|
||||
pub fields: Vec<Field<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Model<'a> {
|
||||
pub fn new(model: &'a dml::Model, datamodel: &'a dml::Datamodel) -> Result<Self, String> {
|
||||
let crdt_attribute = model
|
||||
.documentation
|
||||
.as_ref()
|
||||
.map(Attribute::parse)
|
||||
.map(Result::unwrap)
|
||||
.unwrap();
|
||||
|
||||
let fields = model
|
||||
.fields()
|
||||
.filter(|f| {
|
||||
f.as_relation_field()
|
||||
.filter(|rf| OPERATION_MODELS.contains(&rf.relation_info.to.as_str()))
|
||||
.is_none()
|
||||
})
|
||||
.map(|f| Field::new(f, &model.name, datamodel))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let typ = ModelType::from_attribute(&crdt_attribute, &fields, model)?;
|
||||
|
||||
let model = Self {
|
||||
prisma: model,
|
||||
typ,
|
||||
fields,
|
||||
};
|
||||
|
||||
Ok(model)
|
||||
}
|
||||
|
||||
pub fn field(&self, name: &str) -> Option<&Field<'a>> {
|
||||
self.fields.iter().find(|f| f.name() == name)
|
||||
}
|
||||
|
||||
pub fn is_sync_id(&self, field: &str) -> bool {
|
||||
match &self.typ {
|
||||
ModelType::Local { id } => id.is_sync_id(field),
|
||||
ModelType::Owned { id, .. } => id.is_sync_id(field),
|
||||
ModelType::Shared { id, .. } => id.is_sync_id(field),
|
||||
ModelType::Relation { item, group } => {
|
||||
item.is_sync_id(field) || group.is_sync_id(field)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_pk(&self, field: &str) -> bool {
|
||||
self.primary_key
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.fields
|
||||
.iter()
|
||||
.any(|f| f.name == field)
|
||||
}
|
||||
|
||||
pub fn sync_id_for_pk(&self, primary_key: &str) -> Option<&Field<'a>> {
|
||||
let pk_index = self
|
||||
.primary_key
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.fields
|
||||
.iter()
|
||||
.position(|f| f.name == primary_key);
|
||||
|
||||
pk_index
|
||||
.and_then(|pk_index| match &self.typ {
|
||||
ModelType::Local { id } => id.at_index(pk_index),
|
||||
ModelType::Owned { id, .. } => id.at_index(pk_index),
|
||||
ModelType::Shared { id, .. } => id.at_index(pk_index),
|
||||
ModelType::Relation { item, group } => {
|
||||
item.at_index(0).or_else(|| group.at_index(0))
|
||||
}
|
||||
})
|
||||
.and_then(|f| self.field(f))
|
||||
}
|
||||
|
||||
/// Gets the scalar sync id fields for a model, along with the (possibly) foreign field
|
||||
/// that their types should be resolved from.
|
||||
///
|
||||
/// For example, a scalar field will have no difference between the first and second element.
|
||||
/// A relation, however, will result in the first element being the model's scalar field,
|
||||
/// and the second element being the foreign scalar field. It is important to note that these foreign
|
||||
/// fields could be primary keys that map to sync ids, and this should be checked.
|
||||
pub fn scalar_sync_id_fields(
|
||||
&'a self,
|
||||
datamodel: &'a Datamodel,
|
||||
) -> impl Iterator<Item = (&'a Field, &'a Field)> {
|
||||
self.fields
|
||||
.iter()
|
||||
.filter(|f| self.is_sync_id(f.name()))
|
||||
.flat_map(|field| match &field.typ {
|
||||
FieldType::Scalar { .. } => {
|
||||
vec![(field, field)]
|
||||
}
|
||||
FieldType::Relation { relation_info } => relation_info
|
||||
.fields
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, field)| {
|
||||
let relation_model = datamodel.model(relation_info.to).unwrap();
|
||||
// Scalar field on the relation model. Could be a local id,
|
||||
// so crdt type must be used
|
||||
let referenced_field =
|
||||
relation_model.field(&relation_info.references[i]).unwrap();
|
||||
|
||||
(self.field(field).unwrap(), referenced_field)
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for Model<'a> {
|
||||
type Target = dml::Model;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.prisma
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ModelType {
|
||||
Local {
|
||||
id: SyncIDMapping,
|
||||
},
|
||||
Owned {
|
||||
owner: String,
|
||||
id: SyncIDMapping,
|
||||
},
|
||||
Shared {
|
||||
id: SyncIDMapping,
|
||||
create: SharedCreateType,
|
||||
},
|
||||
Relation {
|
||||
item: SyncIDMapping,
|
||||
group: SyncIDMapping,
|
||||
},
|
||||
}
|
||||
|
||||
impl ModelType {
|
||||
pub fn from_attribute(
|
||||
attribute: &Attribute,
|
||||
fields: &[Field],
|
||||
model: &dml::Model,
|
||||
) -> Result<Self, String> {
|
||||
let ret = match attribute.name {
|
||||
"local" => {
|
||||
let id = SyncIDMapping::from_attribute(attribute.field("id"), fields, model)?;
|
||||
|
||||
ModelType::Local { id }
|
||||
}
|
||||
"owned" => {
|
||||
let id = SyncIDMapping::from_attribute(attribute.field("id"), fields, model)?;
|
||||
|
||||
let owner = attribute
|
||||
.field("owner")
|
||||
.ok_or_else(|| "Missing owner field".to_string())
|
||||
.map(|owner| owner.as_single().expect("Owner field must be a string"))
|
||||
.and_then(|owner| {
|
||||
fields
|
||||
.iter()
|
||||
.find(|f| f.name() == owner)
|
||||
.map(|f| f.name().to_string())
|
||||
.ok_or(format!("Unknown owner field {}", owner))
|
||||
})?;
|
||||
|
||||
ModelType::Owned { id, owner }
|
||||
}
|
||||
"shared" => {
|
||||
let id = SyncIDMapping::from_attribute(attribute.field("id"), fields, model)?;
|
||||
|
||||
let create = attribute
|
||||
.field("create")
|
||||
.map(|create| create.as_single().expect("create field must be a string"))
|
||||
.map(SharedCreateType::from_str)
|
||||
.unwrap_or(Ok(SharedCreateType::Unique))?;
|
||||
|
||||
ModelType::Shared { id, create }
|
||||
}
|
||||
"relation" => {
|
||||
let item = SyncIDMapping::from_attribute(
|
||||
Some(
|
||||
attribute
|
||||
.field("item")
|
||||
.expect("@relation attribute missing `item` field"),
|
||||
),
|
||||
fields,
|
||||
model,
|
||||
)?;
|
||||
let group = SyncIDMapping::from_attribute(
|
||||
Some(
|
||||
attribute
|
||||
.field("group")
|
||||
.expect("@relation attribute missing `group` field"),
|
||||
),
|
||||
fields,
|
||||
model,
|
||||
)?;
|
||||
|
||||
ModelType::Relation { item, group }
|
||||
}
|
||||
name => Err(format!("Invalid attribute type {name}"))?,
|
||||
};
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SyncIDMapping {
|
||||
Single(String),
|
||||
Compound(Vec<String>),
|
||||
}
|
||||
|
||||
impl SyncIDMapping {
|
||||
pub fn from_attribute(
|
||||
attr_value: Option<&AttributeFieldValue>,
|
||||
fields: &[Field],
|
||||
model: &dml::Model,
|
||||
) -> Result<Self, String> {
|
||||
let primary_key = model
|
||||
.primary_key
|
||||
.as_ref()
|
||||
.ok_or(format!("Model {} has no primary key", model.name))?;
|
||||
|
||||
attr_value
|
||||
.map(|attr_value| match attr_value {
|
||||
AttributeFieldValue::Single(field) => {
|
||||
fields
|
||||
.iter()
|
||||
.find(|f| f.name() == *field)
|
||||
.ok_or(format!("Unknown field {}", field))?;
|
||||
|
||||
Ok(SyncIDMapping::Single(field.to_string()))
|
||||
}
|
||||
AttributeFieldValue::List(field_list) => {
|
||||
if primary_key.fields.len() != field_list.len() {
|
||||
return Err(format!(
|
||||
"Sync ID for model {} has inconsistent number of fields",
|
||||
model.name,
|
||||
));
|
||||
}
|
||||
|
||||
field_list
|
||||
.iter()
|
||||
.map(|name| {
|
||||
fields
|
||||
.iter()
|
||||
.find(|f| f.name() == *name)
|
||||
.map(|f| f.name().to_string())
|
||||
})
|
||||
.collect::<Option<_>>()
|
||||
.map(SyncIDMapping::Compound)
|
||||
.ok_or(format!("Invalid sync ID for model {}", model.name))
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
Ok(match primary_key.fields.len() {
|
||||
1 => SyncIDMapping::Single(primary_key.fields[0].name.to_string()),
|
||||
_ => SyncIDMapping::Compound(
|
||||
primary_key
|
||||
.fields
|
||||
.iter()
|
||||
.map(|f| f.name.to_string())
|
||||
.collect(),
|
||||
),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_sync_id(&self, field: &str) -> bool {
|
||||
match self {
|
||||
Self::Single(v) => field == v,
|
||||
Self::Compound(mappings) => mappings.iter().any(|v| field == v),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn at_index(&self, i: usize) -> Option<&str> {
|
||||
match self {
|
||||
Self::Single(v) => Some(v),
|
||||
Self::Compound(mappings) => mappings.get(i).map(|v| v.as_str()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SharedCreateType {
|
||||
Unique,
|
||||
Atomic,
|
||||
}
|
||||
|
||||
impl FromStr for SharedCreateType {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let ret = match s {
|
||||
"Unique" => SharedCreateType::Unique,
|
||||
"Atomic" => SharedCreateType::Atomic,
|
||||
s => Err(format!("Invalid create type {}", s))?,
|
||||
};
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
}
|
||||
182
crates/sync/src/db.rs
Normal file
@@ -0,0 +1,182 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rspc::Type;
|
||||
use serde::*;
|
||||
use serde_json::*;
|
||||
use uhlc::{HLCBuilder, Timestamp, HLC};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::crdt::*;
|
||||
|
||||
#[derive(Default, Debug, Serialize, Type, Clone)]
|
||||
pub struct Color {
|
||||
pub red: u8,
|
||||
pub green: u8,
|
||||
pub blue: u8,
|
||||
}
|
||||
|
||||
/// Unique Shared
|
||||
#[derive(Default, Debug, Serialize, Type, Clone)]
|
||||
pub struct Tag {
|
||||
pub color: Color,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// Atomic Shared
|
||||
#[derive(Default, Debug, Serialize, Type, Clone)]
|
||||
pub struct File {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// Owned
|
||||
#[derive(Serialize, Deserialize, Debug, Type, Clone)]
|
||||
pub struct FilePath {
|
||||
pub path: String,
|
||||
pub file: Option<i32>,
|
||||
}
|
||||
|
||||
pub struct Db {
|
||||
pub files: HashMap<i32, File>,
|
||||
pub file_paths: HashMap<i32, FilePath>,
|
||||
pub tags: HashMap<Uuid, Tag>,
|
||||
_operations: Vec<CRDTOperation>,
|
||||
_clock: HLC,
|
||||
_node: Uuid,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Db {
|
||||
fn fmt(&self, f: &mut __private::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Db")
|
||||
.field("files", &self.files)
|
||||
.field("file_paths", &self.file_paths)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Db {
|
||||
pub fn new(node: Uuid) -> Self {
|
||||
Self {
|
||||
files: Default::default(),
|
||||
file_paths: Default::default(),
|
||||
tags: Default::default(),
|
||||
_node: node,
|
||||
_clock: HLCBuilder::new().with_id(node.into()).build(),
|
||||
_operations: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_crdt_operation(&self, typ: CRDTOperationType) -> CRDTOperation {
|
||||
let hlc_timestamp = self._clock.new_timestamp();
|
||||
|
||||
let op = CRDTOperation {
|
||||
node: self._node,
|
||||
timestamp: *hlc_timestamp.get_time(),
|
||||
id: Uuid::new_v4(),
|
||||
typ,
|
||||
};
|
||||
|
||||
dbg!(op)
|
||||
}
|
||||
|
||||
fn compare_messages(&self, operations: Vec<CRDTOperation>) -> Vec<(CRDTOperation, bool)> {
|
||||
operations
|
||||
.into_iter()
|
||||
.map(|op| {
|
||||
let old = match &op.typ {
|
||||
CRDTOperationType::Owned(_) => false,
|
||||
CRDTOperationType::Shared(shared_op) => {
|
||||
let similar_op = self._operations.iter().find(|find_op| {
|
||||
if let CRDTOperationType::Shared(find_shared_op) = &find_op.typ {
|
||||
shared_op.model == find_shared_op.model
|
||||
&& shared_op.record_id == find_shared_op.record_id
|
||||
&& op.timestamp >= find_op.timestamp
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
similar_op
|
||||
.map(|similar_op| similar_op.timestamp == op.timestamp)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
};
|
||||
|
||||
(op, old)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn receive_crdt_operations(&mut self, ops: Vec<CRDTOperation>) {
|
||||
for op in &ops {
|
||||
self._clock
|
||||
.update_with_timestamp(&Timestamp::new(op.timestamp, op.node.into()))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
for (op, old) in self.compare_messages(ops) {
|
||||
let push_op = op.clone();
|
||||
|
||||
if !old {
|
||||
match op.typ {
|
||||
CRDTOperationType::Shared(shared_op) => match shared_op.model.as_str() {
|
||||
"File" => {
|
||||
let id = from_value(shared_op.record_id).unwrap();
|
||||
|
||||
match shared_op.data {
|
||||
SharedOperationData::Create(SharedOperationCreateData::Atomic) => {
|
||||
self.files.insert(id, Default::default());
|
||||
}
|
||||
SharedOperationData::Update { field, value } => {
|
||||
let mut file = self.files.get_mut(&id).unwrap();
|
||||
|
||||
match field.as_str() {
|
||||
"name" => {
|
||||
file.name = from_value(value).unwrap();
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
SharedOperationData::Delete => {
|
||||
self.files.remove(&id).unwrap();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
CRDTOperationType::Owned(owned_op) => match owned_op.model.as_str() {
|
||||
"FilePath" => {
|
||||
for item in owned_op.items {
|
||||
let id = from_value(item.id).unwrap();
|
||||
|
||||
match item.data {
|
||||
OwnedOperationData::Create(data) => {
|
||||
self.file_paths
|
||||
.insert(id, from_value(Value::Object(data)).unwrap());
|
||||
}
|
||||
OwnedOperationData::Update(data) => {
|
||||
let obj = self.file_paths.get_mut(&id).unwrap();
|
||||
|
||||
for (key, value) in data {
|
||||
match key.as_str() {
|
||||
"path" => obj.path = from_value(value).unwrap(),
|
||||
"file" => obj.file = from_value(value).unwrap(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
OwnedOperationData::Delete => {
|
||||
self.file_paths.remove(&id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
self._operations.push(push_op)
|
||||
}
|
||||
}
|
||||
}
|
||||
182
crates/sync/src/generator/client.rs
Normal file
@@ -0,0 +1,182 @@
|
||||
use crate::generator::prelude::*;
|
||||
|
||||
/// Generates the `_create_operation` function for the CRDT client
|
||||
fn create_operation_fn() -> TokenStream {
|
||||
quote! {
|
||||
pub async fn _create_operation(&self, typ: ::prisma_crdt::CRDTOperationType) {
|
||||
let timestamp = ::uhlc::NTP64(0); // TODO: actual timestamps
|
||||
|
||||
let timestamp_bytes = vec![0];
|
||||
|
||||
match &typ {
|
||||
::prisma_crdt::CRDTOperationType::Shared(::prisma_crdt::SharedOperation {
|
||||
record_id,
|
||||
model,
|
||||
data,
|
||||
}) => {
|
||||
let (kind, data) = match data {
|
||||
::prisma_crdt::SharedOperationData::Create(typ) => {
|
||||
("c".to_string(), ::serde_json::to_vec(typ).unwrap())
|
||||
}
|
||||
::prisma_crdt::SharedOperationData::Update { field, value } => {
|
||||
("u".to_string() + field, ::serde_json::to_vec(value).unwrap())
|
||||
}
|
||||
::prisma_crdt::SharedOperationData::Delete => ("d".to_string(), vec![]),
|
||||
};
|
||||
|
||||
self.client
|
||||
.shared_operation()
|
||||
.create(
|
||||
timestamp_bytes,
|
||||
::serde_json::to_vec(&record_id).unwrap(),
|
||||
kind,
|
||||
model.to_string(),
|
||||
data,
|
||||
crate::prisma::node::local_id::equals(self.node_local_id),
|
||||
vec![],
|
||||
)
|
||||
.exec()
|
||||
.await;
|
||||
}
|
||||
::prisma_crdt::CRDTOperationType::Owned(op) => {
|
||||
self.client
|
||||
.owned_operation()
|
||||
.create(
|
||||
timestamp_bytes,
|
||||
::serde_json::to_vec(op).unwrap(),
|
||||
crate::prisma::node::local_id::equals(self.node_local_id),
|
||||
vec![],
|
||||
)
|
||||
.exec()
|
||||
.await;
|
||||
}
|
||||
::prisma_crdt::CRDTOperationType::Relation(::prisma_crdt::RelationOperation {
|
||||
relation,
|
||||
relation_item,
|
||||
relation_group,
|
||||
data,
|
||||
}) => {
|
||||
let (kind, data) = match data {
|
||||
::prisma_crdt::RelationOperationData::Create => ("c".to_string(), vec![]),
|
||||
::prisma_crdt::RelationOperationData::Update { field, value } => {
|
||||
("u".to_string() + field, ::serde_json::to_vec(value).unwrap())
|
||||
}
|
||||
::prisma_crdt::RelationOperationData::Delete => ("d".to_string(), vec![]),
|
||||
};
|
||||
|
||||
self.client
|
||||
.relation_operation()
|
||||
.create(
|
||||
timestamp_bytes,
|
||||
relation.to_string(),
|
||||
::serde_json::to_vec(&relation_item).unwrap(),
|
||||
::serde_json::to_vec(&relation_group).unwrap(),
|
||||
kind,
|
||||
data,
|
||||
crate::prisma::node::local_id::equals(self.node_local_id),
|
||||
vec![],
|
||||
)
|
||||
.exec()
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
let op = ::prisma_crdt::CRDTOperation::new(self.node_id.clone(), timestamp, typ);
|
||||
|
||||
self.operation_sender.send(op).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates action getters for each model for the CRDT Client
|
||||
fn actions_accessors(datamodel: DatamodelRef) -> Vec<TokenStream> {
|
||||
datamodel
|
||||
.models
|
||||
.iter()
|
||||
.map(|model| {
|
||||
let name_snake = snake_ident(&model.name);
|
||||
|
||||
match &model.typ {
|
||||
ModelType::Local { .. } => quote! {
|
||||
pub fn #name_snake(&self) -> crate::prisma::#name_snake::Actions {
|
||||
self.client.#name_snake()
|
||||
}
|
||||
},
|
||||
_ => quote! {
|
||||
pub fn #name_snake(&self) -> super::#name_snake::Actions {
|
||||
super::#name_snake::Actions::new(self)
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Generates the `_prisma` module and its `PrismaCRDTClient` struct
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// mod _prisma {
|
||||
/// pub struct PrismaCRDTClient {
|
||||
/// pub(super) client: crate::prisma::PrismaClient,
|
||||
/// pub node_id: Vec<u8>,
|
||||
/// pub node_local_id: i32,
|
||||
/// operation_sender: ::tokio::sync::mpsc::Sender<::prisma_crdt::CRDTOperation>
|
||||
/// }
|
||||
///
|
||||
/// impl PrismaCRDTClient {
|
||||
/// pub(super) fn _new(
|
||||
/// client: crate::prisma::PrismaClient,
|
||||
/// (node_id, node_local_id): Vec<u8, i32>,
|
||||
/// operation_sender: ::tokio::sync::mpsc::Sender<::prisma_crdt::CRDTOperation>
|
||||
/// ) -> Self {
|
||||
/// Self {
|
||||
/// client,
|
||||
/// operation_sender,
|
||||
/// node_id,
|
||||
/// node_local_id
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// pub async fn _create_operation(..) { .. }
|
||||
///
|
||||
/// ..
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn generate(datamodel: DatamodelRef) -> TokenStream {
|
||||
let create_operation_fn = create_operation_fn();
|
||||
|
||||
let actions_accessors = actions_accessors(datamodel);
|
||||
|
||||
quote! {
|
||||
mod _prisma {
|
||||
pub struct PrismaCRDTClient {
|
||||
pub(super) client: crate::prisma::PrismaClient,
|
||||
pub node_id: Vec<u8>,
|
||||
pub node_local_id: i32,
|
||||
operation_sender: ::tokio::sync::mpsc::Sender<::prisma_crdt::CRDTOperation>,
|
||||
}
|
||||
|
||||
impl PrismaCRDTClient {
|
||||
pub(super) fn _new(
|
||||
client: crate::prisma::PrismaClient,
|
||||
(node_id, node_local_id): (Vec<u8>, i32),
|
||||
operation_sender: ::tokio::sync::mpsc::Sender<::prisma_crdt::CRDTOperation>,
|
||||
) -> Self {
|
||||
Self {
|
||||
client,
|
||||
operation_sender,
|
||||
node_id,
|
||||
node_local_id,
|
||||
}
|
||||
}
|
||||
|
||||
#create_operation_fn
|
||||
|
||||
#(#actions_accessors)*
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
55
crates/sync/src/generator/mod.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
mod client;
|
||||
mod model;
|
||||
|
||||
use super::prelude::*;
|
||||
use super::*;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct PrismaCRDTGenerator {}
|
||||
|
||||
impl PrismaGenerator for PrismaCRDTGenerator {
|
||||
const NAME: &'static str = "Prisma CRDT Generator";
|
||||
const DEFAULT_OUTPUT: &'static str = "./prisma-crdt.rs";
|
||||
|
||||
fn generate(self, args: GenerateArgs) -> String {
|
||||
let datamodel =
|
||||
datamodel::Datamodel::try_from(&args.dml).expect("Failed to construct datamodel");
|
||||
let datamodel_ref = prelude::DatamodelRef(&datamodel);
|
||||
|
||||
let header = quote! {
|
||||
#![allow(clippy::all)]
|
||||
|
||||
pub async fn new_client(
|
||||
prisma_client: crate::prisma::PrismaClient,
|
||||
node_id: Vec<u8>,
|
||||
node_local_id: i32
|
||||
) -> (
|
||||
_prisma::PrismaCRDTClient,
|
||||
::tokio::sync::mpsc::Receiver<::prisma_crdt::CRDTOperation>,
|
||||
) {
|
||||
let (tx, rx) = ::tokio::sync::mpsc::channel(64);
|
||||
|
||||
let crdt_client = _prisma::PrismaCRDTClient::_new(prisma_client, (node_id, node_local_id), tx);
|
||||
(crdt_client, rx)
|
||||
}
|
||||
pub use _prisma::*;
|
||||
};
|
||||
|
||||
let client = client::generate(datamodel_ref);
|
||||
|
||||
let models = datamodel
|
||||
.models
|
||||
.iter()
|
||||
.map(|model| model::generate(ModelRef::new(model, datamodel_ref)));
|
||||
|
||||
let output = quote! {
|
||||
#header
|
||||
|
||||
#(#models)*
|
||||
|
||||
#client
|
||||
};
|
||||
|
||||
output.to_string()
|
||||
}
|
||||
}
|
||||
90
crates/sync/src/generator/model/actions.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
use crate::generator::prelude::*;
|
||||
|
||||
use super::create;
|
||||
|
||||
/// Generates struct definition for a model's `Actions` struct
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// pub struct Actions<'a> {
|
||||
/// client: &'a super::_prisma::PrismaCRDTClient
|
||||
/// }
|
||||
///
|
||||
/// impl<'a> Actions<'a> {
|
||||
/// pub(super) fn new(client: &'a super::_prisma::PrismaCRDTClient) -> Self {
|
||||
/// Self { client }
|
||||
/// }
|
||||
///
|
||||
/// pub fn create(..) {
|
||||
/// ..
|
||||
/// }
|
||||
///
|
||||
/// pub fn find_unique(
|
||||
/// self,
|
||||
/// param: crate::prisma::#model::UniqueWhereParam
|
||||
/// ) -> crate::prisma::#model::FindUnique<'a> {
|
||||
/// self.client.client.#model().find_unique(param)
|
||||
/// }
|
||||
///
|
||||
/// pub fn find_many(
|
||||
/// self,
|
||||
/// params: Vec<crate::prisma::#model::WhereParam>
|
||||
/// ) -> crate::prisma::#model::FindMany<'a> {
|
||||
/// self.client.client.#model().find_many(params)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn definition(model: ModelRef) -> TokenStream {
|
||||
let name = snake_ident(&model.name);
|
||||
|
||||
let create_fn = create::action_method(model);
|
||||
|
||||
quote! {
|
||||
pub struct Actions<'a> {
|
||||
client: &'a super::_prisma::PrismaCRDTClient,
|
||||
}
|
||||
|
||||
impl<'a> Actions<'a> {
|
||||
pub(super) fn new(client: &'a super::_prisma::PrismaCRDTClient) -> Self {
|
||||
Self { client }
|
||||
}
|
||||
|
||||
#create_fn
|
||||
|
||||
pub fn find_unique(
|
||||
self,
|
||||
param: crate::prisma::#name::UniqueWhereParam,
|
||||
) -> crate::prisma::#name::FindUnique<'a> {
|
||||
self.client.client.#name().find_unique(param)
|
||||
}
|
||||
|
||||
pub fn find_many(
|
||||
self,
|
||||
params: Vec<crate::prisma::#name::WhereParam>,
|
||||
) -> crate::prisma::#name::FindMany<'a> {
|
||||
self.client.client.#name().find_many(params)
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
self,
|
||||
_where: crate::prisma::#name::UniqueWhereParam,
|
||||
set_params: Vec<SetParam>,
|
||||
) -> Update<'a> {
|
||||
Update {
|
||||
client: self.client,
|
||||
where_param: _where,
|
||||
set_params,
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn delete(self, param: crate::prisma::#name::UniqueWhereParam) -> Delete<'a> {
|
||||
// Delete {
|
||||
// client: self.client,
|
||||
// r#where: param,
|
||||
// with_params: vec![],
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
186
crates/sync/src/generator/model/create.rs
Normal file
@@ -0,0 +1,186 @@
|
||||
use crate::generator::prelude::*;
|
||||
|
||||
use super::{create_params, owned, relation, shared};
|
||||
|
||||
/// Generates a call to the underlying Prisma client's `create` method for
|
||||
/// the given model
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// self
|
||||
/// .crdt_client
|
||||
/// .client
|
||||
/// .user()
|
||||
/// .create(
|
||||
/// self.set_params.name.clone(),
|
||||
/// self.set_params.profile_id.clone),
|
||||
/// self.set_params._params.clone().into_iter().map(Into::into).collect()
|
||||
/// )
|
||||
/// .exec()
|
||||
/// .await?
|
||||
/// ```
|
||||
pub fn prisma_create(model: ModelRef) -> TokenStream {
|
||||
let model_name = snake_ident(&model.name);
|
||||
|
||||
let create_args = model
|
||||
.fields
|
||||
.iter()
|
||||
.filter(|f| {
|
||||
f.required_on_create()
|
||||
&& f.as_scalar_field()
|
||||
.map(|sf| !model.scalar_field_has_relation(sf))
|
||||
.unwrap_or(true)
|
||||
})
|
||||
.map(|field| {
|
||||
let field_name_snake = snake_ident(field.name());
|
||||
|
||||
match &field.typ {
|
||||
FieldType::Relation {relation_info} => {
|
||||
let relation_model_snake = snake_ident(relation_info.to);
|
||||
|
||||
if relation_info.fields.len() == 1 {
|
||||
let relation_field_snake = snake_ident(&relation_info.fields[0]);
|
||||
let referenced_field_snake = snake_ident(&relation_info.references[0]);
|
||||
|
||||
quote!(crate::prisma::#relation_model_snake::#referenced_field_snake::equals(self.set_params.#relation_field_snake.clone()))
|
||||
} else {
|
||||
todo!()
|
||||
}
|
||||
},
|
||||
_ => quote!(self.set_params.#field_name_snake.clone()),
|
||||
}
|
||||
});
|
||||
|
||||
quote! {
|
||||
self
|
||||
.crdt_client
|
||||
.client
|
||||
.#model_name()
|
||||
.create(
|
||||
#(#create_args,)*
|
||||
self.set_params._params.clone().into_iter().map(Into::into).collect(),
|
||||
)
|
||||
.exec()
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates the definition for a model's `Create` struct
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// pub struct Create<'a> {
|
||||
/// crdt_client: &'a super::_prisma::PrismaCRDTClient,
|
||||
/// set_params: CreateParams,
|
||||
/// with_params: Vec<crate::prisma::#model_name_snake::WithParam>
|
||||
/// }
|
||||
///
|
||||
/// impl<'a> Create<'a> {
|
||||
/// pub(super) fn new(
|
||||
/// crdt_client: &'a super::_prisma::PrismaCRDTClient,
|
||||
/// set_params: CreateParams,
|
||||
/// with_params: Vec<crate::prisma::#model_name_snake::WithParam>
|
||||
/// )
|
||||
///
|
||||
/// ..
|
||||
///
|
||||
/// pub async fn exec(
|
||||
/// self
|
||||
/// ) -> Result<crate::prisma::#model_name_snake::WithParam, crate::prisma::QueryError> {
|
||||
/// let res = self
|
||||
/// .crdt_client
|
||||
/// .client
|
||||
/// .#model_name_snake()
|
||||
/// .create(
|
||||
/// ..
|
||||
/// )
|
||||
/// .exec()
|
||||
/// .await?;
|
||||
///
|
||||
/// ..
|
||||
///
|
||||
/// Ok(res)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn struct_definition(model: ModelRef) -> TokenStream {
|
||||
let model_name_snake = snake_ident(&model.name);
|
||||
|
||||
let create_call = prisma_create(model);
|
||||
|
||||
let exec_body = match model.typ {
|
||||
ModelType::Owned { .. } => owned::create_exec_body(model),
|
||||
ModelType::Shared { .. } => shared::create_exec_body(model),
|
||||
ModelType::Relation { .. } => relation::create_exec_body(model),
|
||||
// SAFETY: Local models don't have method overrides
|
||||
ModelType::Local { .. } => unreachable!(),
|
||||
};
|
||||
|
||||
quote! {
|
||||
pub struct Create<'a> {
|
||||
crdt_client: &'a super::_prisma::PrismaCRDTClient,
|
||||
set_params: CreateParams,
|
||||
with_params: Vec<crate::prisma::#model_name_snake::WithParam>,
|
||||
}
|
||||
|
||||
impl<'a> Create<'a> {
|
||||
pub(super) fn new(
|
||||
crdt_client: &'a super::_prisma::PrismaCRDTClient,
|
||||
set_params: CreateParams,
|
||||
with_params: Vec<crate::prisma::#model_name_snake::WithParam>,
|
||||
) -> Self {
|
||||
Self {
|
||||
crdt_client,
|
||||
set_params,
|
||||
with_params,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with(mut self, param: impl Into<crate::prisma::#model_name_snake::WithParam>) -> Self {
|
||||
self.with_params.push(param.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn exec(
|
||||
self,
|
||||
) -> Result<crate::prisma::#model_name_snake::Data, crate::prisma::QueryError> {
|
||||
let res = #create_call;
|
||||
|
||||
#exec_body
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a model's `Actions::create` method
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// pub fn create(self, name: String, profile_id: i32, _params: Vec<SetParam>) -> Create<'a> {
|
||||
/// Create::new(
|
||||
/// self.client,
|
||||
/// CreateParams { .. },
|
||||
/// vec![]
|
||||
/// )
|
||||
/// }
|
||||
/// ```
|
||||
pub fn action_method(model: ModelRef) -> TokenStream {
|
||||
let args = create_params::args(model, Some(quote!(super)));
|
||||
|
||||
let create_params_constructor = create_params::constructor(model);
|
||||
|
||||
quote! {
|
||||
pub fn create(self, #(#args),*) -> Create<'a> {
|
||||
Create::new(
|
||||
self.client,
|
||||
#create_params_constructor,
|
||||
vec![]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
203
crates/sync/src/generator/model/create_params.rs
Normal file
@@ -0,0 +1,203 @@
|
||||
use crate::generator::prelude::*;
|
||||
|
||||
use super::sync_id;
|
||||
|
||||
/// Generates definitions for a model's `CreateParams` and `CRDTCreateParams` structs
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// #[derive(Clone)]
|
||||
/// pub struct CreateParams {
|
||||
/// pub _params: Vec<SetParam>,
|
||||
/// pub name: String,
|
||||
/// pub profile_id: i32
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Clone, ::serde::Serialize, ::serde::Deserialize)]
|
||||
/// pub struct CRDTCreateParams {
|
||||
/// #[serde(default, skip_serializing_if = "Vec::is_empty", rename = "_")]
|
||||
/// pub _params: Vec<CRDTSetParam>,
|
||||
/// #[serde(flatten)]
|
||||
/// pub _sync_id: SyncID,
|
||||
/// pub name: String,
|
||||
/// pub profile_id: Vec<u8>
|
||||
/// }
|
||||
/// ```
|
||||
pub fn definition(model: ModelRef) -> TokenStream {
|
||||
let required_scalar_fields = model.required_scalar_fields();
|
||||
|
||||
let required_create_params = required_scalar_fields.iter().map(|field| {
|
||||
let field_name_snake = snake_ident(field.name());
|
||||
|
||||
let field_type = match field.field_type() {
|
||||
dml::FieldType::Scalar(_, _, _) => field.type_tokens(),
|
||||
dml::FieldType::Enum(e) => {
|
||||
let enum_name_pascal = pascal_ident(&e);
|
||||
|
||||
quote!(super::#enum_name_pascal)
|
||||
}
|
||||
_ => todo!(),
|
||||
};
|
||||
|
||||
quote!(#field_name_snake: #field_type)
|
||||
});
|
||||
|
||||
let mut scalar_sync_id_fields = model.scalar_sync_id_fields(&model.datamodel);
|
||||
|
||||
let required_crdt_create_params = required_scalar_fields
|
||||
.iter()
|
||||
.filter(|f| !scalar_sync_id_fields.any(|sf| sf.0.name() == f.name()))
|
||||
.map(|field| {
|
||||
let field_type = field.crdt_type_tokens(&model.datamodel);
|
||||
let field_name_snake = snake_ident(field.name());
|
||||
|
||||
quote!(#field_name_snake: #field_type)
|
||||
});
|
||||
|
||||
quote! {
|
||||
#[derive(Clone)]
|
||||
pub struct CreateParams {
|
||||
pub _params: Vec<SetParam>,
|
||||
#(pub #required_create_params),*
|
||||
}
|
||||
|
||||
#[derive(Clone, ::serde::Serialize, ::serde::Deserialize)]
|
||||
pub struct CRDTCreateParams {
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty", rename = "_")]
|
||||
pub _params: Vec<CRDTSetParam>,
|
||||
#[serde(flatten)]
|
||||
pub _sync_id: SyncID,
|
||||
#(pub #required_crdt_create_params),*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a list of a model's `CreateParams` as function arguments
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// name: String, profile_id: i32, _params: Vec<SetParam>
|
||||
/// ```
|
||||
pub fn args(model: ModelRef, namespace: Option<TokenStream>) -> Vec<TokenStream> {
|
||||
let mut required_args = model
|
||||
.required_scalar_fields()
|
||||
.into_iter()
|
||||
.map(|field| {
|
||||
let field_name_snake = snake_ident(field.name());
|
||||
|
||||
let typ = match &field.field_type() {
|
||||
dml::FieldType::Scalar(_, _, _) => field.type_tokens(),
|
||||
dml::FieldType::Enum(e) => {
|
||||
let enum_name_pascal = pascal_ident(e);
|
||||
|
||||
quote!(#(#namespace::)super::#enum_name_pascal)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
quote!(#field_name_snake: #typ)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
required_args.push(quote!(_params: Vec<SetParam>));
|
||||
|
||||
required_args
|
||||
}
|
||||
|
||||
/// Generates a constructor for the `CreateParams` struct
|
||||
/// that assumes all required fields have been declared beforehand.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// CreateParams {
|
||||
/// name,
|
||||
/// profile_id,
|
||||
/// _params
|
||||
/// }
|
||||
/// ```
|
||||
pub fn constructor(model: ModelRef) -> TokenStream {
|
||||
let required_args = model
|
||||
.required_scalar_fields()
|
||||
.into_iter()
|
||||
.map(|field| snake_ident(field.name()));
|
||||
|
||||
quote! {
|
||||
CreateParams {
|
||||
#(#required_args,)*
|
||||
_params
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a constructor for the CRDTCreateParams struct.
|
||||
/// Assumes all required fields are in scope.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// CRDTCreateParams {
|
||||
/// _param: {
|
||||
/// let mut params = vec![];
|
||||
///
|
||||
/// for _param in self.set_params._params {
|
||||
/// params.push(_param.into_crdt(&self.crdt_client).await);
|
||||
/// }
|
||||
///
|
||||
/// params
|
||||
/// },
|
||||
/// _sync_id: sync_id.clone(),
|
||||
/// name: self.set_params.name,
|
||||
/// profile_id: self
|
||||
/// .crdt_client
|
||||
/// .client
|
||||
/// .profile()
|
||||
/// .find_unique(crate::prisma::profile::local_id::equals(self.set_params.profile_id))
|
||||
/// .exec()
|
||||
/// .await
|
||||
/// .unwrap()
|
||||
/// .unwrap()
|
||||
/// .local_id
|
||||
/// }
|
||||
/// ```
|
||||
pub fn crdt_constructor(model: ModelRef) -> TokenStream {
|
||||
let crdt_create_params = model
|
||||
.fields()
|
||||
.into_iter()
|
||||
.filter(|f| {
|
||||
f.is_scalar_field()
|
||||
&& f.required_on_create()
|
||||
&& model
|
||||
.scalar_sync_id_fields(&model.datamodel)
|
||||
.all(|(sf, _)| sf.name() != f.name())
|
||||
})
|
||||
.map(|field| {
|
||||
let field_name_snake = snake_ident(field.name());
|
||||
|
||||
let value = sync_id::scalar_field_to_crdt(
|
||||
field,
|
||||
quote!(self.crdt_client.client),
|
||||
quote!(self.set_params.#field_name_snake),
|
||||
);
|
||||
|
||||
quote!(#field_name_snake: #value)
|
||||
});
|
||||
|
||||
quote! {
|
||||
CRDTCreateParams {
|
||||
_params: {
|
||||
let mut params = vec![];
|
||||
|
||||
for _param in self.set_params._params {
|
||||
params.push(_param.into_crdt(&self.crdt_client).await);
|
||||
}
|
||||
|
||||
params
|
||||
},
|
||||
_sync_id: sync_id.clone(),
|
||||
#(#crdt_create_params,)*
|
||||
};
|
||||
}
|
||||
}
|
||||
24
crates/sync/src/generator/model/delete.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
// use crate::generator::prelude::*;
|
||||
|
||||
// pub fn generate(model: &Model) -> TokenStream {
|
||||
// let model_name = snake_ident(&model.name);
|
||||
//
|
||||
// quote! {
|
||||
// pub struct Delete<'a> {
|
||||
// client: &'a super::_prisma::PrismaCRDTClient,
|
||||
// where_param: crate::prisma::#model_name::UniqueWhereParam,
|
||||
// with_params: Vec<crate::prisma::#model_name::WithParam>,
|
||||
// }
|
||||
//
|
||||
// impl<'a> Delete<'a> {
|
||||
// pub fn with(mut self, param: impl Into<crate::prisma::location::WithParam>) -> Self {
|
||||
// self.with_params.push(param.into());
|
||||
// self
|
||||
// }
|
||||
//
|
||||
// pub async fn exec(self) -> Result<Option<crate::prisma::#model_name::Data>, crate::prisma::QueryError> {
|
||||
//
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||