Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(webserver, db): Implement github provided repositories #1800

Merged
merged 15 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ee/tabby-db/migrations/0024_github-provided-repos.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE github_provided_repositories;
10 changes: 10 additions & 0 deletions ee/tabby-db/migrations/0024_github-provided-repos.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
CREATE TABLE github_provided_repositories(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
boxbeam marked this conversation as resolved.
Show resolved Hide resolved
github_repository_provider_id INTEGER NOT NULL,
-- vendor_id from https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#list-repositories-for-a-user
vendor_id TEXT NOT NULL,
boxbeam marked this conversation as resolved.
Show resolved Hide resolved
name TEXT NOT NULL,
git_url TEXT NOT NULL,
active BOOLEAN NOT NULL DEFAULT FALSE,
FOREIGN KEY (github_repository_provider_id) REFERENCES github_repository_provider(id) ON DELETE CASCADE
);
Binary file modified ee/tabby-db/schema.sqlite
Binary file not shown.
89 changes: 89 additions & 0 deletions ee/tabby-db/src/github_repository_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ pub struct GithubRepositoryProviderDAO {
pub access_token: Option<String>,
}

#[derive(FromRow)]
pub struct GithubProvidedRepositoryDAO {
pub id: i64,
pub vendor_id: String,
pub github_repository_provider_id: i64,
pub name: String,
pub git_url: String,
pub active: bool,
}

