diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..8ace082 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,153 @@ +# ProjectHelper Copilot Instructions + +## Project Overview +**ProjectHelper** is a PowerShell module for GitHub Projects interaction via GraphQL API. It provides CLI-like functions to manage GitHub projects, items, users, and collaborators from PowerShell. + +## Architecture & Core Patterns + +### Module Structure +- **Loading Order** (`ProjectHelper.psm1`): `config` → `helper` → `include` → `private` → `public` +- **Public Functions**: Exported via `Export-ModuleMember -Function ` in each file +- **Private Functions**: In `/private` folder, not exported; used internally by public functions +- **Include Files** (`/include`): Shared utilities loaded early; example: `callAPI.ps1` (GraphQL/REST), `MyWrite.ps1` (logging) +- **Driver Functions** (`/public/driver`): Low-level integration with GitHub APIs; marked with comment "integration function not intended for direct user use" + +### Key Files & Responsibilities +- `include/callAPI.ps1`: Handles `Invoke-GraphQL` and `Invoke-RestAPI` calls to GitHub +- `helper/invokeCommand.helper.ps1`: Provides `Invoke-MyCommand` for alias-based command dispatch (enables test mocking) +- `include/config.ps1`: Configuration storage in `~/.helpers/ProjectHelper/config/` +- `public/graphql/*.{query,mutant,tag}`: GraphQL template files (fragments and queries) +- `Test/Test.psd1`: Parallel test module with identical structure to main module + +### Invocation Pattern (Critical for Testing) +```powershell +# Production: Direct API calls +Invoke-GraphQL -Query $query -Variables $variables + +# High-level: Uses Invoke-MyCommand alias dispatch (mockable) +Invoke-MyCommand -Command "findProject" -Parameters @{owner=$Owner; pattern=$Pattern} + +# Set custom implementation for testing/mocking: +Set-MyInvokeCommandAlias -Alias $alias -Command $Command +``` + +## Development Workflows + +### Running Tests +```powershell +./test.ps1 # Run all tests +./test.ps1 -ShowTestErrors # Show error details +./test.ps1 -TestName "Test_*" # Run specific test +``` + +Uses **TestingHelper** module from PSGallery (installed automatically). + +### Building & Deploying +```powershell +./build.ps1 # Build module +./deploy.ps1 -VersionTag "v1.0.0" # Deploy to PSGallery +./sync.ps1 # Sync with TestingHelper templates +``` + +### Debugging +Enable module debug output: +```powershell +Enable-ProjectHelperDebug +Disable-ProjectHelperDebug +``` + +## Code Patterns & Conventions + +### GraphQL Integration +1. Store GraphQL in template files: `/public/graphql/queryName.query` or `.mutant` +2. Retrieve via: `Get-GraphQLString "queryName.query"` +3. Execute: `Invoke-GraphQL -Query $query -Variables $variables` + +Example: +```powershell +$query = Get-GraphQLString "findProject.query" +$variables = @{ login = $Owner; pattern = $Pattern } +$response = Invoke-GraphQL -Query $query -Variables $variables +``` + +### Public vs. Private Functions +- **Public** (`/public`): User-facing, high-level; transform data, handle caching +- **Private** (`/private`): Lower-level helpers; return raw GitHub data +- **Driver** (`/public/driver`): Thin wrappers around API calls; minimal logic + +### Command Aliases with Parameter Templates +Use `Set-MyInvokeCommandAlias` for dynamic command dispatch: +```powershell +Set-MyInvokeCommandAlias -Alias "findProject" -Command "Invoke-FindProject -Owner {owner} -Pattern {pattern}" +Invoke-MyCommand -Command "findProject" -Parameters @{owner="foo"; pattern="bar"} +``` +This enables mocking in tests without changing implementation. + +### Pipeline & Object Transformation +Functions support pipeline input for bulk operations: +```powershell +"user1", "user2" | Add-ProjectUser -Owner $owner -ProjectNumber 123 -Role "WRITER" +``` + +### Error Handling +- GraphQL errors: Check `$response.errors` before processing +- Include meaningful context in error messages +- Use `Write-MyError`, `Write-MyVerbose`, `Write-MyDebug` for consistent logging + +## Testing Patterns + +### Test File Location +Test files must mirror the module structure: +- **Source**: `public/code.ps1` → **Test**: `Test/public/code.test.ps1` +- **Source**: `public/driver/invoke-getnode.ps1` → **Test**: `Test/public/driver/invoke-getnode.test.ps1` +- **Source**: `private/dates.ps1` → **Test**: `Test/private/dates.test.ps1` + +The folder structure in `Test/` must exactly match the structure in the main module. + +### Test Function Naming +- **Format**: `Test__` +- **Examples**: + - `Test_FindProject_SUCCESS` (success case) + - `Test_AddProjectUser_SUCCESS_SingleUser` (specific variant) + - `Test_GetProjectIssue_NotFound` (error case) +- **Conventions**: + - Use PascalCase matching the actual function name (e.g., `Get-SomeInfo` → `Test_GetSomeInfo_`) + - `` should be a descriptive word indicating the test goal (SUCCESS, NotFound, InvalidInput, etc.) + - Use assertions: `Assert-IsTrue`, `Assert-Contains`, `Assert-AreEqual`, `Assert-Count`, `Assert-IsNull` + +### Mock System +Located in `Test/include/`: +- `invokeCommand.mock.ps1`: Mocks `Invoke-MyCommand` calls via JSON files in `Test/private/mocks/` +- `callPrivateContext.ps1`: Execute private functions in module context + +### Mock Data Structure +```powershell +# Test/private/mocks/mockCommands.json defines: +{ + "Command": "Invoke-GetUser -Handle rulasg", + "FileName": "invoke-GetUser-rulasg.json" +} +``` + +### Common Test Setup +```powershell +Reset-InvokeCommandMock +Mock_DatabaseRoot +MockCall_GetProject $project -SkipItems +MockCallJson -Command "command" -File "response.json" +``` + +## Key Dependencies +- **GitHub API**: GraphQL (primary), REST (legacy) +- **TestingHelper**: Test framework from PSGallery +- **InvokeHelper**: Command dispatch/mocking library (external) + +## Important Gotchas +1. **Module Loading**: Functions depend on proper load order; new files in `/private` or `/public` auto-loaded +2. **Aliases**: Use `Set-MyInvokeCommandAlias` before calling `Invoke-MyCommand` for consistency +3. **GraphQL Templates**: Fragment files (`.tag`) must match schema; test with actual GitHub API responses +4. **Configuration**: Stored per-module; reset with `Reset-ProjectHelperEnvironment` +5. **PSScriptAnalyzer**: PR checks fail on warnings; review `.github/workflows/powershell.yml` rules + +## PR Branch & Active Work +Currently on `projectv2ContributionUpdate` - Implementing project access management with new user collaboration features. Recent changes focus on `Invoke-UpdateProjectV2Collaborators` and `Add-ProjectUser` functions with proper string splitting via `-split` with `[System.StringSplitOptions]::RemoveEmptyEntries`. diff --git a/.github/instructions/test.instructions.md b/.github/instructions/test.instructions.md new file mode 100644 index 0000000..d3ca8aa --- /dev/null +++ b/.github/instructions/test.instructions.md @@ -0,0 +1,222 @@ +--- +applyTo: '**/*.test.ps1' +--- + +# Test File Creation Guidelines + +## Test File Location and Naming + +### File Placement +Test files must mirror the exact folder structure of the source code: + +- **Source**: `public/code.ps1` → **Test**: `Test/public/code.test.ps1` +- **Source**: `public/driver/invoke-getnode.ps1` → **Test**: `Test/public/driver/invoke-getnode.test.ps1` +- **Source**: `public/issues/Get-ProjectIssue.ps1` → **Test**: `Test/public/issues/Get-ProjectIssue.test.ps1` +- **Source**: `private/dates.ps1` → **Test**: `Test/private/dates.test.ps1` + +The folder structure in `Test/` must exactly match the structure in the main module. + +### Test Function Naming +- **Format**: `Test__` +- **PascalCase**: Match the actual function name (e.g., `Add-ProjectUser` → `Test_AddProjectUser_*`) +- **Scenario Suffix**: Use descriptive words indicating the test goal: + - `SUCCESS` - Happy path / successful execution + - `SUCCESS_SingleUser` - Variant of success with specific condition + - `SUCCESS_MultipleUser` - Another variant with different parameters + - `NotFound` - Resource not found error case + - `InvalidInput` - Invalid parameter error case + - `ERROR` - General error condition + +**Examples**: +```powershell +function Test_FindProject_SUCCESS { } +function Test_AddProjectUser_SUCCESS_SingleUser { } +function Test_AddProjectUser_SUCCESS_MultipleUser { } +function Test_GetProjectIssue_NotFound { } +``` + +## Test Structure - AAA Pattern + +All test functions must follow the **Arrange-Act-Assert** pattern with explicit comments: + +### 1. ARRANGE Phase +```powershell +# Arrange +Reset-InvokeCommandMock # CRITICAL: Always reset mocks at the start +Mock_DatabaseRoot # Setup test database if needed + +$owner = "github" +$projectNumber = 123 +``` + +**Critical Setup Steps**: +- **ALWAYS** call `Reset-InvokeCommandMock` at the very beginning of the Arrange phase +- Setup any mock data using `Mock_*` helpers (e.g., `Mock_DatabaseRoot`, `Get-Mock_Project_700`) +- Define test input parameters and expected values +- Setup mock calls using `MockCallJson` or `MockCall_*` helpers + +### 2. ACT Phase +```powershell +# Act +$result = Add-ProjectUser -Owner $owner -ProjectNumber $projectNumber -Handle "testuser" -Role "WRITER" +``` + +**Important**: +- Execute the function being tested +- Use the parameters and mocks prepared in Arrange phase +- Capture the result for assertions + +### 3. ASSERT Phase +```powershell +# Assert +Assert-IsTrue $result +Assert-AreEqual -Expected "expected-value" -Presented $result.id +Assert-Count -Expected 3 -Presented $result +``` + +**Available Assertions**: +- `Assert-IsTrue` - Verify condition is true +- `Assert-IsNull` - Verify value is null +- `Assert-AreEqual` - Compare expected vs actual value +- `Assert-Contains` - Check if value is in collection +- `Assert-Count` - Verify collection count +- `Assert-NotImplemented` - For stub/empty test implementations + +## Mocking Invoke-MyCommand Dependencies + +When your test function calls a function that uses `Invoke-MyCommand`, you must mock those dependencies. + +### Why Mocking is Critical +The module uses `Invoke-MyCommand` for dynamic command dispatch to enable testing without actual API calls. Any function calling `Invoke-MyCommand` indirectly must have those calls mocked. + +### Mock Setup Pattern +```powershell +# Arrange +Reset-InvokeCommandMock + +# Mock the Invoke-* driver function calls +MockCallJson -Command "Invoke-GetUser -Handle testuser" -File "invoke-GetUser-testuser.json" +MockCallJson -Command "Invoke-UpdateProjectV2Collaborators -ProjectId PVT_123 -collaborators ""ID123"" -Role ""WRITER""" -File "invoke-UpdateProjectV2Collaborators-ID123.json" + +# Act +$result = Add-ProjectUser -Owner "github" -ProjectNumber 123 -Handle "testuser" -Role "WRITER" +``` + +### Mock Files +Mock response files should be stored in `Test/private/mocks/` and contain realistic GitHub API responses as JSON. + +### Common Mock Helpers +- `Reset-InvokeCommandMock` - Clear all mocks (MUST be called first) +- `MockCallJson -Command "..." -File "..."` - Mock a driver function call with JSON response +- `MockCall_GetProject $project -SkipItems` - Pre-configured mock for Get-Project +- `Mock_DatabaseRoot` - Setup database root for project operations +- `Get-Mock_Project_700` - Get pre-configured test project +- `Get-Mock_Users` - Get pre-configured test users + +## Empty/Stub Test Implementation + +When creating a test function as a placeholder to be implemented later: + +```powershell +function Test_SomeFunction_NotYetImplemented { + Assert-NotImplemented +} +``` + +This allows the test infrastructure to recognize the test while explicitly marking it as not ready. + +## Real Test Examples + +### Example 1: Simple Success Case +```powershell +function Test_FindProject_SUCCESS { + # Arrange + Reset-InvokeCommandMock + Enable-InvokeCommandAliasModule + + $owner = "github" + $pattern = "kk" + $command = 'Invoke-FindProject -Owner {owner} -Pattern "{pattern}" -firstProject 100 -afterProject ""' + $command = $command -replace "{owner}", $owner + $command = $command -replace "{pattern}", $pattern + MockCallJson -Command $command -filename "findprojectwithlist.json" + + # Act + $result = Find-Project -Owner $owner -Pattern $pattern + + # Assert + Assert-Count -Expected 3 -Presented $result + Assert-AreEqual -Expected "PVT_kwDNJr_OANANzQ" -Presented $result[1].id +} +``` + +### Example 2: Single vs Multiple User Variants +```powershell +function Test_AddProjectUser_SUCCESS_SingleUser { + # Arrange + 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 + $userId = $u.u1.id + $userName = $u.u1.name + $role = "WRITER" + + MockCallJson -Command "Invoke-GetUser -Handle $userName" -File $u.u1.file + MockCallJson -Command "Invoke-UpdateProjectV2Collaborators -ProjectId $projectId -collaborators ""$userId"" -Role ""$role""" -File "invoke-UpdateProjectV2Collaborators-$userId.json" + + # Act + $result = Add-ProjectUser -Owner $owner -ProjectNumber $projectNumber -Handle $userName -Role $role + + # Assert + Assert-IsTrue $result +} + +function Test_AddProjectUser_SUCCESS_MultipleUser { + # Arrange + 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" + + 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 "invoke-UpdateProjectV2Collaborators-$userId1-$userId2.json" + + # Act + $result = $userNames | Add-ProjectUser -Owner $owner -ProjectNumber $projectNumber -Role $role + + # Assert + Assert-IsTrue $result +} +``` + +## Checklist Before Submitting Test + +- [ ] Test file is in correct location: `Test//.test.ps1` +- [ ] Test function named: `Test__` +- [ ] Three phases clearly marked with comments: `# Arrange`, `# Act`, `# Assert` +- [ ] `Reset-InvokeCommandMock` called at start of Arrange phase +- [ ] All `Invoke-MyCommand` calls mocked with `MockCallJson` +- [ ] Assertions verify expected behavior +- [ ] Empty test uses `Assert-NotImplemented` +- [ ] No actual API calls made (all mocked) 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/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/driver/user/user.test.ps1 b/Test/public/driver/user/user.test.ps1 new file mode 100644 index 0000000..6587f56 --- /dev/null +++ b/Test/public/driver/user/user.test.ps1 @@ -0,0 +1,6 @@ +function Test_GetUser_SUCCESS{ + Reset-InvokeCommandMock + Mock_DatabaseRoot + + Assert-NotImplemented +} \ No newline at end of file diff --git a/Test/public/issues/Get-ProjectIssue.test.ps1 b/Test/public/issues/Get-ProjectIssue.test.ps1 index 7a6150c..1eba548 100644 --- a/Test/public/issues/Get-ProjectIssue.test.ps1 +++ b/Test/public/issues/Get-ProjectIssue.test.ps1 @@ -9,7 +9,7 @@ function Test_GetProjectIssue{ MockCallJson -Command "Invoke-GetIssueOrPullRequest -Url $($i.url)" -FileName "invoke-GetIssueOrPullRequest-26.json" # Act - $result = Get-ProjectIssue -Url $i.url + $result = Get-ProjectIssue -Owner $Owner -ProjectNumber $projectNumber -Url $i.url Assert-AreEqual -Expected $i.contentId -Presented $result.id Assert-AreEqual -Expected $i.title -Presented $result.title 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 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" diff --git a/include/callAPI.ps1 b/include/callAPI.ps1 index 3f910b7..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,10 +60,12 @@ function Invoke-GraphQL { return $response } catch { + "[[THROW]]" | writedebug + $_.Exception.Message | ConvertTo-Json -Depth 100 | writedebug throw New-Object system.Exception("Error calling GraphQL",$_.Exception) } -} Export-ModuleMember -Function Invoke-GraphQL +} function Invoke-RestAPI { param( @@ -132,7 +136,7 @@ function Invoke-RestAPI { catch { throw } -} Export-ModuleMember -Function Invoke-RestAPI +} #################################################################################################### @@ -158,7 +162,7 @@ function Get-ApiHost { "Default host $DEFAULT_GH_HOST" | writedebug return $DEFAULT_GH_HOST -} Export-ModuleMember -Function Get-ApiHost +} #################################################################################################### @@ -198,7 +202,7 @@ function Get-ApiToken { } return $result -} Export-ModuleMember -Function Get-ApiToken +} #################################################################################################### 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/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 diff --git a/public/driver/projectv2/updateProjectV2Collaborators.ps1 b/public/driver/projectv2/updateProjectV2Collaborators.ps1 new file mode 100644 index 0000000..d03433e --- /dev/null +++ b/public/driver/projectv2/updateProjectV2Collaborators.ps1 @@ -0,0 +1,32 @@ +function Invoke-UpdateProjectV2Collaborators{ + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)][string]$ProjectId, + [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 = $array + } + } + + $response = Invoke-GraphQL -Query $query -Variables $variables + + return $response + +} Export-ModuleMember -Function Invoke-UpdateProjectV2Collaborators \ No newline at end of file diff --git a/public/driver/user/user.ps1 b/public/driver/user/user.ps1 new file mode 100644 index 0000000..755c291 --- /dev/null +++ b/public/driver/user/user.ps1 @@ -0,0 +1,45 @@ + +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(-Not $Force -And ($null -ne $cache)){ + Write-MyDebug "Get-User: User found in cache" -Section "Get-User" + $result = $cache + + } else { + Write-MyDebug "Get-User: User retreived" -Section "Get-User" + $result = Invoke-MyCommand -Command "getUser" -Parameters @{handle=$Handle} + + # Cache + Save-Database -Key "user-$Handle" -Database $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 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..c26b82b 100644 --- a/public/graphql/_user.tag +++ b/public/graphql/_user.tag @@ -1 +1 @@ -{login} \ No newline at end of file +{id,name,login,email} 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 diff --git a/public/issues/New-ProjectIssue.ps1 b/public/issues/New-ProjectIssue.ps1 index eea982a..a9719af 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 @@ -28,16 +30,18 @@ function New-ProjectIssueDirect { $issue = $response.data.createIssue.issue if ( ! $issue ) { - throw "Issue not created properlly" + throw "Issue not created properly" } - # TODO: Consider adding the issue to the project - $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()] diff --git a/public/items/project_item.ps1 b/public/items/project_item.ps1 index 442920c..a089f0a 100644 --- a/public/items/project_item.ps1 +++ b/public/items/project_item.ps1 @@ -63,7 +63,7 @@ function Get-ProjectItemByUrl{ begin { ($Owner, $ProjectNumber) = Get-OwnerAndProjectNumber -Owner $Owner -ProjectNumber $ProjectNumber - if ([string]::IsNullOrWhiteSpace($owner) -or [string]::IsNullOrWhiteSpace($ProjectNumber)) { "Owner and ProjectNumber are required" | Write-MyError; return $null } + if ([string]::IsNullOrWhiteSpace($owner) -or [string]::IsNullOrWhiteSpace($ProjectNumber)) { return $null } $db = Get-Project -Owner $Owner -ProjectNumber $ProjectNumber -SkipItems diff --git a/public/items/project_item_draftissue.ps1 b/public/items/project_item_draftissue.ps1 index bc59167..95ca606 100644 --- a/public/items/project_item_draftissue.ps1 +++ b/public/items/project_item_draftissue.ps1 @@ -43,6 +43,10 @@ function New-ProjectDraftIssueDirect { } + if( $OpenOnCreation ) { + Open-Url $item.url + } + return $ret } 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 " 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 { 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 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