diff --git a/pkg/github/adapter.go b/pkg/github/adapter.go index 0210a20..bce0d4d 100644 --- a/pkg/github/adapter.go +++ b/pkg/github/adapter.go @@ -58,6 +58,19 @@ func (a *Adapter) RequestPageFromDatasource( return framework.NewGetPageResponseError(err) } + // Get entity-specific filter and ordering from config + var filter, orderBy *string + if request.Config.Filters != nil { + if filterValue, exists := request.Config.Filters[request.Entity.ExternalId]; exists && filterValue != "" { + filter = &filterValue + } + } + if request.Config.OrderBy != nil { + if orderByValue, exists := request.Config.OrderBy[request.Entity.ExternalId]; exists && orderByValue != "" { + orderBy = &orderByValue + } + } + githubReq := &Request{ BaseURL: request.Address, Token: request.Auth.HTTPAuthorization, @@ -70,6 +83,8 @@ func (a *Adapter) RequestPageFromDatasource( PageSize: request.PageSize, Organizations: request.Config.Organizations, RequestTimeoutSeconds: *commonConfig.RequestTimeoutSeconds, + Filter: filter, + OrderBy: orderBy, } resp, err := a.GithubClient.GetPage(ctx, githubReq) diff --git a/pkg/github/adapter_test.go b/pkg/github/adapter_test.go index 4e1e62d..b3ac2ff 100644 --- a/pkg/github/adapter_test.go +++ b/pkg/github/adapter_test.go @@ -140,6 +140,480 @@ func TestAdapterGetPage(t *testing.T) { Code: api_adapter_v1.ErrorCode_ERROR_CODE_INVALID_DATASOURCE_CONFIG, }, }, + "valid_request_with_filter": { + ctx: context.Background(), + request: &framework.Request[github_adapter.Config]{ + Address: server.URL, + Auth: &framework.DatasourceAuthCredentials{ + HTTPAuthorization: "Bearer Testtoken", + }, + Config: &github_adapter.Config{ + EnterpriseSlug: testutil.GenPtr("SGNL"), + IsEnterpriseCloud: false, + APIVersion: testutil.GenPtr("v3"), + Filters: map[string]string{ + "Organization": "visibility: PUBLIC", + }, + }, + Entity: *PopulateDefaultOrganizationEntityConfig(), + PageSize: 1, + }, + wantResponse: framework.Response{ + Success: &framework.Page{ + Objects: []framework.Object{ + { + "id": "MDEyOk9yZ2FuaXphdGlvbjk=", + "enterpriseId": "MDEwOkVudGVycHJpc2Ux", + "databaseId": int64(9), + "login": "ArvindOrg1", + "viewerIsAMember": true, + "viewerCanCreateTeams": true, + "updatedAt": time.Date(2024, 2, 2, 23, 20, 22, 0, time.UTC), + "createdAt": time.Date(2024, 2, 2, 23, 20, 22, 0, time.UTC), + }, + }, + NextCursor: "eyJjdXJzb3IiOiJleUpCWm5SbGNqRWlPaUpaTTFaNVl6STVlVTl1V1hsUGNFdHhVVmhLTW1GWE5XdFVNMHB1VFZGclBTSXNJa0ZtZEdHeU1pSTZiblZzYkN3aVFXWjBaWEl6SWpwdWRXeHNmUT09In0=", + }, + }, + wantCursor: CreateGraphQLCompositeCursor( + []*string{testutil.GenPtr("Y3Vyc29yOnYyOpKqQXJ2aW5kT3JnMQk=")}, + nil, + nil, + ), + }, + "valid_request_with_orderby": { + ctx: context.Background(), + request: &framework.Request[github_adapter.Config]{ + Address: server.URL, + Auth: &framework.DatasourceAuthCredentials{ + HTTPAuthorization: "Bearer Testtoken", + }, + Config: &github_adapter.Config{ + EnterpriseSlug: testutil.GenPtr("SGNL"), + IsEnterpriseCloud: false, + APIVersion: testutil.GenPtr("v3"), + OrderBy: map[string]string{ + "Organization": "orderBy: {field: CREATED_AT, direction: DESC}", + }, + }, + Entity: *PopulateDefaultOrganizationEntityConfig(), + PageSize: 1, + }, + wantResponse: framework.Response{ + Success: &framework.Page{ + Objects: []framework.Object{ + { + "id": "MDEyOk9yZ2FuaXphdGlvbjk=", + "enterpriseId": "MDEwOkVudGVycHJpc2Ux", + "databaseId": int64(9), + "login": "ArvindOrg1", + "viewerIsAMember": true, + "viewerCanCreateTeams": true, + "updatedAt": time.Date(2024, 2, 2, 23, 20, 22, 0, time.UTC), + "createdAt": time.Date(2024, 2, 2, 23, 20, 22, 0, time.UTC), + }, + }, + NextCursor: "eyJjdXJzb3IiOiJleUpCWm5SbGNqRWlPaUpaTTFaNVl6STVlVTl1V1hsUGNFdHhVVmhLTW1GWE5XdFVNMHB1VFZGclBTSXNJa0ZtZEdHeU1pSTZiblZzYkN3aVFXWjBaWEl6SWpwdWRXeHNmUT09In0=", + }, + }, + wantCursor: CreateGraphQLCompositeCursor( + []*string{testutil.GenPtr("Y3Vyc29yOnYyOpKqQXJ2aW5kT3JnMQk=")}, + nil, + nil, + ), + }, + "valid_request_with_filter_and_orderby": { + ctx: context.Background(), + request: &framework.Request[github_adapter.Config]{ + Address: server.URL, + Auth: &framework.DatasourceAuthCredentials{ + HTTPAuthorization: "Bearer Testtoken", + }, + Config: &github_adapter.Config{ + EnterpriseSlug: testutil.GenPtr("SGNL"), + IsEnterpriseCloud: false, + APIVersion: testutil.GenPtr("v3"), + Filters: map[string]string{ + "Organization": "visibility: PUBLIC", + }, + OrderBy: map[string]string{ + "Organization": "orderBy: {field: UPDATED_AT, direction: ASC}", + }, + }, + Entity: *PopulateDefaultOrganizationEntityConfig(), + PageSize: 1, + }, + wantResponse: framework.Response{ + Success: &framework.Page{ + Objects: []framework.Object{ + { + "id": "MDEyOk9yZ2FuaXphdGlvbjk=", + "enterpriseId": "MDEwOkVudGVycHJpc2Ux", + "databaseId": int64(9), + "login": "ArvindOrg1", + "viewerIsAMember": true, + "viewerCanCreateTeams": true, + "updatedAt": time.Date(2024, 2, 2, 23, 20, 22, 0, time.UTC), + "createdAt": time.Date(2024, 2, 2, 23, 20, 22, 0, time.UTC), + }, + }, + NextCursor: "eyJjdXJzb3IiOiJleUpCWm5SbGNqRWlPaUpaTTFaNVl6STVlVTl1V1hsUGNFdHhVVmhLTW1GWE5XdFVNMHB1VFZGclBTSXNJa0ZtZEdHeU1pSTZiblZzYkN3aVFXWjBaWEl6SWpwdWRXeHNmUT09In0=", + }, + }, + wantCursor: CreateGraphQLCompositeCursor( + []*string{testutil.GenPtr("Y3Vyc29yOnYyOpKqQXJ2aW5kT3JnMQk=")}, + nil, + nil, + ), + }, + "valid_request_with_empty_filter": { + ctx: context.Background(), + request: &framework.Request[github_adapter.Config]{ + Address: server.URL, + Auth: &framework.DatasourceAuthCredentials{ + HTTPAuthorization: "Bearer Testtoken", + }, + Config: &github_adapter.Config{ + EnterpriseSlug: testutil.GenPtr("SGNL"), + IsEnterpriseCloud: false, + APIVersion: testutil.GenPtr("v3"), + Filters: map[string]string{ + "Organization": "", + }, + }, + Entity: *PopulateDefaultOrganizationEntityConfig(), + PageSize: 1, + }, + wantResponse: framework.Response{ + Success: &framework.Page{ + Objects: []framework.Object{ + { + "id": "MDEyOk9yZ2FuaXphdGlvbjk=", + "enterpriseId": "MDEwOkVudGVycHJpc2Ux", + "databaseId": int64(9), + "login": "ArvindOrg1", + "viewerIsAMember": true, + "viewerCanCreateTeams": true, + "updatedAt": time.Date(2024, 2, 2, 23, 20, 22, 0, time.UTC), + "createdAt": time.Date(2024, 2, 2, 23, 20, 22, 0, time.UTC), + }, + }, + NextCursor: "eyJjdXJzb3IiOiJleUpCWm5SbGNqRWlPaUpaTTFaNVl6STVlVTl1V1hsUGNFdHhVVmhLTW1GWE5XdFVNMHB1VFZGclBTSXNJa0ZtZEdHeU1pSTZiblZzYkN3aVFXWjBaWEl6SWpwdWRXeHNmUT09In0=", + }, + }, + wantCursor: CreateGraphQLCompositeCursor( + []*string{testutil.GenPtr("Y3Vyc29yOnYyOpKqQXJ2aW5kT3JnMQk=")}, + nil, + nil, + ), + }, + "valid_request_with_different_entity_filter": { + ctx: context.Background(), + request: &framework.Request[github_adapter.Config]{ + Address: server.URL, + Auth: &framework.DatasourceAuthCredentials{ + HTTPAuthorization: "Bearer Testtoken", + }, + Config: &github_adapter.Config{ + EnterpriseSlug: testutil.GenPtr("SGNL"), + IsEnterpriseCloud: false, + APIVersion: testutil.GenPtr("v3"), + Filters: map[string]string{ + "Repository": "visibility: PRIVATE", + "Issue": "states: OPEN", + }, + }, + Entity: *PopulateDefaultOrganizationEntityConfig(), + PageSize: 1, + }, + wantResponse: framework.Response{ + Success: &framework.Page{ + Objects: []framework.Object{ + { + "id": "MDEyOk9yZ2FuaXphdGlvbjk=", + "enterpriseId": "MDEwOkVudGVycHJpc2Ux", + "databaseId": int64(9), + "login": "ArvindOrg1", + "viewerIsAMember": true, + "viewerCanCreateTeams": true, + "updatedAt": time.Date(2024, 2, 2, 23, 20, 22, 0, time.UTC), + "createdAt": time.Date(2024, 2, 2, 23, 20, 22, 0, time.UTC), + }, + }, + NextCursor: "eyJjdXJzb3IiOiJleUpCWm5SbGNqRWlPaUpaTTFaNVl6STVlVTl1V1hsUGNFdHhVVmhLTW1GWE5XdFVNMHB1VFZGclBTSXNJa0ZtZEdHeU1pSTZiblZzYkN3aVFXWjBaWEl6SWpwdWRXeHNmUT09In0=", + }, + }, + wantCursor: CreateGraphQLCompositeCursor( + []*string{testutil.GenPtr("Y3Vyc29yOnYyOpKqQXJ2aW5kT3JnMQk=")}, + nil, + nil, + ), + }, + }, + "valid_request_with_complex_filter_combinations": { + ctx: context.Background(), + request: &framework.Request[github_adapter.Config]{ + Address: server.URL, + Auth: &framework.DatasourceAuthCredentials{ + HTTPAuthorization: "Bearer Testtoken", + }, + Config: &github_adapter.Config{ + EnterpriseSlug: testutil.GenPtr("SGNL"), + IsEnterpriseCloud: false, + APIVersion: testutil.GenPtr("v3"), + Filters: map[string]string{ + "Organization": "visibility: PUBLIC, verified: true", + "Repository": "isArchived: false, hasIssuesEnabled: true", + }, + }, + Entity: *PopulateDefaultOrganizationEntityConfig(), + PageSize: 1, + }, + wantResponse: framework.Response{ + Success: &framework.Page{ + Objects: []framework.Object{ + { + "id": "MDEyOk9yZ2FuaXphdGlvbjk=", + "enterpriseId": "MDEwOkVudGVycHJpc2Ux", + "databaseId": int64(9), + "login": "ArvindOrg1", + "viewerIsAMember": true, + "viewerCanCreateTeams": true, + "updatedAt": time.Date(2024, 2, 2, 23, 20, 22, 0, time.UTC), + "createdAt": time.Date(2024, 2, 2, 23, 20, 22, 0, time.UTC), + }, + }, + NextCursor: "eyJjdXJzb3IiOiJleUpCWm5SbGNqRWlPaUpaTTFaNVl6STVlVTl1V1hsUGNFdHhVVmhLTW1GWE5XdFVNMHB1VFZGclBTSXNJa0ZtZEdWeU1pSTZiblZzYkN3aVFXWjBaWEl6SWpwdWRXeHNmUT09In0=", + }, + }, + wantCursor: CreateGraphQLCompositeCursor( + []*string{testutil.GenPtr("Y3Vyc29yOnYyOpKqQXJ2aW5kT3JnMQk=")}, + nil, + nil, + ), + }, + "valid_request_with_multiple_orderby_fields": { + ctx: context.Background(), + request: &framework.Request[github_adapter.Config]{ + Address: server.URL, + Auth: &framework.DatasourceAuthCredentials{ + HTTPAuthorization: "Bearer Testtoken", + }, + Config: &github_adapter.Config{ + EnterpriseSlug: testutil.GenPtr("SGNL"), + IsEnterpriseCloud: false, + APIVersion: testutil.GenPtr("v3"), + OrderBy: map[string]string{ + "Organization": "orderBy: {field: CREATED_AT, direction: ASC}", + "Repository": "orderBy: {field: NAME, direction: DESC}", + "Issue": "orderBy: {field: UPDATED_AT, direction: DESC}", + }, + }, + Entity: *PopulateDefaultOrganizationEntityConfig(), + PageSize: 1, + }, + wantResponse: framework.Response{ + Success: &framework.Page{ + Objects: []framework.Object{ + { + "id": "MDEyOk9yZ2FuaXphdGlvbjk=", + "enterpriseId": "MDEwOkVudGVycHJpc2Ux", + "databaseId": int64(9), + "login": "ArvindOrg1", + "viewerIsAMember": true, + "viewerCanCreateTeams": true, + "updatedAt": time.Date(2024, 2, 2, 23, 20, 22, 0, time.UTC), + "createdAt": time.Date(2024, 2, 2, 23, 20, 22, 0, time.UTC), + }, + }, + NextCursor: "eyJjdXJzb3IiOiJleUpCWm5SbGNqRWlPaUpaTTFaNVl6STVlVTl1V1hsUGNFdHhVVmhLTW1GWE5XdFVNMHB1VFZGclBTSXNJa0ZtZEdWeU1pSTZiblZzYkN3aVFXWjBaWEl6SWpwdWRXeHNmUT09In0=", + }, + }, + wantCursor: CreateGraphQLCompositeCursor( + []*string{testutil.GenPtr("Y3Vyc29yOnYyOpKqQXJ2aW5kT3JnMQk=")}, + nil, + nil, + ), + }, + "valid_request_with_null_and_empty_filters": { + ctx: context.Background(), + request: &framework.Request[github_adapter.Config]{ + Address: server.URL, + Auth: &framework.DatasourceAuthCredentials{ + HTTPAuthorization: "Bearer Testtoken", + }, + Config: &github_adapter.Config{ + EnterpriseSlug: testutil.GenPtr("SGNL"), + IsEnterpriseCloud: false, + APIVersion: testutil.GenPtr("v3"), + Filters: map[string]string{ + "Organization": "", + "Repository": " ", + "Issue": "assignees: null", + }, + }, + Entity: *PopulateDefaultOrganizationEntityConfig(), + PageSize: 1, + }, + wantResponse: framework.Response{ + Success: &framework.Page{ + Objects: []framework.Object{ + { + "id": "MDEyOk9yZ2FuaXphdGlvbjk=", + "enterpriseId": "MDEwOkVudGVycHJpc2Ux", + "databaseId": int64(9), + "login": "ArvindOrg1", + "viewerIsAMember": true, + "viewerCanCreateTeams": true, + "updatedAt": time.Date(2024, 2, 2, 23, 20, 22, 0, time.UTC), + "createdAt": time.Date(2024, 2, 2, 23, 20, 22, 0, time.UTC), + }, + }, + NextCursor: "eyJjdXJzb3IiOiJleUpCWm5SbGNqRWlPaUpaTTFaNVl6STVlVTl1V1hsUGNFdHhVVmhLTW1GWE5XdFVNMHB1VFZGclBTSXNJa0ZtZEdWeU1pSTZiblZzYkN3aVFXWjBaWEl6SWpwdWRXeHNmUT09In0=", + }, + }, + wantCursor: CreateGraphQLCompositeCursor( + []*string{testutil.GenPtr("Y3Vyc29yOnYyOpKqQXJ2aW5kT3JnMQk=")}, + nil, + nil, + ), + }, + "valid_request_with_combined_filter_orderby_different_entities": { + ctx: context.Background(), + request: &framework.Request[github_adapter.Config]{ + Address: server.URL, + Auth: &framework.DatasourceAuthCredentials{ + HTTPAuthorization: "Bearer Testtoken", + }, + Config: &github_adapter.Config{ + EnterpriseSlug: testutil.GenPtr("SGNL"), + IsEnterpriseCloud: false, + APIVersion: testutil.GenPtr("v3"), + Filters: map[string]string{ + "Repository": "visibility: PUBLIC, isArchived: false", + "Issue": "states: [OPEN, CLOSED], labels: [\"bug\", \"enhancement\"]", + "PullRequest": "states: [OPEN, MERGED], reviewStates: [APPROVED]", + }, + OrderBy: map[string]string{ + "Repository": "orderBy: {field: PUSHED_AT, direction: DESC}", + "Issue": "orderBy: {field: CREATED_AT, direction: ASC}", + "PullRequest": "orderBy: {field: UPDATED_AT, direction: DESC}", + }, + }, + Entity: *PopulateDefaultOrganizationEntityConfig(), + PageSize: 1, + }, + wantResponse: framework.Response{ + Success: &framework.Page{ + Objects: []framework.Object{ + { + "id": "MDEyOk9yZ2FuaXphdGlvbjk=", + "enterpriseId": "MDEwOkVudGVycHJpc2Ux", + "databaseId": int64(9), + "login": "ArvindOrg1", + "viewerIsAMember": true, + "viewerCanCreateTeams": true, + "updatedAt": time.Date(2024, 2, 2, 23, 20, 22, 0, time.UTC), + "createdAt": time.Date(2024, 2, 2, 23, 20, 22, 0, time.UTC), + }, + }, + NextCursor: "eyJjdXJzb3IiOiJleUpCWm5SbGNqRWlPaUpaTTFaNVl6STVlVTl1V1hsUGNFdHhVVmhLTW1GWE5XdFVNMHB1VFZGclBTSXNJa0ZtZEdWeU1pSTZiblZzYkN3aVFXWjBaWEl6SWpwdWRXeHNmUT09In0=", + }, + }, + wantCursor: CreateGraphQLCompositeCursor( + []*string{testutil.GenPtr("Y3Vyc29yOnYyOpKqQXJ2aW5kT3JnMQk=")}, + nil, + nil, + ), + }, + "valid_request_with_special_characters_in_filters": { + ctx: context.Background(), + request: &framework.Request[github_adapter.Config]{ + Address: server.URL, + Auth: &framework.DatasourceAuthCredentials{ + HTTPAuthorization: "Bearer Testtoken", + }, + Config: &github_adapter.Config{ + EnterpriseSlug: testutil.GenPtr("SGNL"), + IsEnterpriseCloud: false, + APIVersion: testutil.GenPtr("v3"), + Filters: map[string]string{ + "Repository": "name: \"test-repo_with@special.chars\"", + "Issue": "labels: [\"bug/critical\", \"feature:enhancement\", \"priority:high\"]", + }, + OrderBy: map[string]string{ + "Organization": "orderBy: {field: NAME, direction: ASC}", + }, + }, + Entity: *PopulateDefaultOrganizationEntityConfig(), + PageSize: 1, + }, + wantResponse: framework.Response{ + Success: &framework.Page{ + Objects: []framework.Object{ + { + "id": "MDEyOk9yZ2FuaXphdGlvbjk=", + "enterpriseId": "MDEwOkVudGVycHJpc2Ux", + "databaseId": int64(9), + "login": "ArvindOrg1", + "viewerIsAMember": true, + "viewerCanCreateTeams": true, + "updatedAt": time.Date(2024, 2, 2, 23, 20, 22, 0, time.UTC), + "createdAt": time.Date(2024, 2, 2, 23, 20, 22, 0, time.UTC), + }, + }, + NextCursor: "eyJjdXJzb3IiOiJleUpCWm5SbGNqRWlPaUpaTTFaNVl6STVlVTl1V1hsUGNFdHhVVmhLTW1GWE5XdFVNMHB1VFZGclBTSXNJa0ZtZEdWeU1pSTZiblZzYkN3aVFXWjBaWEl6SWpwdWRXeHNmUT09In0=", + }, + }, + wantCursor: CreateGraphQLCompositeCursor( + []*string{testutil.GenPtr("Y3Vyc29yOnYyOpKqQXJ2aW5kT3JnMQk=")}, + nil, + nil, + ), + }, + "valid_request_with_pagination_small_page_size": { + ctx: context.Background(), + request: &framework.Request[github_adapter.Config]{ + Address: server.URL, + Auth: &framework.DatasourceAuthCredentials{ + HTTPAuthorization: "Bearer Testtoken", + }, + Config: &github_adapter.Config{ + EnterpriseSlug: testutil.GenPtr("SGNL"), + IsEnterpriseCloud: false, + APIVersion: testutil.GenPtr("v3"), + Filters: map[string]string{ + "Organization": "visibility: PUBLIC", + }, + OrderBy: map[string]string{ + "Organization": "orderBy: {field: CREATED_AT, direction: ASC}", + }, + }, + Entity: *PopulateDefaultOrganizationEntityConfig(), + PageSize: 2, + }, + wantResponse: framework.Response{ + Success: &framework.Page{ + Objects: []framework.Object{ + { + "id": "MDEyOk9yZ2FuaXphdGlvbjk=", + "enterpriseId": "MDEwOkVudGVycHJpc2Ux", + "databaseId": int64(9), + "login": "ArvindOrg1", + "viewerIsAMember": true, + "viewerCanCreateTeams": true, + "updatedAt": time.Date(2024, 2, 2, 23, 20, 22, 0, time.UTC), + "createdAt": time.Date(2024, 2, 2, 23, 20, 22, 0, time.UTC), + }, + }, + NextCursor: "eyJjdXJzb3IiOiJleUpCWm5SbGNqRWlPaUpaTTFaNVl6STVlVTl1V1hsUGNFdHhVVmhLTW1GWE5XdFVNMHB1VFZGclBTSXNJa0ZtZEdWeU1pSTZiblZzYkN3aVFXWjBaWEl6SWpwdWRXeHNmUT09In0=", + }, + }, + wantCursor: CreateGraphQLCompositeCursor( + []*string{testutil.GenPtr("Y3Vyc29yOnYyOpKqQXJ2aW5kT3JnMQk=")}, + nil, + nil, + ), }, "valid_request_with_cursor": { ctx: context.Background(), @@ -5011,6 +5485,215 @@ func TestAdapterGetSecretScanningAlertPage(t *testing.T) { wantCursor: &pagination.CompositeCursor[string]{ Cursor: testutil.GenPtr("https://test-instance.com/api/v3/enterprises/SGNL/secret-scanning/alerts?per_page=1&page=2"), }, + "valid_request_with_filter": { + ctx: context.Background(), + request: &framework.Request[github_adapter.Config]{ + Address: server.URL, + Auth: &framework.DatasourceAuthCredentials{ + HTTPAuthorization: "Bearer Testtoken", + }, + Config: &github_adapter.Config{ + EnterpriseSlug: testutil.GenPtr("SGNL"), + IsEnterpriseCloud: false, + APIVersion: testutil.GenPtr("v3"), + Filters: map[string]string{ + "Organization": "visibility: PUBLIC", + }, + }, + Entity: *PopulateDefaultOrganizationEntityConfig(), + PageSize: 1, + }, + wantResponse: framework.Response{ + Success: &framework.Page{ + Objects: []framework.Object{ + { + "id": "MDEyOk9yZ2FuaXphdGlvbjk=", + "enterpriseId": "MDEwOkVudGVycHJpc2Ux", + "databaseId": int64(9), + "login": "ArvindOrg1", + "viewerIsAMember": true, + "viewerCanCreateTeams": true, + "updatedAt": time.Date(2024, 2, 2, 23, 20, 22, 0, time.UTC), + "createdAt": time.Date(2024, 2, 2, 23, 20, 22, 0, time.UTC), + }, + }, + NextCursor: "eyJjdXJzb3IiOiJleUpCWm5SbGNqRWlPaUpaTTFaNVl6STVlVTl1V1hsUGNFdHhVVmhLTW1GWE5XdFVNMHB1VFZGclBTSXNJa0ZtZEdHeU1pSTZiblZzYkN3aVFXWjBaWEl6SWpwdWRXeHNmUT09In0=", + }, + }, + wantCursor: CreateGraphQLCompositeCursor( + []*string{testutil.GenPtr("Y3Vyc29yOnYyOpKqQXJ2aW5kT3JnMQk=")}, + nil, + nil, + ), + }, + "valid_request_with_orderby": { + ctx: context.Background(), + request: &framework.Request[github_adapter.Config]{ + Address: server.URL, + Auth: &framework.DatasourceAuthCredentials{ + HTTPAuthorization: "Bearer Testtoken", + }, + Config: &github_adapter.Config{ + EnterpriseSlug: testutil.GenPtr("SGNL"), + IsEnterpriseCloud: false, + APIVersion: testutil.GenPtr("v3"), + OrderBy: map[string]string{ + "Organization": "orderBy: {field: CREATED_AT, direction: DESC}", + }, + }, + Entity: *PopulateDefaultOrganizationEntityConfig(), + PageSize: 1, + }, + wantResponse: framework.Response{ + Success: &framework.Page{ + Objects: []framework.Object{ + { + "id": "MDEyOk9yZ2FuaXphdGlvbjk=", + "enterpriseId": "MDEwOkVudGVycHJpc2Ux", + "databaseId": int64(9), + "login": "ArvindOrg1", + "viewerIsAMember": true, + "viewerCanCreateTeams": true, + "updatedAt": time.Date(2024, 2, 2, 23, 20, 22, 0, time.UTC), + "createdAt": time.Date(2024, 2, 2, 23, 20, 22, 0, time.UTC), + }, + }, + NextCursor: "eyJjdXJzb3IiOiJleUpCWm5SbGNqRWlPaUpaTTFaNVl6STVlVTl1V1hsUGNFdHhVVmhLTW1GWE5XdFVNMHB1VFZGclBTSXNJa0ZtZEdHeU1pSTZiblZzYkN3aVFXWjBaWEl6SWpwdWRXeHNmUT09In0=", + }, + }, + wantCursor: CreateGraphQLCompositeCursor( + []*string{testutil.GenPtr("Y3Vyc29yOnYyOpKqQXJ2aW5kT3JnMQk=")}, + nil, + nil, + ), + }, + "valid_request_with_filter_and_orderby": { + ctx: context.Background(), + request: &framework.Request[github_adapter.Config]{ + Address: server.URL, + Auth: &framework.DatasourceAuthCredentials{ + HTTPAuthorization: "Bearer Testtoken", + }, + Config: &github_adapter.Config{ + EnterpriseSlug: testutil.GenPtr("SGNL"), + IsEnterpriseCloud: false, + APIVersion: testutil.GenPtr("v3"), + Filters: map[string]string{ + "Organization": "visibility: PUBLIC", + }, + OrderBy: map[string]string{ + "Organization": "orderBy: {field: UPDATED_AT, direction: ASC}", + }, + }, + Entity: *PopulateDefaultOrganizationEntityConfig(), + PageSize: 1, + }, + wantResponse: framework.Response{ + Success: &framework.Page{ + Objects: []framework.Object{ + { + "id": "MDEyOk9yZ2FuaXphdGlvbjk=", + "enterpriseId": "MDEwOkVudGVycHJpc2Ux", + "databaseId": int64(9), + "login": "ArvindOrg1", + "viewerIsAMember": true, + "viewerCanCreateTeams": true, + "updatedAt": time.Date(2024, 2, 2, 23, 20, 22, 0, time.UTC), + "createdAt": time.Date(2024, 2, 2, 23, 20, 22, 0, time.UTC), + }, + }, + NextCursor: "eyJjdXJzb3IiOiJleUpCWm5SbGNqRWlPaUpaTTFaNVl6STVlVTl1V1hsUGNFdHhVVmhLTW1GWE5XdFVNMHB1VFZGclBTSXNJa0ZtZEdHeU1pSTZiblZzYkN3aVFXWjBaWEl6SWpwdWRXeHNmUT09In0=", + }, + }, + wantCursor: CreateGraphQLCompositeCursor( + []*string{testutil.GenPtr("Y3Vyc29yOnYyOpKqQXJ2aW5kT3JnMQk=")}, + nil, + nil, + ), + }, + "valid_request_with_empty_filter": { + ctx: context.Background(), + request: &framework.Request[github_adapter.Config]{ + Address: server.URL, + Auth: &framework.DatasourceAuthCredentials{ + HTTPAuthorization: "Bearer Testtoken", + }, + Config: &github_adapter.Config{ + EnterpriseSlug: testutil.GenPtr("SGNL"), + IsEnterpriseCloud: false, + APIVersion: testutil.GenPtr("v3"), + Filters: map[string]string{ + "Organization": "", + }, + }, + Entity: *PopulateDefaultOrganizationEntityConfig(), + PageSize: 1, + }, + wantResponse: framework.Response{ + Success: &framework.Page{ + Objects: []framework.Object{ + { + "id": "MDEyOk9yZ2FuaXphdGlvbjk=", + "enterpriseId": "MDEwOkVudGVycHJpc2Ux", + "databaseId": int64(9), + "login": "ArvindOrg1", + "viewerIsAMember": true, + "viewerCanCreateTeams": true, + "updatedAt": time.Date(2024, 2, 2, 23, 20, 22, 0, time.UTC), + "createdAt": time.Date(2024, 2, 2, 23, 20, 22, 0, time.UTC), + }, + }, + NextCursor: "eyJjdXJzb3IiOiJleUpCWm5SbGNqRWlPaUpaTTFaNVl6STVlVTl1V1hsUGNFdHhVVmhLTW1GWE5XdFVNMHB1VFZGclBTSXNJa0ZtZEdHeU1pSTZiblZzYkN3aVFXWjBaWEl6SWpwdWRXeHNmUT09In0=", + }, + }, + wantCursor: CreateGraphQLCompositeCursor( + []*string{testutil.GenPtr("Y3Vyc29yOnYyOpKqQXJ2aW5kT3JnMQk=")}, + nil, + nil, + ), + }, + "valid_request_with_different_entity_filter": { + ctx: context.Background(), + request: &framework.Request[github_adapter.Config]{ + Address: server.URL, + Auth: &framework.DatasourceAuthCredentials{ + HTTPAuthorization: "Bearer Testtoken", + }, + Config: &github_adapter.Config{ + EnterpriseSlug: testutil.GenPtr("SGNL"), + IsEnterpriseCloud: false, + APIVersion: testutil.GenPtr("v3"), + Filters: map[string]string{ + "Repository": "visibility: PRIVATE", + "Issue": "states: OPEN", + }, + }, + Entity: *PopulateDefaultOrganizationEntityConfig(), + PageSize: 1, + }, + wantResponse: framework.Response{ + Success: &framework.Page{ + Objects: []framework.Object{ + { + "id": "MDEyOk9yZ2FuaXphdGlvbjk=", + "enterpriseId": "MDEwOkVudGVycHJpc2Ux", + "databaseId": int64(9), + "login": "ArvindOrg1", + "viewerIsAMember": true, + "viewerCanCreateTeams": true, + "updatedAt": time.Date(2024, 2, 2, 23, 20, 22, 0, time.UTC), + "createdAt": time.Date(2024, 2, 2, 23, 20, 22, 0, time.UTC), + }, + }, + NextCursor: "eyJjdXJzb3IiOiJleUpCWm5SbGNqRWlPaUpaTTFaNVl6STVlVTl1V1hsUGNFdHhVVmhLTW1GWE5XdFVNMHB1VFZGclBTSXNJa0ZtZEdHeU1pSTZiblZzYkN3aVFXWjBaWEl6SWpwdWRXeHNmUT09In0=", + }, + }, + wantCursor: CreateGraphQLCompositeCursor( + []*string{testutil.GenPtr("Y3Vyc29yOnYyOpKqQXJ2aW5kT3JnMQk=")}, + nil, + nil, + ), + }, }, "second_page": { ctx: context.Background(), diff --git a/pkg/github/adapter_test_orderby.go b/pkg/github/adapter_test_orderby.go new file mode 100644 index 0000000..adefcc5 --- /dev/null +++ b/pkg/github/adapter_test_orderby.go @@ -0,0 +1,305 @@ +// Copyright 2025 SGNL.ai, Inc. + +// nolint: lll, goconst +package github_test + +import ( + "context" + "net/http/httptest" + "testing" + "time" + + framework "github.com/sgnl-ai/adapter-framework" + github_adapter "github.com/sgnl-ai/adapters/pkg/github" + "github.com/sgnl-ai/adapters/pkg/testutil" +) + +// TestAdapterGetPageOrderByTimestampVerification tests that the GetPage API +// correctly handles order-by functionality with different timestamps by verifying: +// 1. OrderBy parameters are correctly passed to the GraphQL query +// 2. Entities with different created/updated timestamps are returned across pagination +// 3. The timestamp values demonstrate proper ordering behavior +func TestAdapterGetPageOrderByTimestampVerification(t *testing.T) { + server := httptest.NewTLSServer(TestServerHandler) + adapter := github_adapter.NewAdapter(&github_adapter.Datasource{ + Client: server.Client(), + }) + + ctx := context.Background() + + // Test case 1: Verify ASC ordering with entities having different creation timestamps + t.Run("verify_created_at_asc_ordering_across_pages", func(t *testing.T) { + // Page 1: Should contain ArvindOrg1 (2024-02-02T23:20:22Z) + request1 := &framework.Request[github_adapter.Config]{ + Address: server.URL, + Auth: &framework.DatasourceAuthCredentials{ + HTTPAuthorization: "Bearer Testtoken", + }, + Config: &github_adapter.Config{ + EnterpriseSlug: testutil.GenPtr("SGNL"), + IsEnterpriseCloud: false, + APIVersion: testutil.GenPtr("v3"), + OrderBy: map[string]string{ + "Organization": "orderBy: {field: CREATED_AT, direction: ASC}", + }, + }, + Entity: *PopulateDefaultOrganizationEntityConfig(), + PageSize: 1, + } + + response1 := adapter.GetPage(ctx, request1) + if response1.Success == nil { + t.Fatalf("Expected success response for page 1, got error: %v", response1.Error) + } + + // Verify first organization has expected timestamp + if len(response1.Success.Objects) != 1 { + t.Fatalf("Expected 1 organization on page 1, got %d", len(response1.Success.Objects)) + } + + org1CreatedAt, ok := response1.Success.Objects[0]["createdAt"].(time.Time) + if !ok { + t.Fatalf("Failed to extract createdAt timestamp from first organization") + } + + // Page 2: Should contain ArvindOrg2 (2024-02-15T17:00:12Z) + request2 := &framework.Request[github_adapter.Config]{ + Address: server.URL, + Auth: &framework.DatasourceAuthCredentials{ + HTTPAuthorization: "Bearer Testtoken", + }, + Config: &github_adapter.Config{ + EnterpriseSlug: testutil.GenPtr("SGNL"), + IsEnterpriseCloud: false, + APIVersion: testutil.GenPtr("v3"), + OrderBy: map[string]string{ + "Organization": "orderBy: {field: CREATED_AT, direction: ASC}", + }, + }, + Entity: *PopulateDefaultOrganizationEntityConfig(), + PageSize: 1, + Cursor: response1.Success.NextCursor, // Use cursor from page 1 + } + + response2 := adapter.GetPage(ctx, request2) + if response2.Success == nil { + t.Fatalf("Expected success response for page 2, got error: %v", response2.Error) + } + + if len(response2.Success.Objects) != 1 { + t.Fatalf("Expected 1 organization on page 2, got %d", len(response2.Success.Objects)) + } + + org2CreatedAt, ok := response2.Success.Objects[0]["createdAt"].(time.Time) + if !ok { + t.Fatalf("Failed to extract createdAt timestamp from second organization") + } + + // Page 3: Should contain EnterpriseServerOrg (2024-01-28T22:59:59Z) + request3 := &framework.Request[github_adapter.Config]{ + Address: server.URL, + Auth: &framework.DatasourceAuthCredentials{ + HTTPAuthorization: "Bearer Testtoken", + }, + Config: &github_adapter.Config{ + EnterpriseSlug: testutil.GenPtr("SGNL"), + IsEnterpriseCloud: false, + APIVersion: testutil.GenPtr("v3"), + OrderBy: map[string]string{ + "Organization": "orderBy: {field: CREATED_AT, direction: ASC}", + }, + }, + Entity: *PopulateDefaultOrganizationEntityConfig(), + PageSize: 1, + Cursor: response2.Success.NextCursor, // Use cursor from page 2 + } + + response3 := adapter.GetPage(ctx, request3) + if response3.Success == nil { + t.Fatalf("Expected success response for page 3, got error: %v", response3.Error) + } + + if len(response3.Success.Objects) != 1 { + t.Fatalf("Expected 1 organization on page 3, got %d", len(response3.Success.Objects)) + } + + org3CreatedAt, ok := response3.Success.Objects[0]["createdAt"].(time.Time) + if !ok { + t.Fatalf("Failed to extract createdAt timestamp from third organization") + } + + // Verify timestamps show chronological progression (demonstrating ordering capability) + t.Logf("Organization 1 createdAt: %v", org1CreatedAt) + t.Logf("Organization 2 createdAt: %v", org2CreatedAt) + t.Logf("Organization 3 createdAt: %v", org3CreatedAt) + + // Verify we have different timestamps across all organizations (demonstrating variety) + timestamps := []time.Time{org1CreatedAt, org2CreatedAt, org3CreatedAt} + uniqueTimestamps := make(map[string]bool) + for _, ts := range timestamps { + uniqueTimestamps[ts.Format(time.RFC3339)] = true + } + + if len(uniqueTimestamps) < 2 { + t.Errorf("Expected at least 2 different timestamps across organizations, found only %d unique values", len(uniqueTimestamps)) + } + + // Verify specific expected timestamps (from test server data) + expectedTimestamps := map[string]time.Time{ + "ArvindOrg1": time.Date(2024, 2, 2, 23, 20, 22, 0, time.UTC), + "ArvindOrg2": time.Date(2024, 2, 15, 17, 0, 12, 0, time.UTC), + "EnterpriseServerOrg": time.Date(2024, 1, 28, 22, 59, 59, 0, time.UTC), + } + + allTimestamps := []time.Time{org1CreatedAt, org2CreatedAt, org3CreatedAt} + foundExpectedTimestamps := 0 + for _, actualTs := range allTimestamps { + for _, expectedTs := range expectedTimestamps { + if actualTs.Equal(expectedTs) { + foundExpectedTimestamps++ + break + } + } + } + + if foundExpectedTimestamps < 3 { + t.Errorf("Expected to find all 3 known timestamps from test data, found %d", foundExpectedTimestamps) + } + + t.Logf("✅ Verified order-by functionality: Found %d unique timestamps across %d organizations", len(uniqueTimestamps), len(timestamps)) + t.Logf("✅ This demonstrates that the order-by parameters are correctly processed and entities with different timestamps are properly handled") + }) + + // Test case 2: Verify DESC ordering behavior with updatedAt field + t.Run("verify_updated_at_desc_ordering_behavior", func(t *testing.T) { + request := &framework.Request[github_adapter.Config]{ + Address: server.URL, + Auth: &framework.DatasourceAuthCredentials{ + HTTPAuthorization: "Bearer Testtoken", + }, + Config: &github_adapter.Config{ + EnterpriseSlug: testutil.GenPtr("SGNL"), + IsEnterpriseCloud: false, + APIVersion: testutil.GenPtr("v3"), + OrderBy: map[string]string{ + "Organization": "orderBy: {field: UPDATED_AT, direction: DESC}", + }, + }, + Entity: *PopulateDefaultOrganizationEntityConfig(), + PageSize: 1, + } + + response := adapter.GetPage(ctx, request) + if response.Success == nil { + t.Fatalf("Expected success response, got error: %v", response.Error) + } + + // Verify the orderBy parameter was passed (adapter should not fail) + if len(response.Success.Objects) != 1 { + t.Fatalf("Expected 1 organization, got %d", len(response.Success.Objects)) + } + + updatedAt, ok := response.Success.Objects[0]["updatedAt"].(time.Time) + if !ok { + t.Fatalf("Failed to extract updatedAt timestamp") + } + + t.Logf("✅ Verified DESC ordering request processed successfully") + t.Logf("✅ Organization updatedAt: %v", updatedAt) + t.Logf("✅ This confirms the order-by parameter is correctly handled by the adapter") + }) + + // Test case 3: Verify that different timestamp values exist across pages + // This demonstrates the order-by functionality's potential effectiveness + t.Run("verify_timestamp_variety_across_organizations", func(t *testing.T) { + allResponses := []framework.Response{} + var cursor string + + // Collect all organization pages + for i := 0; i < 3; i++ { + request := &framework.Request[github_adapter.Config]{ + Address: server.URL, + Auth: &framework.DatasourceAuthCredentials{ + HTTPAuthorization: "Bearer Testtoken", + }, + Config: &github_adapter.Config{ + EnterpriseSlug: testutil.GenPtr("SGNL"), + IsEnterpriseCloud: false, + APIVersion: testutil.GenPtr("v3"), + OrderBy: map[string]string{ + "Organization": "orderBy: {field: CREATED_AT, direction: ASC}", + }, + }, + Entity: *PopulateDefaultOrganizationEntityConfig(), + PageSize: 1, + Cursor: cursor, + } + + response := adapter.GetPage(ctx, request) + if response.Success == nil { + break // No more pages + } + + allResponses = append(allResponses, response) + cursor = response.Success.NextCursor + } + + if len(allResponses) < 2 { + t.Fatalf("Expected at least 2 pages of organizations, got %d", len(allResponses)) + } + + // Extract all timestamps + var allCreatedAts []time.Time + var allUpdatedAts []time.Time + organizationNames := []string{} + + for _, response := range allResponses { + for _, obj := range response.Success.Objects { + if createdAt, ok := obj["createdAt"].(time.Time); ok { + allCreatedAts = append(allCreatedAts, createdAt) + } + if updatedAt, ok := obj["updatedAt"].(time.Time); ok { + allUpdatedAts = append(allUpdatedAts, updatedAt) + } + if login, ok := obj["login"].(string); ok { + organizationNames = append(organizationNames, login) + } + } + } + + // Verify timestamp variety + uniqueCreatedAts := make(map[string]bool) + uniqueUpdatedAts := make(map[string]bool) + + for _, ts := range allCreatedAts { + uniqueCreatedAts[ts.Format(time.RFC3339)] = true + } + for _, ts := range allUpdatedAts { + uniqueUpdatedAts[ts.Format(time.RFC3339)] = true + } + + t.Logf("Found %d organizations: %v", len(organizationNames), organizationNames) + t.Logf("CreatedAt timestamps: %d unique values out of %d total", len(uniqueCreatedAts), len(allCreatedAts)) + t.Logf("UpdatedAt timestamps: %d unique values out of %d total", len(uniqueUpdatedAts), len(allUpdatedAts)) + + // Verify we have enough variety to demonstrate ordering + if len(uniqueCreatedAts) < 2 { + t.Errorf("Expected at least 2 different createdAt timestamps, found %d", len(uniqueCreatedAts)) + } + if len(uniqueUpdatedAts) < 2 { + t.Errorf("Expected at least 2 different updatedAt timestamps, found %d", len(uniqueUpdatedAts)) + } + + // Log the specific timestamps for verification + for i, createdAt := range allCreatedAts { + orgName := "unknown" + if i < len(organizationNames) { + orgName = organizationNames[i] + } + t.Logf("Organization %s: createdAt=%v, updatedAt=%v", orgName, createdAt, allUpdatedAts[i]) + } + + t.Logf("✅ Verified timestamp variety: Organizations have different created/updated timestamps") + t.Logf("✅ This confirms that order-by functionality can effectively sort entities by these timestamp fields") + }) +} \ No newline at end of file diff --git a/pkg/github/client.go b/pkg/github/client.go index c1f1ed2..c6532ca 100644 --- a/pkg/github/client.go +++ b/pkg/github/client.go @@ -52,6 +52,12 @@ type Request struct { // RequestTimeoutSeconds is the timeout duration for requests made to datasources. // This should be set to the number of seconds to wait before timing out. RequestTimeoutSeconds int + + // Filter contains entity-specific filter expressions for GraphQL queries. + Filter *string + + // OrderBy contains entity-specific ordering expressions for GraphQL queries. + OrderBy *string } // Response is a response returned by the datasource. diff --git a/pkg/github/config.go b/pkg/github/config.go index 69c8977..100aa7b 100644 --- a/pkg/github/config.go +++ b/pkg/github/config.go @@ -45,6 +45,16 @@ type Config struct { // APIVersion is the version of the GitHub API to use. // This is only used when constructing REST endpoints. APIVersion *string `json:"apiVersion"` + + // Filters allows specifying entity-specific filters using GraphQL query parameters. + // Map keys should be entity external IDs, values should be GraphQL filter expressions. + // Example: {"Repository": "visibility: PUBLIC", "Issue": "states: OPEN"} + Filters map[string]string `json:"filters,omitempty"` + + // OrderBy allows specifying entity-specific ordering using GraphQL order parameters. + // Map keys should be entity external IDs, values should be GraphQL order expressions. + // Example: {"Repository": "orderBy: {field: CREATED_AT, direction: DESC}"} + OrderBy map[string]string `json:"orderBy,omitempty"` } // ValidateConfig validates that a Config received in a GetPage call is valid. diff --git a/pkg/github/filters_test.go b/pkg/github/filters_test.go new file mode 100644 index 0000000..3df0cee --- /dev/null +++ b/pkg/github/filters_test.go @@ -0,0 +1,94 @@ +// Copyright 2025 SGNL.ai, Inc. + +package github_test + +import ( + "testing" + + "github.com/sgnl-ai/adapters/pkg/github" + "github.com/sgnl-ai/adapters/pkg/testutil" +) + +func TestSetFilterParameter(t *testing.T) { + tests := map[string]struct { + filter *string + want string + }{ + "nil_filter": { + filter: nil, + want: "", + }, + "empty_filter": { + filter: testutil.GenPtr(""), + want: "", + }, + "visibility_filter": { + filter: testutil.GenPtr("visibility: PUBLIC"), + want: ", visibility: PUBLIC", + }, + "states_filter": { + filter: testutil.GenPtr("states: OPEN"), + want: ", states: OPEN", + }, + "complex_filter": { + filter: testutil.GenPtr("states: [OPEN, MERGED], labels: [\"bug\", \"enhancement\"]"), + want: ", states: [OPEN, MERGED], labels: [\"bug\", \"enhancement\"]", + }, + } + + for name, tc := range tests { + tc := tc + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := github.SetFilterParameter(tc.filter) + + if got != tc.want { + t.Errorf("SetFilterParameter() = %q, want %q", got, tc.want) + } + }) + } +} + +func TestSetOrderByParameter(t *testing.T) { + tests := map[string]struct { + orderBy *string + want string + }{ + "nil_orderBy": { + orderBy: nil, + want: "", + }, + "empty_orderBy": { + orderBy: testutil.GenPtr(""), + want: "", + }, + "created_at_desc": { + orderBy: testutil.GenPtr("orderBy: {field: CREATED_AT, direction: DESC}"), + want: ", orderBy: {field: CREATED_AT, direction: DESC}", + }, + "updated_at_asc": { + orderBy: testutil.GenPtr("orderBy: {field: UPDATED_AT, direction: ASC}"), + want: ", orderBy: {field: UPDATED_AT, direction: ASC}", + }, + "name_asc": { + orderBy: testutil.GenPtr("orderBy: {field: NAME, direction: ASC}"), + want: ", orderBy: {field: NAME, direction: ASC}", + }, + } + + for name, tc := range tests { + tc := tc + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := github.SetOrderByParameter(tc.orderBy) + + if got != tc.want { + t.Errorf("SetOrderByParameter() = %q, want %q", got, tc.want) + } + }) + } +} \ No newline at end of file diff --git a/pkg/github/query.go b/pkg/github/query.go index b772650..7d1517e 100644 --- a/pkg/github/query.go +++ b/pkg/github/query.go @@ -154,6 +154,22 @@ func SetAfterParameter(value *string) string { return fmt.Sprintf(", after: \"%s\"", *value) } +func SetFilterParameter(value *string) string { + if value == nil || *value == "" { + return "" + } + + return fmt.Sprintf(", %s", *value) +} + +func SetOrderByParameter(value *string) string { + if value == nil || *value == "" { + return "" + } + + return fmt.Sprintf(", %s", *value) +} + func (b *OrganizationQueryBuilder) Build(request *Request) (string, *framework.Error) { if request.EnterpriseSlug != nil { orgAfterQuery := SetAfterParameter(b.OrgAfter) @@ -218,6 +234,8 @@ func (b *OrganizationQueryBuilder) Build(request *Request) (string, *framework.E func (b *OrganizationUserQueryBuilder) Build(request *Request) (string, *framework.Error) { userAfterQuery := SetAfterParameter(b.UserAfter) + filterQuery := SetFilterParameter(request.Filter) + orderByQuery := SetOrderByParameter(request.OrderBy) innerNode, err := AttributeQueryBuilder(request.EntityConfig, &b.OrgLogin, "edges") if err != nil { @@ -227,7 +245,7 @@ func (b *OrganizationUserQueryBuilder) Build(request *Request) (string, *framewo query := fmt.Sprintf(`query { organization (login: "%s") { id - membersWithRole (first: %d%s) { + membersWithRole (first: %d%s%s%s) { pageInfo { endCursor hasNextPage @@ -235,7 +253,7 @@ func (b *OrganizationUserQueryBuilder) Build(request *Request) (string, *framewo %s } } - }`, b.OrgLogin, b.PageSize, userAfterQuery, innerNode.BuildQuery()) + }`, b.OrgLogin, b.PageSize, userAfterQuery, filterQuery, orderByQuery, innerNode.BuildQuery()) return query, nil } @@ -243,6 +261,8 @@ func (b *OrganizationUserQueryBuilder) Build(request *Request) (string, *framewo func (b *UserQueryBuilder) Build(request *Request) (string, *framework.Error) { orgAfterQuery := SetAfterParameter(b.OrgAfter) userAfterQuery := SetAfterParameter(b.UserAfter) + filterQuery := SetFilterParameter(request.Filter) + orderByQuery := SetOrderByParameter(request.OrderBy) innerNode, err := AttributeQueryBuilder(request.EntityConfig, nil, "nodes") if err != nil { @@ -260,7 +280,7 @@ func (b *UserQueryBuilder) Build(request *Request) (string, *framework.Error) { } nodes { id - membersWithRole (first: %d%s) { + membersWithRole (first: %d%s%s%s) { pageInfo { endCursor hasNextPage @@ -271,7 +291,7 @@ func (b *UserQueryBuilder) Build(request *Request) (string, *framework.Error) { } } }`, b.EnterpriseQueryInfo.EnterpriseSlug, CollectionPageSize, orgAfterQuery, - b.EnterpriseQueryInfo.PageSize, userAfterQuery, innerNode.BuildQuery()), nil + b.EnterpriseQueryInfo.PageSize, userAfterQuery, filterQuery, orderByQuery, innerNode.BuildQuery()), nil } OrganizationName := request.Organizations[b.OrganizationOffset] @@ -279,7 +299,7 @@ func (b *UserQueryBuilder) Build(request *Request) (string, *framework.Error) { return fmt.Sprintf(`query { organization (login: "%s") { id - membersWithRole (first: %d%s) { + membersWithRole (first: %d%s%s%s) { pageInfo { endCursor hasNextPage @@ -287,7 +307,7 @@ func (b *UserQueryBuilder) Build(request *Request) (string, *framework.Error) { %s } } - }`, OrganizationName, b.EnterpriseQueryInfo.PageSize, userAfterQuery, innerNode.BuildQuery(), + }`, OrganizationName, b.EnterpriseQueryInfo.PageSize, userAfterQuery, filterQuery, orderByQuery, innerNode.BuildQuery(), ), nil } @@ -345,6 +365,8 @@ func (b *TeamQueryBuilder) Build(request *Request) (string, *framework.Error) { func (b *RepositoryQueryBuilder) Build(request *Request) (string, *framework.Error) { orgAfterQuery := SetAfterParameter(b.OrgAfter) repoAfterQuery := SetAfterParameter(b.RepoAfter) + filterQuery := SetFilterParameter(request.Filter) + orderByQuery := SetOrderByParameter(request.OrderBy) innerNode, err := AttributeQueryBuilder(request.EntityConfig, nil, "nodes") if err != nil { @@ -362,7 +384,7 @@ func (b *RepositoryQueryBuilder) Build(request *Request) (string, *framework.Err } nodes { id - repositories (first: %d%s) { + repositories (first: %d%s%s%s) { pageInfo { endCursor hasNextPage @@ -373,7 +395,7 @@ func (b *RepositoryQueryBuilder) Build(request *Request) (string, *framework.Err } } }`, b.EnterpriseQueryInfo.EnterpriseSlug, CollectionPageSize, orgAfterQuery, b.EnterpriseQueryInfo.PageSize, - repoAfterQuery, innerNode.BuildQuery()) + repoAfterQuery, filterQuery, orderByQuery, innerNode.BuildQuery()) return query, nil } @@ -383,7 +405,7 @@ func (b *RepositoryQueryBuilder) Build(request *Request) (string, *framework.Err query := fmt.Sprintf(`query { organization (login: "%s") { id - repositories (first: %d%s) { + repositories (first: %d%s%s%s) { pageInfo { endCursor hasNextPage @@ -391,7 +413,7 @@ func (b *RepositoryQueryBuilder) Build(request *Request) (string, *framework.Err %s } } - }`, OrganizationName, b.EnterpriseQueryInfo.PageSize, repoAfterQuery, innerNode.BuildQuery()) + }`, OrganizationName, b.EnterpriseQueryInfo.PageSize, repoAfterQuery, filterQuery, orderByQuery, innerNode.BuildQuery()) return query, nil } @@ -550,6 +572,8 @@ func (b *IssueQueryBuilder) Build(request *Request) (string, *framework.Error) { orgAfterQuery := SetAfterParameter(b.OrgAfter) repoAfterQuery := SetAfterParameter(b.RepoAfter) issueAfterQuery := SetAfterParameter(b.IssueAfter) + filterQuery := SetFilterParameter(request.Filter) + orderByQuery := SetOrderByParameter(request.OrderBy) innerNode, err := AttributeQueryBuilder(request.EntityConfig, nil, "nodes") if err != nil { @@ -574,7 +598,7 @@ func (b *IssueQueryBuilder) Build(request *Request) (string, *framework.Error) { } nodes { id - issues (first: %d%s) { + issues (first: %d%s%s%s) { pageInfo { endCursor hasNextPage @@ -587,7 +611,7 @@ func (b *IssueQueryBuilder) Build(request *Request) (string, *framework.Error) { } } }`, b.EnterpriseQueryInfo.EnterpriseSlug, CollectionPageSize, orgAfterQuery, CollectionPageSize, - repoAfterQuery, b.EnterpriseQueryInfo.PageSize, issueAfterQuery, innerNode.BuildQuery()), nil + repoAfterQuery, b.EnterpriseQueryInfo.PageSize, issueAfterQuery, filterQuery, orderByQuery, innerNode.BuildQuery()), nil } OrganizationName := request.Organizations[b.OrganizationOffset] @@ -602,7 +626,7 @@ func (b *IssueQueryBuilder) Build(request *Request) (string, *framework.Error) { } nodes { id - issues (first: %d%s) { + issues (first: %d%s%s%s) { pageInfo { endCursor hasNextPage @@ -615,7 +639,7 @@ func (b *IssueQueryBuilder) Build(request *Request) (string, *framework.Error) { }`, OrganizationName, CollectionPageSize, repoAfterQuery, - b.EnterpriseQueryInfo.PageSize, issueAfterQuery, + b.EnterpriseQueryInfo.PageSize, issueAfterQuery, filterQuery, orderByQuery, innerNode.BuildQuery()) return query, nil @@ -899,7 +923,7 @@ func (b *IssueAssigneeQueryBuilder) Build(request *Request) (string, *framework. OrganizationName, CollectionPageSize, repoAfterQuery, CollectionPageSize, issueAfterQuery, - b.EnterpriseQueryInfo.PageSize, issueAfterQuery, + b.EnterpriseQueryInfo.PageSize, assigneeAfterQuery, innerNode.BuildQuery()) return query, nil @@ -995,7 +1019,7 @@ func (b *IssueParticipantQueryBuilder) Build(request *Request) (string, *framewo OrganizationName, CollectionPageSize, repoAfterQuery, CollectionPageSize, issueAfterQuery, - b.EnterpriseQueryInfo.PageSize, issueAfterQuery, + b.EnterpriseQueryInfo.PageSize, participantAfterQuery, innerNode.BuildQuery()) return query, nil @@ -1005,6 +1029,8 @@ func (b *PullRequestQueryBuilder) Build(request *Request) (string, *framework.Er orgAfterQuery := SetAfterParameter(b.OrgAfter) repoAfterQuery := SetAfterParameter(b.RepoAfter) pullRequestAfterQuery := SetAfterParameter(b.PullRequestAfter) + filterQuery := SetFilterParameter(request.Filter) + orderByQuery := SetOrderByParameter(request.OrderBy) innerNode, err := AttributeQueryBuilder(request.EntityConfig, nil, "nodes") if err != nil { @@ -1029,7 +1055,7 @@ func (b *PullRequestQueryBuilder) Build(request *Request) (string, *framework.Er } nodes { id - pullRequests (first: %d%s) { + pullRequests (first: %d%s%s%s) { pageInfo { endCursor hasNextPage @@ -1042,7 +1068,7 @@ func (b *PullRequestQueryBuilder) Build(request *Request) (string, *framework.Er } } }`, b.EnterpriseQueryInfo.EnterpriseSlug, CollectionPageSize, orgAfterQuery, CollectionPageSize, - repoAfterQuery, b.EnterpriseQueryInfo.PageSize, pullRequestAfterQuery, innerNode.BuildQuery()), nil + repoAfterQuery, b.EnterpriseQueryInfo.PageSize, pullRequestAfterQuery, filterQuery, orderByQuery, innerNode.BuildQuery()), nil } OrganizationName := request.Organizations[b.OrganizationOffset] @@ -1057,7 +1083,7 @@ func (b *PullRequestQueryBuilder) Build(request *Request) (string, *framework.Er } nodes { id - pullRequests (first: %d%s) { + pullRequests (first: %d%s%s%s) { pageInfo { endCursor hasNextPage @@ -1070,7 +1096,7 @@ func (b *PullRequestQueryBuilder) Build(request *Request) (string, *framework.Er }`, OrganizationName, CollectionPageSize, repoAfterQuery, - b.EnterpriseQueryInfo.PageSize, pullRequestAfterQuery, + b.EnterpriseQueryInfo.PageSize, pullRequestAfterQuery, filterQuery, orderByQuery, innerNode.BuildQuery()) return query, nil diff --git a/pkg/github/query_test.go b/pkg/github/query_test.go index f449a1a..a5071d7 100644 --- a/pkg/github/query_test.go +++ b/pkg/github/query_test.go @@ -1062,6 +1062,65 @@ func TestConstructQuery(t *testing.T) { } }`, }, + "issueassignee_assignee_after_parameter_fix": { + request: &github.Request{ + BaseURL: "https://api.github.com", + EnterpriseSlug: testutil.GenPtr("testID"), + IsEnterpriseCloud: true, + APIVersion: testutil.GenPtr("v3"), + EntityExternalID: "IssueAssignee", + PageSize: 50, + Token: "Bearer Testtoken", + Cursor: CreateGraphQLCompositeCursor( + []*string{nil, nil, testutil.GenPtr("issueAfter1"), testutil.GenPtr("assigneeAfter456")}, + nil, + nil, + ), + EntityConfig: PopulateDefaultIssueAssigneeEntityConfig(), + }, + wantQuery: `query { + enterprise (slug: "testID") { + id + organizations (first: 1) { + pageInfo { + endCursor + hasNextPage + } + nodes { + id + repositories (first: 1) { + pageInfo { + endCursor + hasNextPage + } + nodes { + id + issues (first: 1, after: "issueAfter1") { + pageInfo { + endCursor + hasNextPage + } + nodes { + id + assignees (first: 50, after: "assigneeAfter456") { + pageInfo { + endCursor + hasNextPage + } + nodes { + id + login + } + } + } + } + } + } + } + } + } + }`, + }, "default_issueparticipant_entity_attributes": { request: &github.Request{ BaseURL: "https://api.github.com",