impl DbConn {
pub async fn create_github_provider(
&self,
Expand Down Expand Up @@ -95,4 +105,83 @@ impl DbConn {
.await?;
Ok(providers)
}

pub async fn create_github_provided_repository(
&self,
github_provider_id: i64,
vendor_id: String,
name: String,
git_url: String,
) -> Result<i64> {
let res = query!("INSERT INTO github_provided_repositories (github_repository_provider_id, vendor_id, name, git_url) VALUES (?, ?, ?, ?)",
github_provider_id, vendor_id, name, git_url).execute(&self.pool).await?;
Ok(res.last_insert_rowid())
}

pub async fn delete_github_provided_repository(&self, id: i64) -> Result<()> {
let res = query!("DELETE FROM github_provided_repositories WHERE id = ?", id)
.execute(&self.pool)
.await?;

if res.rows_affected() != 1 {
return Err(anyhow!("Repository not found"));
}
Ok(())
}

pub async fn list_github_provided_repositories(
&self,
provider_ids: Vec<i64>,
limit: Option<usize>,
skip_id: Option<i32>,
backwards: bool,
) -> Result<Vec<GithubProvidedRepositoryDAO>> {
let provider_ids = provider_ids
.into_iter()
.map(|id| id.to_string())
.collect::<Vec<_>>()
.join(", ");
let repos = query_paged_as!(
GithubProvidedRepositoryDAO,
"github_provided_repositories",
[
"id",
"vendor_id",
"name",
"git_url",
"active",
"github_repository_provider_id"
],
limit,
skip_id,
backwards,
(!provider_ids.is_empty())
.then(|| format!("github_repository_provider_id IN ({provider_ids})"))
)
.fetch_all(&self.pool)
.await?;
Ok(repos)
}

pub async fn update_github_provided_repository_active(
&self,
id: i64,
active: bool,
) -> Result<()> {
let not_active = !active;
let res = query!(
"UPDATE github_provided_repositories SET active = ? WHERE id = ? AND active = ?",
active,
id,
not_active
)
.execute(&self.pool)
.await?;

if res.rows_affected() != 1 {
return Err(anyhow!("Repository active status was not changed"));
}

Ok(())
}
}
2 changes: 1 addition & 1 deletion ee/tabby-db/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use cache::Cache;
use cached::TimedSizedCache;
use chrono::{DateTime, NaiveDateTime, Utc};
pub use email_setting::EmailSettingDAO;
pub use github_repository_provider::GithubRepositoryProviderDAO;
pub use github_repository_provider::{GithubProvidedRepositoryDAO, GithubRepositoryProviderDAO};
pub use invitations::InvitationDAO;
pub use job_runs::JobRunDAO;
pub use oauth_credential::OAuthCredentialDAO;
Expand Down
30 changes: 28 additions & 2 deletions ee/tabby-webserver/graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ enum AuthMethod {
LOGIN
}

type GithubProvidedRepositoryConnection {
edges: [GithubProvidedRepositoryEdge!]!
pageInfo: PageInfo!
}

type RegisterResponse {
accessToken: String!
refreshToken: String!
Expand Down Expand Up @@ -97,6 +102,11 @@ type GithubRepositoryProvider {
id: ID!
displayName: String!
applicationId: String!
"""
Will never be returned to the client / from GraphQL endpoints.
Make sure to strip using [Self::strip_access_token].
"""
accessToken: String
}

input PasswordChangeInput {
Expand Down Expand Up @@ -175,6 +185,7 @@ type Mutation {
deleteEmailSetting: Boolean!
uploadLicense(license: String!): Boolean!
resetLicense: Boolean!
updateGithubProvidedRepositoryActive(id: ID!, active: Boolean!): Boolean!
}

type RepositoryEdge {
Expand All @@ -192,8 +203,13 @@ type FileEntrySearchResult {
indices: [Int!]!
}

input NetworkSettingInput {
externalUrl: String!
type GithubProvidedRepository {
id: ID!
vendorId: String!
githubRepositoryProviderId: ID!
name: String!
gitUrl: String!
active: Boolean!
}

type Query {
Expand All @@ -204,6 +220,7 @@ type Query {
users(after: String, before: String, first: Int, last: Int): UserConnection!
invitations(after: String, before: String, first: Int, last: Int): InvitationConnection!
githubRepositoryProviders(after: String, before: String, first: Int, last: Int): GithubRepositoryProviderConnection!
githubRepositories(providerIds: [ID!]!, after: String, before: String, first: Int, last: Int): GithubProvidedRepositoryConnection!
jobRuns(ids: [ID!], jobs: [String!], after: String, before: String, first: Int, last: Int): JobRunConnection!
jobRunStats(jobs: [String!]): JobStats!
emailSetting: EmailSetting
Expand All @@ -221,6 +238,10 @@ type Query {
dailyStats(start: DateTimeUtc!, end: DateTimeUtc!, users: [ID!], languages: [Language!]): [CompletionStats!]!
}

input NetworkSettingInput {
externalUrl: String!
}

enum Encryption {
START_TLS
SSL_TLS
Expand Down Expand Up @@ -328,6 +349,11 @@ type PageInfo {
endCursor: String
}

type GithubProvidedRepositoryEdge {
node: GithubProvidedRepository!
cursor: String!
}

type Repository {
id: ID!
name: String!
Expand Down
6 changes: 5 additions & 1 deletion ee/tabby-webserver/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,11 @@ impl WebserverHandle {
)
.nest(
"/integrations/github",
integrations::github::routes(ctx.setting(), ctx.github_repository_provider()),
integrations::github::routes(
ctx.auth(),
ctx.setting(),
ctx.github_repository_provider(),
),
)
.route(
"/avatar/:id",
Expand Down
11 changes: 9 additions & 2 deletions ee/tabby-webserver/src/integrations/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::sync::Arc;
use anyhow::Result;
use axum::{
extract::{Path, Query, State},
middleware::from_fn_with_state,
response::Redirect,
routing, Router,
};
Expand All @@ -12,8 +13,12 @@ use serde::Deserialize;
use tracing::error;
use url::Url;

use crate::schema::{
github_repository_provider::GithubRepositoryProviderService, setting::SettingService,
use crate::{
handler::require_login_middleware,
schema::{
auth::AuthenticationService, github_repository_provider::GithubRepositoryProviderService,
setting::SettingService,
},
};

#[derive(Debug, Deserialize)]
Expand Down Expand Up @@ -47,6 +52,7 @@ struct IntegrationState {
}

pub fn routes(
auth: Arc<dyn AuthenticationService>,
settings: Arc<dyn SettingService>,
github_repository_provider: Arc<dyn GithubRepositoryProviderService>,
) -> Router {
Expand All @@ -57,6 +63,7 @@ pub fn routes(
Router::new()
.route("/connect/:id", routing::get(connect))
.route("/callback", routing::get(callback))
.layer(from_fn_with_state(auth, require_login_middleware))
.with_state(state)
}

Expand Down
51 changes: 51 additions & 0 deletions ee/tabby-webserver/src/schema/github_repository_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ pub struct GithubRepositoryProvider {
pub id: ID,
pub display_name: String,
pub application_id: String,
/// Will never be returned to the client / from GraphQL endpoints.
/// Make sure to strip using [Self::strip_access_token].
pub access_token: Option<String>,
boxbeam marked this conversation as resolved.
Show resolved Hide resolved
boxbeam marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub access_token: Option<String>,
pub access_token: String,

Copy link
Contributor Author

@boxbeam boxbeam Apr 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The access token can be None (if the user has not gone through the OAuth connect flow yet), and this will only be used internally. I think we should prefer a closer representation of the data, since otherwise I'll be having to check if access_token.is_empty() instead.

}

impl GithubRepositoryProvider {
/// Remove the access token for external representation.
pub fn strip_access_token(self) -> Self {
Self {
access_token: None,
..self
}
}
}

impl NodeType for GithubRepositoryProvider {
Expand All @@ -29,6 +42,33 @@ impl NodeType for GithubRepositoryProvider {
}
}

#[derive(GraphQLObject, Debug)]
#[graphql(context = Context)]
pub struct GithubProvidedRepository {
pub id: ID,
pub vendor_id: String,
pub github_repository_provider_id: ID,
pub name: String,
pub git_url: String,
pub active: bool,
}

impl NodeType for GithubProvidedRepository {
type Cursor = String;

fn cursor(&self) -> Self::Cursor {
self.id.to_string()
}

fn connection_type_name() -> &'static str {
"GithubProvidedRepositoryConnection"
}

fn edge_type_name() -> &'static str {
"GithubProvidedRepositoryEdge"
}
}

#[async_trait]
pub trait GithubRepositoryProviderService: Send + Sync {
async fn get_github_repository_provider(&self, id: ID) -> Result<GithubRepositoryProvider>;
Expand All @@ -46,4 +86,15 @@ pub trait GithubRepositoryProviderService: Send + Sync {
first: Option<usize>,
last: Option<usize>,
) -> Result<Vec<GithubRepositoryProvider>>;

async fn list_github_provided_repositories_by_provider(
&self,
provider: Vec<ID>,
after: Option<String>,
before: Option<String>,
first: Option<usize>,
last: Option<usize>,
) -> Result<Vec<GithubProvidedRepository>>;

async fn update_github_provided_repository_active(&self, id: ID, active: bool) -> Result<()>;
}
50 changes: 49 additions & 1 deletion ee/tabby-webserver/src/schema/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ use self::{
RequestInvitationInput, RequestPasswordResetEmailInput, UpdateOAuthCredentialInput,
},
email::{EmailService, EmailSetting, EmailSettingInput},
github_repository_provider::{GithubRepositoryProvider, GithubRepositoryProviderService},
github_repository_provider::{
GithubProvidedRepository, GithubRepositoryProvider, GithubRepositoryProviderService,
},
job::JobStats,
license::{IsLicenseValid, LicenseInfo, LicenseService, LicenseType},
repository::{Repository, RepositoryService},
Expand Down Expand Up @@ -248,6 +250,40 @@ impl Query {
.locator
.github_repository_provider()
.list_github_repository_providers(after, before, first, last)
.await?
.into_iter()
.map(|provider| provider.strip_access_token())
.collect())
},
)
.await
}

async fn github_repositories(
ctx: &Context,
provider_ids: Vec<ID>,
after: Option<String>,
before: Option<String>,
first: Option<i32>,
last: Option<i32>,
) -> FieldResult<Connection<GithubProvidedRepository>> {
check_admin(ctx).await?;
relay::query_async(
after,
before,
first,
last,
|after, before, first, last| async move {
Ok(ctx
.locator
.github_repository_provider()
.list_github_provided_repositories_by_provider(
provider_ids,
after,
before,
first,
last,
)
.await?)
},
)
Expand Down Expand Up @@ -669,6 +705,18 @@ impl Mutation {
ctx.locator.license().reset_license().await?;
Ok(true)
}

async fn update_github_provided_repository_active(
ctx: &Context,
id: ID,
active: bool,
) -> Result<bool> {
ctx.locator
.github_repository_provider()
.update_github_provided_repository_active(id, active)
.await?;
Ok(true)
}
}

async fn check_analytic_access(ctx: &Context, users: &[ID]) -> Result<(), CoreError> {
Expand Down
Loading
Loading