From f9964c6bdd1625ecbfe7b857a077bd07e9286b7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20=28Dibildos=29=20Gonz=C3=A1lez?= Date: Fri, 31 Oct 2025 02:36:02 -0700 Subject: [PATCH 01/24] feat(driver): get projectv2 by url and contentid --- .../Invoke-OrgProjectItemByContentId.ps1 | 20 +++++++ .../issue/Invoke-OrgProjectItemByUrl.ps1 | 20 +++++++ public/graphql/_content.tag | 16 +++++ public/graphql/_field.tag | 5 ++ public/graphql/_fieldValuesNodes.tag | 58 +++++++++++++++++++ public/graphql/_item.tag | 8 +++ .../graphql/orgprojectitembycontentid.query | 8 +++ public/graphql/orgprojectitembyurl.query | 8 +++ 8 files changed, 143 insertions(+) create mode 100644 public/driver/issue/Invoke-OrgProjectItemByContentId.ps1 create mode 100644 public/driver/issue/Invoke-OrgProjectItemByUrl.ps1 create mode 100644 public/graphql/_content.tag create mode 100644 public/graphql/_field.tag create mode 100644 public/graphql/_fieldValuesNodes.tag create mode 100644 public/graphql/_item.tag create mode 100644 public/graphql/orgprojectitembycontentid.query create mode 100644 public/graphql/orgprojectitembyurl.query diff --git a/public/driver/issue/Invoke-OrgProjectItemByContentId.ps1 b/public/driver/issue/Invoke-OrgProjectItemByContentId.ps1 new file mode 100644 index 0000000..aea18b9 --- /dev/null +++ b/public/driver/issue/Invoke-OrgProjectItemByContentId.ps1 @@ -0,0 +1,20 @@ +function Invoke-OrgProjectItemByContentId{ + [cmdletbinding()] + param( + [Parameter(Mandatory)][string]$Owner, + [Parameter(Mandatory)][string]$ProjectNumber, + [Parameter(Mandatory)][string]$ContentId + ) + + $query = Get-GraphQLString "orgprojectitembycontentid.query" + + $variables = @{ + login = $Owner + number = $ProjectNumber + contentid = $ContentId + } + + $response = Invoke-GraphQL -Query $query -Variables $variables + + return $response +} Export-ModuleMember -Function Invoke-AddSubIssue diff --git a/public/driver/issue/Invoke-OrgProjectItemByUrl.ps1 b/public/driver/issue/Invoke-OrgProjectItemByUrl.ps1 new file mode 100644 index 0000000..b9d0ac4 --- /dev/null +++ b/public/driver/issue/Invoke-OrgProjectItemByUrl.ps1 @@ -0,0 +1,20 @@ +function Invoke-OrgProjectItemByUrl{ + [cmdletbinding()] + param( + [Parameter(Mandatory)][string]$Owner, + [Parameter(Mandatory)][string]$ProjectNumber, + [Parameter(Mandatory)][string]$url + ) + + $query = Get-GraphQLString "orgprojectitembyurl.query" + + $variables = @{ + login = $Owner + number = $ProjectNumber + url = $Url + } + + $response = Invoke-GraphQL -Query $query -Variables $variables + + return $response +} Export-ModuleMember -Function Invoke-AddSubIssue diff --git a/public/graphql/_content.tag b/public/graphql/_content.tag new file mode 100644 index 0000000..1ade5a8 --- /dev/null +++ b/public/graphql/_content.tag @@ -0,0 +1,16 @@ +{ + __typename, + ... on DraftIssue {id,body,title,updatedAt,createdAt}, + ... on PullRequest{id,body,title,updatedAt,createdAt,number,url,state,repository{name,owner{login}} + comments(last: $lastComments){ + totalCount, + nodes{createdAt,updatedAt,url,body,fullDatabaseId,author{login}} + } + }, + ... on Issue{id,body,title,updatedAt,createdAt,number,url,state,repository{name,owner{login}} + comments(last: $lastComments){ + totalCount, + nodes{createdAt,updatedAt,url,body,fullDatabaseId,author{login}} + } + } +} \ No newline at end of file diff --git a/public/graphql/_field.tag b/public/graphql/_field.tag new file mode 100644 index 0000000..6b8ed2a --- /dev/null +++ b/public/graphql/_field.tag @@ -0,0 +1,5 @@ +{__typename, + ... on ProjectV2Field{id,name,dataType}, + ... on ProjectV2IterationField{id,name,dataType}, + ... on ProjectV2SingleSelectField{id,name,dataType,options{id,name}} +} \ No newline at end of file diff --git a/public/graphql/_fieldValuesNodes.tag b/public/graphql/_fieldValuesNodes.tag new file mode 100644 index 0000000..167cf53 --- /dev/null +++ b/public/graphql/_fieldValuesNodes.tag @@ -0,0 +1,58 @@ +{ + __typename, + ... on ProjectV2ItemFieldDateValue{ + date, + field{{field}} + }, + ... on ProjectV2ItemFieldIterationValue{ + title,startDate,duration, + field{{field}} + }, + ... on ProjectV2ItemFieldLabelValue{ + labels(first: 10){ + nodes{name} + }, + field{{field}} + }, + ... on ProjectV2ItemFieldNumberValue{ + number, + field{{field}} + }, + ... on ProjectV2ItemFieldSingleSelectValue{ + name, + field{{field}} + }, + ... on ProjectV2ItemFieldTextValue{ + text, + field{{field}} + }, + ... on ProjectV2ItemFieldMilestoneValue{ + milestone{title,description,dueOn}, + field{{field}} + }, + ... on ProjectV2ItemFieldPullRequestValue{ + pullRequests(first: 10){ + nodes{url} + }, + field{{field}} + }, + ... on ProjectV2ItemFieldRepositoryValue{ + repository{url}, + field{{field}} + }, + ... on ProjectV2ItemFieldUserValue{ + users(first: 10){ + nodes{login} + }, + field{{field}} + }, + ... on ProjectV2ItemFieldReviewerValue{ + reviewers(first: 10){ + nodes{__typename, + ... on Team{name}, + ... on User{login} + } + }, + field{{field}} + } +} \ No newline at end of file diff --git a/public/graphql/_item.tag b/public/graphql/_item.tag new file mode 100644 index 0000000..95159f4 --- /dev/null +++ b/public/graphql/_item.tag @@ -0,0 +1,8 @@ +{ + id,fullDatabaseId, + project{ id, number, title, url}, + content{{content}}, + fieldValues(first: 100){ + nodes{{fieldValuesNodes}} + } +} \ No newline at end of file diff --git a/public/graphql/orgprojectitembycontentid.query b/public/graphql/orgprojectitembycontentid.query new file mode 100644 index 0000000..1873a3c --- /dev/null +++ b/public/graphql/orgprojectitembycontentid.query @@ -0,0 +1,8 @@ +query OrgProjectItemByUrl($login:String! $number:Int! $contentid:String!){ + organization(login: $login){ + projectV2(number: $number){ + item(id: $contentid) + {{item}} + } + } +} \ No newline at end of file diff --git a/public/graphql/orgprojectitembyurl.query b/public/graphql/orgprojectitembyurl.query new file mode 100644 index 0000000..ec49f58 --- /dev/null +++ b/public/graphql/orgprojectitembyurl.query @@ -0,0 +1,8 @@ +query OrgProjectItemByUrl($login:String! $number:Int! $url:String!){ + organization(login: $login){ + projectV2(number: $number){ + item(url: $url) + {{item}} + } + } +} \ No newline at end of file From ce1ebaea3a1fcc33cf0be6033918e07074b65ca6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20=28Dibildos=29=20Gonz=C3=A1lez?= Date: Fri, 31 Oct 2025 14:13:41 +0100 Subject: [PATCH 02/24] feat(get-project): Add query parameter --- Test/mockfiles.log | 28 ++++++++++++ .../MockCall_GitHubOrgProjectWithFields.ps1 | 4 ++ Test/public/project_item_list.test.ps1 | 10 ++--- Test/traceInvoke.log | 7 +++ .../project_database_update.ps1 | 7 +-- public/driver/driver_gh.ps1 | 45 +++++-------------- .../orgprojectwithfieldsAndItems.query | 4 +- public/project/getproject.ps1 | 3 +- 8 files changed, 61 insertions(+), 47 deletions(-) diff --git a/Test/mockfiles.log b/Test/mockfiles.log index f4b8fdd..e6f384a 100644 --- a/Test/mockfiles.log +++ b/Test/mockfiles.log @@ -150,5 +150,33 @@ { "FileName": "invoke-GitHubOrgProjectWithFields-octodemo-626.json", "Command": "Invoke-GitHubOrgProjectWithFields -Owner octodemo -ProjectNumber 626 -afterFields \"\" -afterItems \"\"" + }, + { + "Command": "Invoke-GitHubOrgProjectWithFields -Owner octodemo -ProjectNumber 700 -afterFields \"\" -afterItems \"\" -firstItems 0 -query \"\"", + "FileName": "invoke-GitHubOrgProjectWithFields-octodemo-700-skipitems.json" + }, + { + "Command": "Invoke-GitHubOrgProjectWithFields -Owner octodemo -ProjectNumber 700 -afterFields \"\" -afterItems \"\" -query \"\"", + "FileName": "invoke-GitHubOrgProjectWithFields-octodemo-700.json" + }, + { + "Command": "Invoke-GitHubOrgProjectWithFields -Owner octodemo -ProjectNumber 625 -afterFields \"\" -afterItems \"\" -firstItems 0 -query \"\"", + "FileName": "invoke-GitHubOrgProjectWithFields-octodemo-625.2-skipitems.json" + }, + { + "Command": "Invoke-GitHubOrgProjectWithFields -Owner SomeOrg -ProjectNumber 164 -afterFields \"\" -afterItems \"\" -firstItems 0 -query \"\"", + "FileName": "projectV2-skipitems.json" + }, + { + "Command": "Invoke-GitHubOrgProjectWithFields -Owner SomeOrg -ProjectNumber 164 -afterFields \"\" -afterItems \"\" -query \"\"", + "FileName": "projectV2.json" + }, + { + "Command": "Invoke-GitHubOrgProjectWithFields -Owner octodemo -ProjectNumber 625 -afterFields \"\" -afterItems \"\" -query \"\"", + "FileName": "invoke-GitHubOrgProjectWithFields-octodemo-625.json" + }, + { + "Command": "Invoke-GitHubOrgProjectWithFields -Owner octodemo -ProjectNumber 626 -afterFields \"\" -afterItems \"\" -query \"\"", + "FileName": "invoke-GitHubOrgProjectWithFields-octodemo-626.json" } ] diff --git a/Test/private/MockCall_GitHubOrgProjectWithFields.ps1 b/Test/private/MockCall_GitHubOrgProjectWithFields.ps1 index bd807fe..faca619 100644 --- a/Test/private/MockCall_GitHubOrgProjectWithFields.ps1 +++ b/Test/private/MockCall_GitHubOrgProjectWithFields.ps1 @@ -3,6 +3,7 @@ function MockCall_GitHubOrgProjectWithFields { Param( [string]$Owner, [string]$ProjectNumber, + [string]$Query, [string]$FileName, [switch]$SkipItems ) @@ -14,6 +15,7 @@ function MockCall_GitHubOrgProjectWithFields { $cmd = $cmd -replace '{projectnumber}', $ProjectNumber $cmd = $cmd -replace '{afterFields}', "" $cmd = $cmd -replace '{afterItems}', "" + $cmd = $cmd -replace '{query}', $query # Check if filename contains "skipitems" and throw error if it doesn't if ( $SkipItems -and $FileName -notlike '*skipitems*') { @@ -34,6 +36,8 @@ function MockCall_GitHubOrgProjectWithFields_Null { $cmd = $cmd -replace '{projectnumber}', $ProjectNumber $cmd = $cmd -replace '{afterFields}', "" $cmd = $cmd -replace '{afterItems}', "" + $cmd = $cmd -replace '{query}', "" + MockCalltoNull -Command $cmd } \ No newline at end of file diff --git a/Test/public/project_item_list.test.ps1 b/Test/public/project_item_list.test.ps1 index eadc631..4818f3c 100644 --- a/Test/public/project_item_list.test.ps1 +++ b/Test/public/project_item_list.test.ps1 @@ -1,4 +1,4 @@ -function Test_GetProjetItemList_SUCCESS{ +function Test_GetProjetItems_SUCCESS{ Reset-InvokeCommandMock Mock_DatabaseRoot @@ -50,16 +50,14 @@ function Test_GetProjetItemList_SUCCESS{ Assert-Count -Expected $itemsCount -Presented $result } -function Test_GetProjetItemList_FAIL{ +function Test_GetProjetItems_FAIL{ Reset-InvokeCommandMock Mock_DatabaseRoot - $Owner = "SomeOrg" ; $ProjectNumber = 164 ; $itemsCount = 12 + $Owner = "SomeOrg" ; $ProjectNumber = 164 - MockCall_GitHubOrgProjectWithFields_Null -Owner $owner -ProjectNumber $projectNumber - - Mock_DatabaseRoot + MockCall_GitHubOrgProjectWithFields_Null -Owner $owner -ProjectNumber $projectNumber # Start the transcript diff --git a/Test/traceInvoke.log b/Test/traceInvoke.log index a97125a..2215e0d 100644 --- a/Test/traceInvoke.log +++ b/Test/traceInvoke.log @@ -66,3 +66,10 @@ Get-Date -Format yyyy-MM-dd Invoke-ProjectInjectionFunctions Get-SfAccount "https://some.com/1234/viuew" Get-SfAccount "https://some.com/4321/viuew" +Invoke-GitHubOrgProjectWithFields -Owner octodemo -ProjectNumber 700 -afterFields "" -afterItems "" -firstItems 0 -query "" +Invoke-GitHubOrgProjectWithFields -Owner octodemo -ProjectNumber 700 -afterFields "" -afterItems "" -query "" +Invoke-GitHubOrgProjectWithFields -Owner octodemo -ProjectNumber 625 -afterFields "" -afterItems "" -firstItems 0 -query "" +Invoke-GitHubOrgProjectWithFields -Owner SomeOrg -ProjectNumber 164 -afterFields "" -afterItems "" -firstItems 0 -query "" +Invoke-GitHubOrgProjectWithFields -Owner SomeOrg -ProjectNumber 164 -afterFields "" -afterItems "" -query "" +Invoke-GitHubOrgProjectWithFields -Owner octodemo -ProjectNumber 625 -afterFields "" -afterItems "" -query "" +Invoke-GitHubOrgProjectWithFields -Owner octodemo -ProjectNumber 626 -afterFields "" -afterItems "" -query "" diff --git a/private/projectDatabase/project_database_update.ps1 b/private/projectDatabase/project_database_update.ps1 index 01fbbfb..be0ab9c 100644 --- a/private/projectDatabase/project_database_update.ps1 +++ b/private/projectDatabase/project_database_update.ps1 @@ -1,6 +1,6 @@ # We need to invoke a call back to allow the mock of this call on testing -Set-MyInvokeCommandAlias -Alias GitHubOrgProjectWithFields -Command 'Invoke-GitHubOrgProjectWithFields -Owner {owner} -ProjectNumber {projectnumber} -afterFields "{afterFields}" -afterItems "{afterItems}"' -Set-MyInvokeCommandAlias -Alias GitHubOrgProjectWithFieldsSkipItems -Command 'Invoke-GitHubOrgProjectWithFields -Owner {owner} -ProjectNumber {projectnumber} -afterFields "{afterFields}" -afterItems "{afterItems}" -firstItems 0' +Set-MyInvokeCommandAlias -Alias GitHubOrgProjectWithFields -Command 'Invoke-GitHubOrgProjectWithFields -Owner {owner} -ProjectNumber {projectnumber} -afterFields "{afterFields}" -afterItems "{afterItems}" -query "{query}"' +Set-MyInvokeCommandAlias -Alias GitHubOrgProjectWithFieldsSkipItems -Command 'Invoke-GitHubOrgProjectWithFields -Owner {owner} -ProjectNumber {projectnumber} -afterFields "{afterFields}" -afterItems "{afterItems}" -firstItems 0 -query "{query}"' function Update-ProjectDatabase { [CmdletBinding()] @@ -8,11 +8,12 @@ function Update-ProjectDatabase { param( [Parameter()][string]$Owner, [Parameter()][int]$ProjectNumber, + [Parameter()][string]$Query, [Parameter()][switch]$SkipItems, [Parameter()][switch]$Force ) - $params = @{ owner = $Owner ; projectnumber = $ProjectNumber ; afterFields = "" ; afterItems = "" } + $params = @{ owner = $Owner ; projectnumber = $ProjectNumber ; afterFields = "" ; afterItems = "" ; query = "$query" } # This means that the ProjectNumber has a empty string value if($ProjectNumber -eq 0){ diff --git a/public/driver/driver_gh.ps1 b/public/driver/driver_gh.ps1 index 4e5a246..391c684 100644 --- a/public/driver/driver_gh.ps1 +++ b/public/driver/driver_gh.ps1 @@ -14,30 +14,16 @@ Set-MyinvokeCommandAlias -Alias GetToken -Command "gh auth token" #> function Invoke-GitHubOrgProjectWithFields { param( - [Parameter(Mandatory=$true)] [string]$Owner, - [Parameter(Mandatory=$true)] [string]$ProjectNumber, - [Parameter(Mandatory=$false)] [string]$afterFields = $null, - [Parameter(Mandatory=$false)] [int]$firstFields = 100, - [Parameter(Mandatory=$false)] [string]$afterItems = $null, - [Parameter(Mandatory=$false)] [int]$firstItems = 100, - [Parameter(Mandatory=$false)] [int]$lastComments = 1 + [Parameter(Mandatory)] [string]$Owner, + [Parameter(Mandatory)] [string]$ProjectNumber, + [Parameter()] [string]$Query = "", + [Parameter()] [string]$afterFields = $null, + [Parameter()] [int]$firstFields = 100, + [Parameter()] [string]$afterItems = $null, + [Parameter()] [int]$firstItems = 100, + [Parameter()] [int]$lastComments = 1 ) - # Use the environmentraviable - $token = Get-GithubToken - if(-not $token){ - throw "GH Cli Auth Token not available. Run 'gh auth login' in your terminal." - } - - # Define the GraphQL query with variables - $query = Get-GraphQLString "orgprojectwithfieldsAndItems.query" - - # Define the headers for the request - $headers = @{ - "Authorization" = "Bearer $token" - "Content-Type" = "application/json" - } - # Define the variables for the request [int]$pn = $ProjectNumber $variables = @{ @@ -48,22 +34,11 @@ function Invoke-GitHubOrgProjectWithFields { firstFields = $firstFields firstItems = $firstItems lastComments = $lastComments - } - - # Define the body for the request - $body = @{ query = $query - variables = $variables - } | ConvertTo-Json + } # Send the request - $response = Invoke-RestMethod -Uri 'https://api.github.com/graphql' -Method Post -Body $body -Headers $headers - - # Check if here are errors - if($response.errors){ - getErrrorString -Errors $response.errors | Write-MyError - return - } + $response = Invoke-GraphQL -Variables $variables -Query (Get-GraphQLString "orgprojectwithfieldsAndItems.query") # Return the field names return $response diff --git a/public/graphql/orgprojectwithfieldsAndItems.query b/public/graphql/orgprojectwithfieldsAndItems.query index 50c649b..ee071af 100644 --- a/public/graphql/orgprojectwithfieldsAndItems.query +++ b/public/graphql/orgprojectwithfieldsAndItems.query @@ -1,6 +1,6 @@ -query OrgProjectWithFields( $afterFields:String! $afterItems:String! $lastComments: Int=10 $firstFields:Int!$firstItems:Int!$login:String!$number:Int!){organization(login: $login){ +query OrgProjectWithFields( $afterFields:String! $afterItems:String! $lastComments: Int=10 $firstFields:Int!$firstItems:Int!$login:String!$number:Int! $query:String=""){organization(login: $login){ projectV2(number: $number){number,url,shortDescription,public,closed,title,id,readme, - items(first: $firstItems, after: $afterItems){ + items(first: $firstItems, after: $afterItems, query:$query){ pageInfo{endCursor,hasNextPage}, totalCount, nodes{ diff --git a/public/project/getproject.ps1 b/public/project/getproject.ps1 index c690524..7c4a2b0 100644 --- a/public/project/getproject.ps1 +++ b/public/project/getproject.ps1 @@ -3,6 +3,7 @@ function Get-Project { param( [Parameter()][string]$Owner, [Parameter()][int]$ProjectNumber, + [Parameter()][string]$Query, [Parameter()][switch]$SkipItems, [Parameter()][switch]$Force ) @@ -13,7 +14,7 @@ function Get-Project { } if ($Force -or -Not (Test-ProjectDatabase -Owner $Owner -ProjectNumber $ProjectNumber)) { - $result = Update-ProjectDatabase -Owner $Owner -ProjectNumber $ProjectNumber -SkipItems:$SkipItems + $result = Update-ProjectDatabase -Owner $Owner -ProjectNumber $ProjectNumber -Query $query -SkipItems:$SkipItems if ( ! $result) { return } } From ba143f32cbd8ce15886f4c871b0c6e20dea5af3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20=28Dibildos=29=20Gonz=C3=A1lez?= Date: Mon, 3 Nov 2025 15:15:21 +0100 Subject: [PATCH 03/24] feat(Save-ProjectV2toDatabase): update each of the new items and not all the list at once --- Test/private/MockCall_Project700.ps1 | 1 + ...ectWithFields-octodemo-700-singleItem.json | 338 ++++++++++++++++++ Test/public/project/getproject.test.ps1 | 15 + private/projectDatabase/project_database.ps1 | 17 +- public/project/getproject.ps1 | 3 +- 5 files changed, 369 insertions(+), 5 deletions(-) create mode 100644 Test/private/mocks/invoke-GitHubOrgProjectWithFields-octodemo-700-singleItem.json create mode 100644 Test/public/project/getproject.test.ps1 diff --git a/Test/private/MockCall_Project700.ps1 b/Test/private/MockCall_Project700.ps1 index e6105f4..3d939b9 100644 --- a/Test/private/MockCall_Project700.ps1 +++ b/Test/private/MockCall_Project700.ps1 @@ -18,6 +18,7 @@ function Get-Mock_Project_700 { # Version of the project file modified manually to have two items with same id case sensitive # this is used to test case sensitivity of item ids in hashtables $project.projectFile_caseSensitive = "invoke-GitHubOrgProjectWithFields-octodemo-700-caseSensitive.json" + $project.projectFile_singleItem = "invoke-GitHubOrgProjectWithFields-octodemo-700-singleItem.json" $content = Get-MockFileContentJson -FileName $project.projectFile $pActual = $content.data.organization.projectV2 diff --git a/Test/private/mocks/invoke-GitHubOrgProjectWithFields-octodemo-700-singleItem.json b/Test/private/mocks/invoke-GitHubOrgProjectWithFields-octodemo-700-singleItem.json new file mode 100644 index 0000000..4e6bbdf --- /dev/null +++ b/Test/private/mocks/invoke-GitHubOrgProjectWithFields-octodemo-700-singleItem.json @@ -0,0 +1,338 @@ +{ + "data": { + "organization": { + "projectV2": { + "number": 700, + "url": "https://github.com/orgs/octodemo/projects/700", + "shortDescription": "Project used as development environment by rulasg", + "public": false, + "closed": false, + "title": "rulasg-dev-700", + "id": "PVT_kwDOAlIw4c4BCe3V", + "readme": null, + "items": { + "pageInfo": { + "endCursor": "Y3Vyc29yOnYyOpK5MDAwMDAwMDAuMDMzMzMzMzMzMzMzMzMzM84HwRmY", + "hasNextPage": false + }, + "totalCount": 1, + "nodes": [ + { + "id": "PVTI_lADOAlIw4c4BCe3Vzgeio4o", + "fullDatabaseId": "128099210", + "project": { + "id": "PVT_kwDOAlIw4c4BCe3V", + "number": 700, + "title": "rulasg-dev-700", + "url": "https://github.com/orgs/octodemo/projects/700" + }, + "content": { + "__typename": "Issue", + "id": "I_kwDOPrRnkc7KkwSq", + "body": "Body of issue for development", + "title": "Issue for development New Value", + "updatedAt": "2025-10-15T21:30:02Z", + "createdAt": "2025-09-09T14:01:17Z", + "number": 26, + "url": "https://github.com/octodemo/rulasg-dev-1/issues/26", + "state": "OPEN", + "repository": { + "name": "rulasg-dev-1", + "owner": { + "login": "octodemo" + } + }, + "comments": { + "totalCount": 3, + "nodes": [ + { + "createdAt": "2025-09-23T17:51:06Z", + "updatedAt": "2025-10-15T21:29:45Z", + "url": "https://github.com/octodemo/rulasg-dev-1/issues/26#issuecomment-3324995787", + "body": "Sample comment 1", + "fullDatabaseId": "3324995787", + "author": { + "login": "rulasg" + } + }, + { + "createdAt": "2025-09-24T08:29:13Z", + "updatedAt": "2025-10-15T21:29:55Z", + "url": "https://github.com/octodemo/rulasg-dev-1/issues/26#issuecomment-3327194303", + "body": "Sample comment 2", + "fullDatabaseId": "3327194303", + "author": { + "login": "rulasg" + } + }, + { + "createdAt": "2025-09-30T05:42:49Z", + "updatedAt": "2025-10-15T21:30:02Z", + "url": "https://github.com/octodemo/rulasg-dev-1/issues/26#issuecomment-3350059109", + "body": "Sample comment 3", + "fullDatabaseId": "3350059109", + "author": { + "login": "rulasg" + } + } + ] + } + }, + "fieldValues": { + "nodes": [ + { + "__typename": "ProjectV2ItemFieldRepositoryValue", + "repository": { + "url": "https://github.com/octodemo/rulasg-dev-1" + }, + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-k", + "name": "Repository", + "dataType": "REPOSITORY" + } + }, + { + "__typename": "ProjectV2ItemFieldMilestoneValue", + "milestone": { + "title": "Milestone 3: Quality and Deployment", + "description": "Testing, documentation, and deployment pipeline setup", + "dueOn": "2025-10-18T00:00:00Z" + }, + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-g", + "name": "Milestone", + "dataType": "MILESTONE" + } + }, + { + "__typename": "ProjectV2ItemFieldTextValue", + "text": "Issue for development", + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-M", + "name": "Title", + "dataType": "TITLE" + } + }, + { + "__typename": "ProjectV2ItemFieldSingleSelectValue", + "name": "Todo", + "field": { + "__typename": "ProjectV2SingleSelectField", + "id": "PVTSSF_lADOAlIw4c4BCe3Vzg0rg-U", + "name": "Status", + "dataType": "SINGLE_SELECT", + "options": [ + { + "id": "f75ad846", + "name": "Todo" + }, + { + "id": "47fc9ee4", + "name": "In Progress" + }, + { + "id": "98236657", + "name": "Done" + } + ] + } + }, + { + "__typename": "ProjectV2ItemFieldNumberValue", + "number": 333.0, + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rhjU", + "name": "field-number", + "dataType": "NUMBER" + } + }, + { + "__typename": "ProjectV2ItemFieldIterationValue", + "title": "field-iteration 3", + "startDate": "2025-10-05", + "duration": 14, + "field": { + "__typename": "ProjectV2IterationField", + "id": "PVTIF_lADOAlIw4c4BCe3Vzg0rhqQ", + "name": "field-iteration", + "dataType": "ITERATION" + } + }, + { + "__typename": "ProjectV2ItemFieldTextValue", + "text": "text3", + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rhko", + "name": "field-text", + "dataType": "TEXT" + } + }, + { + "__typename": "ProjectV2ItemFieldSingleSelectValue", + "name": "option-3", + "field": { + "__typename": "ProjectV2SingleSelectField", + "id": "PVTSSF_lADOAlIw4c4BCe3Vzg0rhno", + "name": "field-singleselect", + "dataType": "SINGLE_SELECT", + "options": [ + { + "id": "7d96a925", + "name": "option-1" + }, + { + "id": "cd02c585", + "name": "option-2" + }, + { + "id": "6b66c93a", + "name": "option-3" + } + ] + } + } + ] + } + } + ] + }, + "fields": { + "totalCount": 15, + "nodes": [ + { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-M", + "name": "Title", + "dataType": "TITLE" + }, + { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-Q", + "name": "Assignees", + "dataType": "ASSIGNEES" + }, + { + "__typename": "ProjectV2SingleSelectField", + "id": "PVTSSF_lADOAlIw4c4BCe3Vzg0rg-U", + "name": "Status", + "dataType": "SINGLE_SELECT", + "options": [ + { + "id": "f75ad846", + "name": "Todo" + }, + { + "id": "47fc9ee4", + "name": "In Progress" + }, + { + "id": "98236657", + "name": "Done" + } + ] + }, + { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-Y", + "name": "Labels", + "dataType": "LABELS" + }, + { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-c", + "name": "Linked pull requests", + "dataType": "LINKED_PULL_REQUESTS" + }, + { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-g", + "name": "Milestone", + "dataType": "MILESTONE" + }, + { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-k", + "name": "Repository", + "dataType": "REPOSITORY" + }, + { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-s", + "name": "Reviewers", + "dataType": "REVIEWERS" + }, + { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-w", + "name": "Parent issue", + "dataType": "PARENT_ISSUE" + }, + { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-0", + "name": "Sub-issues progress", + "dataType": "SUB_ISSUES_PROGRESS" + }, + { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rhjU", + "name": "field-number", + "dataType": "NUMBER" + }, + { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rhko", + "name": "field-text", + "dataType": "TEXT" + }, + { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rhlU", + "name": "field-date", + "dataType": "DATE" + }, + { + "__typename": "ProjectV2SingleSelectField", + "id": "PVTSSF_lADOAlIw4c4BCe3Vzg0rhno", + "name": "field-singleselect", + "dataType": "SINGLE_SELECT", + "options": [ + { + "id": "7d96a925", + "name": "option-1" + }, + { + "id": "cd02c585", + "name": "option-2" + }, + { + "id": "6b66c93a", + "name": "option-3" + } + ] + }, + { + "__typename": "ProjectV2IterationField", + "id": "PVTIF_lADOAlIw4c4BCe3Vzg0rhqQ", + "name": "field-iteration", + "dataType": "ITERATION" + } + ], + "pageInfo": { + "endCursor": "MTU", + "hasNextPage": false + } + }, + "owner": { + "__typename": "Organization", + "login": "octodemo" + } + } + } + } +} diff --git a/Test/public/project/getproject.test.ps1 b/Test/public/project/getproject.test.ps1 new file mode 100644 index 0000000..3f54917 --- /dev/null +++ b/Test/public/project/getproject.test.ps1 @@ -0,0 +1,15 @@ +function Test_GetProject_With_Query{ + Reset-InvokeCommandMock + Mock_DatabaseRoot + + $p = Get-Mock_Project_700 ; $owner = $p.owner ; $projectNumber = $p.projectNumber + $i = $p.issue + $query = "some query" + + MockCall_GitHubOrgProjectWithFields -Owner $owner -ProjectNumber $projectNumber -Query $query + + $result = Get-Project + + Assert-NotImplemented + +} \ No newline at end of file diff --git a/private/projectDatabase/project_database.ps1 b/private/projectDatabase/project_database.ps1 index d074652..fdf10f7 100644 --- a/private/projectDatabase/project_database.ps1 +++ b/private/projectDatabase/project_database.ps1 @@ -72,8 +72,9 @@ function Save-ProjectV2toDatabase{ [CmdletBinding()] param( [Parameter(Position = 0)][object]$ProjectV2, - [Parameter(Position = 1)][Object[]]$Items, - [Parameter(Position = 2)][Object[]]$Fields + [Parameter(Position = 1)][hashtable]$Items, + [Parameter(Position = 2)][Object[]]$Fields, + [Parameter()][switch]$ItemsUpdate ) $owner = $ProjectV2.owner.login @@ -92,7 +93,17 @@ function Save-ProjectV2toDatabase{ $db.owner = $owner $db.number = $projectnumber - $db.items = $items + if($ItemsUpdate){ + # Update each of the items to avoid replacing all + foreach ($item in $items.Values){ + Set-Item $db $item + } + } else { + # Add items + $db.items = $Items + } + + # Update fields $db.fields = $fields # This is the only Save.ProjectDatabase that should not be called with -Safe diff --git a/public/project/getproject.ps1 b/public/project/getproject.ps1 index 7c4a2b0..c690524 100644 --- a/public/project/getproject.ps1 +++ b/public/project/getproject.ps1 @@ -3,7 +3,6 @@ function Get-Project { param( [Parameter()][string]$Owner, [Parameter()][int]$ProjectNumber, - [Parameter()][string]$Query, [Parameter()][switch]$SkipItems, [Parameter()][switch]$Force ) @@ -14,7 +13,7 @@ function Get-Project { } if ($Force -or -Not (Test-ProjectDatabase -Owner $Owner -ProjectNumber $ProjectNumber)) { - $result = Update-ProjectDatabase -Owner $Owner -ProjectNumber $ProjectNumber -Query $query -SkipItems:$SkipItems + $result = Update-ProjectDatabase -Owner $Owner -ProjectNumber $ProjectNumber -SkipItems:$SkipItems if ( ! $result) { return } } From e92d5554cae86fe3d446403cd8daff1bed940f3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20=28Dibildos=29=20Gonz=C3=A1lez?= Date: Wed, 5 Nov 2025 20:37:57 +0100 Subject: [PATCH 04/24] wip --- Test/public/project/getproject.test.ps1 | 8 +++++--- private/projectDatabase/project_database_update.ps1 | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Test/public/project/getproject.test.ps1 b/Test/public/project/getproject.test.ps1 index 3f54917..3042465 100644 --- a/Test/public/project/getproject.test.ps1 +++ b/Test/public/project/getproject.test.ps1 @@ -2,13 +2,15 @@ function Test_GetProject_With_Query{ Reset-InvokeCommandMock Mock_DatabaseRoot - $p = Get-Mock_Project_700 ; $owner = $p.owner ; $projectNumber = $p.projectNumber + $p = Get-Mock_Project_700 ; $owner = $p.owner ; $projectNumber = $p.number $i = $p.issue $query = "some query" - MockCall_GitHubOrgProjectWithFields -Owner $owner -ProjectNumber $projectNumber -Query $query + # MockCall_GitHubOrgProjectWithFields -Owner $owner -ProjectNumber $projectNumber -Query $query -Fieldname - $result = Get-Project + $result = Invoke-GitHubOrgProjectWithFields -Owner $owner -ProjectNumber $projectNumber -query $query + + $result = Update-ProjectDatabase -Owner $owner -ProjectNumber $projectNumber -Query $query Assert-NotImplemented diff --git a/private/projectDatabase/project_database_update.ps1 b/private/projectDatabase/project_database_update.ps1 index be0ab9c..92c4619 100644 --- a/private/projectDatabase/project_database_update.ps1 +++ b/private/projectDatabase/project_database_update.ps1 @@ -88,7 +88,7 @@ function Update-ProjectDatabase { Save-ProjectV2toDatabase $projectV2 -Items $items -Fields $fields return $true -} +} Export-ModuleMember -Function Update-ProjectDatabase <# .SYNOPSIS From 47170c0b908f12e2c03b67639b979c70e81363f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20=28Dibildos=29=20Gonz=C3=A1lez?= Date: Wed, 5 Nov 2025 20:56:44 +0100 Subject: [PATCH 05/24] feat(database): log database store path during retrieval --- private/database/databaseV2.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/private/database/databaseV2.ps1 b/private/database/databaseV2.ps1 index 0dea988..ae89031 100644 --- a/private/database/databaseV2.ps1 +++ b/private/database/databaseV2.ps1 @@ -30,6 +30,7 @@ function Get-DatabaseStore { if ($Force -or -Not $script:databaseRoot) { $script:databaseRoot = Invoke-MyCommand -Command GetDatabaseStorePath + "Using DatabaseStore path: $script:databaseRoot" | Write-MyDebug -Section DatabaseStore } return $script:databaseRoot From 60cf5e6b04b96113e89a2f603800ec5c6218f172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20=28Dibildos=29=20Gonz=C3=A1lez?= Date: Mon, 10 Nov 2025 07:52:40 +0100 Subject: [PATCH 06/24] refactor(Show-ProjectItem) improve tracing and jumplines --- public/items/project_item_show.ps1 | 41 +++++++++++++++++++----------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/public/items/project_item_show.ps1 b/public/items/project_item_show.ps1 index 27c578b..663ce6a 100644 --- a/public/items/project_item_show.ps1 +++ b/public/items/project_item_show.ps1 @@ -9,7 +9,8 @@ function Show-ProjectItem{ [Parameter(Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline, Position = 0)][Alias("id")][string]$ItemId, [Parameter()][array[]]$FieldsToShow, [Parameter()][switch]$AllComments, - [Parameter()][switch]$OpenInEditor + [Parameter()][switch]$OpenInEditor, + [Parameter()][Alias("W")][switch]$OpenInWebBrowser ) begin{ @@ -25,6 +26,10 @@ function Show-ProjectItem{ $item = Get-ProjectItem -ItemId $ItemId + if($OpenInWebBrowser){ + Open-Url -Url $item.url + } + if($null -eq $item){ "Item not found" | Write-MyError return $null @@ -91,7 +96,7 @@ function Show-ProjectItem{ # Id at the end writeHeader1 - $item.id | write -Color DarkGray ; addJumpLine + $item.id | write -Color DarkGray ; addJumpLine -message "Id End" } end{ @@ -135,13 +140,13 @@ function writeHeader1{ [Parameter(ValueFromPipeline, Position=0)][string]$Text ) - addJumpLine ; + addJumpLine -message "Header 1 Start " if(-not [string]::IsNullOrWhiteSpace($Text)){ $Text | write Cyan + addJumpLine -message "Header 1 End Line" } - addJumpLine "------------" | write Cyan - addJumpLine + addJumpLine -message "Header 1 End " } @@ -160,7 +165,7 @@ function writeHeader2{ "## $Text" | write $color - addJumpLine + addJumpLine -message "Header 2 before subinfo " write -Color $subcolor -Text ">" @@ -173,7 +178,7 @@ function writeHeader2{ $updatedAt | write -Color $subcolor -PreFix "At: " } - addJumpLine + addJumpLine -message "Header 2 End " } @@ -191,8 +196,8 @@ function writeComment1{ if($order -eq $item.commentsTotalCount) {$header += " Last"} - addJumpLine - addJumpLine + addJumpLine -message "Comment 1 Start" + addJumpLine -message "Comment 1 before header" $header | write Cyan addSpace @@ -201,12 +206,12 @@ function writeComment1{ $Comment.author | write -Color DarkGray -PreFix "<" -SuFix ">" ; addSpace $Comment.updatedAt | write -Color DarkGray -PreFix "At: " - addJumpLine + addJumpLine -message "Comment End header" "------------" | write Cyan # addSpace ; addSpace - addJumpLine + addJumpLine -message "Comment 1 before body" $Comment.body | write -Color Gray } } @@ -224,15 +229,15 @@ function writeComment2{ if($order -eq $item.commentsTotalCount) {$header += " Last"} - addJumpLine -message "Start Comment" + addJumpLine -message "Comment 2 Start" writeHeader $header -Author $Comment.author -UpdatedAt $Comment.updatedAt - addJumpLine -message "Body Start" + addJumpLine -message "Comment 2 Body Start" $Comment.body | write -Color Gray - addJumpLine -message "Body End" + addJumpLine -message "Comment 2 Body End" } } @@ -302,6 +307,7 @@ function ShowAttribLine{ ) $isfirst = $true + $isAdded = $false $AttributesToShow | ForEach-Object { $name = $_.Name @@ -324,8 +330,13 @@ function ShowAttribLine{ } $value | write $color -PreFix $prefix -BetweenQuotes:$BetweenQuotes -DefaultValue $DefaultValue + + $isAdded = $true + } + + if($isAdded){ + addJumpLine -message "Attributes Line End " } - addJumpLine } function Start-WriteBuffer{ From 44e3b78cb84e1938a765c74ee4080613c45dd788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20=28Dibildos=29=20Gonz=C3=A1lez?= Date: Mon, 10 Nov 2025 07:54:23 +0100 Subject: [PATCH 07/24] feat(mockdatabase): add Update-Mock_DatabaseFileWithReplace to allow json file replace --- Test/private/mockdatabase.ps1 | 8 +++++++- Test/public/mockdatabase.test.ps1 | 30 ++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 Test/public/mockdatabase.test.ps1 diff --git a/Test/private/mockdatabase.ps1 b/Test/private/mockdatabase.ps1 index 5d80d84..bd46e96 100644 --- a/Test/private/mockdatabase.ps1 +++ b/Test/private/mockdatabase.ps1 @@ -1,5 +1,5 @@ - $folderName = "test_database_path" + function Mock_DatabaseRoot([switch]$NotReset){ # Check if the database path exists @@ -23,3 +23,9 @@ function Get-Mock_DatabaseRootPath{ } +function Update-Mock_DatabaseFileWithReplace([string]$FileName, [string]$SearchString, [string]$ReplaceString){ + $dbpath = Get-Mock_DatabaseRootPath | Join-Path -ChildPath $FileName + $content = Get-Content $dbpath + $content = $content -replace $SearchString, $ReplaceString + $content | Set-Content $dbpath +} \ No newline at end of file diff --git a/Test/public/mockdatabase.test.ps1 b/Test/public/mockdatabase.test.ps1 new file mode 100644 index 0000000..7310733 --- /dev/null +++ b/Test/public/mockdatabase.test.ps1 @@ -0,0 +1,30 @@ +function Test_UpdateMock_DatabaseFileWithReplace{ + Reset-InvokeCommandMock + Mock_DatabaseRoot + + $p = Get-Mock_Project_700 ; $owner = $p.owner ; $projectNumber = $p.number + $cacheFileName = $p.cacheFileName + $q = $p.getProjectWithQuery + $fieldName = $q.FieldName + $fieldValueActual = $q.FieldValueActual + $fieldValueNew = $q.FieldValueNew + $totalCount = $q.totalCount + + MockCall_GetProject $p -Cache + + # find "Issue for development" from database file + $result = Search-ProjectItem -Owner $owner -ProjectNumber $projectNumber -FieldName $fieldName -Filter $fieldValueActual -Exact -IncludeDone + Assert-Count -Expected $totalCount -Presented $result + + $result = Search-ProjectItem -Owner $owner -ProjectNumber $projectNumber -FieldName $fieldName -Filter $fieldValueNew -Exact -IncludeDone + Assert-Count -Expected 0 -Presented $result + + # Add content to the title of a file + Update-Mock_DatabaseFileWithReplace -Filename $cacheFileName -SearchString $q.stringToReplaceFrom -ReplaceString $q.stringToReplaceTo + + $result = Search-ProjectItem -Owner $owner -ProjectNumber $projectNumber -FieldName $fieldName -Filter $fieldValueActual -Exact -IncludeDone + Assert-Count -Expected 0 -Presented $result + $result = Search-ProjectItem -Owner $owner -ProjectNumber $projectNumber -FieldName $fieldName -Filter $fieldValueNew -Exact -IncludeDone + Assert-Count -Expected $totalCount -Presented $result + +} \ No newline at end of file From 62f27ba1e5bfcaf81a4aedecda1acfa2aae41154 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20=28Dibildos=29=20Gonz=C3=A1lez?= Date: Mon, 10 Nov 2025 08:11:56 +0100 Subject: [PATCH 08/24] refactor(test): use Update-Mock_DatabaseFileWithReplace in test --- Test/private/MockCall_Project700.ps1 | 1 + Test/public/project_item.searchprojectitem.test.ps1 | 9 +++------ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Test/private/MockCall_Project700.ps1 b/Test/private/MockCall_Project700.ps1 index 3d939b9..9b2addc 100644 --- a/Test/private/MockCall_Project700.ps1 +++ b/Test/private/MockCall_Project700.ps1 @@ -46,6 +46,7 @@ function Get-Mock_Project_700 { $project.owner = $pActual.owner.login $project.number = $pActual.number $project.url = $pActual.url + $project.cacheFileName = "$($pActual.owner.login)_$($pActual.number).json" # Fields info $project.fields = @{} diff --git a/Test/public/project_item.searchprojectitem.test.ps1 b/Test/public/project_item.searchprojectitem.test.ps1 index 0a68593..1c36e1b 100644 --- a/Test/public/project_item.searchprojectitem.test.ps1 +++ b/Test/public/project_item.searchprojectitem.test.ps1 @@ -108,17 +108,14 @@ function Test_SearchProjectItem_AND_Filter_SUCCESS { Mock_DatabaseRoot $p = Get-Mock_Project_700 ; $owner = $p.owner ; $projectNumber = $p.number + $cacheFileName = $p.cacheFileName MockCall_GetProject $p -Cache + $i = $p.issue - $i = $p.issue $str = '"{title}"' -replace '{title}',$i.title $newStr = '"{title} UniqueSearchAlpha UniqueSearchBeta"' -replace '{title}',$i.title + Update-Mock_DatabaseFileWithReplace -FileName $cacheFileName -SearchString $str -ReplaceString $newStr - # Add content to the title of a file - $dbpathh = Invoke-MyCommand -Command "Invoke-ProjectHelperGetDatabaseStorePath" | Join-Path -ChildPath octodemo_700.json - $content = Get-Content $dbpathh - $content = $content -replace $str,$newStr - $content | Set-Content $dbpathh # Act $found = Search-ProjectItem -Owner $Owner -ProjectNumber $ProjectNumber -Filter "UniqueSearchAlpha","UniqueSearchBeta" From 819e80888ca6d211f9fa411609d09231661d4ece Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20=28Dibildos=29=20Gonz=C3=A1lez?= Date: Mon, 10 Nov 2025 08:13:08 +0100 Subject: [PATCH 09/24] faet(test): getproject with query test --- Test/mockfiles.log | 4 + Test/private/MockCall_Project700.ps1 | 15 +- ...hFields-octodemo-700-query-field-text.json | 1310 +++++++++++++++++ Test/public/project/getproject.test.ps1 | 50 +- Test/traceInvoke.log | 1 + 5 files changed, 1372 insertions(+), 8 deletions(-) create mode 100644 Test/private/mocks/invoke-GitHubOrgProjectWithFields-octodemo-700-query-field-text.json diff --git a/Test/mockfiles.log b/Test/mockfiles.log index e6f384a..effc515 100644 --- a/Test/mockfiles.log +++ b/Test/mockfiles.log @@ -178,5 +178,9 @@ { "Command": "Invoke-GitHubOrgProjectWithFields -Owner octodemo -ProjectNumber 626 -afterFields \"\" -afterItems \"\" -query \"\"", "FileName": "invoke-GitHubOrgProjectWithFields-octodemo-626.json" + }, + { + "FileName": "invoke-GitHubOrgProjectWithFields-octodemo-700-query-field-text.json", + "Command": "Invoke-GitHubOrgProjectWithFields -Owner octodemo -ProjectNumber 700 -afterFields \"\" -afterItems \"\" -query \"field-text:text1\"" } ] diff --git a/Test/private/MockCall_Project700.ps1 b/Test/private/MockCall_Project700.ps1 index 9b2addc..044019b 100644 --- a/Test/private/MockCall_Project700.ps1 +++ b/Test/private/MockCall_Project700.ps1 @@ -136,7 +136,6 @@ function Get-Mock_Project_700 { } } - # SubIssue to show $project.subIssueToShow = @{ id = "PVTI_lADOAlIw4c4BCe3Vzgec8p8" @@ -147,12 +146,24 @@ function Get-Mock_Project_700 { ) } - # SubIssue $project.subIssuesToAdd = $pActual.items.nodes | Where-Object { $_.content.title -like "Implement API*" } | ForEach-Object{ @{ contentId = $_.content.Id ; url = $_.content.url ; title = $_.content.title ; addSubIssueMockfile = "invoke-addSubIssue-$($project.issue.contentId)-$($_.content.number).json" } } + # Get Project with query + + $project.getProjectWithQuery = @{ + # query = "for development" + query = 'field-text:text1' + fieldName = "field-text" + fieldValueActual = "text1" + fieldValueNew = "text1_updated" + stringToReplaceFrom = '"field-text": "text1"' + stringToReplaceTo = '"field-text": "text1_updated"' + getProjectWithQueryMockFile = "invoke-GitHubOrgProjectWithFields-octodemo-700-query-field-text.json" + totalCount = 7 + } # searchIn Title like $project.searchInTitle = @{} diff --git a/Test/private/mocks/invoke-GitHubOrgProjectWithFields-octodemo-700-query-field-text.json b/Test/private/mocks/invoke-GitHubOrgProjectWithFields-octodemo-700-query-field-text.json new file mode 100644 index 0000000..d9f6409 --- /dev/null +++ b/Test/private/mocks/invoke-GitHubOrgProjectWithFields-octodemo-700-query-field-text.json @@ -0,0 +1,1310 @@ +{ + "data": { + "organization": { + "projectV2": { + "number": 700, + "url": "https://github.com/orgs/octodemo/projects/700", + "shortDescription": "Project used as development environment by rulasg", + "public": false, + "closed": false, + "title": "rulasg-dev-700", + "id": "PVT_kwDOAlIw4c4BCe3V", + "readme": null, + "items": { + "pageInfo": { + "endCursor": "Y3Vyc29yOnYyOpK5MDAwMDAwMDAuMDM4NDYxNTM4NDYxNTM4NM4HoqAW", + "hasNextPage": false + }, + "totalCount": 7, + "nodes": [ + { + "id": "PVTI_lADOAlIw4c4BCe3Vzgec8pY", + "fullDatabaseId": "127726230", + "project": { + "id": "PVT_kwDOAlIw4c4BCe3V", + "number": 700, + "title": "rulasg-dev-700", + "url": "https://github.com/orgs/octodemo/projects/700" + }, + "content": { + "__typename": "Issue", + "id": "I_kwDOPrRnkc7KJA25", + "body": "## Description\nConfigure unit, integration, and API testing infrastructure for the Web API project.\n\n## Tasks\n- [ ] Set up test project with appropriate framework (xUnit, NUnit, MSTest)\n- [ ] Configure mocking library (Moq, NSubstitute)\n- [ ] Create base classes for different test types\n- [ ] Set up test database for integration tests\n- [ ] Implement API endpoint tests\n- [ ] Configure code coverage reporting\n- [ ] Set up continuous testing in CI/CD\n\n## Acceptance Criteria\n- Unit tests for service and business logic\n- Integration tests for database operations\n- API tests for endpoint functionality\n- Test configuration is environment-independent\n- Code coverage reports are generated", + "title": "Set up testing infrastructure", + "updatedAt": "2025-09-07T09:46:59Z", + "createdAt": "2025-09-07T08:27:12Z", + "number": 12, + "url": "https://github.com/octodemo/rulasg-dev-1/issues/12", + "state": "OPEN", + "repository": { + "name": "rulasg-dev-1", + "owner": { + "login": "octodemo" + } + }, + "comments": { + "totalCount": 0, + "nodes": [] + } + }, + "fieldValues": { + "nodes": [ + { + "__typename": "ProjectV2ItemFieldRepositoryValue", + "repository": { + "url": "https://github.com/octodemo/rulasg-dev-1" + }, + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-k", + "name": "Repository", + "dataType": "REPOSITORY" + } + }, + { + "__typename": "ProjectV2ItemFieldLabelValue", + "labels": { + "nodes": [ + { + "name": "testing" + }, + { + "name": "quality" + } + ] + }, + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-Y", + "name": "Labels", + "dataType": "LABELS" + } + }, + { + "__typename": "ProjectV2ItemFieldMilestoneValue", + "milestone": { + "title": "Milestone 3: Quality and Deployment", + "description": "Testing, documentation, and deployment pipeline setup", + "dueOn": "2025-10-18T00:00:00Z" + }, + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-g", + "name": "Milestone", + "dataType": "MILESTONE" + } + }, + { + "__typename": "ProjectV2ItemFieldTextValue", + "text": "Set up testing infrastructure", + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-M", + "name": "Title", + "dataType": "TITLE" + } + }, + { + "__typename": "ProjectV2ItemFieldNumberValue", + "number": 66.0, + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rhjU", + "name": "field-number", + "dataType": "NUMBER" + } + }, + { + "__typename": "ProjectV2ItemFieldSingleSelectValue", + "name": "option-3", + "field": { + "__typename": "ProjectV2SingleSelectField", + "id": "PVTSSF_lADOAlIw4c4BCe3Vzg0rhno", + "name": "field-singleselect", + "dataType": "SINGLE_SELECT", + "options": [ + { + "id": "7d96a925", + "name": "option-1" + }, + { + "id": "cd02c585", + "name": "option-2" + }, + { + "id": "6b66c93a", + "name": "option-3" + } + ] + } + }, + { + "__typename": "ProjectV2ItemFieldTextValue", + "text": "text1", + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rhko", + "name": "field-text", + "dataType": "TEXT" + } + }, + { + "__typename": "ProjectV2ItemFieldDateValue", + "date": "2025-09-27", + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rhlU", + "name": "field-date", + "dataType": "DATE" + } + }, + { + "__typename": "ProjectV2ItemFieldSingleSelectValue", + "name": "Todo", + "field": { + "__typename": "ProjectV2SingleSelectField", + "id": "PVTSSF_lADOAlIw4c4BCe3Vzg0rg-U", + "name": "Status", + "dataType": "SINGLE_SELECT", + "options": [ + { + "id": "f75ad846", + "name": "Todo" + }, + { + "id": "47fc9ee4", + "name": "In Progress" + }, + { + "id": "98236657", + "name": "Done" + } + ] + } + }, + { + "__typename": "ProjectV2ItemFieldIterationValue", + "title": "field-iteration 1", + "startDate": "2025-09-07", + "duration": 14, + "field": { + "__typename": "ProjectV2IterationField", + "id": "PVTIF_lADOAlIw4c4BCe3Vzg0rhqQ", + "name": "field-iteration", + "dataType": "ITERATION" + } + } + ] + } + }, + { + "id": "PVTI_lADOAlIw4c4BCe3Vzgec8ps", + "fullDatabaseId": "127726235", + "project": { + "id": "PVT_kwDOAlIw4c4BCe3V", + "number": 700, + "title": "rulasg-dev-700", + "url": "https://github.com/orgs/octodemo/projects/700" + }, + "content": { + "__typename": "Issue", + "id": "I_kwDOPrRnkc7KJAq7", + "body": "## Description\nSet up Dependency Injection (DI) to handle service registration and lifecycle management for the API.\n\n## Tasks\n- [ ] Configure built-in DI container in Program.cs/Startup.cs\n- [ ] Register services with appropriate lifetimes (Singleton, Scoped, Transient)\n- [ ] Implement service interfaces and their implementations\n- [ ] Set up options pattern for configuration objects\n- [ ] Document service dependencies\n\n## Acceptance Criteria\n- All services properly registered in DI container\n- Services have appropriate lifetime scopes\n- Service interfaces follow SOLID principles\n- Dependencies are properly injected into controllers", + "title": "Configure Dependency Injection and services", + "updatedAt": "2025-09-07T09:46:24Z", + "createdAt": "2025-09-07T08:26:06Z", + "number": 7, + "url": "https://github.com/octodemo/rulasg-dev-1/issues/7", + "state": "OPEN", + "repository": { + "name": "rulasg-dev-1", + "owner": { + "login": "octodemo" + } + }, + "comments": { + "totalCount": 0, + "nodes": [] + } + }, + "fieldValues": { + "nodes": [ + { + "__typename": "ProjectV2ItemFieldUserValue", + "users": { + "nodes": [ + { + "login": "rauldibildos" + } + ] + }, + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-Q", + "name": "Assignees", + "dataType": "ASSIGNEES" + } + }, + { + "__typename": "ProjectV2ItemFieldRepositoryValue", + "repository": { + "url": "https://github.com/octodemo/rulasg-dev-1" + }, + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-k", + "name": "Repository", + "dataType": "REPOSITORY" + } + }, + { + "__typename": "ProjectV2ItemFieldLabelValue", + "labels": { + "nodes": [ + { + "name": "setup" + }, + { + "name": "architecture" + } + ] + }, + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-Y", + "name": "Labels", + "dataType": "LABELS" + } + }, + { + "__typename": "ProjectV2ItemFieldMilestoneValue", + "milestone": { + "title": "Milestone 1: Project Setup", + "description": "Initial project setup and basic infrastructure", + "dueOn": "2025-09-20T00:00:00Z" + }, + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-g", + "name": "Milestone", + "dataType": "MILESTONE" + } + }, + { + "__typename": "ProjectV2ItemFieldTextValue", + "text": "Configure Dependency Injection and services", + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-M", + "name": "Title", + "dataType": "TITLE" + } + }, + { + "__typename": "ProjectV2ItemFieldSingleSelectValue", + "name": "option-2", + "field": { + "__typename": "ProjectV2SingleSelectField", + "id": "PVTSSF_lADOAlIw4c4BCe3Vzg0rhno", + "name": "field-singleselect", + "dataType": "SINGLE_SELECT", + "options": [ + { + "id": "7d96a925", + "name": "option-1" + }, + { + "id": "cd02c585", + "name": "option-2" + }, + { + "id": "6b66c93a", + "name": "option-3" + } + ] + } + }, + { + "__typename": "ProjectV2ItemFieldTextValue", + "text": "text1", + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rhko", + "name": "field-text", + "dataType": "TEXT" + } + }, + { + "__typename": "ProjectV2ItemFieldDateValue", + "date": "2025-09-05", + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rhlU", + "name": "field-date", + "dataType": "DATE" + } + }, + { + "__typename": "ProjectV2ItemFieldSingleSelectValue", + "name": "Todo", + "field": { + "__typename": "ProjectV2SingleSelectField", + "id": "PVTSSF_lADOAlIw4c4BCe3Vzg0rg-U", + "name": "Status", + "dataType": "SINGLE_SELECT", + "options": [ + { + "id": "f75ad846", + "name": "Todo" + }, + { + "id": "47fc9ee4", + "name": "In Progress" + }, + { + "id": "98236657", + "name": "Done" + } + ] + } + }, + { + "__typename": "ProjectV2ItemFieldIterationValue", + "title": "field-iteration 3", + "startDate": "2025-10-05", + "duration": 14, + "field": { + "__typename": "ProjectV2IterationField", + "id": "PVTIF_lADOAlIw4c4BCe3Vzg0rhqQ", + "name": "field-iteration", + "dataType": "ITERATION" + } + } + ] + } + }, + { + "id": "PVTI_lADOAlIw4c4BCe3Vzgec8p0", + "fullDatabaseId": "127726237", + "project": { + "id": "PVT_kwDOAlIw4c4BCe3V", + "number": 700, + "title": "rulasg-dev-700", + "url": "https://github.com/orgs/octodemo/projects/700" + }, + "content": { + "__typename": "Issue", + "id": "I_kwDOPrRnkc7KJAm8", + "body": "## Description\nCreate controllers and endpoints for the Web API to handle HTTP requests.\n\n## Tasks\n- [ ] Create a base controller class with common functionality\n- [ ] Implement CRUD controllers for main resources\n- [ ] Add appropriate routing attributes\n- [ ] Implement proper HTTP status code responses\n- [ ] Document endpoints using XML comments for Swagger/OpenAPI\n\n## Acceptance Criteria\n- Controllers properly handle GET, POST, PUT, DELETE operations\n- Routes follow RESTful principles\n- Appropriate HTTP status codes are returned\n- Controller methods have XML documentation", + "title": "Implement API endpoints and controllers", + "updatedAt": "2025-09-07T09:46:41Z", + "createdAt": "2025-09-07T08:25:44Z", + "number": 5, + "url": "https://github.com/octodemo/rulasg-dev-1/issues/5", + "state": "OPEN", + "repository": { + "name": "rulasg-dev-1", + "owner": { + "login": "octodemo" + } + }, + "comments": { + "totalCount": 0, + "nodes": [] + } + }, + "fieldValues": { + "nodes": [ + { + "__typename": "ProjectV2ItemFieldUserValue", + "users": { + "nodes": [ + { + "login": "rauldibildos" + } + ] + }, + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-Q", + "name": "Assignees", + "dataType": "ASSIGNEES" + } + }, + { + "__typename": "ProjectV2ItemFieldRepositoryValue", + "repository": { + "url": "https://github.com/octodemo/rulasg-dev-1" + }, + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-k", + "name": "Repository", + "dataType": "REPOSITORY" + } + }, + { + "__typename": "ProjectV2ItemFieldLabelValue", + "labels": { + "nodes": [ + { + "name": "feature" + }, + { + "name": "api-design" + } + ] + }, + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-Y", + "name": "Labels", + "dataType": "LABELS" + } + }, + { + "__typename": "ProjectV2ItemFieldMilestoneValue", + "milestone": { + "title": "Milestone 2: Core Functionality", + "description": "Implementation of core API features and data handling", + "dueOn": "2025-10-04T00:00:00Z" + }, + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-g", + "name": "Milestone", + "dataType": "MILESTONE" + } + }, + { + "__typename": "ProjectV2ItemFieldTextValue", + "text": "Implement API endpoints and controllers", + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-M", + "name": "Title", + "dataType": "TITLE" + } + }, + { + "__typename": "ProjectV2ItemFieldSingleSelectValue", + "name": "option-1", + "field": { + "__typename": "ProjectV2SingleSelectField", + "id": "PVTSSF_lADOAlIw4c4BCe3Vzg0rhno", + "name": "field-singleselect", + "dataType": "SINGLE_SELECT", + "options": [ + { + "id": "7d96a925", + "name": "option-1" + }, + { + "id": "cd02c585", + "name": "option-2" + }, + { + "id": "6b66c93a", + "name": "option-3" + } + ] + } + }, + { + "__typename": "ProjectV2ItemFieldTextValue", + "text": "text1", + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rhko", + "name": "field-text", + "dataType": "TEXT" + } + }, + { + "__typename": "ProjectV2ItemFieldDateValue", + "date": "2025-09-29", + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rhlU", + "name": "field-date", + "dataType": "DATE" + } + }, + { + "__typename": "ProjectV2ItemFieldSingleSelectValue", + "name": "Todo", + "field": { + "__typename": "ProjectV2SingleSelectField", + "id": "PVTSSF_lADOAlIw4c4BCe3Vzg0rg-U", + "name": "Status", + "dataType": "SINGLE_SELECT", + "options": [ + { + "id": "f75ad846", + "name": "Todo" + }, + { + "id": "47fc9ee4", + "name": "In Progress" + }, + { + "id": "98236657", + "name": "Done" + } + ] + } + }, + { + "__typename": "ProjectV2ItemFieldIterationValue", + "title": "field-iteration 2", + "startDate": "2025-09-21", + "duration": 14, + "field": { + "__typename": "ProjectV2IterationField", + "id": "PVTIF_lADOAlIw4c4BCe3Vzg0rhqQ", + "name": "field-iteration", + "dataType": "ITERATION" + } + } + ] + } + }, + { + "id": "PVTI_lADOAlIw4c4BCe3VzgedAIE", + "fullDatabaseId": "127729793", + "project": { + "id": "PVT_kwDOAlIw4c4BCe3V", + "number": 700, + "title": "rulasg-dev-700", + "url": "https://github.com/orgs/octodemo/projects/700" + }, + "content": { + "__typename": "DraftIssue", + "id": "DI_lADOAlIw4c4BCe3VzgJwMMA", + "body": "Draft roadmap for implementing comprehensive API security including authentication mechanisms review, authorization strategy, data encryption approach, API key management, and security monitoring.", + "title": "Security Implementation Roadmap", + "updatedAt": "2025-09-07T10:07:24Z", + "createdAt": "2025-09-07T10:07:24Z" + }, + "fieldValues": { + "nodes": [ + { + "__typename": "ProjectV2ItemFieldTextValue", + "text": "Security Implementation Roadmap", + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-M", + "name": "Title", + "dataType": "TITLE" + } + }, + { + "__typename": "ProjectV2ItemFieldNumberValue", + "number": 84.0, + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rhjU", + "name": "field-number", + "dataType": "NUMBER" + } + }, + { + "__typename": "ProjectV2ItemFieldSingleSelectValue", + "name": "option-3", + "field": { + "__typename": "ProjectV2SingleSelectField", + "id": "PVTSSF_lADOAlIw4c4BCe3Vzg0rhno", + "name": "field-singleselect", + "dataType": "SINGLE_SELECT", + "options": [ + { + "id": "7d96a925", + "name": "option-1" + }, + { + "id": "cd02c585", + "name": "option-2" + }, + { + "id": "6b66c93a", + "name": "option-3" + } + ] + } + }, + { + "__typename": "ProjectV2ItemFieldTextValue", + "text": "text1", + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rhko", + "name": "field-text", + "dataType": "TEXT" + } + }, + { + "__typename": "ProjectV2ItemFieldDateValue", + "date": "2025-09-11", + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rhlU", + "name": "field-date", + "dataType": "DATE" + } + }, + { + "__typename": "ProjectV2ItemFieldSingleSelectValue", + "name": "In Progress", + "field": { + "__typename": "ProjectV2SingleSelectField", + "id": "PVTSSF_lADOAlIw4c4BCe3Vzg0rg-U", + "name": "Status", + "dataType": "SINGLE_SELECT", + "options": [ + { + "id": "f75ad846", + "name": "Todo" + }, + { + "id": "47fc9ee4", + "name": "In Progress" + }, + { + "id": "98236657", + "name": "Done" + } + ] + } + }, + { + "__typename": "ProjectV2ItemFieldIterationValue", + "title": "field-iteration 2", + "startDate": "2025-09-21", + "duration": 14, + "field": { + "__typename": "ProjectV2IterationField", + "id": "PVTIF_lADOAlIw4c4BCe3Vzg0rhqQ", + "name": "field-iteration", + "dataType": "ITERATION" + } + } + ] + } + }, + { + "id": "PVTI_lADOAlIw4c4BCe3VzgedAIc", + "fullDatabaseId": "127729799", + "project": { + "id": "PVT_kwDOAlIw4c4BCe3V", + "number": 700, + "title": "rulasg-dev-700", + "url": "https://github.com/orgs/octodemo/projects/700" + }, + "content": { + "__typename": "DraftIssue", + "id": "DI_lADOAlIw4c4BCe3VzgJwMME", + "body": "Draft plan for containerizing the API and setting up deployment infrastructure including Docker containerization, Kubernetes orchestration, and CI/CD pipeline configuration.", + "title": "Containerization and Deployment Strategy", + "updatedAt": "2025-09-07T10:07:40Z", + "createdAt": "2025-09-07T10:07:40Z" + }, + "fieldValues": { + "nodes": [ + { + "__typename": "ProjectV2ItemFieldTextValue", + "text": "Containerization and Deployment Strategy", + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-M", + "name": "Title", + "dataType": "TITLE" + } + }, + { + "__typename": "ProjectV2ItemFieldSingleSelectValue", + "name": "option-1", + "field": { + "__typename": "ProjectV2SingleSelectField", + "id": "PVTSSF_lADOAlIw4c4BCe3Vzg0rhno", + "name": "field-singleselect", + "dataType": "SINGLE_SELECT", + "options": [ + { + "id": "7d96a925", + "name": "option-1" + }, + { + "id": "cd02c585", + "name": "option-2" + }, + { + "id": "6b66c93a", + "name": "option-3" + } + ] + } + }, + { + "__typename": "ProjectV2ItemFieldTextValue", + "text": "text1", + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rhko", + "name": "field-text", + "dataType": "TEXT" + } + }, + { + "__typename": "ProjectV2ItemFieldDateValue", + "date": "2025-09-07", + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rhlU", + "name": "field-date", + "dataType": "DATE" + } + }, + { + "__typename": "ProjectV2ItemFieldSingleSelectValue", + "name": "Done", + "field": { + "__typename": "ProjectV2SingleSelectField", + "id": "PVTSSF_lADOAlIw4c4BCe3Vzg0rg-U", + "name": "Status", + "dataType": "SINGLE_SELECT", + "options": [ + { + "id": "f75ad846", + "name": "Todo" + }, + { + "id": "47fc9ee4", + "name": "In Progress" + }, + { + "id": "98236657", + "name": "Done" + } + ] + } + }, + { + "__typename": "ProjectV2ItemFieldIterationValue", + "title": "field-iteration 2", + "startDate": "2025-09-21", + "duration": 14, + "field": { + "__typename": "ProjectV2IterationField", + "id": "PVTIF_lADOAlIw4c4BCe3Vzg0rhqQ", + "name": "field-iteration", + "dataType": "ITERATION" + } + } + ] + } + }, + { + "id": "PVTI_lADOAlIw4c4BCe3VzgedAWU", + "fullDatabaseId": "127730021", + "project": { + "id": "PVT_kwDOAlIw4c4BCe3V", + "number": 700, + "title": "rulasg-dev-700", + "url": "https://github.com/orgs/octodemo/projects/700" + }, + "content": { + "__typename": "Issue", + "id": "I_kwDOPrRnkc7KJQ0r", + "body": "## Description\nImplement rate limiting and throttling for the API to prevent abuse and ensure fair usage.\n\n## Tasks\n- [ ] Add rate limiting middleware (e.g., AspNetCoreRateLimit)\n- [ ] Configure client-based rate limits\n- [ ] Implement endpoint-specific throttling rules\n- [ ] Set up IP-based rate limiting\n- [ ] Add rate limit information to response headers\n- [ ] Configure response handling for rate-limited requests\n- [ ] Document rate limiting policy for API consumers\n\n## Acceptance Criteria\n- Rate limiting prevents excessive requests from single clients\n- Different limits can be applied based on client identity\n- Rate limit headers are included in API responses\n- Appropriate status codes (429 Too Many Requests) are returned\n- Rate limiting policy is clearly documented", + "title": "Configure rate limiting and throttling", + "updatedAt": "2025-09-07T10:24:55Z", + "createdAt": "2025-09-07T09:49:32Z", + "number": 16, + "url": "https://github.com/octodemo/rulasg-dev-1/issues/16", + "state": "CLOSED", + "repository": { + "name": "rulasg-dev-1", + "owner": { + "login": "octodemo" + } + }, + "comments": { + "totalCount": 0, + "nodes": [] + } + }, + "fieldValues": { + "nodes": [ + { + "__typename": "ProjectV2ItemFieldRepositoryValue", + "repository": { + "url": "https://github.com/octodemo/rulasg-dev-1" + }, + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-k", + "name": "Repository", + "dataType": "REPOSITORY" + } + }, + { + "__typename": "ProjectV2ItemFieldLabelValue", + "labels": { + "nodes": [ + { + "name": "api-design" + }, + { + "name": "security" + } + ] + }, + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-Y", + "name": "Labels", + "dataType": "LABELS" + } + }, + { + "__typename": "ProjectV2ItemFieldMilestoneValue", + "milestone": { + "title": "Milestone 2: Core Functionality", + "description": "Implementation of core API features and data handling", + "dueOn": "2025-10-04T00:00:00Z" + }, + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-g", + "name": "Milestone", + "dataType": "MILESTONE" + } + }, + { + "__typename": "ProjectV2ItemFieldTextValue", + "text": "Configure rate limiting and throttling", + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-M", + "name": "Title", + "dataType": "TITLE" + } + }, + { + "__typename": "ProjectV2ItemFieldNumberValue", + "number": 77.0, + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rhjU", + "name": "field-number", + "dataType": "NUMBER" + } + }, + { + "__typename": "ProjectV2ItemFieldSingleSelectValue", + "name": "option-2", + "field": { + "__typename": "ProjectV2SingleSelectField", + "id": "PVTSSF_lADOAlIw4c4BCe3Vzg0rhno", + "name": "field-singleselect", + "dataType": "SINGLE_SELECT", + "options": [ + { + "id": "7d96a925", + "name": "option-1" + }, + { + "id": "cd02c585", + "name": "option-2" + }, + { + "id": "6b66c93a", + "name": "option-3" + } + ] + } + }, + { + "__typename": "ProjectV2ItemFieldTextValue", + "text": "text1", + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rhko", + "name": "field-text", + "dataType": "TEXT" + } + }, + { + "__typename": "ProjectV2ItemFieldDateValue", + "date": "2025-09-21", + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rhlU", + "name": "field-date", + "dataType": "DATE" + } + }, + { + "__typename": "ProjectV2ItemFieldSingleSelectValue", + "name": "Done", + "field": { + "__typename": "ProjectV2SingleSelectField", + "id": "PVTSSF_lADOAlIw4c4BCe3Vzg0rg-U", + "name": "Status", + "dataType": "SINGLE_SELECT", + "options": [ + { + "id": "f75ad846", + "name": "Todo" + }, + { + "id": "47fc9ee4", + "name": "In Progress" + }, + { + "id": "98236657", + "name": "Done" + } + ] + } + }, + { + "__typename": "ProjectV2ItemFieldIterationValue", + "title": "field-iteration 2", + "startDate": "2025-09-21", + "duration": 14, + "field": { + "__typename": "ProjectV2IterationField", + "id": "PVTIF_lADOAlIw4c4BCe3Vzg0rhqQ", + "name": "field-iteration", + "dataType": "ITERATION" + } + } + ] + } + }, + { + "id": "PVTI_lADOAlIw4c4BCe3VzgeioBY", + "fullDatabaseId": "128098326", + "project": { + "id": "PVT_kwDOAlIw4c4BCe3V", + "number": 700, + "title": "rulasg-dev-700", + "url": "https://github.com/orgs/octodemo/projects/700" + }, + "content": { + "__typename": "PullRequest", + "id": "PR_kwDOPrRnkc6nndcE", + "body": "", + "title": "PullRequest for development", + "updatedAt": "2025-09-24T08:56:12Z", + "createdAt": "2025-09-09T13:56:05Z", + "number": 25, + "url": "https://github.com/octodemo/rulasg-dev-1/pull/25", + "state": "OPEN", + "repository": { + "name": "rulasg-dev-1", + "owner": { + "login": "octodemo" + } + }, + "comments": { + "totalCount": 3, + "nodes": [ + { + "createdAt": "2025-09-24T08:56:12Z", + "updatedAt": "2025-09-24T08:56:12Z", + "url": "https://github.com/octodemo/rulasg-dev-1/pull/25#issuecomment-3327293422", + "body": "sample comment 1", + "fullDatabaseId": "3327293422", + "author": { + "login": "rulasg" + } + } + ] + } + }, + "fieldValues": { + "nodes": [ + { + "__typename": "ProjectV2ItemFieldUserValue", + "users": { + "nodes": [ + { + "login": "rulasg" + } + ] + }, + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-Q", + "name": "Assignees", + "dataType": "ASSIGNEES" + } + }, + { + "__typename": "ProjectV2ItemFieldRepositoryValue", + "repository": { + "url": "https://github.com/octodemo/rulasg-dev-1" + }, + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-k", + "name": "Repository", + "dataType": "REPOSITORY" + } + }, + { + "__typename": "ProjectV2ItemFieldMilestoneValue", + "milestone": { + "title": "Milestone 1: Project Setup", + "description": "Initial project setup and basic infrastructure", + "dueOn": "2025-09-20T00:00:00Z" + }, + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-g", + "name": "Milestone", + "dataType": "MILESTONE" + } + }, + { + "__typename": "ProjectV2ItemFieldTextValue", + "text": "PullRequest for development", + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-M", + "name": "Title", + "dataType": "TITLE" + } + }, + { + "__typename": "ProjectV2ItemFieldSingleSelectValue", + "name": "Todo", + "field": { + "__typename": "ProjectV2SingleSelectField", + "id": "PVTSSF_lADOAlIw4c4BCe3Vzg0rg-U", + "name": "Status", + "dataType": "SINGLE_SELECT", + "options": [ + { + "id": "f75ad846", + "name": "Todo" + }, + { + "id": "47fc9ee4", + "name": "In Progress" + }, + { + "id": "98236657", + "name": "Done" + } + ] + } + }, + { + "__typename": "ProjectV2ItemFieldNumberValue", + "number": 111.0, + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rhjU", + "name": "field-number", + "dataType": "NUMBER" + } + }, + { + "__typename": "ProjectV2ItemFieldTextValue", + "text": "text1", + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rhko", + "name": "field-text", + "dataType": "TEXT" + } + }, + { + "__typename": "ProjectV2ItemFieldSingleSelectValue", + "name": "option-1", + "field": { + "__typename": "ProjectV2SingleSelectField", + "id": "PVTSSF_lADOAlIw4c4BCe3Vzg0rhno", + "name": "field-singleselect", + "dataType": "SINGLE_SELECT", + "options": [ + { + "id": "7d96a925", + "name": "option-1" + }, + { + "id": "cd02c585", + "name": "option-2" + }, + { + "id": "6b66c93a", + "name": "option-3" + } + ] + } + }, + { + "__typename": "ProjectV2ItemFieldIterationValue", + "title": "field-iteration 1", + "startDate": "2025-09-07", + "duration": 14, + "field": { + "__typename": "ProjectV2IterationField", + "id": "PVTIF_lADOAlIw4c4BCe3Vzg0rhqQ", + "name": "field-iteration", + "dataType": "ITERATION" + } + }, + { + "__typename": "ProjectV2ItemFieldDateValue", + "date": "2025-09-01", + "field": { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rhlU", + "name": "field-date", + "dataType": "DATE" + } + } + ] + } + } + ] + }, + "fields": { + "totalCount": 15, + "nodes": [ + { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-M", + "name": "Title", + "dataType": "TITLE" + }, + { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-Q", + "name": "Assignees", + "dataType": "ASSIGNEES" + }, + { + "__typename": "ProjectV2SingleSelectField", + "id": "PVTSSF_lADOAlIw4c4BCe3Vzg0rg-U", + "name": "Status", + "dataType": "SINGLE_SELECT", + "options": [ + { + "id": "f75ad846", + "name": "Todo" + }, + { + "id": "47fc9ee4", + "name": "In Progress" + }, + { + "id": "98236657", + "name": "Done" + } + ] + }, + { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-Y", + "name": "Labels", + "dataType": "LABELS" + }, + { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-c", + "name": "Linked pull requests", + "dataType": "LINKED_PULL_REQUESTS" + }, + { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-g", + "name": "Milestone", + "dataType": "MILESTONE" + }, + { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-k", + "name": "Repository", + "dataType": "REPOSITORY" + }, + { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-s", + "name": "Reviewers", + "dataType": "REVIEWERS" + }, + { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-w", + "name": "Parent issue", + "dataType": "PARENT_ISSUE" + }, + { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rg-0", + "name": "Sub-issues progress", + "dataType": "SUB_ISSUES_PROGRESS" + }, + { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rhjU", + "name": "field-number", + "dataType": "NUMBER" + }, + { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rhko", + "name": "field-text", + "dataType": "TEXT" + }, + { + "__typename": "ProjectV2Field", + "id": "PVTF_lADOAlIw4c4BCe3Vzg0rhlU", + "name": "field-date", + "dataType": "DATE" + }, + { + "__typename": "ProjectV2SingleSelectField", + "id": "PVTSSF_lADOAlIw4c4BCe3Vzg0rhno", + "name": "field-singleselect", + "dataType": "SINGLE_SELECT", + "options": [ + { + "id": "7d96a925", + "name": "option-1" + }, + { + "id": "cd02c585", + "name": "option-2" + }, + { + "id": "6b66c93a", + "name": "option-3" + } + ] + }, + { + "__typename": "ProjectV2IterationField", + "id": "PVTIF_lADOAlIw4c4BCe3Vzg0rhqQ", + "name": "field-iteration", + "dataType": "ITERATION" + } + ], + "pageInfo": { + "endCursor": "MTU", + "hasNextPage": false + } + }, + "owner": { + "__typename": "Organization", + "login": "octodemo" + } + } + } + } +} diff --git a/Test/public/project/getproject.test.ps1 b/Test/public/project/getproject.test.ps1 index 3042465..b1a84a2 100644 --- a/Test/public/project/getproject.test.ps1 +++ b/Test/public/project/getproject.test.ps1 @@ -1,17 +1,55 @@ -function Test_GetProject_With_Query{ +function Test_GetProject_With_Query_Success{ Reset-InvokeCommandMock Mock_DatabaseRoot + # enable-invokeCommandAliasModule + $p = Get-Mock_Project_700 ; $owner = $p.owner ; $projectNumber = $p.number + $query = $p.getProjectWithQuery.query + $fileName = $p.getProjectWithQuery.getProjectWithQueryMockFile + $totalCount = $p.getProjectWithQuery.totalCount $i = $p.issue - $query = "some query" - # MockCall_GitHubOrgProjectWithFields -Owner $owner -ProjectNumber $projectNumber -Query $query -Fieldname + MockCall_GitHubOrgProjectWithFields -Owner $owner -ProjectNumber $projectNumber -Query $query -FileName $fileName + + $result = Update-ProjectDatabase -Owner $owner -ProjectNumber $projectNumber -Query $query + + Assert-IsTrue $result - $result = Invoke-GitHubOrgProjectWithFields -Owner $owner -ProjectNumber $projectNumber -query $query + $result = Get-Project -Owner $owner -ProjectNumber $projectNumber + + Assert-Count -Expected $totalCount -Presented $result.items +} + +function Test_GetProject_With_Query_Success_Update{ + Reset-InvokeCommandMock + Mock_DatabaseRoot + $p = Get-Mock_Project_700 ; $owner = $p.owner ; $projectNumber = $p.number + $cacheFileName = $p.cacheFileName + $q = $p.getProjectWithQuery + $fieldName = $q.FieldName + $fieldValueActual = $q.FieldValueActual + $fieldValueNew = $q.FieldValueNew + $totalCount = $q.totalCount + + MockCall_GetProject $p -Cache + + # update field-text to a new value from Actual to check if it´s updated when calling Update-ProjectDatabase with a query + Update-Mock_DatabaseFileWithReplace -Filename $cacheFileName -SearchString $q.stringToReplaceFrom -ReplaceString $q.stringToReplaceTo + + # Assert the arrangement + $result = Search-ProjectItem -Owner $owner -ProjectNumber $projectNumber -FieldName $fieldName -Filter $fieldValueActual -Exact -IncludeDone + Assert-Count -Expected 0 -Presented $result + $result = Search-ProjectItem -Owner $owner -ProjectNumber $projectNumber -FieldName $fieldName -Filter $fieldValueNew -Exact -IncludeDone + Assert-Count -Expected $totalCount -Presented $result + + # Act - Should replace new value back to actual $result = Update-ProjectDatabase -Owner $owner -ProjectNumber $projectNumber -Query $query - Assert-NotImplemented - + # Assert confirm field-text value is back to actual + Assert-IsTrue $result + + $result = Search-ProjectItem -Owner $owner -ProjectNumber $projectNumber -FieldName $fieldName -Filter $fieldValueActual -Exact -IncludeDone + Assert-Count -Expected $totalCount -Presented $result } \ No newline at end of file diff --git a/Test/traceInvoke.log b/Test/traceInvoke.log index 2215e0d..b4da7ba 100644 --- a/Test/traceInvoke.log +++ b/Test/traceInvoke.log @@ -73,3 +73,4 @@ Invoke-GitHubOrgProjectWithFields -Owner SomeOrg -ProjectNumber 164 -afterFields Invoke-GitHubOrgProjectWithFields -Owner SomeOrg -ProjectNumber 164 -afterFields "" -afterItems "" -query "" Invoke-GitHubOrgProjectWithFields -Owner octodemo -ProjectNumber 625 -afterFields "" -afterItems "" -query "" Invoke-GitHubOrgProjectWithFields -Owner octodemo -ProjectNumber 626 -afterFields "" -afterItems "" -query "" +Invoke-GitHubOrgProjectWithFields -Owner octodemo -ProjectNumber 700 -afterFields "" -afterItems "" -query "field-text:text1" From 1a53e94a124c7b1d8446d59977238bd485e36db2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20=28Dibildos=29=20Gonz=C3=A1lez?= Date: Mon, 10 Nov 2025 08:15:00 +0100 Subject: [PATCH 10/24] refactor(graphql): add more tags and refactor querys for clarity --- public/graphql/_projectv2item.tag | 10 +++++----- public/graphql/_user.tag | 1 + public/graphql/addItemToProject.mutant | 5 +++-- 3 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 public/graphql/_user.tag diff --git a/public/graphql/_projectv2item.tag b/public/graphql/_projectv2item.tag index 822f003..ea36d0b 100644 --- a/public/graphql/_projectv2item.tag +++ b/public/graphql/_projectv2item.tag @@ -5,14 +5,14 @@ ... on DraftIssue {id,body,title,updatedAt,createdAt}, ... on PullRequest{id,body,title,updatedAt,createdAt,number,url,state,repository{name,owner{login}} comments(last: $lastComments){ - totalCount, - nodes{createdAt,updatedAt,url,body,fullDatabaseId,author{login}} + totalCount, + nodes{createdAt,updatedAt,url,body,fullDatabaseId,author{login}} } }, - ... on Issue{id,body,title,updatedAt,createdAt,number,url,state,repository{name,owner{login}} + ... on Issue{id,body,title,updatedAt,createdAt,number,url,state,repository{name,owner{login}} comments(last: $lastComments){ - totalCount, - nodes{createdAt,updatedAt,url,body,fullDatabaseId,author{login}} + totalCount, + nodes{createdAt,updatedAt,url,body,fullDatabaseId,author{login}} } } }, diff --git a/public/graphql/_user.tag b/public/graphql/_user.tag new file mode 100644 index 0000000..d4f771d --- /dev/null +++ b/public/graphql/_user.tag @@ -0,0 +1 @@ +{login} \ No newline at end of file diff --git a/public/graphql/addItemToProject.mutant b/public/graphql/addItemToProject.mutant index 4b4ba4e..92f707f 100644 --- a/public/graphql/addItemToProject.mutant +++ b/public/graphql/addItemToProject.mutant @@ -1,5 +1,6 @@ -mutation AddItem($input:AddProjectV2ItemByIdInput! $lastComments: Int=10){addProjectV2ItemById(input:$input){ - item{{projectv2item}} +mutation AddItem($input:AddProjectV2ItemByIdInput! $lastComments: Int=10){ + addProjectV2ItemById(input:$input){ + item{{projectv2item}} } } \ No newline at end of file From 5bd7c277564fb51b07a21f115ff48aaac7afe178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20=28Dibildos=29=20Gonz=C3=A1lez?= Date: Wed, 12 Nov 2025 01:01:33 +0100 Subject: [PATCH 11/24] feat(graphql): add updateProjectV2Collaborators mutation and related functions --- Test/include/config.mock.ps1 | 37 ++++ include/config.ps1 | 188 ++++++++++++++++++ include/featureflag.ps1 | 144 ++++++++++++++ .../updateProjectV2Collaborators.ps1 | 21 ++ public/graphql/_content.tag | 6 +- public/graphql/_team.tag | 1 + public/graphql/_user.tag | 2 +- .../updateProjectV2Collaborators.mutant | 12 ++ 8 files changed, 409 insertions(+), 2 deletions(-) create mode 100644 Test/include/config.mock.ps1 create mode 100644 include/config.ps1 create mode 100644 include/featureflag.ps1 create mode 100644 public/driver/projectv2/updateProjectV2Collaborators.ps1 create mode 100644 public/graphql/_team.tag create mode 100644 public/graphql/updateProjectV2Collaborators.mutant diff --git a/Test/include/config.mock.ps1 b/Test/include/config.mock.ps1 new file mode 100644 index 0000000..5e86527 --- /dev/null +++ b/Test/include/config.mock.ps1 @@ -0,0 +1,37 @@ + +# CONFIG MOCK +# +# This file is used to mock the config path and the config file +# for the tests. It creates a mock config path and a mock config file +# and sets the config path to the mock config path. +# +# THIS INCLUDE REQURED module.helper.ps1 +if(-not $MODULE_NAME){ throw "Missing MODULE_NAME varaible initialization. Check for module.helerp.ps1 file." } + +$MOCK_CONFIG_PATH = "test_config_path" +$CONFIG_INVOKE_GET_ROOT_PATH_CMD = "Invoke-$($MODULE_NAME)GetConfigRootPath" + +function Mock_Config{ + param( + [Parameter(Position=0)][string] $key = "config", + [Parameter(Position=1)][object] $Config + ) + + # Remove mock config path if exists + if(Test-Path $MOCK_CONFIG_PATH){ + Remove-Item -Path $MOCK_CONFIG_PATH -ErrorAction SilentlyContinue -Recurse -Force + } + + # create mock config path + New-Item -Path $MOCK_CONFIG_PATH -ItemType Directory -Force + + # if $config is not null save it to a file + if($null -ne $Config){ + $configfile = Join-Path -Path $MOCK_CONFIG_PATH -ChildPath "$key.json" + $Config | ConvertTo-Json -Depth 10 | Set-Content $configfile + } + + # Mock invoke call + MockCallToString $CONFIG_INVOKE_GET_ROOT_PATH_CMD -OutString $MOCK_CONFIG_PATH + +} diff --git a/include/config.ps1 b/include/config.ps1 new file mode 100644 index 0000000..5e372ca --- /dev/null +++ b/include/config.ps1 @@ -0,0 +1,188 @@ +# CONFIG +# +# Configuration management module +# +# Include design description +# This is the function ps1. This file is the same for all modules. +# Create a public psq with variables, Set-MyInvokeCommandAlias call and Invoke public function. +# Invoke function will call back `GetConfigRootPath` to use production root path +# Mock this Invoke function with Set-MyInvokeCommandAlias to set the Store elsewhere +# This ps1 has function `GetConfigFile` that will call `Invoke-MyCommand -Command $CONFIG_INVOKE_GET_ROOT_PATH_ALIAS` +# to use the store path, mocked or not, to create the final store file name. +# All functions of this ps1 will depend on `GetConfigFile` for functionality. +# + +# MODULE_NAME +$MODULE_NAME = ($PSScriptRoot | Split-Path -Parent | Get-ChildItem -Filter *.psd1 | Select-Object -First 1).BaseName +if(-Not $MODULE_NAME){ throw "Module name not found. Please check the module structure." } + +$CONFIG_ROOT = [System.Environment]::GetFolderPath('UserProfile') | Join-Path -ChildPath ".helpers" -AdditionalChildPath $MODULE_NAME, "config" + +# Create the config root if it does not exist +if(-Not (Test-Path $CONFIG_ROOT)){ + New-Item -Path $CONFIG_ROOT -ItemType Directory +} + +function GetConfigRootPath { + [CmdletBinding()] + param() + + $configRoot = $CONFIG_ROOT + return $configRoot +} + +function GetConfigFile { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, Position = 0)][string]$Key + ) + + $configRoot = Invoke-MyCommand -Command $CONFIG_INVOKE_GET_ROOT_PATH_ALIAS + $path = Join-Path -Path $configRoot -ChildPath "$Key.json" + return $path +} + +function Test-ConfigurationFile { + [CmdletBinding()] + param( + [Parameter(Position = 0)][string]$Key = "config" + ) + + $path = GetConfigFile -Key $Key + + return Test-Path $path +} + +function Get-Configuration { + [CmdletBinding()] + param( + [Parameter(Position = 0)][string]$Key = "config" + ) + + # Check for cached configuration + $configVar = Get-Variable -scope Script -Name "config-$Key" -ErrorAction SilentlyContinue + if($configVar){ + return $configVar + } + + # No cached configuration; read from file + $path = GetConfigFile -Key $Key + + if(-Not (Test-ConfigurationFile -Key $Key)){ + return $null + } + + try{ + $ret = Get-Content $path | ConvertFrom-Json -AsHashtable -ErrorAction Stop + return $ret + } + catch{ + Write-Warning "Error reading configuration ($Key) file: $($path). $($_.Exception.Message)" + return $null + } +} + +function Save-Configuration { + [CmdletBinding()] + param( + [Parameter()][string]$Key = "config", + [Parameter(Mandatory = $true, Position = 1)][Object]$Config + ) + + $path = GetConfigFile -Key $Key + + try { + $Config | ConvertTo-Json -Depth 10 | Set-Content $path -ErrorAction Stop + } + catch { + Write-Warning "Error saving configuration ($Key) to file: $($path). $($_.Exception.Message)" + return $false + } + finally{ + Remove-Variable -Scope Script -Name "config-$Key" -ErrorAction SilentlyContinue + } + + return $true +} + +############ + + +# Define unique aliases for "ModuleName" +$CONFIG_INVOKE_GET_ROOT_PATH_ALIAS = "$($MODULE_NAME)GetConfigRootPath" +$CONFIG_INVOKE_GET_ROOT_PATH_CMD = "Invoke-$($MODULE_NAME)GetConfigRootPath" + +# Set the alias for the root path command +Set-MyInvokeCommandAlias -Alias $CONFIG_INVOKE_GET_ROOT_PATH_ALIAS -Command $CONFIG_INVOKE_GET_ROOT_PATH_CMD + +# Define the function to get the configuration root path +function Invoke-ModuleNameGetConfigRootPath { + [CmdletBinding()] + param() + + $configRoot = GetConfigRootPath + return $configRoot +} +$function = "Invoke-ModuleNameGetConfigRootPath" +$destFunction = $function -replace "ModuleName", $MODULE_NAME +if( -not (Test-Path function:$destFunction )){ + Rename-Item -path Function:$function -NewName $destFunction + Export-ModuleMember -Function $destFunction +} + +# Extra functions not needed by INCLUDE CONFIG + +function Get-ModuleNameConfig{ + [CmdletBinding()] + param() + + $config = Get-Configuration + + return $config +} +$function = "Get-ModuleNameConfig" +$destFunction = $function -replace "ModuleName", $MODULE_NAME +if( -not (Test-Path function:$destFunction )){ + Rename-Item -path Function:$function -NewName $destFunction + Export-ModuleMember -Function $destFunction +} + +function Open-ModuleNameConfig{ + [CmdletBinding()] + param() + + $path = GetConfigFile -Key "config" + + code $path +} +$function = "Open-ModuleNameConfig" +$destFunction = $function -replace "ModuleName", $MODULE_NAME +if( -not (Test-Path function:$destFunction )){ + Rename-Item -path Function:$function -NewName $destFunction + Export-ModuleMember -Function $destFunction +} + +function Set-ModuleNameConfigValue{ + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)][string]$Name, + [Parameter(Mandatory = $true)][object]$Value + ) + + $config = Get-Configuration + + if(-Not $config){ + $config = @{} + } + + $config.$Name = $Value + + Save-Configuration -Key "config" -Config $config +} +$function = "Set-ModuleNameConfigValue" +$destFunction = $function -replace "ModuleName", $MODULE_NAME +if( -not (Test-Path function:$destFunction )){ + # Rename-Item -path Function:$function -NewName $destFunction + Copy-Item -path Function:$function -Destination Function:$destFunction + Export-ModuleMember -Function $destFunction +} diff --git a/include/featureflag.ps1 b/include/featureflag.ps1 new file mode 100644 index 0000000..5184202 --- /dev/null +++ b/include/featureflag.ps1 @@ -0,0 +1,144 @@ +# Feature Flag +# +# Feature Flags management module +# +# Include design description +# This module depends on Config Include +# This module will allow set Feature Flags to the module to quicker release +# features with less risk +# + +$MODULE_NAME_PATH = ($PSScriptRoot | Split-Path -Parent | Get-ChildItem -Filter *.psd1 | Select-Object -First 1) | Split-Path -Parent + +function Get-FeatureFlags{ + [CmdletBinding()] + param() + + $config = Get-Configuration + + if(! $config){ + return @{} + } + + if(! $config.FeatureFlags){ + $config.FeatureFlags = @{} + } + + return $config.FeatureFlags +} + +function Save-FeatureFlags{ + [CmdletBinding()] + param( + [Parameter(Mandatory,Position=0)][hashtable]$FeatureFlags + ) + + $result = Set-ModuleNameConfigValue -Name "FeatureFlags" -Value $FeatureFlags + + if(! $result){ + throw "Failed to save Feature Flags" + } +} + +function Test-FeatureFlag { + [CmdletBinding()] + [Alias("tff")] + param( + [Parameter(Mandatory,Position=0)][string]$Key + ) + + $ffs = Get-FeatureFlags + + $value = $ffs.$Key + + if($null -eq $value){ + Set-FeatureFlag -Key $Key -Value $false + return $false + } + + return $value +} + +function Set-FeatureFlag{ + [CmdletBinding()] + param( + [Parameter(Mandatory,Position=0)][string]$Key, + [Parameter()][bool]$Value = $true + + ) + + $featureFlags = Get-FeatureFlags + + $featureFlags.$Key = $Value + + Save-FeatureFlags $featureFlags + +} + +function Clear-FeatureFlag{ + [CmdletBinding()] + param( + [Parameter(Mandatory,Position=0)][string]$Key + + ) + + Set-FeatureFlag -Key $Key -Value $false + +} + +###### + +# function Get-ModuleNameRegisteredFeatureFlags{ +# [cmdletbinding()] +# param() + +# $ffPath = $MODULE_NAME_PATH | Join-Path -ChildPath "featureflags.json" + +# if(! ($ffPath | Test-Path)){ +# return +# } + +# $Json = Get-Content $ffPath + +# $ff = $Json | ConvertFrom-Json + +# return $ff + +# } +# $function = "Get-ModuleNameRegisteredFeatureFlags" +# $destFunction = $function -replace "ModuleName", $MODULE_NAME +# if( -not (Test-Path function:$destFunction )){ +# Rename-Item -path Function:$function -NewName $destFunction +# Export-ModuleMember -Function $destFunction +# } + +function Get-ModuleNameFeatureFlags{ + [cmdletbinding()] + param() + + $ffs = Get-FeatureFlags + + return $ffs +} +$function = "Get-ModuleNameFeatureFlags" +$destFunction = $function -replace "ModuleName", $MODULE_NAME +if( -not (Test-Path function:$destFunction )){ + Rename-Item -path Function:$function -NewName $destFunction + Export-ModuleMember -Function $destFunction +} + +function Set-ModuleNameFeatureFlag{ + [cmdletbinding()] + param( + [Parameter(Mandatory,Position=0)][string]$Key, + [Parameter()][bool]$Value = $true + ) + + Set-FeatureFlag -Key $Key -Value $Value +} +$function = "Set-ModuleNameFeatureFlag" +$destFunction = $function -replace "ModuleName", $MODULE_NAME +if( -not (Test-Path function:$destFunction )){ + Rename-Item -path Function:$function -NewName $destFunction + Export-ModuleMember -Function $destFunction +} diff --git a/public/driver/projectv2/updateProjectV2Collaborators.ps1 b/public/driver/projectv2/updateProjectV2Collaborators.ps1 new file mode 100644 index 0000000..36467c3 --- /dev/null +++ b/public/driver/projectv2/updateProjectV2Collaborators.ps1 @@ -0,0 +1,21 @@ +function Invoke-UpdateProjectV2Collaborators{ + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)][string]$ProjectId, + [Parameter(Mandatory=$true)][array]$collaborators + ) + + $query = Get-GraphQLString "updateProjectV2Collaborators.mutant" + + $variables = @{ + input = @{ + projectId = $ProjectId + collaborators = $collaborators + } + } + + $response = Invoke-GraphQL -Query $query -Variables $variables + + return $response + +} Export-ModuleMember -Function Invoke-UpdateProjectV2Collaborators \ No newline at end of file diff --git a/public/graphql/_content.tag b/public/graphql/_content.tag index 1ade5a8..4ef0df5 100644 --- a/public/graphql/_content.tag +++ b/public/graphql/_content.tag @@ -7,7 +7,11 @@ nodes{createdAt,updatedAt,url,body,fullDatabaseId,author{login}} } }, - ... on Issue{id,body,title,updatedAt,createdAt,number,url,state,repository{name,owner{login}} + ... on Issue{ + id,body,title,updatedAt,createdAt,number,url,state,repository{name,owner{login}} + assignees(first:100){ + nodes{{user}} + } comments(last: $lastComments){ totalCount, nodes{createdAt,updatedAt,url,body,fullDatabaseId,author{login}} diff --git a/public/graphql/_team.tag b/public/graphql/_team.tag new file mode 100644 index 0000000..f042d23 --- /dev/null +++ b/public/graphql/_team.tag @@ -0,0 +1 @@ +{id,name} \ No newline at end of file diff --git a/public/graphql/_user.tag b/public/graphql/_user.tag index d4f771d..07fe8a4 100644 --- a/public/graphql/_user.tag +++ b/public/graphql/_user.tag @@ -1 +1 @@ -{login} \ No newline at end of file +{id,name,login,email} \ No newline at end of file diff --git a/public/graphql/updateProjectV2Collaborators.mutant b/public/graphql/updateProjectV2Collaborators.mutant new file mode 100644 index 0000000..0f68f03 --- /dev/null +++ b/public/graphql/updateProjectV2Collaborators.mutant @@ -0,0 +1,12 @@ +mutation UpdateCollaborationUpdate($input:UpdateProjectV2CollaboratorsInput!){ + updateProjectV2Collaborators(input: $input){ + collaborators(first:100) { + totalCount, + nodes{ + __typename + ... on User {{user}} + ...on Team {{team}} + } + } + } +} \ No newline at end of file From a09205ea9166e017b5bf5cdb9a6dde40a80a01d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20=28Dibildos=29=20Gonz=C3=A1lez?= Date: Wed, 12 Nov 2025 09:22:19 +0100 Subject: [PATCH 12/24] feat(user): add Invoke-GetUser and Get-User functions for user retrieval --- public/driver/user/user.ps1 | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 public/driver/user/user.ps1 diff --git a/public/driver/user/user.ps1 b/public/driver/user/user.ps1 new file mode 100644 index 0000000..635b1a7 --- /dev/null +++ b/public/driver/user/user.ps1 @@ -0,0 +1,35 @@ + +Set-MyinvokeCommandAlias -Alias getUser -Command "Invoke-GetUser -Handle {handle}" +function Invoke-GetUser{ + param( + [Parameter(Mandatory)][string]$Handle + ) + + $result = Invoke-RestAPI -Api /users/$Handle + + return $result + +} Export-ModuleMember -Function Invoke-GetUser + +function Get-User{ + param( + [Parameter(Mandatory)][string]$Handle, + [Parameter()][switch]$Force + ) + + $key = "user-$Handle" + + # Check cache + $cache = Get-Database -Key $key + if($force -or ($null -ne $cache)){ + return $cache + } + + $result = Invoke-MyCommand -Command "getUser" -Parameters @{handle=$Handle} + + # Cache + Save-Database -Key "user-$Handle" -Database $result + + return $result + +} Export-ModuleMember -Function Get-User \ No newline at end of file From 27b2fc1e33d02dafb3bc59d3f03c37417c714a34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20=28Dibildos=29=20Gonz=C3=A1lez?= Date: Wed, 12 Nov 2025 09:22:26 +0100 Subject: [PATCH 13/24] refactor(callAPI): remove Export-ModuleMember statements for clarity --- include/callAPI.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/include/callAPI.ps1 b/include/callAPI.ps1 index 3f910b7..d6af0ea 100644 --- a/include/callAPI.ps1 +++ b/include/callAPI.ps1 @@ -61,7 +61,7 @@ function Invoke-GraphQL { throw New-Object system.Exception("Error calling GraphQL",$_.Exception) } -} Export-ModuleMember -Function Invoke-GraphQL +} function Invoke-RestAPI { param( @@ -132,7 +132,7 @@ function Invoke-RestAPI { catch { throw } -} Export-ModuleMember -Function Invoke-RestAPI +} #################################################################################################### @@ -158,7 +158,7 @@ function Get-ApiHost { "Default host $DEFAULT_GH_HOST" | writedebug return $DEFAULT_GH_HOST -} Export-ModuleMember -Function Get-ApiHost +} #################################################################################################### @@ -198,7 +198,7 @@ function Get-ApiToken { } return $result -} Export-ModuleMember -Function Get-ApiToken +} #################################################################################################### From 32d131d2dfe532c9c2b00b9ad1d565c73719505e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20=28Dibildos=29=20Gonz=C3=A1lez?= Date: Wed, 12 Nov 2025 09:22:38 +0100 Subject: [PATCH 14/24] refactor(invokeRestMethod): comment out function implementation for clarity --- private/invokeRestMethord.ps1 | 46 +++++++++++++++++------------------ 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/private/invokeRestMethord.ps1 b/private/invokeRestMethord.ps1 index d6ee4bb..a01d66c 100644 --- a/private/invokeRestMethord.ps1 +++ b/private/invokeRestMethord.ps1 @@ -1,29 +1,29 @@ -function Invoke-RestMethod{ - [CmdletBinding()] - param( - [Parameter(Position = 0)][string]$Method, - [Parameter(Position = 1)][string]$Uri, - [Parameter(Position = 2)][hashtable]$Headers, - [Parameter(Position = 3)][string]$Body, - [Parameter()][string]$OutFile - ) +# function Invoke-RestMethod{ +# [CmdletBinding()] +# param( +# [Parameter(Position = 0)][string]$Method, +# [Parameter(Position = 1)][string]$Uri, +# [Parameter(Position = 2)][hashtable]$Headers, +# [Parameter(Position = 3)][string]$Body, +# [Parameter()][string]$OutFile +# ) - $params = @{ - Method = $Method - Uri = $Uri - Headers = $Headers - Body = $Body - } +# $params = @{ +# Method = $Method +# Uri = $Uri +# Headers = $Headers +# Body = $Body +# } - if (-not [string]::IsNullOrWhiteSpace($OutFile)) { - $params.OutFile = $OutFile - } +# if (-not [string]::IsNullOrWhiteSpace($OutFile)) { +# $params.OutFile = $OutFile +# } - ">> $Method $Uri" | Write-MyDebug -section "invokeRestMethod" - $result = Microsoft.PowerShell.Utility\Invoke-RestMethod @params - "<< $Method $Uri" | Write-MyDebug -section "invokeRestMethod" +# ">> $Method $Uri" | Write-MyDebug -section "invokeRestMethod" +# $result = Microsoft.PowerShell.Utility\Invoke-RestMethod @params +# "<< $Method $Uri" | Write-MyDebug -section "invokeRestMethod" - return $result -} \ No newline at end of file +# return $result +# } \ No newline at end of file From 11323cd6102a2bcf385a2a6bfd2d605b6a81e1a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20=28Dibildos=29=20Gonz=C3=A1lez?= Date: Wed, 12 Nov 2025 10:33:32 +0100 Subject: [PATCH 15/24] refactor(user): update cache check logic in Get-User function --- public/driver/user/user.ps1 | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/public/driver/user/user.ps1 b/public/driver/user/user.ps1 index 635b1a7..1e429eb 100644 --- a/public/driver/user/user.ps1 +++ b/public/driver/user/user.ps1 @@ -21,7 +21,7 @@ function Get-User{ # Check cache $cache = Get-Database -Key $key - if($force -or ($null -ne $cache)){ + if(-Not $Force -And ($null -ne $cache)){ return $cache } @@ -30,6 +30,13 @@ function Get-User{ # Cache Save-Database -Key "user-$Handle" -Database $result - return $result + $ret = [PSCustomObject]@{ + Id = $result.node_id + Name = $result.Name + Email = $result.Email + Login = $result.Login + } + + return $ret } Export-ModuleMember -Function Get-User \ No newline at end of file From 8248bfcbf63242f7d1b083eed17a7bfabf2da084 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20=28Dibildos=29=20Gonz=C3=A1lez?= Date: Wed, 12 Nov 2025 19:52:00 +0100 Subject: [PATCH 16/24] refactor(callAPI): allow call with outfile parameter --- include/callAPI.ps1 | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/include/callAPI.ps1 b/include/callAPI.ps1 index d6af0ea..efce501 100644 --- a/include/callAPI.ps1 +++ b/include/callAPI.ps1 @@ -44,7 +44,9 @@ function Invoke-GraphQL { # Send the request $start = Get-Date ">>> Invoke-RestMethod - $apiUri" | writedebug - $response = Invoke-RestMethod -Uri $apiUri -Method Post -Body $body -Headers $headers -OutFile $OutFile + if([string]::IsNullOrWhiteSpace($OutFile)) + { $response = Invoke-RestMethod -Uri $apiUri -Method Post -Body $body -Headers $headers } + else { $response = Invoke-RestMethod -Uri $apiUri -Method Post -Body $body -Headers $headers -OutFile $OutFile } "<<< Invoke-RestMethod - $apiUri [ $(((Get-Date) - $start).TotalSeconds) seconds]" | writedebug # Trace response @@ -58,6 +60,8 @@ function Invoke-GraphQL { return $response } catch { + "[[THROW]]" | writedebug + $_.Exception.Message | ConvertTo-Json -Depth 100 | writedebug throw New-Object system.Exception("Error calling GraphQL",$_.Exception) } From dff98c0bc00bc47e44b76637f8c1e4241e84a737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20=28Dibildos=29=20Gonz=C3=A1lez?= Date: Wed, 12 Nov 2025 19:55:42 +0100 Subject: [PATCH 17/24] feat(driver): Invoke-UpdateProjectV2Collaborators --- .../projectv2/updateProjectV2Collaborators.ps1 | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/public/driver/projectv2/updateProjectV2Collaborators.ps1 b/public/driver/projectv2/updateProjectV2Collaborators.ps1 index 36467c3..d03433e 100644 --- a/public/driver/projectv2/updateProjectV2Collaborators.ps1 +++ b/public/driver/projectv2/updateProjectV2Collaborators.ps1 @@ -2,15 +2,26 @@ function Invoke-UpdateProjectV2Collaborators{ [CmdletBinding()] param( [Parameter(Mandatory=$true)][string]$ProjectId, - [Parameter(Mandatory=$true)][array]$collaborators + [Parameter(Mandatory=$true)][ValidateSet("READER","WRITER","NONE","ADMIN")] + [string]$Role, + [Parameter(Mandatory=$true)][string] $CollaboratorsIds ) + $list = $CollaboratorsIds.Split(@(" "),[System.StringSplitOptions]::RemoveEmptyEntries) + + $array = $list | ForEach-Object { + @{ + userId = $_ + role = $Role + } + } + $query = Get-GraphQLString "updateProjectV2Collaborators.mutant" $variables = @{ input = @{ projectId = $ProjectId - collaborators = $collaborators + collaborators = $array } } From 1244cddc815125b1703cbd2c1c8d3fb24b271425 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20=28Dibildos=29=20Gonz=C3=A1lez?= Date: Wed, 12 Nov 2025 19:56:21 +0100 Subject: [PATCH 18/24] feat(project): Add-ProjectUser --- public/project/addprojectuser.ps1 | 73 +++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 public/project/addprojectuser.ps1 diff --git a/public/project/addprojectuser.ps1 b/public/project/addprojectuser.ps1 new file mode 100644 index 0000000..0a55810 --- /dev/null +++ b/public/project/addprojectuser.ps1 @@ -0,0 +1,73 @@ +Set-MyInvokeCommandAlias -Alias "updateProjectV2Collaborators" -Command 'Invoke-UpdateProjectV2Collaborators -ProjectId {projectid} -collaborators "{collaboratorsIds}" -Role "{role}"' + +function Add-ProjectUser { + [CmdletBinding()] + param( + [Parameter()][string]$Owner, + [Parameter()][int]$ProjectNumber, + [Parameter(Mandatory,ValueFromPipeline)][string]$Handle, + [Parameter()][string]$Role ="WRITER" + + ) + + begin{ + + ($Owner, $ProjectNumber) = Get-OwnerAndProjectNumber -Owner $Owner -ProjectNumber $ProjectNumber + if ([string]::IsNullOrWhiteSpace($owner) -or [string]::IsNullOrWhiteSpace($ProjectNumber)) { + throw "Owner and ProjectNumber are required on Get-Project" + } + + $project = Get-Project -Owner $Owner -ProjectNumber $ProjectNumber -SkipItems + + $projectId = $project.ProjectId + + $userIds = @() + + } + + process{ + + $user = Get-User -Handle $Handle + $userId = $user.Id + + if([string]::IsNullOrWhiteSpace($userId)){ + Write-Error "No user found for handle [$Handle]" + } + + $userIds += $userId + } + + end{ + + $userIdsString = $userIds -join " " + + if([string]::IsNullOrWhiteSpace($userIdsString)){ + Write-Error "No users found" + return $false + } + + $response = Invoke-MyCommand -Command "updateProjectV2Collaborators" -Parameters @{ + projectid = $projectId + role = $Role + collaboratorsIds = $userIdsString + } + + # Check reply data to confirm users were added + if($response.data.updateProjectV2Collaborators.collaborators.totalCount -ne $userIds.Count){ + Write-Error "Not all users were added to the project" + return $false + } + + return $true + } + + + + + + + + + + +} Export-ModuleMember -Function Add-ProjectUser From 9da3983d034a3569c0035ff5abab9cd1c7387eef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20=28Dibildos=29=20Gonz=C3=A1lez?= Date: Wed, 12 Nov 2025 19:59:11 +0100 Subject: [PATCH 19/24] feat(test): tests for add-projectUser --- Test/mockfiles.log | 20 ++++++++ Test/private/MockUsers.ps1 | 16 ++++++ .../mocks/invoke-GetUser-rauldibildos.json | 4 ++ Test/private/mocks/invoke-GetUser-rulasg.json | 47 +++++++++++++++++ ...ors-MDQ6VXNlcjY4ODQ0MDg=-U_kgDOC_E3gw.json | 38 ++++++++++++++ ...tV2Collaborators-MDQ6VXNlcjY4ODQ0MDg=.json | 31 ++++++++++++ Test/public/project/addprojectuser.test.ps1 | 50 +++++++++++++++++++ Test/traceInvoke.log | 4 ++ 8 files changed, 210 insertions(+) create mode 100644 Test/private/MockUsers.ps1 create mode 100644 Test/private/mocks/invoke-GetUser-rauldibildos.json create mode 100644 Test/private/mocks/invoke-GetUser-rulasg.json create mode 100644 Test/private/mocks/invoke-UpdateProjectV2Collaborators-MDQ6VXNlcjY4ODQ0MDg=-U_kgDOC_E3gw.json create mode 100644 Test/private/mocks/invoke-UpdateProjectV2Collaborators-MDQ6VXNlcjY4ODQ0MDg=.json create mode 100644 Test/public/project/addprojectuser.test.ps1 diff --git a/Test/mockfiles.log b/Test/mockfiles.log index effc515..5ab0797 100644 --- a/Test/mockfiles.log +++ b/Test/mockfiles.log @@ -182,5 +182,25 @@ { "FileName": "invoke-GitHubOrgProjectWithFields-octodemo-700-query-field-text.json", "Command": "Invoke-GitHubOrgProjectWithFields -Owner octodemo -ProjectNumber 700 -afterFields \"\" -afterItems \"\" -query \"field-text:text1\"" + }, + { + "FileName": "invoke-GetUser-rulasg.json", + "Command": "Invoke-GetUser -Handle rulasg" + }, + { + "FileName": "invoke-UpdateProjectV2Collaborators-MDQ6VXNlcjY4ODQ0MDg=.json", + "Command": "Invoke-UpdateProjectV2Collaborators -ProjectId PVT_kwDOAlIw4c4BCe3V -collaborators \"MDQ6VXNlcjY4ODQ0MDg=\" -Role \"WRITER\"" + }, + { + "FileName": "invoke-GetUser-rauldibildos.json", + "Command": "Invoke-GetUser -Handle rauldibildos" + }, + { + "FileName": "invoke-UpdateProjectV2Collaborators-MDQ6VXNlcjY4ODQ0MDg=.json", + "Command": "Invoke-UpdateProjectV2Collaborators -ProjectId PVT_kwDOAlIw4c4BCe3V -collaborators \"MDQ6VXNlcjY4ODQ0MDg= U_kgDOC_E3gw\" -Role \"WRITER\"" + }, + { + "FileName": "invoke-UpdateProjectV2Collaborators-MDQ6VXNlcjY4ODQ0MDg=.json", + "Command": "Invoke-UpdateProjectV2Collaborators -ProjectId PVT_kwDOAlIw4c4BCe3V -collaborators \"\" -Role \"WRITER\"" } ] diff --git a/Test/private/MockUsers.ps1 b/Test/private/MockUsers.ps1 new file mode 100644 index 0000000..5f8adc1 --- /dev/null +++ b/Test/private/MockUsers.ps1 @@ -0,0 +1,16 @@ +function Get-Mock_Users{ + $users = @{ + u1 = @{ + id = "MDQ6VXNlcjY4ODQ0MDg=" + name = "rulasg" + file = "invoke-GetUser-rulasg.json" + } + u2 = @{ + id = "U_kgDOC_E3gw" + name = "rauldibildos" + file = "invoke-GetUser-rauldibildos.json" + } + } + + return $users +} \ No newline at end of file diff --git a/Test/private/mocks/invoke-GetUser-rauldibildos.json b/Test/private/mocks/invoke-GetUser-rauldibildos.json new file mode 100644 index 0000000..a87bb61 --- /dev/null +++ b/Test/private/mocks/invoke-GetUser-rauldibildos.json @@ -0,0 +1,4 @@ +{ + "login": "raulDibildos", + "node_id": "U_kgDOC_E3gw" +} diff --git a/Test/private/mocks/invoke-GetUser-rulasg.json b/Test/private/mocks/invoke-GetUser-rulasg.json new file mode 100644 index 0000000..e4d3449 --- /dev/null +++ b/Test/private/mocks/invoke-GetUser-rulasg.json @@ -0,0 +1,47 @@ +{ + "login": "rulasg", + "id": 6884408, + "node_id": "MDQ6VXNlcjY4ODQ0MDg=", + "avatar_url": "https://avatars.githubusercontent.com/u/6884408?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/rulasg", + "html_url": "https://github.com/rulasg", + "followers_url": "https://api.github.com/users/rulasg/followers", + "following_url": "https://api.github.com/users/rulasg/following{/other_user}", + "gists_url": "https://api.github.com/users/rulasg/gists{/gist_id}", + "starred_url": "https://api.github.com/users/rulasg/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/rulasg/subscriptions", + "organizations_url": "https://api.github.com/users/rulasg/orgs", + "repos_url": "https://api.github.com/users/rulasg/repos", + "events_url": "https://api.github.com/users/rulasg/events{/privacy}", + "received_events_url": "https://api.github.com/users/rulasg/received_events", + "type": "User", + "user_view_type": "private", + "site_admin": true, + "name": "Raúl (Dibildos) González", + "company": "GitHub", + "blog": "https://rulasg.github.io", + "location": "Madrid, Spain", + "email": "rulasg@github.com", + "hireable": null, + "bio": "A father, an optimist, an idealist, an engineer, a bit of a philosopher, a strategist, passionate about understanding and learning from everyone.", + "twitter_username": "rulasg", + "public_repos": 82, + "public_gists": 8, + "followers": 18, + "following": 12, + "created_at": "2014-03-07T14:47:11Z", + "updated_at": "2025-11-11T23:39:44Z", + "private_gists": 8, + "total_private_repos": 42, + "owned_private_repos": 42, + "disk_usage": 217904, + "collaborators": 4, + "two_factor_authentication": true, + "plan": { + "name": "pro", + "space": 976562499, + "collaborators": 0, + "private_repos": 9999 + } +} diff --git a/Test/private/mocks/invoke-UpdateProjectV2Collaborators-MDQ6VXNlcjY4ODQ0MDg=-U_kgDOC_E3gw.json b/Test/private/mocks/invoke-UpdateProjectV2Collaborators-MDQ6VXNlcjY4ODQ0MDg=-U_kgDOC_E3gw.json new file mode 100644 index 0000000..341c1d1 --- /dev/null +++ b/Test/private/mocks/invoke-UpdateProjectV2Collaborators-MDQ6VXNlcjY4ODQ0MDg=-U_kgDOC_E3gw.json @@ -0,0 +1,38 @@ +{ + "data": { + "updateProjectV2Collaborators": { + "collaborators": { + "totalCount": 2, + "nodes": [ + { + "__typename": "User", + "id": "MDQ6VXNlcjY4ODQ0MDg=", + "name": "Raúl (Dibildos) González", + "login": "rulasg", + "email": "rulasg@github.com" + }, + { + "__typename": "User", + "id": "U_kgDOC_E3gw", + "name": "Raúl Dibildos", + "login": "rauldibildos", + "email": "rauldibildos@gmail.com" + } + ] + } + } + }, + "extensions": { + "warnings": [ + { + "type": "DEPRECATION", + "message": "The id MDQ6VXNlcjY4ODQ0MDg= is deprecated. Update your cache to use the next_global_id from the data payload.", + "data": { + "legacy_global_id": "MDQ6VXNlcjY4ODQ0MDg=", + "next_global_id": "U_kgDOAGkMOA" + }, + "link": "https://docs.github.com" + } + ] + } +} diff --git a/Test/private/mocks/invoke-UpdateProjectV2Collaborators-MDQ6VXNlcjY4ODQ0MDg=.json b/Test/private/mocks/invoke-UpdateProjectV2Collaborators-MDQ6VXNlcjY4ODQ0MDg=.json new file mode 100644 index 0000000..ba86309 --- /dev/null +++ b/Test/private/mocks/invoke-UpdateProjectV2Collaborators-MDQ6VXNlcjY4ODQ0MDg=.json @@ -0,0 +1,31 @@ +{ + "data": { + "updateProjectV2Collaborators": { + "collaborators": { + "totalCount": 1, + "nodes": [ + { + "__typename": "User", + "id": "MDQ6VXNlcjY4ODQ0MDg=", + "name": "Raúl (Dibildos) González", + "login": "rulasg", + "email": "rulasg@github.com" + } + ] + } + } + }, + "extensions": { + "warnings": [ + { + "type": "DEPRECATION", + "message": "The id MDQ6VXNlcjY4ODQ0MDg= is deprecated. Update your cache to use the next_global_id from the data payload.", + "data": { + "legacy_global_id": "MDQ6VXNlcjY4ODQ0MDg=", + "next_global_id": "U_kgDOAGkMOA" + }, + "link": "https://docs.github.com" + } + ] + } +} diff --git a/Test/public/project/addprojectuser.test.ps1 b/Test/public/project/addprojectuser.test.ps1 new file mode 100644 index 0000000..f345f39 --- /dev/null +++ b/Test/public/project/addprojectuser.test.ps1 @@ -0,0 +1,50 @@ +function Test_AddProjectUser_SUCCESS_SingleUser{ + Reset-InvokeCommandMock + Mock_DatabaseRoot + + # Enable-invokeCommandAliasModule + # Invoke-UpdateProjectV2Collaborators -ProjectId PVT_kwDOAlIw4c4BCe3V -collaborators "MDQ6VXNlcjY4ODQ0MDg=" -Role "WRITER" + + + $p =Get-Mock_Project_700 ; $owner = $p.Owner ; $projectNumber = $p.Number ; $projectId = $p.id + MockCall_GetProject $p -SkipItems + + $u = Get-Mock_Users + + $userId1 = $u.u1.id ; $userName1 = $u.u1.name + $role ="WRITER" + + $fileName = "invoke-UpdateProjectV2Collaborators-$userId1.json" + + MockCallJson -Command "Invoke-GetUser -Handle $userName1" -File $u.u1.file + MockCallJson -Command "Invoke-UpdateProjectV2Collaborators -ProjectId $projectId -collaborators ""$userId1"" -Role ""$role""" -File $fileName + + $result = Add-ProjectUser -Owner $owner -ProjectNumber $projectNumber -Handle $userName1 -Role $role + + Assert-IsTrue $result +} + +function Test_AddProjectUser_SUCCESS_MultipleUser{ + Reset-InvokeCommandMock + Mock_DatabaseRoot + + $p =Get-Mock_Project_700 ; $owner = $p.Owner ; $projectNumber = $p.Number ; $projectId = $p.id + MockCall_GetProject $p -SkipItems + + $u = Get-Mock_Users + + $userId1 = $u.u1.id ; $userName1 = $u.u1.name + $userId2 = $u.u2.id ; $userName2 = $u.u2.name + $userNames = "$userName1","$userName2" + $usersIds ="$userId1 $userId2" + $role ="WRITER" + $fileName = "invoke-UpdateProjectV2Collaborators-$userId1-$userId2.json" + + MockCallJson -Command "Invoke-GetUser -Handle $userName1" -File $u.u1.file + MockCallJson -Command "Invoke-GetUser -Handle $userName2" -File $u.u2.file + MockCallJson -Command "Invoke-UpdateProjectV2Collaborators -ProjectId $projectId -collaborators ""$usersIds"" -Role ""$role""" -File $fileName + + $result = $userNames | Add-ProjectUser -Owner $owner -ProjectNumber $projectNumber -Role $role + + Assert-IsTrue $result +} \ No newline at end of file diff --git a/Test/traceInvoke.log b/Test/traceInvoke.log index b4da7ba..95513a3 100644 --- a/Test/traceInvoke.log +++ b/Test/traceInvoke.log @@ -74,3 +74,7 @@ Invoke-GitHubOrgProjectWithFields -Owner SomeOrg -ProjectNumber 164 -afterFields Invoke-GitHubOrgProjectWithFields -Owner octodemo -ProjectNumber 625 -afterFields "" -afterItems "" -query "" Invoke-GitHubOrgProjectWithFields -Owner octodemo -ProjectNumber 626 -afterFields "" -afterItems "" -query "" Invoke-GitHubOrgProjectWithFields -Owner octodemo -ProjectNumber 700 -afterFields "" -afterItems "" -query "field-text:text1" +Invoke-GetUser -Handle rulasg +Invoke-UpdateProjectV2Collaborators -ProjectId PVT_kwDOAlIw4c4BCe3V -collaborators "MDQ6VXNlcjY4ODQ0MDg=" -Role "WRITER" +Invoke-GetUser -Handle rauldibildos +Invoke-UpdateProjectV2Collaborators -ProjectId PVT_kwDOAlIw4c4BCe3V -collaborators "MDQ6VXNlcjY4ODQ0MDg= U_kgDOC_E3gw" -Role "WRITER" From 5f2cd0eb1ef5fb601bbae398012ef65cf49d427e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20=28Dibildos=29=20Gonz=C3=A1lez?= Date: Mon, 24 Nov 2025 07:47:00 +0100 Subject: [PATCH 20/24] style(header): update author and date formatting in writeHeader2 --- public/items/project_item_show.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/items/project_item_show.ps1 b/public/items/project_item_show.ps1 index 663ce6a..0e218e2 100644 --- a/public/items/project_item_show.ps1 +++ b/public/items/project_item_show.ps1 @@ -171,11 +171,11 @@ function writeHeader2{ if(-not [string]::IsNullOrWhiteSpace($Author)){ addSpace - $Author | write -Color $subcolor -PreFix "By: " + $Author | write -Color $subcolor -PreFix "By:[" -SuFix "]" } if(-not [string]::IsNullOrWhiteSpace($updatedAt)){ addSpace - $updatedAt | write -Color $subcolor -PreFix "At: " + $updatedAt | write -Color $subcolor -PreFix "At:[" -SuFix "]" } addJumpLine -message "Header 2 End " From f05e59e2c92e64f3d04b08bf140592c699dd82a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20=28Dibildos=29=20Gonz=C3=A1lez?= Date: Mon, 24 Nov 2025 09:33:50 +0100 Subject: [PATCH 21/24] style(use_order): add alias for OpenInBrowser parameter --- public/items/use_order.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/items/use_order.ps1 b/public/items/use_order.ps1 index 70323a2..6cacdb8 100644 --- a/public/items/use_order.ps1 +++ b/public/items/use_order.ps1 @@ -5,7 +5,7 @@ function Use-Order { [Parameter(Position = 0)][int]$Ordinal = -1, [Parameter(ValueFromPipeline)][array]$List, [Parameter()][switch]$OpenInEditor, - [Parameter()][switch]$OpenInBrowser + [Parameter()][Alias("w")][switch]$OpenInBrowser ) begin { From e0424eed9157a710c8e29480257330ece3698702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20=28Dibildos=29=20Gonz=C3=A1lez?= Date: Tue, 25 Nov 2025 08:12:32 +0100 Subject: [PATCH 22/24] fix(test): correct formatting in assertions for author and updatedAt --- Test/public/items/project_item_show.test.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Test/public/items/project_item_show.test.ps1 b/Test/public/items/project_item_show.test.ps1 index 2ee17a7..0074309 100644 --- a/Test/public/items/project_item_show.test.ps1 +++ b/Test/public/items/project_item_show.test.ps1 @@ -82,8 +82,8 @@ function Test_ShowProjectItem_SUCESS{ Assert-Contains -Presented $tt -Expected "$($i.status)" Assert-Contains -Presented $tt -Expected "$($i.Body)" - Assert-Contains -Presented $tt -Expected "By: $($i.comments.last.author.login)" - Assert-Contains -Presented $tt -Expected "At: $($i.comments.last.updatedAt)" + Assert-Contains -Presented $tt -Expected "By:[$($i.comments.last.author.login)]" + Assert-Contains -Presented $tt -Expected "At:[$($i.comments.last.updatedAt)]" Assert-Contains -Presented $tt -Expected $i.comments.last.body Assert-Contains -Presented $tt -Expected $i.id From 18a7e18908029a17bb25f70e347966d237117118 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20=28Dibildos=29=20Gonz=C3=A1lez?= Date: Thu, 11 Dec 2025 08:41:41 +0100 Subject: [PATCH 23/24] refactor(project): enable ValueFromPipelineByPropertyName for parameters in Open-Project --- public/project/getproject.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/project/getproject.ps1 b/public/project/getproject.ps1 index c690524..1aa96fd 100644 --- a/public/project/getproject.ps1 +++ b/public/project/getproject.ps1 @@ -49,8 +49,8 @@ function Get-ProjectId { function Open-Project{ [CmdletBinding()] param( - [Parameter()][string]$Owner, - [Parameter()][int]$ProjectNumber + [Parameter(ValueFromPipelineByPropertyName)][string]$Owner, + [Parameter(ValueFromPipelineByPropertyName)][int]$ProjectNumber ) ($Owner, $ProjectNumber) = Get-OwnerAndProjectNumber -Owner $Owner -ProjectNumber $ProjectNumber From d8f3da0ca5b4b86ebf44e216e0aacd05ecaa2522 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20=28Dibildos=29=20Gonz=C3=A1lez?= Date: Fri, 12 Dec 2025 13:47:31 +0100 Subject: [PATCH 24/24] refactor(issue): add OpenOnCreation parameter to New-ProjectIssueDirect function --- public/issues/New-ProjectIssue.ps1 | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/public/issues/New-ProjectIssue.ps1 b/public/issues/New-ProjectIssue.ps1 index eea982a..263c77b 100644 --- a/public/issues/New-ProjectIssue.ps1 +++ b/public/issues/New-ProjectIssue.ps1 @@ -3,11 +3,13 @@ Set-MyInvokeCommandAlias -Alias CreateIssue -Command 'Invoke-CreateIssue -Reposi function New-ProjectIssueDirect { [CmdletBinding()] + [Alias("New-Issue")] param ( [Parameter(Mandatory, Position = 1)][string]$RepoOwner, [Parameter(Mandatory, Position = 2)][string]$RepoName, [Parameter(Mandatory, Position = 3)][string]$Title, - [Parameter(Position = 4)][string]$Body + [Parameter(Position = 4)][string]$Body, + [Parameter()][switch]$OpenOnCreation ) $repo = Get-Repository -Owner $RepoOwner -Name $RepoName @@ -35,9 +37,13 @@ function New-ProjectIssueDirect { $ret = $issue.url + if( $OpenOnCreation ) { + Open-Url $ret + } + return $ret -} Export-ModuleMember -Function New-ProjectIssueDirect +} Export-ModuleMember -Function New-ProjectIssueDirect -Alias New-Issue function New-ProjectIssue { [CmdletBinding()]