├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── build.yml ├── .gitignore ├── .prettierrc.js ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── action.yml ├── dist ├── README.md ├── index.js └── licenses.txt ├── jest.config.js ├── media ├── Logo-600.png ├── Logo.png ├── github_readme_learn_section.png ├── notion_full_page_db.png ├── notion_full_page_db_id.png └── notion_table_schema_options.png ├── package-lock.json ├── package.json ├── scripts └── codecov.sh ├── src ├── action.ts ├── index.ts ├── types.ts └── utils │ ├── checkForSections.ts │ ├── commitFile.ts │ ├── constructCategoriesMap.ts │ ├── constructNewContents.ts │ ├── fetchData.ts │ ├── getSchemaEntries.ts │ ├── index.ts │ ├── modifyRows.ts │ └── populateCategoriesMapItems.ts ├── tests ├── action.test.ts └── utils │ ├── checkForSections.test.ts │ ├── constructCategoriesMap.test.ts │ ├── constructNewContents.test.ts │ ├── fetchData.test.ts │ ├── getSchemaEntries.test.ts │ ├── modifyRows.test.ts │ └── populateCategoriesMapItems.test.ts └── tsconfig.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '🐛 Bug report' 3 | about: Report a reproducible bug or regression. 4 | --- 5 | 6 | **Describe the bug** 7 | A clear and concise description of what the bug is. 8 | **To Reproduce** 9 | Steps to reproduce the behavior: 10 | 11 | 1. Go to '...' 12 | 2. Click on '....' 13 | 3. Scroll down to '....' 14 | 4. See error 15 | **Expected behavior** 16 | A clear and concise description of what you expected to happen. 17 | **Screenshots** 18 | If applicable, add screenshots to help explain your problem. 19 | **Desktop (please complete the following information):** 20 | 21 | - OS: [e.g. iOS] 22 | - Browser [e.g. chrome, safari] 23 | - Version [e.g. 22] 24 | **Smartphone (please complete the following information):** 25 | - Device: [e.g. iPhone6] 26 | - OS: [e.g. iOS8.1] 27 | - Browser [e.g. stock browser, safari] 28 | - Version [e.g. 22] 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | **Describe the solution you'd like** 12 | A clear and concise description of what you want to happen. 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | **Additional context** 16 | Add any other context or screenshots about the feature request here. 17 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: Generate build and check code formatting 8 | 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Use Node.js 12.x 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: 12 17 | - run: npm ci 18 | - run: npm run test 19 | - run: npm run build 20 | - name: Uploading test coverage reports 21 | run: | 22 | chmod +x "${GITHUB_WORKSPACE}/scripts/codecov.sh" 23 | "${GITHUB_WORKSPACE}/scripts/codecov.sh" 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test.js 3 | test.md 4 | build 5 | coverage -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: "none", 3 | tabWidth: 2, 4 | semi: true, 5 | singleQuote: true, 6 | }; 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing and Maintaining 2 | 3 | First, thank you for taking the time to contribute! 4 | The following is a set of guidelines for contributors as well as information and instructions around our maintenance process. The two are closely tied together in terms of how we all work together and set expectations, so while you may not need to know everything in here to submit an issue or pull request, it's best to keep them in the same document. 5 | 6 | ## Ways to contribute 7 | 8 | Contributing isn't just writing code - it's anything that improves the project. All contributions are managed right here on GitHub. Here are some ways you can help: 9 | 10 | ### Reporting bugs 11 | 12 | If you're running into an issue, please take a look through [existing issues](/issues) and [open a new one](/issues/new) if needed. If you're able, include steps to reproduce, environment information, and screenshots/screencasts as relevant. 13 | 14 | ### Suggesting enhancements 15 | 16 | New features and enhancements are also managed via [issues](/issues). 17 | 18 | ### Pull requests 19 | 20 | Pull requests represent a proposed solution to a specified problem. They should always reference an issue that describes the problem and contains discussion about the problem itself. Discussion on pull requests should be limited to the pull request itself, i.e. code review. 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Varun Sridharan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Logo

2 | 3 |

Github Readme Learn Section - Github Action

4 |
Automatically update your github README with data fetched from a notion database
5 |
6 |

7 | 8 | 9 | 10 | 11 |

12 | 13 | ## Configuration 14 | 15 | | Option | Description | Required | Default | 16 | | :-----------: | :----------------------------------------------------------------------: | :------: | :-----: | 17 | | `database_id` | Set this to the id of your remote notion database | true | - | 18 | | `token_v2` | Set this to your notion `token_v2` (Required only for private databases) | false | - | 19 | 20 | ## Usage 21 | 22 | ### In Repository File 23 | 24 | #### 1. Add the following content to your `README.md` 25 | 26 | ```markdown 27 | ## What I know so far 28 | 29 | 30 | 31 | ``` 32 | 33 | #### 2. Configure the workflow 34 | 35 | ```yaml 36 | name: 'Github Readme Updater' 37 | on: 38 | workflow_dispatch: 39 | schedule: 40 | - cron: '0 0 * * *' # Runs Every Day 41 | jobs: 42 | update_learn: 43 | name: 'Update learn section' 44 | runs-on: ubuntu-latest 45 | steps: 46 | - name: 'Fetching Repository Contents' 47 | uses: actions/checkout@main 48 | - name: 'Learn Section Updater' 49 | uses: 'devorein/github-readme-learn-section-notion@master' 50 | with: 51 | database_id: '6626c1ebc5a44db78e3f2fe285171ab7' 52 | token_v2: ${{ secrets.NOTION_TOKEN_V2 }} # Required only if your database is private 53 | ``` 54 | 55 | **TIP**: You can test out using [this template](https://www.notion.so/devorein/6c46c1ebc5a44db78e3f5fe285071ab6?v=0bc36e7c59e54f34b0838956e35b4490) that I've created, or [this repo](https://github.com/Devorein/test-github-action). 56 | 57 | ### In your notion account 58 | 59 | #### 1. Create a full page database 60 | 61 | ![Notion Full Page Database](./media/notion_full_page_db.png) 62 | 63 | **NOTE**: Your database must maintain the following structure/schema 64 | 65 | | Name | Type | Required | Default | Description | Value | Example | 66 | | :------: | :----: | :------: | :-----: | :-------------------------------------: | :-----------------------------------------------------------------------------: | :--------------------------------------: | 67 | | Name | title | true | - | The name of the item you've learnt | Must be a valid icon from `https://simple-icons.github.io/simple-icons-website` | React, Typescript | 68 | | Category | select | true | - | The category under which the item falls | Any string | Language, Library | 69 | | Color | text | false | black | Background Color of the badge | Any keyword color or hex value without alpha and # | red,00ff00 | 70 | | Base64 | text | false | "" | Custom base64 of the svg logo | Any base64 encoded svg | data:image/svg%2bxml;base64,PHN2ZyB4b... | 71 | 72 | #### 2. Get the id of the database 73 | 74 | ![Notion Full Page Database Id](./media/notion_full_page_db_id.png) 75 | 76 | #### 3. Add it in workflow file 77 | 78 | ```yaml 79 | with: 80 | database_id: '6626c1ebc5a44db78e3f2fe285171ab7' 81 | ``` 82 | 83 | Follow the rest of the steps only if your database is not public, if its public you don't need to set the token_v2 84 | 85 | #### To make your database public 86 | 87 | 1. Navigate to the database in your notion account 88 | 2. Click on Share at the top right corner 89 | 3. Click on Share to Web button. 90 | 91 | #### 1. Get your notion `token_v2` 92 | 93 | **NOTE**: By no means should you share or expose your notion `token_v2`. If you feel like you've done so accidentally, immediately log out from that account in all of your devices. 94 | 95 | Follow the steps below to obtain your `token_v2`: 96 | 97 | 1. Open up the devtools of your preferred browser. 98 | 2. Go to the Application > Cookies section. 99 | 3. There you'll find a `token_v2` cookie. 100 | 101 | **NOTE**: Its highly recommended to store your `token_v2` as a github secret rather than pasting it in your workflow file. And if you want to embed it in your workflow file make sure unauthorized sources can't access/view it. 102 | 103 | #### 2. Create a github secret to store `token_v2` 104 | 105 | 1. navigate to the url `https://github.com///settings/secrets/actions` 106 | 2. Click on `New repository secret` 107 | 3. You can name your secret as anything you want 108 | 4. Paste the `token_v2` value in the `Value` textarea 109 | 5. Use the secret in your workflow file 110 | 111 | ```yaml 112 | with: 113 | token_v2: ${{ secrets.NOTION_TOKEN_V2 }} # The secret was named NOTION_TOKEN_V2 114 | ``` 115 | 116 | ### Outcome 117 | 118 | If you follow all the steps properly your readme should look something like this. 119 | 120 | ![Github Readme Learn Section](./media/github_readme_learn_section.png) 121 | 122 | Feel free to submit a pull request or open a new issue, contributions are more than welcome !!! 123 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: "Github Readme Learn Section Notion" 2 | description: "Populates the learning section in your github readme with data from notion" 3 | author: devorein 4 | 5 | inputs: 6 | database_id: 7 | description: "The id of the database" 8 | required: true 9 | token_v2: 10 | description: "Your notion token_v2 string, required only for private databases" 11 | required: false 12 | 13 | branding: 14 | icon: "box" 15 | color: "gray-dark" 16 | 17 | runs: 18 | using: "node12" 19 | main: "dist/index.js" 20 | -------------------------------------------------------------------------------- /dist/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devorein/github-readme-learn-section-notion/f523364bf0b18514fedc2d19894d731ac131554a/dist/README.md -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | /******/ (() => { // webpackBootstrap 2 | /******/ var __webpack_modules__ = ({ 3 | 4 | /***/ 351: 5 | /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { 6 | 7 | "use strict"; 8 | 9 | var __importStar = (this && this.__importStar) || function (mod) { 10 | if (mod && mod.__esModule) return mod; 11 | var result = {}; 12 | if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; 13 | result["default"] = mod; 14 | return result; 15 | }; 16 | Object.defineProperty(exports, "__esModule", ({ value: true })); 17 | const os = __importStar(__nccwpck_require__(87)); 18 | const utils_1 = __nccwpck_require__(278); 19 | /** 20 | * Commands 21 | * 22 | * Command Format: 23 | * ::name key=value,key=value::message 24 | * 25 | * Examples: 26 | * ::warning::This is the message 27 | * ::set-env name=MY_VAR::some value 28 | */ 29 | function issueCommand(command, properties, message) { 30 | const cmd = new Command(command, properties, message); 31 | process.stdout.write(cmd.toString() + os.EOL); 32 | } 33 | exports.issueCommand = issueCommand; 34 | function issue(name, message = '') { 35 | issueCommand(name, {}, message); 36 | } 37 | exports.issue = issue; 38 | const CMD_STRING = '::'; 39 | class Command { 40 | constructor(command, properties, message) { 41 | if (!command) { 42 | command = 'missing.command'; 43 | } 44 | this.command = command; 45 | this.properties = properties; 46 | this.message = message; 47 | } 48 | toString() { 49 | let cmdStr = CMD_STRING + this.command; 50 | if (this.properties && Object.keys(this.properties).length > 0) { 51 | cmdStr += ' '; 52 | let first = true; 53 | for (const key in this.properties) { 54 | if (this.properties.hasOwnProperty(key)) { 55 | const val = this.properties[key]; 56 | if (val) { 57 | if (first) { 58 | first = false; 59 | } 60 | else { 61 | cmdStr += ','; 62 | } 63 | cmdStr += `${key}=${escapeProperty(val)}`; 64 | } 65 | } 66 | } 67 | } 68 | cmdStr += `${CMD_STRING}${escapeData(this.message)}`; 69 | return cmdStr; 70 | } 71 | } 72 | function escapeData(s) { 73 | return utils_1.toCommandValue(s) 74 | .replace(/%/g, '%25') 75 | .replace(/\r/g, '%0D') 76 | .replace(/\n/g, '%0A'); 77 | } 78 | function escapeProperty(s) { 79 | return utils_1.toCommandValue(s) 80 | .replace(/%/g, '%25') 81 | .replace(/\r/g, '%0D') 82 | .replace(/\n/g, '%0A') 83 | .replace(/:/g, '%3A') 84 | .replace(/,/g, '%2C'); 85 | } 86 | //# sourceMappingURL=command.js.map 87 | 88 | /***/ }), 89 | 90 | /***/ 186: 91 | /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { 92 | 93 | "use strict"; 94 | 95 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 96 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 97 | return new (P || (P = Promise))(function (resolve, reject) { 98 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 99 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 100 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 101 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 102 | }); 103 | }; 104 | var __importStar = (this && this.__importStar) || function (mod) { 105 | if (mod && mod.__esModule) return mod; 106 | var result = {}; 107 | if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; 108 | result["default"] = mod; 109 | return result; 110 | }; 111 | Object.defineProperty(exports, "__esModule", ({ value: true })); 112 | const command_1 = __nccwpck_require__(351); 113 | const file_command_1 = __nccwpck_require__(717); 114 | const utils_1 = __nccwpck_require__(278); 115 | const os = __importStar(__nccwpck_require__(87)); 116 | const path = __importStar(__nccwpck_require__(622)); 117 | /** 118 | * The code to exit an action 119 | */ 120 | var ExitCode; 121 | (function (ExitCode) { 122 | /** 123 | * A code indicating that the action was successful 124 | */ 125 | ExitCode[ExitCode["Success"] = 0] = "Success"; 126 | /** 127 | * A code indicating that the action was a failure 128 | */ 129 | ExitCode[ExitCode["Failure"] = 1] = "Failure"; 130 | })(ExitCode = exports.ExitCode || (exports.ExitCode = {})); 131 | //----------------------------------------------------------------------- 132 | // Variables 133 | //----------------------------------------------------------------------- 134 | /** 135 | * Sets env variable for this action and future actions in the job 136 | * @param name the name of the variable to set 137 | * @param val the value of the variable. Non-string values will be converted to a string via JSON.stringify 138 | */ 139 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 140 | function exportVariable(name, val) { 141 | const convertedVal = utils_1.toCommandValue(val); 142 | process.env[name] = convertedVal; 143 | const filePath = process.env['GITHUB_ENV'] || ''; 144 | if (filePath) { 145 | const delimiter = '_GitHubActionsFileCommandDelimeter_'; 146 | const commandValue = `${name}<<${delimiter}${os.EOL}${convertedVal}${os.EOL}${delimiter}`; 147 | file_command_1.issueCommand('ENV', commandValue); 148 | } 149 | else { 150 | command_1.issueCommand('set-env', { name }, convertedVal); 151 | } 152 | } 153 | exports.exportVariable = exportVariable; 154 | /** 155 | * Registers a secret which will get masked from logs 156 | * @param secret value of the secret 157 | */ 158 | function setSecret(secret) { 159 | command_1.issueCommand('add-mask', {}, secret); 160 | } 161 | exports.setSecret = setSecret; 162 | /** 163 | * Prepends inputPath to the PATH (for this action and future actions) 164 | * @param inputPath 165 | */ 166 | function addPath(inputPath) { 167 | const filePath = process.env['GITHUB_PATH'] || ''; 168 | if (filePath) { 169 | file_command_1.issueCommand('PATH', inputPath); 170 | } 171 | else { 172 | command_1.issueCommand('add-path', {}, inputPath); 173 | } 174 | process.env['PATH'] = `${inputPath}${path.delimiter}${process.env['PATH']}`; 175 | } 176 | exports.addPath = addPath; 177 | /** 178 | * Gets the value of an input. The value is also trimmed. 179 | * 180 | * @param name name of the input to get 181 | * @param options optional. See InputOptions. 182 | * @returns string 183 | */ 184 | function getInput(name, options) { 185 | const val = process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] || ''; 186 | if (options && options.required && !val) { 187 | throw new Error(`Input required and not supplied: ${name}`); 188 | } 189 | return val.trim(); 190 | } 191 | exports.getInput = getInput; 192 | /** 193 | * Sets the value of an output. 194 | * 195 | * @param name name of the output to set 196 | * @param value value to store. Non-string values will be converted to a string via JSON.stringify 197 | */ 198 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 199 | function setOutput(name, value) { 200 | process.stdout.write(os.EOL); 201 | command_1.issueCommand('set-output', { name }, value); 202 | } 203 | exports.setOutput = setOutput; 204 | /** 205 | * Enables or disables the echoing of commands into stdout for the rest of the step. 206 | * Echoing is disabled by default if ACTIONS_STEP_DEBUG is not set. 207 | * 208 | */ 209 | function setCommandEcho(enabled) { 210 | command_1.issue('echo', enabled ? 'on' : 'off'); 211 | } 212 | exports.setCommandEcho = setCommandEcho; 213 | //----------------------------------------------------------------------- 214 | // Results 215 | //----------------------------------------------------------------------- 216 | /** 217 | * Sets the action status to failed. 218 | * When the action exits it will be with an exit code of 1 219 | * @param message add error issue message 220 | */ 221 | function setFailed(message) { 222 | process.exitCode = ExitCode.Failure; 223 | error(message); 224 | } 225 | exports.setFailed = setFailed; 226 | //----------------------------------------------------------------------- 227 | // Logging Commands 228 | //----------------------------------------------------------------------- 229 | /** 230 | * Gets whether Actions Step Debug is on or not 231 | */ 232 | function isDebug() { 233 | return process.env['RUNNER_DEBUG'] === '1'; 234 | } 235 | exports.isDebug = isDebug; 236 | /** 237 | * Writes debug message to user log 238 | * @param message debug message 239 | */ 240 | function debug(message) { 241 | command_1.issueCommand('debug', {}, message); 242 | } 243 | exports.debug = debug; 244 | /** 245 | * Adds an error issue 246 | * @param message error issue message. Errors will be converted to string via toString() 247 | */ 248 | function error(message) { 249 | command_1.issue('error', message instanceof Error ? message.toString() : message); 250 | } 251 | exports.error = error; 252 | /** 253 | * Adds an warning issue 254 | * @param message warning issue message. Errors will be converted to string via toString() 255 | */ 256 | function warning(message) { 257 | command_1.issue('warning', message instanceof Error ? message.toString() : message); 258 | } 259 | exports.warning = warning; 260 | /** 261 | * Writes info to log with console.log. 262 | * @param message info message 263 | */ 264 | function info(message) { 265 | process.stdout.write(message + os.EOL); 266 | } 267 | exports.info = info; 268 | /** 269 | * Begin an output group. 270 | * 271 | * Output until the next `groupEnd` will be foldable in this group 272 | * 273 | * @param name The name of the output group 274 | */ 275 | function startGroup(name) { 276 | command_1.issue('group', name); 277 | } 278 | exports.startGroup = startGroup; 279 | /** 280 | * End an output group. 281 | */ 282 | function endGroup() { 283 | command_1.issue('endgroup'); 284 | } 285 | exports.endGroup = endGroup; 286 | /** 287 | * Wrap an asynchronous function call in a group. 288 | * 289 | * Returns the same type as the function itself. 290 | * 291 | * @param name The name of the group 292 | * @param fn The function to wrap in the group 293 | */ 294 | function group(name, fn) { 295 | return __awaiter(this, void 0, void 0, function* () { 296 | startGroup(name); 297 | let result; 298 | try { 299 | result = yield fn(); 300 | } 301 | finally { 302 | endGroup(); 303 | } 304 | return result; 305 | }); 306 | } 307 | exports.group = group; 308 | //----------------------------------------------------------------------- 309 | // Wrapper action state 310 | //----------------------------------------------------------------------- 311 | /** 312 | * Saves state for current action, the state can only be retrieved by this action's post job execution. 313 | * 314 | * @param name name of the state to store 315 | * @param value value to store. Non-string values will be converted to a string via JSON.stringify 316 | */ 317 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 318 | function saveState(name, value) { 319 | command_1.issueCommand('save-state', { name }, value); 320 | } 321 | exports.saveState = saveState; 322 | /** 323 | * Gets the value of an state set by this action's main execution. 324 | * 325 | * @param name name of the state to get 326 | * @returns string 327 | */ 328 | function getState(name) { 329 | return process.env[`STATE_${name}`] || ''; 330 | } 331 | exports.getState = getState; 332 | //# sourceMappingURL=core.js.map 333 | 334 | /***/ }), 335 | 336 | /***/ 717: 337 | /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { 338 | 339 | "use strict"; 340 | 341 | // For internal use, subject to change. 342 | var __importStar = (this && this.__importStar) || function (mod) { 343 | if (mod && mod.__esModule) return mod; 344 | var result = {}; 345 | if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; 346 | result["default"] = mod; 347 | return result; 348 | }; 349 | Object.defineProperty(exports, "__esModule", ({ value: true })); 350 | // We use any as a valid input type 351 | /* eslint-disable @typescript-eslint/no-explicit-any */ 352 | const fs = __importStar(__nccwpck_require__(747)); 353 | const os = __importStar(__nccwpck_require__(87)); 354 | const utils_1 = __nccwpck_require__(278); 355 | function issueCommand(command, message) { 356 | const filePath = process.env[`GITHUB_${command}`]; 357 | if (!filePath) { 358 | throw new Error(`Unable to find environment variable for file command ${command}`); 359 | } 360 | if (!fs.existsSync(filePath)) { 361 | throw new Error(`Missing file at path: ${filePath}`); 362 | } 363 | fs.appendFileSync(filePath, `${utils_1.toCommandValue(message)}${os.EOL}`, { 364 | encoding: 'utf8' 365 | }); 366 | } 367 | exports.issueCommand = issueCommand; 368 | //# sourceMappingURL=file-command.js.map 369 | 370 | /***/ }), 371 | 372 | /***/ 278: 373 | /***/ ((__unused_webpack_module, exports) => { 374 | 375 | "use strict"; 376 | 377 | // We use any as a valid input type 378 | /* eslint-disable @typescript-eslint/no-explicit-any */ 379 | Object.defineProperty(exports, "__esModule", ({ value: true })); 380 | /** 381 | * Sanitizes an input into a string so it can be passed into issueCommand safely 382 | * @param input input to sanitize into a string 383 | */ 384 | function toCommandValue(input) { 385 | if (input === null || input === undefined) { 386 | return ''; 387 | } 388 | else if (typeof input === 'string' || input instanceof String) { 389 | return input; 390 | } 391 | return JSON.stringify(input); 392 | } 393 | exports.toCommandValue = toCommandValue; 394 | //# sourceMappingURL=utils.js.map 395 | 396 | /***/ }), 397 | 398 | /***/ 925: 399 | /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { 400 | 401 | "use strict"; 402 | var __webpack_unused_export__; 403 | 404 | __webpack_unused_export__ = ({ value: true }); 405 | const http = __nccwpck_require__(605); 406 | const https = __nccwpck_require__(211); 407 | const pm = __nccwpck_require__(443); 408 | let tunnel; 409 | var HttpCodes; 410 | (function (HttpCodes) { 411 | HttpCodes[HttpCodes["OK"] = 200] = "OK"; 412 | HttpCodes[HttpCodes["MultipleChoices"] = 300] = "MultipleChoices"; 413 | HttpCodes[HttpCodes["MovedPermanently"] = 301] = "MovedPermanently"; 414 | HttpCodes[HttpCodes["ResourceMoved"] = 302] = "ResourceMoved"; 415 | HttpCodes[HttpCodes["SeeOther"] = 303] = "SeeOther"; 416 | HttpCodes[HttpCodes["NotModified"] = 304] = "NotModified"; 417 | HttpCodes[HttpCodes["UseProxy"] = 305] = "UseProxy"; 418 | HttpCodes[HttpCodes["SwitchProxy"] = 306] = "SwitchProxy"; 419 | HttpCodes[HttpCodes["TemporaryRedirect"] = 307] = "TemporaryRedirect"; 420 | HttpCodes[HttpCodes["PermanentRedirect"] = 308] = "PermanentRedirect"; 421 | HttpCodes[HttpCodes["BadRequest"] = 400] = "BadRequest"; 422 | HttpCodes[HttpCodes["Unauthorized"] = 401] = "Unauthorized"; 423 | HttpCodes[HttpCodes["PaymentRequired"] = 402] = "PaymentRequired"; 424 | HttpCodes[HttpCodes["Forbidden"] = 403] = "Forbidden"; 425 | HttpCodes[HttpCodes["NotFound"] = 404] = "NotFound"; 426 | HttpCodes[HttpCodes["MethodNotAllowed"] = 405] = "MethodNotAllowed"; 427 | HttpCodes[HttpCodes["NotAcceptable"] = 406] = "NotAcceptable"; 428 | HttpCodes[HttpCodes["ProxyAuthenticationRequired"] = 407] = "ProxyAuthenticationRequired"; 429 | HttpCodes[HttpCodes["RequestTimeout"] = 408] = "RequestTimeout"; 430 | HttpCodes[HttpCodes["Conflict"] = 409] = "Conflict"; 431 | HttpCodes[HttpCodes["Gone"] = 410] = "Gone"; 432 | HttpCodes[HttpCodes["TooManyRequests"] = 429] = "TooManyRequests"; 433 | HttpCodes[HttpCodes["InternalServerError"] = 500] = "InternalServerError"; 434 | HttpCodes[HttpCodes["NotImplemented"] = 501] = "NotImplemented"; 435 | HttpCodes[HttpCodes["BadGateway"] = 502] = "BadGateway"; 436 | HttpCodes[HttpCodes["ServiceUnavailable"] = 503] = "ServiceUnavailable"; 437 | HttpCodes[HttpCodes["GatewayTimeout"] = 504] = "GatewayTimeout"; 438 | })(HttpCodes = exports.o8 || (exports.o8 = {})); 439 | var Headers; 440 | (function (Headers) { 441 | Headers["Accept"] = "accept"; 442 | Headers["ContentType"] = "content-type"; 443 | })(Headers = exports.PM || (exports.PM = {})); 444 | var MediaTypes; 445 | (function (MediaTypes) { 446 | MediaTypes["ApplicationJson"] = "application/json"; 447 | })(MediaTypes = exports.Tr || (exports.Tr = {})); 448 | /** 449 | * Returns the proxy URL, depending upon the supplied url and proxy environment variables. 450 | * @param serverUrl The server URL where the request will be sent. For example, https://api.github.com 451 | */ 452 | function getProxyUrl(serverUrl) { 453 | let proxyUrl = pm.getProxyUrl(new URL(serverUrl)); 454 | return proxyUrl ? proxyUrl.href : ''; 455 | } 456 | __webpack_unused_export__ = getProxyUrl; 457 | const HttpRedirectCodes = [ 458 | HttpCodes.MovedPermanently, 459 | HttpCodes.ResourceMoved, 460 | HttpCodes.SeeOther, 461 | HttpCodes.TemporaryRedirect, 462 | HttpCodes.PermanentRedirect 463 | ]; 464 | const HttpResponseRetryCodes = [ 465 | HttpCodes.BadGateway, 466 | HttpCodes.ServiceUnavailable, 467 | HttpCodes.GatewayTimeout 468 | ]; 469 | const RetryableHttpVerbs = ['OPTIONS', 'GET', 'DELETE', 'HEAD']; 470 | const ExponentialBackoffCeiling = 10; 471 | const ExponentialBackoffTimeSlice = 5; 472 | class HttpClientError extends Error { 473 | constructor(message, statusCode) { 474 | super(message); 475 | this.name = 'HttpClientError'; 476 | this.statusCode = statusCode; 477 | Object.setPrototypeOf(this, HttpClientError.prototype); 478 | } 479 | } 480 | __webpack_unused_export__ = HttpClientError; 481 | class HttpClientResponse { 482 | constructor(message) { 483 | this.message = message; 484 | } 485 | readBody() { 486 | return new Promise(async (resolve, reject) => { 487 | let output = Buffer.alloc(0); 488 | this.message.on('data', (chunk) => { 489 | output = Buffer.concat([output, chunk]); 490 | }); 491 | this.message.on('end', () => { 492 | resolve(output.toString()); 493 | }); 494 | }); 495 | } 496 | } 497 | __webpack_unused_export__ = HttpClientResponse; 498 | function isHttps(requestUrl) { 499 | let parsedUrl = new URL(requestUrl); 500 | return parsedUrl.protocol === 'https:'; 501 | } 502 | __webpack_unused_export__ = isHttps; 503 | class HttpClient { 504 | constructor(userAgent, handlers, requestOptions) { 505 | this._ignoreSslError = false; 506 | this._allowRedirects = true; 507 | this._allowRedirectDowngrade = false; 508 | this._maxRedirects = 50; 509 | this._allowRetries = false; 510 | this._maxRetries = 1; 511 | this._keepAlive = false; 512 | this._disposed = false; 513 | this.userAgent = userAgent; 514 | this.handlers = handlers || []; 515 | this.requestOptions = requestOptions; 516 | if (requestOptions) { 517 | if (requestOptions.ignoreSslError != null) { 518 | this._ignoreSslError = requestOptions.ignoreSslError; 519 | } 520 | this._socketTimeout = requestOptions.socketTimeout; 521 | if (requestOptions.allowRedirects != null) { 522 | this._allowRedirects = requestOptions.allowRedirects; 523 | } 524 | if (requestOptions.allowRedirectDowngrade != null) { 525 | this._allowRedirectDowngrade = requestOptions.allowRedirectDowngrade; 526 | } 527 | if (requestOptions.maxRedirects != null) { 528 | this._maxRedirects = Math.max(requestOptions.maxRedirects, 0); 529 | } 530 | if (requestOptions.keepAlive != null) { 531 | this._keepAlive = requestOptions.keepAlive; 532 | } 533 | if (requestOptions.allowRetries != null) { 534 | this._allowRetries = requestOptions.allowRetries; 535 | } 536 | if (requestOptions.maxRetries != null) { 537 | this._maxRetries = requestOptions.maxRetries; 538 | } 539 | } 540 | } 541 | options(requestUrl, additionalHeaders) { 542 | return this.request('OPTIONS', requestUrl, null, additionalHeaders || {}); 543 | } 544 | get(requestUrl, additionalHeaders) { 545 | return this.request('GET', requestUrl, null, additionalHeaders || {}); 546 | } 547 | del(requestUrl, additionalHeaders) { 548 | return this.request('DELETE', requestUrl, null, additionalHeaders || {}); 549 | } 550 | post(requestUrl, data, additionalHeaders) { 551 | return this.request('POST', requestUrl, data, additionalHeaders || {}); 552 | } 553 | patch(requestUrl, data, additionalHeaders) { 554 | return this.request('PATCH', requestUrl, data, additionalHeaders || {}); 555 | } 556 | put(requestUrl, data, additionalHeaders) { 557 | return this.request('PUT', requestUrl, data, additionalHeaders || {}); 558 | } 559 | head(requestUrl, additionalHeaders) { 560 | return this.request('HEAD', requestUrl, null, additionalHeaders || {}); 561 | } 562 | sendStream(verb, requestUrl, stream, additionalHeaders) { 563 | return this.request(verb, requestUrl, stream, additionalHeaders); 564 | } 565 | /** 566 | * Gets a typed object from an endpoint 567 | * Be aware that not found returns a null. Other errors (4xx, 5xx) reject the promise 568 | */ 569 | async getJson(requestUrl, additionalHeaders = {}) { 570 | additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson); 571 | let res = await this.get(requestUrl, additionalHeaders); 572 | return this._processResponse(res, this.requestOptions); 573 | } 574 | async postJson(requestUrl, obj, additionalHeaders = {}) { 575 | let data = JSON.stringify(obj, null, 2); 576 | additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson); 577 | additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.ContentType, MediaTypes.ApplicationJson); 578 | let res = await this.post(requestUrl, data, additionalHeaders); 579 | return this._processResponse(res, this.requestOptions); 580 | } 581 | async putJson(requestUrl, obj, additionalHeaders = {}) { 582 | let data = JSON.stringify(obj, null, 2); 583 | additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson); 584 | additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.ContentType, MediaTypes.ApplicationJson); 585 | let res = await this.put(requestUrl, data, additionalHeaders); 586 | return this._processResponse(res, this.requestOptions); 587 | } 588 | async patchJson(requestUrl, obj, additionalHeaders = {}) { 589 | let data = JSON.stringify(obj, null, 2); 590 | additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson); 591 | additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.ContentType, MediaTypes.ApplicationJson); 592 | let res = await this.patch(requestUrl, data, additionalHeaders); 593 | return this._processResponse(res, this.requestOptions); 594 | } 595 | /** 596 | * Makes a raw http request. 597 | * All other methods such as get, post, patch, and request ultimately call this. 598 | * Prefer get, del, post and patch 599 | */ 600 | async request(verb, requestUrl, data, headers) { 601 | if (this._disposed) { 602 | throw new Error('Client has already been disposed.'); 603 | } 604 | let parsedUrl = new URL(requestUrl); 605 | let info = this._prepareRequest(verb, parsedUrl, headers); 606 | // Only perform retries on reads since writes may not be idempotent. 607 | let maxTries = this._allowRetries && RetryableHttpVerbs.indexOf(verb) != -1 608 | ? this._maxRetries + 1 609 | : 1; 610 | let numTries = 0; 611 | let response; 612 | while (numTries < maxTries) { 613 | response = await this.requestRaw(info, data); 614 | // Check if it's an authentication challenge 615 | if (response && 616 | response.message && 617 | response.message.statusCode === HttpCodes.Unauthorized) { 618 | let authenticationHandler; 619 | for (let i = 0; i < this.handlers.length; i++) { 620 | if (this.handlers[i].canHandleAuthentication(response)) { 621 | authenticationHandler = this.handlers[i]; 622 | break; 623 | } 624 | } 625 | if (authenticationHandler) { 626 | return authenticationHandler.handleAuthentication(this, info, data); 627 | } 628 | else { 629 | // We have received an unauthorized response but have no handlers to handle it. 630 | // Let the response return to the caller. 631 | return response; 632 | } 633 | } 634 | let redirectsRemaining = this._maxRedirects; 635 | while (HttpRedirectCodes.indexOf(response.message.statusCode) != -1 && 636 | this._allowRedirects && 637 | redirectsRemaining > 0) { 638 | const redirectUrl = response.message.headers['location']; 639 | if (!redirectUrl) { 640 | // if there's no location to redirect to, we won't 641 | break; 642 | } 643 | let parsedRedirectUrl = new URL(redirectUrl); 644 | if (parsedUrl.protocol == 'https:' && 645 | parsedUrl.protocol != parsedRedirectUrl.protocol && 646 | !this._allowRedirectDowngrade) { 647 | throw new Error('Redirect from HTTPS to HTTP protocol. This downgrade is not allowed for security reasons. If you want to allow this behavior, set the allowRedirectDowngrade option to true.'); 648 | } 649 | // we need to finish reading the response before reassigning response 650 | // which will leak the open socket. 651 | await response.readBody(); 652 | // strip authorization header if redirected to a different hostname 653 | if (parsedRedirectUrl.hostname !== parsedUrl.hostname) { 654 | for (let header in headers) { 655 | // header names are case insensitive 656 | if (header.toLowerCase() === 'authorization') { 657 | delete headers[header]; 658 | } 659 | } 660 | } 661 | // let's make the request with the new redirectUrl 662 | info = this._prepareRequest(verb, parsedRedirectUrl, headers); 663 | response = await this.requestRaw(info, data); 664 | redirectsRemaining--; 665 | } 666 | if (HttpResponseRetryCodes.indexOf(response.message.statusCode) == -1) { 667 | // If not a retry code, return immediately instead of retrying 668 | return response; 669 | } 670 | numTries += 1; 671 | if (numTries < maxTries) { 672 | await response.readBody(); 673 | await this._performExponentialBackoff(numTries); 674 | } 675 | } 676 | return response; 677 | } 678 | /** 679 | * Needs to be called if keepAlive is set to true in request options. 680 | */ 681 | dispose() { 682 | if (this._agent) { 683 | this._agent.destroy(); 684 | } 685 | this._disposed = true; 686 | } 687 | /** 688 | * Raw request. 689 | * @param info 690 | * @param data 691 | */ 692 | requestRaw(info, data) { 693 | return new Promise((resolve, reject) => { 694 | let callbackForResult = function (err, res) { 695 | if (err) { 696 | reject(err); 697 | } 698 | resolve(res); 699 | }; 700 | this.requestRawWithCallback(info, data, callbackForResult); 701 | }); 702 | } 703 | /** 704 | * Raw request with callback. 705 | * @param info 706 | * @param data 707 | * @param onResult 708 | */ 709 | requestRawWithCallback(info, data, onResult) { 710 | let socket; 711 | if (typeof data === 'string') { 712 | info.options.headers['Content-Length'] = Buffer.byteLength(data, 'utf8'); 713 | } 714 | let callbackCalled = false; 715 | let handleResult = (err, res) => { 716 | if (!callbackCalled) { 717 | callbackCalled = true; 718 | onResult(err, res); 719 | } 720 | }; 721 | let req = info.httpModule.request(info.options, (msg) => { 722 | let res = new HttpClientResponse(msg); 723 | handleResult(null, res); 724 | }); 725 | req.on('socket', sock => { 726 | socket = sock; 727 | }); 728 | // If we ever get disconnected, we want the socket to timeout eventually 729 | req.setTimeout(this._socketTimeout || 3 * 60000, () => { 730 | if (socket) { 731 | socket.end(); 732 | } 733 | handleResult(new Error('Request timeout: ' + info.options.path), null); 734 | }); 735 | req.on('error', function (err) { 736 | // err has statusCode property 737 | // res should have headers 738 | handleResult(err, null); 739 | }); 740 | if (data && typeof data === 'string') { 741 | req.write(data, 'utf8'); 742 | } 743 | if (data && typeof data !== 'string') { 744 | data.on('close', function () { 745 | req.end(); 746 | }); 747 | data.pipe(req); 748 | } 749 | else { 750 | req.end(); 751 | } 752 | } 753 | /** 754 | * Gets an http agent. This function is useful when you need an http agent that handles 755 | * routing through a proxy server - depending upon the url and proxy environment variables. 756 | * @param serverUrl The server URL where the request will be sent. For example, https://api.github.com 757 | */ 758 | getAgent(serverUrl) { 759 | let parsedUrl = new URL(serverUrl); 760 | return this._getAgent(parsedUrl); 761 | } 762 | _prepareRequest(method, requestUrl, headers) { 763 | const info = {}; 764 | info.parsedUrl = requestUrl; 765 | const usingSsl = info.parsedUrl.protocol === 'https:'; 766 | info.httpModule = usingSsl ? https : http; 767 | const defaultPort = usingSsl ? 443 : 80; 768 | info.options = {}; 769 | info.options.host = info.parsedUrl.hostname; 770 | info.options.port = info.parsedUrl.port 771 | ? parseInt(info.parsedUrl.port) 772 | : defaultPort; 773 | info.options.path = 774 | (info.parsedUrl.pathname || '') + (info.parsedUrl.search || ''); 775 | info.options.method = method; 776 | info.options.headers = this._mergeHeaders(headers); 777 | if (this.userAgent != null) { 778 | info.options.headers['user-agent'] = this.userAgent; 779 | } 780 | info.options.agent = this._getAgent(info.parsedUrl); 781 | // gives handlers an opportunity to participate 782 | if (this.handlers) { 783 | this.handlers.forEach(handler => { 784 | handler.prepareRequest(info.options); 785 | }); 786 | } 787 | return info; 788 | } 789 | _mergeHeaders(headers) { 790 | const lowercaseKeys = obj => Object.keys(obj).reduce((c, k) => ((c[k.toLowerCase()] = obj[k]), c), {}); 791 | if (this.requestOptions && this.requestOptions.headers) { 792 | return Object.assign({}, lowercaseKeys(this.requestOptions.headers), lowercaseKeys(headers)); 793 | } 794 | return lowercaseKeys(headers || {}); 795 | } 796 | _getExistingOrDefaultHeader(additionalHeaders, header, _default) { 797 | const lowercaseKeys = obj => Object.keys(obj).reduce((c, k) => ((c[k.toLowerCase()] = obj[k]), c), {}); 798 | let clientHeader; 799 | if (this.requestOptions && this.requestOptions.headers) { 800 | clientHeader = lowercaseKeys(this.requestOptions.headers)[header]; 801 | } 802 | return additionalHeaders[header] || clientHeader || _default; 803 | } 804 | _getAgent(parsedUrl) { 805 | let agent; 806 | let proxyUrl = pm.getProxyUrl(parsedUrl); 807 | let useProxy = proxyUrl && proxyUrl.hostname; 808 | if (this._keepAlive && useProxy) { 809 | agent = this._proxyAgent; 810 | } 811 | if (this._keepAlive && !useProxy) { 812 | agent = this._agent; 813 | } 814 | // if agent is already assigned use that agent. 815 | if (!!agent) { 816 | return agent; 817 | } 818 | const usingSsl = parsedUrl.protocol === 'https:'; 819 | let maxSockets = 100; 820 | if (!!this.requestOptions) { 821 | maxSockets = this.requestOptions.maxSockets || http.globalAgent.maxSockets; 822 | } 823 | if (useProxy) { 824 | // If using proxy, need tunnel 825 | if (!tunnel) { 826 | tunnel = __nccwpck_require__(294); 827 | } 828 | const agentOptions = { 829 | maxSockets: maxSockets, 830 | keepAlive: this._keepAlive, 831 | proxy: { 832 | ...((proxyUrl.username || proxyUrl.password) && { 833 | proxyAuth: `${proxyUrl.username}:${proxyUrl.password}` 834 | }), 835 | host: proxyUrl.hostname, 836 | port: proxyUrl.port 837 | } 838 | }; 839 | let tunnelAgent; 840 | const overHttps = proxyUrl.protocol === 'https:'; 841 | if (usingSsl) { 842 | tunnelAgent = overHttps ? tunnel.httpsOverHttps : tunnel.httpsOverHttp; 843 | } 844 | else { 845 | tunnelAgent = overHttps ? tunnel.httpOverHttps : tunnel.httpOverHttp; 846 | } 847 | agent = tunnelAgent(agentOptions); 848 | this._proxyAgent = agent; 849 | } 850 | // if reusing agent across request and tunneling agent isn't assigned create a new agent 851 | if (this._keepAlive && !agent) { 852 | const options = { keepAlive: this._keepAlive, maxSockets: maxSockets }; 853 | agent = usingSsl ? new https.Agent(options) : new http.Agent(options); 854 | this._agent = agent; 855 | } 856 | // if not using private agent and tunnel agent isn't setup then use global agent 857 | if (!agent) { 858 | agent = usingSsl ? https.globalAgent : http.globalAgent; 859 | } 860 | if (usingSsl && this._ignoreSslError) { 861 | // we don't want to set NODE_TLS_REJECT_UNAUTHORIZED=0 since that will affect request for entire process 862 | // http.RequestOptions doesn't expose a way to modify RequestOptions.agent.options 863 | // we have to cast it to any and change it directly 864 | agent.options = Object.assign(agent.options || {}, { 865 | rejectUnauthorized: false 866 | }); 867 | } 868 | return agent; 869 | } 870 | _performExponentialBackoff(retryNumber) { 871 | retryNumber = Math.min(ExponentialBackoffCeiling, retryNumber); 872 | const ms = ExponentialBackoffTimeSlice * Math.pow(2, retryNumber); 873 | return new Promise(resolve => setTimeout(() => resolve(), ms)); 874 | } 875 | static dateTimeDeserializer(key, value) { 876 | if (typeof value === 'string') { 877 | let a = new Date(value); 878 | if (!isNaN(a.valueOf())) { 879 | return a; 880 | } 881 | } 882 | return value; 883 | } 884 | async _processResponse(res, options) { 885 | return new Promise(async (resolve, reject) => { 886 | const statusCode = res.message.statusCode; 887 | const response = { 888 | statusCode: statusCode, 889 | result: null, 890 | headers: {} 891 | }; 892 | // not found leads to null obj returned 893 | if (statusCode == HttpCodes.NotFound) { 894 | resolve(response); 895 | } 896 | let obj; 897 | let contents; 898 | // get the result from the body 899 | try { 900 | contents = await res.readBody(); 901 | if (contents && contents.length > 0) { 902 | if (options && options.deserializeDates) { 903 | obj = JSON.parse(contents, HttpClient.dateTimeDeserializer); 904 | } 905 | else { 906 | obj = JSON.parse(contents); 907 | } 908 | response.result = obj; 909 | } 910 | response.headers = res.message.headers; 911 | } 912 | catch (err) { 913 | // Invalid resource (contents not json); leaving result obj null 914 | } 915 | // note that 3xx redirects are handled by the http layer. 916 | if (statusCode > 299) { 917 | let msg; 918 | // if exception/error in body, attempt to get better error 919 | if (obj && obj.message) { 920 | msg = obj.message; 921 | } 922 | else if (contents && contents.length > 0) { 923 | // it may be the case that the exception is in the body message as string 924 | msg = contents; 925 | } 926 | else { 927 | msg = 'Failed request: (' + statusCode + ')'; 928 | } 929 | let err = new HttpClientError(msg, statusCode); 930 | err.result = response.result; 931 | reject(err); 932 | } 933 | else { 934 | resolve(response); 935 | } 936 | }); 937 | } 938 | } 939 | exports.eN = HttpClient; 940 | 941 | 942 | /***/ }), 943 | 944 | /***/ 443: 945 | /***/ ((__unused_webpack_module, exports) => { 946 | 947 | "use strict"; 948 | 949 | Object.defineProperty(exports, "__esModule", ({ value: true })); 950 | function getProxyUrl(reqUrl) { 951 | let usingSsl = reqUrl.protocol === 'https:'; 952 | let proxyUrl; 953 | if (checkBypass(reqUrl)) { 954 | return proxyUrl; 955 | } 956 | let proxyVar; 957 | if (usingSsl) { 958 | proxyVar = process.env['https_proxy'] || process.env['HTTPS_PROXY']; 959 | } 960 | else { 961 | proxyVar = process.env['http_proxy'] || process.env['HTTP_PROXY']; 962 | } 963 | if (proxyVar) { 964 | proxyUrl = new URL(proxyVar); 965 | } 966 | return proxyUrl; 967 | } 968 | exports.getProxyUrl = getProxyUrl; 969 | function checkBypass(reqUrl) { 970 | if (!reqUrl.hostname) { 971 | return false; 972 | } 973 | let noProxy = process.env['no_proxy'] || process.env['NO_PROXY'] || ''; 974 | if (!noProxy) { 975 | return false; 976 | } 977 | // Determine the request port 978 | let reqPort; 979 | if (reqUrl.port) { 980 | reqPort = Number(reqUrl.port); 981 | } 982 | else if (reqUrl.protocol === 'http:') { 983 | reqPort = 80; 984 | } 985 | else if (reqUrl.protocol === 'https:') { 986 | reqPort = 443; 987 | } 988 | // Format the request hostname and hostname with port 989 | let upperReqHosts = [reqUrl.hostname.toUpperCase()]; 990 | if (typeof reqPort === 'number') { 991 | upperReqHosts.push(`${upperReqHosts[0]}:${reqPort}`); 992 | } 993 | // Compare request host against noproxy 994 | for (let upperNoProxyItem of noProxy 995 | .split(',') 996 | .map(x => x.trim().toUpperCase()) 997 | .filter(x => x)) { 998 | if (upperReqHosts.some(x => x === upperNoProxyItem)) { 999 | return true; 1000 | } 1001 | } 1002 | return false; 1003 | } 1004 | exports.checkBypass = checkBypass; 1005 | 1006 | 1007 | /***/ }), 1008 | 1009 | /***/ 294: 1010 | /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { 1011 | 1012 | module.exports = __nccwpck_require__(219); 1013 | 1014 | 1015 | /***/ }), 1016 | 1017 | /***/ 219: 1018 | /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { 1019 | 1020 | "use strict"; 1021 | 1022 | 1023 | var net = __nccwpck_require__(631); 1024 | var tls = __nccwpck_require__(16); 1025 | var http = __nccwpck_require__(605); 1026 | var https = __nccwpck_require__(211); 1027 | var events = __nccwpck_require__(614); 1028 | var assert = __nccwpck_require__(357); 1029 | var util = __nccwpck_require__(669); 1030 | 1031 | 1032 | exports.httpOverHttp = httpOverHttp; 1033 | exports.httpsOverHttp = httpsOverHttp; 1034 | exports.httpOverHttps = httpOverHttps; 1035 | exports.httpsOverHttps = httpsOverHttps; 1036 | 1037 | 1038 | function httpOverHttp(options) { 1039 | var agent = new TunnelingAgent(options); 1040 | agent.request = http.request; 1041 | return agent; 1042 | } 1043 | 1044 | function httpsOverHttp(options) { 1045 | var agent = new TunnelingAgent(options); 1046 | agent.request = http.request; 1047 | agent.createSocket = createSecureSocket; 1048 | agent.defaultPort = 443; 1049 | return agent; 1050 | } 1051 | 1052 | function httpOverHttps(options) { 1053 | var agent = new TunnelingAgent(options); 1054 | agent.request = https.request; 1055 | return agent; 1056 | } 1057 | 1058 | function httpsOverHttps(options) { 1059 | var agent = new TunnelingAgent(options); 1060 | agent.request = https.request; 1061 | agent.createSocket = createSecureSocket; 1062 | agent.defaultPort = 443; 1063 | return agent; 1064 | } 1065 | 1066 | 1067 | function TunnelingAgent(options) { 1068 | var self = this; 1069 | self.options = options || {}; 1070 | self.proxyOptions = self.options.proxy || {}; 1071 | self.maxSockets = self.options.maxSockets || http.Agent.defaultMaxSockets; 1072 | self.requests = []; 1073 | self.sockets = []; 1074 | 1075 | self.on('free', function onFree(socket, host, port, localAddress) { 1076 | var options = toOptions(host, port, localAddress); 1077 | for (var i = 0, len = self.requests.length; i < len; ++i) { 1078 | var pending = self.requests[i]; 1079 | if (pending.host === options.host && pending.port === options.port) { 1080 | // Detect the request to connect same origin server, 1081 | // reuse the connection. 1082 | self.requests.splice(i, 1); 1083 | pending.request.onSocket(socket); 1084 | return; 1085 | } 1086 | } 1087 | socket.destroy(); 1088 | self.removeSocket(socket); 1089 | }); 1090 | } 1091 | util.inherits(TunnelingAgent, events.EventEmitter); 1092 | 1093 | TunnelingAgent.prototype.addRequest = function addRequest(req, host, port, localAddress) { 1094 | var self = this; 1095 | var options = mergeOptions({request: req}, self.options, toOptions(host, port, localAddress)); 1096 | 1097 | if (self.sockets.length >= this.maxSockets) { 1098 | // We are over limit so we'll add it to the queue. 1099 | self.requests.push(options); 1100 | return; 1101 | } 1102 | 1103 | // If we are under maxSockets create a new one. 1104 | self.createSocket(options, function(socket) { 1105 | socket.on('free', onFree); 1106 | socket.on('close', onCloseOrRemove); 1107 | socket.on('agentRemove', onCloseOrRemove); 1108 | req.onSocket(socket); 1109 | 1110 | function onFree() { 1111 | self.emit('free', socket, options); 1112 | } 1113 | 1114 | function onCloseOrRemove(err) { 1115 | self.removeSocket(socket); 1116 | socket.removeListener('free', onFree); 1117 | socket.removeListener('close', onCloseOrRemove); 1118 | socket.removeListener('agentRemove', onCloseOrRemove); 1119 | } 1120 | }); 1121 | }; 1122 | 1123 | TunnelingAgent.prototype.createSocket = function createSocket(options, cb) { 1124 | var self = this; 1125 | var placeholder = {}; 1126 | self.sockets.push(placeholder); 1127 | 1128 | var connectOptions = mergeOptions({}, self.proxyOptions, { 1129 | method: 'CONNECT', 1130 | path: options.host + ':' + options.port, 1131 | agent: false, 1132 | headers: { 1133 | host: options.host + ':' + options.port 1134 | } 1135 | }); 1136 | if (options.localAddress) { 1137 | connectOptions.localAddress = options.localAddress; 1138 | } 1139 | if (connectOptions.proxyAuth) { 1140 | connectOptions.headers = connectOptions.headers || {}; 1141 | connectOptions.headers['Proxy-Authorization'] = 'Basic ' + 1142 | new Buffer(connectOptions.proxyAuth).toString('base64'); 1143 | } 1144 | 1145 | debug('making CONNECT request'); 1146 | var connectReq = self.request(connectOptions); 1147 | connectReq.useChunkedEncodingByDefault = false; // for v0.6 1148 | connectReq.once('response', onResponse); // for v0.6 1149 | connectReq.once('upgrade', onUpgrade); // for v0.6 1150 | connectReq.once('connect', onConnect); // for v0.7 or later 1151 | connectReq.once('error', onError); 1152 | connectReq.end(); 1153 | 1154 | function onResponse(res) { 1155 | // Very hacky. This is necessary to avoid http-parser leaks. 1156 | res.upgrade = true; 1157 | } 1158 | 1159 | function onUpgrade(res, socket, head) { 1160 | // Hacky. 1161 | process.nextTick(function() { 1162 | onConnect(res, socket, head); 1163 | }); 1164 | } 1165 | 1166 | function onConnect(res, socket, head) { 1167 | connectReq.removeAllListeners(); 1168 | socket.removeAllListeners(); 1169 | 1170 | if (res.statusCode !== 200) { 1171 | debug('tunneling socket could not be established, statusCode=%d', 1172 | res.statusCode); 1173 | socket.destroy(); 1174 | var error = new Error('tunneling socket could not be established, ' + 1175 | 'statusCode=' + res.statusCode); 1176 | error.code = 'ECONNRESET'; 1177 | options.request.emit('error', error); 1178 | self.removeSocket(placeholder); 1179 | return; 1180 | } 1181 | if (head.length > 0) { 1182 | debug('got illegal response body from proxy'); 1183 | socket.destroy(); 1184 | var error = new Error('got illegal response body from proxy'); 1185 | error.code = 'ECONNRESET'; 1186 | options.request.emit('error', error); 1187 | self.removeSocket(placeholder); 1188 | return; 1189 | } 1190 | debug('tunneling connection has established'); 1191 | self.sockets[self.sockets.indexOf(placeholder)] = socket; 1192 | return cb(socket); 1193 | } 1194 | 1195 | function onError(cause) { 1196 | connectReq.removeAllListeners(); 1197 | 1198 | debug('tunneling socket could not be established, cause=%s\n', 1199 | cause.message, cause.stack); 1200 | var error = new Error('tunneling socket could not be established, ' + 1201 | 'cause=' + cause.message); 1202 | error.code = 'ECONNRESET'; 1203 | options.request.emit('error', error); 1204 | self.removeSocket(placeholder); 1205 | } 1206 | }; 1207 | 1208 | TunnelingAgent.prototype.removeSocket = function removeSocket(socket) { 1209 | var pos = this.sockets.indexOf(socket) 1210 | if (pos === -1) { 1211 | return; 1212 | } 1213 | this.sockets.splice(pos, 1); 1214 | 1215 | var pending = this.requests.shift(); 1216 | if (pending) { 1217 | // If we have pending requests and a socket gets closed a new one 1218 | // needs to be created to take over in the pool for the one that closed. 1219 | this.createSocket(pending, function(socket) { 1220 | pending.request.onSocket(socket); 1221 | }); 1222 | } 1223 | }; 1224 | 1225 | function createSecureSocket(options, cb) { 1226 | var self = this; 1227 | TunnelingAgent.prototype.createSocket.call(self, options, function(socket) { 1228 | var hostHeader = options.request.getHeader('host'); 1229 | var tlsOptions = mergeOptions({}, self.options, { 1230 | socket: socket, 1231 | servername: hostHeader ? hostHeader.replace(/:.*$/, '') : options.host 1232 | }); 1233 | 1234 | // 0 is dummy port for v0.6 1235 | var secureSocket = tls.connect(0, tlsOptions); 1236 | self.sockets[self.sockets.indexOf(socket)] = secureSocket; 1237 | cb(secureSocket); 1238 | }); 1239 | } 1240 | 1241 | 1242 | function toOptions(host, port, localAddress) { 1243 | if (typeof host === 'string') { // since v0.10 1244 | return { 1245 | host: host, 1246 | port: port, 1247 | localAddress: localAddress 1248 | }; 1249 | } 1250 | return host; // for v0.11 or later 1251 | } 1252 | 1253 | function mergeOptions(target) { 1254 | for (var i = 1, len = arguments.length; i < len; ++i) { 1255 | var overrides = arguments[i]; 1256 | if (typeof overrides === 'object') { 1257 | var keys = Object.keys(overrides); 1258 | for (var j = 0, keyLen = keys.length; j < keyLen; ++j) { 1259 | var k = keys[j]; 1260 | if (overrides[k] !== undefined) { 1261 | target[k] = overrides[k]; 1262 | } 1263 | } 1264 | } 1265 | } 1266 | return target; 1267 | } 1268 | 1269 | 1270 | var debug; 1271 | if (process.env.NODE_DEBUG && /\btunnel\b/.test(process.env.NODE_DEBUG)) { 1272 | debug = function() { 1273 | var args = Array.prototype.slice.call(arguments); 1274 | if (typeof args[0] === 'string') { 1275 | args[0] = 'TUNNEL: ' + args[0]; 1276 | } else { 1277 | args.unshift('TUNNEL:'); 1278 | } 1279 | console.error.apply(console, args); 1280 | } 1281 | } else { 1282 | debug = function() {}; 1283 | } 1284 | exports.debug = debug; // for test 1285 | 1286 | 1287 | /***/ }), 1288 | 1289 | /***/ 357: 1290 | /***/ ((module) => { 1291 | 1292 | "use strict"; 1293 | module.exports = require("assert");; 1294 | 1295 | /***/ }), 1296 | 1297 | /***/ 614: 1298 | /***/ ((module) => { 1299 | 1300 | "use strict"; 1301 | module.exports = require("events");; 1302 | 1303 | /***/ }), 1304 | 1305 | /***/ 747: 1306 | /***/ ((module) => { 1307 | 1308 | "use strict"; 1309 | module.exports = require("fs");; 1310 | 1311 | /***/ }), 1312 | 1313 | /***/ 605: 1314 | /***/ ((module) => { 1315 | 1316 | "use strict"; 1317 | module.exports = require("http");; 1318 | 1319 | /***/ }), 1320 | 1321 | /***/ 211: 1322 | /***/ ((module) => { 1323 | 1324 | "use strict"; 1325 | module.exports = require("https");; 1326 | 1327 | /***/ }), 1328 | 1329 | /***/ 631: 1330 | /***/ ((module) => { 1331 | 1332 | "use strict"; 1333 | module.exports = require("net");; 1334 | 1335 | /***/ }), 1336 | 1337 | /***/ 87: 1338 | /***/ ((module) => { 1339 | 1340 | "use strict"; 1341 | module.exports = require("os");; 1342 | 1343 | /***/ }), 1344 | 1345 | /***/ 622: 1346 | /***/ ((module) => { 1347 | 1348 | "use strict"; 1349 | module.exports = require("path");; 1350 | 1351 | /***/ }), 1352 | 1353 | /***/ 16: 1354 | /***/ ((module) => { 1355 | 1356 | "use strict"; 1357 | module.exports = require("tls");; 1358 | 1359 | /***/ }), 1360 | 1361 | /***/ 669: 1362 | /***/ ((module) => { 1363 | 1364 | "use strict"; 1365 | module.exports = require("util");; 1366 | 1367 | /***/ }) 1368 | 1369 | /******/ }); 1370 | /************************************************************************/ 1371 | /******/ // The module cache 1372 | /******/ var __webpack_module_cache__ = {}; 1373 | /******/ 1374 | /******/ // The require function 1375 | /******/ function __nccwpck_require__(moduleId) { 1376 | /******/ // Check if module is in cache 1377 | /******/ var cachedModule = __webpack_module_cache__[moduleId]; 1378 | /******/ if (cachedModule !== undefined) { 1379 | /******/ return cachedModule.exports; 1380 | /******/ } 1381 | /******/ // Create a new module (and put it into the cache) 1382 | /******/ var module = __webpack_module_cache__[moduleId] = { 1383 | /******/ // no module.id needed 1384 | /******/ // no module.loaded needed 1385 | /******/ exports: {} 1386 | /******/ }; 1387 | /******/ 1388 | /******/ // Execute the module function 1389 | /******/ var threw = true; 1390 | /******/ try { 1391 | /******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __nccwpck_require__); 1392 | /******/ threw = false; 1393 | /******/ } finally { 1394 | /******/ if(threw) delete __webpack_module_cache__[moduleId]; 1395 | /******/ } 1396 | /******/ 1397 | /******/ // Return the exports of the module 1398 | /******/ return module.exports; 1399 | /******/ } 1400 | /******/ 1401 | /************************************************************************/ 1402 | /******/ /* webpack/runtime/compat get default export */ 1403 | /******/ (() => { 1404 | /******/ // getDefaultExport function for compatibility with non-harmony modules 1405 | /******/ __nccwpck_require__.n = (module) => { 1406 | /******/ var getter = module && module.__esModule ? 1407 | /******/ () => (module['default']) : 1408 | /******/ () => (module); 1409 | /******/ __nccwpck_require__.d(getter, { a: getter }); 1410 | /******/ return getter; 1411 | /******/ }; 1412 | /******/ })(); 1413 | /******/ 1414 | /******/ /* webpack/runtime/define property getters */ 1415 | /******/ (() => { 1416 | /******/ // define getter functions for harmony exports 1417 | /******/ __nccwpck_require__.d = (exports, definition) => { 1418 | /******/ for(var key in definition) { 1419 | /******/ if(__nccwpck_require__.o(definition, key) && !__nccwpck_require__.o(exports, key)) { 1420 | /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); 1421 | /******/ } 1422 | /******/ } 1423 | /******/ }; 1424 | /******/ })(); 1425 | /******/ 1426 | /******/ /* webpack/runtime/hasOwnProperty shorthand */ 1427 | /******/ (() => { 1428 | /******/ __nccwpck_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) 1429 | /******/ })(); 1430 | /******/ 1431 | /******/ /* webpack/runtime/make namespace object */ 1432 | /******/ (() => { 1433 | /******/ // define __esModule on exports 1434 | /******/ __nccwpck_require__.r = (exports) => { 1435 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 1436 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 1437 | /******/ } 1438 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 1439 | /******/ }; 1440 | /******/ })(); 1441 | /******/ 1442 | /******/ /* webpack/runtime/compat */ 1443 | /******/ 1444 | /******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/";/************************************************************************/ 1445 | var __webpack_exports__ = {}; 1446 | // This entry need to be wrapped in an IIFE because it need to be in strict mode. 1447 | (() => { 1448 | "use strict"; 1449 | // ESM COMPAT FLAG 1450 | __nccwpck_require__.r(__webpack_exports__); 1451 | 1452 | // EXTERNAL MODULE: ./node_modules/@actions/core/lib/core.js 1453 | var core = __nccwpck_require__(186); 1454 | // EXTERNAL MODULE: ./node_modules/@actions/http-client/index.js 1455 | var http_client = __nccwpck_require__(925); 1456 | // EXTERNAL MODULE: external "fs" 1457 | var external_fs_ = __nccwpck_require__(747); 1458 | var external_fs_default = /*#__PURE__*/__nccwpck_require__.n(external_fs_); 1459 | ;// CONCATENATED MODULE: ./src/utils/checkForSections.ts 1460 | 1461 | const checkForSections = (readmeLines) => { 1462 | const startIdx = readmeLines.findIndex((content) => content.trim() === ''); 1463 | if (startIdx === -1) { 1464 | core.setFailed(`Couldn't find the comment. Exiting!`); 1465 | } 1466 | const endIdx = readmeLines.findIndex((content) => content.trim() === ''); 1467 | if (endIdx === -1) { 1468 | core.setFailed(`Couldn't find the comment. Exiting!`); 1469 | } 1470 | return [startIdx, endIdx]; 1471 | }; 1472 | 1473 | ;// CONCATENATED MODULE: external "child_process" 1474 | const external_child_process_namespaceObject = require("child_process");; 1475 | ;// CONCATENATED MODULE: ./src/utils/commitFile.ts 1476 | var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { 1477 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 1478 | return new (P || (P = Promise))(function (resolve, reject) { 1479 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 1480 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 1481 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 1482 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 1483 | }); 1484 | }; 1485 | 1486 | const exec = (cmd, args = []) => new Promise((resolve, reject) => { 1487 | const app = (0,external_child_process_namespaceObject.spawn)(cmd, args, { stdio: 'pipe' }); 1488 | let stdout = ''; 1489 | app.stdout.on('data', (data) => { 1490 | stdout = data; 1491 | }); 1492 | app.on('close', (code) => { 1493 | if (code !== 0 && !stdout.includes('nothing to commit')) { 1494 | const err = new Error(`Invalid status code: ${code}`); 1495 | err.code = code; 1496 | return reject(err); 1497 | } 1498 | return resolve(code); 1499 | }); 1500 | app.on('error', reject); 1501 | }); 1502 | const commitFile = () => __awaiter(void 0, void 0, void 0, function* () { 1503 | yield exec('git', [ 1504 | 'config', 1505 | '--global', 1506 | 'user.email', 1507 | '41898282+github-actions[bot]@users.noreply.github.com' 1508 | ]); 1509 | yield exec('git', ['config', '--global', 'user.name', 'readme-bot']); 1510 | yield exec('git', ['add', 'README.md']); 1511 | yield exec('git', ['commit', '-m', 'Updated readme with learn section']); 1512 | yield exec('git', ['push']); 1513 | }); 1514 | 1515 | ;// CONCATENATED MODULE: ./src/utils/constructCategoriesMap.ts 1516 | const constructCategoriesMap = (schema_unit) => { 1517 | const categories = schema_unit.options 1518 | .map((option) => ({ 1519 | color: option.color, 1520 | value: option.value 1521 | })) 1522 | .sort((categoryA, categoryB) => categoryA.value > categoryB.value ? 1 : -1); 1523 | const categories_map = new Map(); 1524 | categories.forEach((category) => { 1525 | categories_map.set(category.value, Object.assign({ items: [] }, category)); 1526 | }); 1527 | return categories_map; 1528 | }; 1529 | 1530 | ;// CONCATENATED MODULE: external "querystring" 1531 | const external_querystring_namespaceObject = require("querystring");; 1532 | var external_querystring_default = /*#__PURE__*/__nccwpck_require__.n(external_querystring_namespaceObject); 1533 | ;// CONCATENATED MODULE: ./src/utils/constructNewContents.ts 1534 | 1535 | const ColorMap = { 1536 | default: '505558', 1537 | gray: '979a9b', 1538 | brown: '695b55', 1539 | orange: '9f7445', 1540 | yellow: '9f9048', 1541 | green: '467870', 1542 | blue: '487088', 1543 | purple: '6c598f', 1544 | pink: '904d74', 1545 | red: '9f5c58', 1546 | teal: '467870' 1547 | }; 1548 | const constructNewContents = (categoriesMap, colorSchemaUnitKey, base64SchemaUnitKey) => { 1549 | const newContents = []; 1550 | for (const [category, categoryInfo] of categoriesMap) { 1551 | const content = [ 1552 | `

` 1553 | ]; 1554 | categoryInfo.items.forEach((item) => { 1555 | var _a, _b, _c; 1556 | const title = item.title && item.title[0][0]; 1557 | if (!title) 1558 | throw new Error(`Each row must have value in the Name column`); 1559 | let logo = external_querystring_default().escape(title); 1560 | if ((_a = item[base64SchemaUnitKey]) === null || _a === void 0 ? void 0 : _a[0][0]) { 1561 | logo = item[base64SchemaUnitKey][0][0]; 1562 | } 1563 | content.push(`${title}`); 1564 | }); 1565 | newContents.push(...content, '
'); 1566 | } 1567 | return newContents; 1568 | }; 1569 | 1570 | ;// CONCATENATED MODULE: ./src/utils/fetchData.ts 1571 | var fetchData_awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { 1572 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 1573 | return new (P || (P = Promise))(function (resolve, reject) { 1574 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 1575 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 1576 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 1577 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 1578 | }); 1579 | }; 1580 | 1581 | const fetchData = (id, table, http) => fetchData_awaiter(void 0, void 0, void 0, function* () { 1582 | const response = yield http.post(`https://www.notion.so/api/v3/syncRecordValues`, JSON.stringify({ 1583 | requests: [ 1584 | { 1585 | id, 1586 | table, 1587 | version: -1 1588 | } 1589 | ] 1590 | })); 1591 | const body = JSON.parse(yield response.readBody()); 1592 | const data = body.recordMap[table][id].value; 1593 | if (!data) { 1594 | core.setFailed(`Either your NOTION_TOKEN_V2 has expired or a ${table} with id:${id} doesn't exist`); 1595 | } 1596 | return data; 1597 | }); 1598 | 1599 | ;// CONCATENATED MODULE: ./src/utils/getSchemaEntries.ts 1600 | 1601 | const getSchemaEntries = (schema) => { 1602 | const schemaEntries = Object.entries(schema); 1603 | let categorySchemaEntry = undefined, nameSchemaEntry = undefined, colorSchemaEntry = undefined, base64SchemaEntry = undefined; 1604 | schemaEntries.forEach((schemaEntry) => { 1605 | if (schemaEntry[1].type === 'text' && schemaEntry[1].name === 'Color') { 1606 | colorSchemaEntry = schemaEntry; 1607 | } 1608 | else if (schemaEntry[1].type === 'title' && 1609 | schemaEntry[1].name === 'Name') { 1610 | nameSchemaEntry = schemaEntry; 1611 | } 1612 | else if (schemaEntry[1].type === 'select' && 1613 | schemaEntry[1].name === 'Category') { 1614 | categorySchemaEntry = schemaEntry; 1615 | } 1616 | else if (schemaEntry[1].type === 'text' && 1617 | schemaEntry[1].name === 'Base64') { 1618 | base64SchemaEntry = schemaEntry; 1619 | } 1620 | }); 1621 | if (!categorySchemaEntry) 1622 | core.setFailed("Couldn't find Category named select type column in the database"); 1623 | if (!nameSchemaEntry) 1624 | core.setFailed("Couldn't find Color named text type column in the database"); 1625 | if (!colorSchemaEntry) 1626 | core.setFailed("Couldn't find Name named title type column in the database"); 1627 | return [ 1628 | categorySchemaEntry, 1629 | colorSchemaEntry, 1630 | nameSchemaEntry, 1631 | base64SchemaEntry 1632 | ]; 1633 | }; 1634 | 1635 | ;// CONCATENATED MODULE: ./src/utils/modifyRows.ts 1636 | const modifyRows = (recordMap, databaseId) => { 1637 | return Object.values(recordMap.block) 1638 | .filter((block) => block.value.id !== databaseId) 1639 | .map((block) => block.value) 1640 | .sort((rowA, rowB) => rowA.properties.title[0][0] > rowB.properties.title[0][0] ? 1 : -1); 1641 | }; 1642 | 1643 | ;// CONCATENATED MODULE: ./src/utils/populateCategoriesMapItems.ts 1644 | const populateCategoriesMapItems = (rows, category_schema_id, categories_map) => { 1645 | rows.forEach((row) => { 1646 | const category = row.properties[category_schema_id] && 1647 | row.properties[category_schema_id][0][0]; 1648 | if (!category) 1649 | throw new Error('Each row must have a category value'); 1650 | const category_value = categories_map.get(category); 1651 | category_value.items.push(row.properties); 1652 | }); 1653 | }; 1654 | 1655 | ;// CONCATENATED MODULE: ./src/utils/index.ts 1656 | 1657 | 1658 | 1659 | 1660 | 1661 | 1662 | 1663 | 1664 | const ActionUtils = { 1665 | checkForSections: checkForSections, 1666 | commitFile: commitFile, 1667 | constructCategoriesMap: constructCategoriesMap, 1668 | constructNewContents: constructNewContents, 1669 | fetchData: fetchData, 1670 | getSchemaEntries: getSchemaEntries, 1671 | modifyRows: modifyRows, 1672 | populateCategoriesMapItems: populateCategoriesMapItems 1673 | }; 1674 | 1675 | ;// CONCATENATED MODULE: ./src/action.ts 1676 | var action_awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { 1677 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 1678 | return new (P || (P = Promise))(function (resolve, reject) { 1679 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 1680 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 1681 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 1682 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 1683 | }); 1684 | }; 1685 | 1686 | 1687 | 1688 | 1689 | function action() { 1690 | return action_awaiter(this, void 0, void 0, function* () { 1691 | try { 1692 | const NOTION_TOKEN_V2 = core.getInput('token_v2'); 1693 | let id = core.getInput('database_id').replace(/-/g, ''); 1694 | const databaseId = `${id.substr(0, 8)}-${id.substr(8, 4)}-${id.substr(12, 4)}-${id.substr(16, 4)}-${id.substr(20)}`; 1695 | const headers = { 1696 | Accept: 'application/json', 1697 | 'Content-Type': 'application/json', 1698 | cookie: `token_v2=${NOTION_TOKEN_V2}` 1699 | }; 1700 | const http = new http_client/* HttpClient */.eN(undefined, undefined, { 1701 | headers 1702 | }); 1703 | const collectionView = yield ActionUtils.fetchData(databaseId, 'block', http); 1704 | core.info('Fetched database'); 1705 | const collection_id = collectionView.collection_id; 1706 | const collection = yield ActionUtils.fetchData(collection_id, 'collection', http); 1707 | core.info('Fetched collection'); 1708 | const response = yield http.post(`https://www.notion.so/api/v3/queryCollection`, JSON.stringify({ 1709 | collection: { 1710 | id: collection_id, 1711 | spaceId: collectionView.space_id 1712 | }, 1713 | collectionView: { 1714 | id: collectionView.view_ids[0], 1715 | spaceId: collectionView.space_id 1716 | }, 1717 | loader: { 1718 | type: 'reducer', 1719 | reducers: { 1720 | collection_group_results: { 1721 | type: 'results' 1722 | } 1723 | }, 1724 | searchQuery: '', 1725 | userTimeZone: 'Asia/Dhaka' 1726 | } 1727 | })); 1728 | const { recordMap } = JSON.parse(yield response.readBody()); 1729 | core.info('Fetched rows'); 1730 | const { schema } = collection; 1731 | const [categorySchemaEntry, colorSchemaEntry, , base64SchemaEntry] = ActionUtils.getSchemaEntries(schema); 1732 | const rows = ActionUtils.modifyRows(recordMap, databaseId); 1733 | const categoriesMap = ActionUtils.constructCategoriesMap(categorySchemaEntry[1]); 1734 | ActionUtils.populateCategoriesMapItems(rows, categorySchemaEntry[0], categoriesMap); 1735 | const README_PATH = `${process.env.GITHUB_WORKSPACE}/README.md`; 1736 | core.info(`Reading from ${README_PATH}`); 1737 | const readmeLines = external_fs_default().readFileSync(README_PATH, 'utf-8').split('\n'); 1738 | const [startIdx, endIdx] = ActionUtils.checkForSections(readmeLines); 1739 | const newLines = ActionUtils.constructNewContents(categoriesMap, colorSchemaEntry[0], base64SchemaEntry[0]); 1740 | const finalLines = [ 1741 | ...readmeLines.slice(0, startIdx + 1), 1742 | ...newLines, 1743 | ...readmeLines.slice(endIdx) 1744 | ]; 1745 | core.info(`Writing to ${README_PATH}`); 1746 | external_fs_default().writeFileSync(README_PATH, finalLines.join('\n'), 'utf-8'); 1747 | yield ActionUtils.commitFile(); 1748 | } 1749 | catch (err) { 1750 | core.error(err.message); 1751 | core.setFailed(err.message); 1752 | } 1753 | }); 1754 | } 1755 | 1756 | ;// CONCATENATED MODULE: ./src/index.ts 1757 | 1758 | action(); 1759 | 1760 | })(); 1761 | 1762 | module.exports = __webpack_exports__; 1763 | /******/ })() 1764 | ; -------------------------------------------------------------------------------- /dist/licenses.txt: -------------------------------------------------------------------------------- 1 | @actions/core 2 | MIT 3 | The MIT License (MIT) 4 | 5 | Copyright 2019 GitHub 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | 13 | @vercel/ncc 14 | MIT 15 | Copyright 2018 ZEIT, Inc. 16 | 17 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 18 | 19 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = async () => { 2 | return { 3 | rootDir: process.cwd(), 4 | testTimeout: 30000, 5 | testEnvironment: 'node', 6 | verbose: true, 7 | testPathIgnorePatterns: [ 8 | '/node_modules', 9 | '/dist', 10 | '/src/utils/commitFile.ts' 11 | ], 12 | modulePathIgnorePatterns: ['/dist'], 13 | roots: ['/tests'], 14 | testMatch: ['/tests/**/*.test.ts'], 15 | transform: { 16 | '^.+\\.(ts)$': 'ts-jest' 17 | }, 18 | collectCoverageFrom: ['src/utils/{!(commitFile),}.ts', 'src/action.ts'], 19 | collectCoverage: true, 20 | coverageDirectory: './coverage', 21 | coverageThreshold: { 22 | global: { 23 | branches: 95, 24 | functions: 95, 25 | lines: 95, 26 | statements: -10 27 | } 28 | } 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /media/Logo-600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devorein/github-readme-learn-section-notion/f523364bf0b18514fedc2d19894d731ac131554a/media/Logo-600.png -------------------------------------------------------------------------------- /media/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devorein/github-readme-learn-section-notion/f523364bf0b18514fedc2d19894d731ac131554a/media/Logo.png -------------------------------------------------------------------------------- /media/github_readme_learn_section.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devorein/github-readme-learn-section-notion/f523364bf0b18514fedc2d19894d731ac131554a/media/github_readme_learn_section.png -------------------------------------------------------------------------------- /media/notion_full_page_db.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devorein/github-readme-learn-section-notion/f523364bf0b18514fedc2d19894d731ac131554a/media/notion_full_page_db.png -------------------------------------------------------------------------------- /media/notion_full_page_db_id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devorein/github-readme-learn-section-notion/f523364bf0b18514fedc2d19894d731ac131554a/media/notion_full_page_db_id.png -------------------------------------------------------------------------------- /media/notion_table_schema_options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devorein/github-readme-learn-section-notion/f523364bf0b18514fedc2d19894d731ac131554a/media/notion_table_schema_options.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-readme-learn-section-notion", 3 | "version": "1.0.2", 4 | "description": "A github action to auto-populate github readme learn section with data fetched from a remote notion database", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "prebuild": "npm run format && npm run transpile", 8 | "build": "npx ncc build ./src/index.ts -o dist -t", 9 | "format": "npx prettier --write src/**/*.ts", 10 | "transpile": "npx tsc", 11 | "test": "npx jest" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/Devorein/github-readme-learn-section-notion.git" 16 | }, 17 | "keywords": [], 18 | "author": "Safwan Shaheer ", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/Devorein/github-readme-learn-section-notion/issues" 22 | }, 23 | "homepage": "https://github.com/Devorein/github-readme-learn-section-notion#readme", 24 | "dependencies": { 25 | "@actions/core": "^1.2.7", 26 | "@actions/http-client": "^1.0.11" 27 | }, 28 | "devDependencies": { 29 | "@nishans/types": "^0.0.35", 30 | "@types/jest": "^26.0.23", 31 | "@types/node": "^15.0.1", 32 | "jest": "^26.6.3", 33 | "prettier": "^2.2.1", 34 | "ts-jest": "^26.5.5", 35 | "@vercel/ncc": "^0.28.5", 36 | "typescript": "^4.2.4" 37 | } 38 | } -------------------------------------------------------------------------------- /scripts/codecov.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | codecov_file="${GITHUB_WORKSPACE}/scripts/codecov.sh" 4 | 5 | curl -s https://codecov.io/bash > $codecov_file 6 | chmod +x $codecov_file 7 | 8 | file="${GITHUB_WORKSPACE}/coverage/lcov.info" 9 | $codecov_file -f $file -v -t $CODECOV_TOKEN -------------------------------------------------------------------------------- /src/action.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import { HttpClient } from '@actions/http-client'; 3 | import { IRequestOptions } from '@actions/http-client/interfaces'; 4 | import { ICollection, RecordMap, TCollectionBlock } from '@nishans/types'; 5 | import fs from 'fs'; 6 | import { ActionUtils } from './utils'; 7 | 8 | export async function action() { 9 | try { 10 | const NOTION_TOKEN_V2 = core.getInput('token_v2'); 11 | let id = core.getInput('database_id').replace(/-/g, ''); 12 | const databaseId = `${id.substr(0, 8)}-${id.substr(8, 4)}-${id.substr( 13 | 12, 14 | 4 15 | )}-${id.substr(16, 4)}-${id.substr(20)}`; 16 | 17 | const headers: IRequestOptions['headers'] = { 18 | Accept: 'application/json', 19 | 'Content-Type': 'application/json', 20 | cookie: `token_v2=${NOTION_TOKEN_V2}` 21 | }; 22 | 23 | const http = new HttpClient(undefined, undefined, { 24 | headers 25 | }); 26 | 27 | const collectionView = await ActionUtils.fetchData( 28 | databaseId, 29 | 'block', 30 | http 31 | ); 32 | core.info('Fetched database'); 33 | 34 | const collection_id = collectionView.collection_id; 35 | const collection = await ActionUtils.fetchData( 36 | collection_id, 37 | 'collection', 38 | http 39 | ); 40 | 41 | core.info('Fetched collection'); 42 | 43 | const response = await http.post( 44 | `https://www.notion.so/api/v3/queryCollection`, 45 | JSON.stringify({ 46 | collection: { 47 | id: collection_id, 48 | spaceId: collectionView.space_id 49 | }, 50 | collectionView: { 51 | id: collectionView.view_ids[0], 52 | spaceId: collectionView.space_id 53 | }, 54 | loader: { 55 | type: 'reducer', 56 | reducers: { 57 | collection_group_results: { 58 | type: 'results' 59 | } 60 | }, 61 | searchQuery: '', 62 | userTimeZone: 'Asia/Dhaka' 63 | } 64 | }) 65 | ); 66 | 67 | const { recordMap } = JSON.parse(await response.readBody()) as { 68 | recordMap: RecordMap; 69 | }; 70 | 71 | core.info('Fetched rows'); 72 | const { schema } = collection; 73 | const [ 74 | categorySchemaEntry, 75 | colorSchemaEntry, 76 | , 77 | base64SchemaEntry 78 | ] = ActionUtils.getSchemaEntries(schema); 79 | 80 | const rows = ActionUtils.modifyRows(recordMap, databaseId); 81 | const categoriesMap = ActionUtils.constructCategoriesMap( 82 | categorySchemaEntry[1] 83 | ); 84 | ActionUtils.populateCategoriesMapItems( 85 | rows, 86 | categorySchemaEntry[0], 87 | categoriesMap 88 | ); 89 | 90 | const README_PATH = `${process.env.GITHUB_WORKSPACE}/README.md`; 91 | core.info(`Reading from ${README_PATH}`); 92 | 93 | const readmeLines = fs.readFileSync(README_PATH, 'utf-8').split('\n'); 94 | 95 | const [startIdx, endIdx] = ActionUtils.checkForSections(readmeLines); 96 | const newLines = ActionUtils.constructNewContents( 97 | categoriesMap, 98 | colorSchemaEntry[0], 99 | base64SchemaEntry[0] 100 | ); 101 | 102 | const finalLines = [ 103 | ...readmeLines.slice(0, startIdx + 1), 104 | ...newLines, 105 | ...readmeLines.slice(endIdx) 106 | ]; 107 | 108 | core.info(`Writing to ${README_PATH}`); 109 | 110 | fs.writeFileSync(README_PATH, finalLines.join('\n'), 'utf-8'); 111 | await ActionUtils.commitFile(); 112 | } catch (err) { 113 | core.error(err.message); 114 | core.setFailed(err.message); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { action } from './action'; 2 | 3 | action(); 4 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { IPage, TTextColor } from '@nishans/types'; 2 | 3 | export type ICategoryMap = Map< 4 | string, 5 | { 6 | items: IPage['properties'][]; 7 | color: TTextColor; 8 | } 9 | >; 10 | -------------------------------------------------------------------------------- /src/utils/checkForSections.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | 3 | export const checkForSections = (readmeLines: string[]) => { 4 | const startIdx = readmeLines.findIndex( 5 | (content) => content.trim() === '' 6 | ); 7 | 8 | if (startIdx === -1) { 9 | core.setFailed( 10 | `Couldn't find the comment. Exiting!` 11 | ); 12 | } 13 | 14 | const endIdx = readmeLines.findIndex( 15 | (content) => content.trim() === '' 16 | ); 17 | 18 | if (endIdx === -1) { 19 | core.setFailed( 20 | `Couldn't find the comment. Exiting!` 21 | ); 22 | } 23 | 24 | return [startIdx, endIdx] as const; 25 | }; 26 | -------------------------------------------------------------------------------- /src/utils/commitFile.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process'; 2 | 3 | const exec = (cmd: string, args: string[] = []) => 4 | new Promise((resolve, reject) => { 5 | const app = spawn(cmd, args, { stdio: 'pipe' }); 6 | let stdout = ''; 7 | app.stdout.on('data', (data) => { 8 | stdout = data; 9 | }); 10 | app.on('close', (code) => { 11 | if (code !== 0 && !stdout.includes('nothing to commit')) { 12 | const err = new Error(`Invalid status code: ${code}`) as any; 13 | err.code = code; 14 | return reject(err); 15 | } 16 | return resolve(code); 17 | }); 18 | app.on('error', reject); 19 | }); 20 | 21 | export const commitFile = async () => { 22 | await exec('git', [ 23 | 'config', 24 | '--global', 25 | 'user.email', 26 | '41898282+github-actions[bot]@users.noreply.github.com' 27 | ]); 28 | await exec('git', ['config', '--global', 'user.name', 'readme-bot']); 29 | await exec('git', ['add', 'README.md']); 30 | await exec('git', ['commit', '-m', 'Updated readme with learn section']); 31 | await exec('git', ['push']); 32 | }; 33 | -------------------------------------------------------------------------------- /src/utils/constructCategoriesMap.ts: -------------------------------------------------------------------------------- 1 | import { SelectSchemaUnit } from '@nishans/types'; 2 | import { ICategoryMap } from '../types'; 3 | 4 | export const constructCategoriesMap = (schema_unit: SelectSchemaUnit) => { 5 | const categories = schema_unit.options 6 | .map((option) => ({ 7 | color: option.color, 8 | value: option.value 9 | })) 10 | .sort((categoryA, categoryB) => 11 | categoryA.value > categoryB.value ? 1 : -1 12 | ); 13 | 14 | const categories_map: ICategoryMap = new Map(); 15 | 16 | categories.forEach((category) => { 17 | categories_map.set(category.value, { 18 | items: [], 19 | ...category 20 | }); 21 | }); 22 | return categories_map; 23 | }; 24 | -------------------------------------------------------------------------------- /src/utils/constructNewContents.ts: -------------------------------------------------------------------------------- 1 | import { TTextColor } from '@nishans/types'; 2 | import qs from 'querystring'; 3 | import { ICategoryMap } from '../types'; 4 | 5 | const ColorMap: Record = { 6 | default: '505558', 7 | gray: '979a9b', 8 | brown: '695b55', 9 | orange: '9f7445', 10 | yellow: '9f9048', 11 | green: '467870', 12 | blue: '487088', 13 | purple: '6c598f', 14 | pink: '904d74', 15 | red: '9f5c58', 16 | teal: '467870' 17 | }; 18 | 19 | export const constructNewContents = ( 20 | categoriesMap: ICategoryMap, 21 | colorSchemaUnitKey: string, 22 | base64SchemaUnitKey: string 23 | ) => { 24 | const newContents: string[] = []; 25 | for (const [category, categoryInfo] of categoriesMap) { 26 | const content = [ 27 | `

` 30 | ]; 31 | categoryInfo.items.forEach((item) => { 32 | const title = item.title && item.title[0][0]; 33 | if (!title) 34 | throw new Error(`Each row must have value in the Name column`); 35 | let logo: string = qs.escape(title); 36 | // At first check if the user provided a base64 encoded svg logo 37 | if (item[base64SchemaUnitKey]?.[0][0]) { 38 | logo = item[base64SchemaUnitKey][0][0]; 39 | } 40 | content.push( 41 | `${title}` 44 | ); 45 | }); 46 | newContents.push(...content, '
'); 47 | } 48 | return newContents; 49 | }; 50 | -------------------------------------------------------------------------------- /src/utils/fetchData.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import { HttpClient } from '@actions/http-client'; 3 | import { RecordMap, TData } from '@nishans/types'; 4 | 5 | export const fetchData = async ( 6 | id: string, 7 | table: keyof RecordMap, 8 | http: HttpClient 9 | ) => { 10 | const response = await http.post( 11 | `https://www.notion.so/api/v3/syncRecordValues`, 12 | JSON.stringify({ 13 | requests: [ 14 | { 15 | id, 16 | table, 17 | version: -1 18 | } 19 | ] 20 | }) 21 | ); 22 | 23 | const body = JSON.parse(await response.readBody()) as { 24 | recordMap: RecordMap; 25 | }; 26 | 27 | const data = body.recordMap[table]![id].value as T; 28 | 29 | if (!data) { 30 | core.setFailed( 31 | `Either your NOTION_TOKEN_V2 has expired or a ${table} with id:${id} doesn't exist` 32 | ); 33 | } 34 | 35 | return data; 36 | }; 37 | -------------------------------------------------------------------------------- /src/utils/getSchemaEntries.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import { 3 | Schema, 4 | SelectSchemaUnit, 5 | TextSchemaUnit, 6 | TitleSchemaUnit 7 | } from '@nishans/types'; 8 | 9 | export const getSchemaEntries = (schema: Schema) => { 10 | const schemaEntries = Object.entries(schema); 11 | let categorySchemaEntry: [string, SelectSchemaUnit] = undefined as any, 12 | nameSchemaEntry: [string, TitleSchemaUnit] = undefined as any, 13 | colorSchemaEntry: [string, TextSchemaUnit] = undefined as any, 14 | base64SchemaEntry: [string, TextSchemaUnit] = undefined as any; 15 | 16 | schemaEntries.forEach((schemaEntry) => { 17 | if (schemaEntry[1].type === 'text' && schemaEntry[1].name === 'Color') { 18 | colorSchemaEntry = schemaEntry as [string, TextSchemaUnit]; 19 | } else if ( 20 | schemaEntry[1].type === 'title' && 21 | schemaEntry[1].name === 'Name' 22 | ) { 23 | nameSchemaEntry = schemaEntry as [string, TitleSchemaUnit]; 24 | } else if ( 25 | schemaEntry[1].type === 'select' && 26 | schemaEntry[1].name === 'Category' 27 | ) { 28 | categorySchemaEntry = schemaEntry as [string, SelectSchemaUnit]; 29 | } else if ( 30 | schemaEntry[1].type === 'text' && 31 | schemaEntry[1].name === 'Base64' 32 | ) { 33 | base64SchemaEntry = schemaEntry as [string, TextSchemaUnit]; 34 | } 35 | }); 36 | 37 | if (!categorySchemaEntry) 38 | core.setFailed( 39 | "Couldn't find Category named select type column in the database" 40 | ); 41 | if (!nameSchemaEntry) 42 | core.setFailed( 43 | "Couldn't find Color named text type column in the database" 44 | ); 45 | if (!colorSchemaEntry) 46 | core.setFailed( 47 | "Couldn't find Name named title type column in the database" 48 | ); 49 | return [ 50 | categorySchemaEntry, 51 | colorSchemaEntry, 52 | nameSchemaEntry, 53 | base64SchemaEntry 54 | ] as const; 55 | }; 56 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { checkForSections } from './checkForSections'; 2 | import { commitFile } from './commitFile'; 3 | import { constructCategoriesMap } from './constructCategoriesMap'; 4 | import { constructNewContents } from './constructNewContents'; 5 | import { fetchData } from './fetchData'; 6 | import { getSchemaEntries } from './getSchemaEntries'; 7 | import { modifyRows } from './modifyRows'; 8 | import { populateCategoriesMapItems } from './populateCategoriesMapItems'; 9 | 10 | export const ActionUtils = { 11 | checkForSections, 12 | commitFile, 13 | constructCategoriesMap, 14 | constructNewContents, 15 | fetchData, 16 | getSchemaEntries, 17 | modifyRows, 18 | populateCategoriesMapItems 19 | }; 20 | -------------------------------------------------------------------------------- /src/utils/modifyRows.ts: -------------------------------------------------------------------------------- 1 | import { IPage, RecordMap } from '@nishans/types'; 2 | 3 | /** 4 | * Sorts an array of page blocks by their title 5 | * @param recordMap Record map to sort blocks from 6 | * @param databaseId Database id to filter block 7 | * @returns An array of pages sorted by their title 8 | */ 9 | export const modifyRows = ( 10 | recordMap: Pick, 11 | databaseId: string 12 | ) => { 13 | return Object.values(recordMap.block) 14 | .filter((block) => block.value.id !== databaseId) 15 | .map((block) => block.value as IPage) 16 | .sort((rowA, rowB) => 17 | rowA.properties.title[0][0] > rowB.properties.title[0][0] ? 1 : -1 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/utils/populateCategoriesMapItems.ts: -------------------------------------------------------------------------------- 1 | import { IPage } from '@nishans/types'; 2 | import { ICategoryMap } from '../types'; 3 | 4 | export const populateCategoriesMapItems = ( 5 | rows: IPage[], 6 | category_schema_id: string, 7 | categories_map: ICategoryMap 8 | ) => { 9 | rows.forEach((row) => { 10 | const category = 11 | row.properties[category_schema_id] && 12 | row.properties[category_schema_id][0][0]; 13 | if (!category) throw new Error('Each row must have a category value'); 14 | const category_value = categories_map.get(category); 15 | category_value!.items.push(row.properties); 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /tests/action.test.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import { HttpClient } from '@actions/http-client'; 3 | import { Schema, SelectSchemaUnit } from '@nishans/types'; 4 | import fs from 'fs'; 5 | import { action } from '../src/action'; 6 | import { ActionUtils } from '../src/utils'; 7 | 8 | afterEach(() => { 9 | jest.restoreAllMocks(); 10 | }); 11 | 12 | it(`Should work`, async () => { 13 | const GITHUB_WORKSPACE = `https://github.com/Devorein/github-readme-learn-section-notion`; 14 | process.env.GITHUB_WORKSPACE = GITHUB_WORKSPACE; 15 | 16 | const category_schema_unit: SelectSchemaUnit = { 17 | name: 'Category', 18 | options: [ 19 | { 20 | color: 'teal', 21 | id: '1', 22 | value: 'Runtime' 23 | }, 24 | { 25 | color: 'yellow', 26 | id: '2', 27 | value: 'Library' 28 | } 29 | ], 30 | type: 'select' 31 | }, 32 | block_3 = { 33 | id: 'block_3', 34 | properties: { 35 | title: [['Node.js']], 36 | color: 'green', 37 | category: [['Runtime']] 38 | } 39 | } as any, 40 | block_2 = { 41 | id: 'block_2', 42 | properties: { 43 | title: [['React']], 44 | color: 'blue', 45 | category: [['Library']] 46 | } 47 | } as any, 48 | schema = { 49 | title: { 50 | type: 'title', 51 | name: 'Name' 52 | }, 53 | color: { 54 | type: 'text', 55 | name: 'Color' 56 | }, 57 | category: category_schema_unit 58 | } as Schema, 59 | recordMap = { 60 | block: { 61 | block_1: { 62 | role: 'editor', 63 | value: { 64 | id: 'block_1' 65 | } 66 | }, 67 | block_2: { 68 | role: 'editor', 69 | value: block_2 70 | }, 71 | block_3: { 72 | role: 'editor', 73 | value: block_3 74 | } 75 | } 76 | } as any; 77 | 78 | const getInputMock = jest 79 | .spyOn(core, 'getInput') 80 | .mockImplementationOnce(() => 'token_v2') 81 | .mockImplementationOnce(() => 'block_1'); 82 | const coreInfo = jest.spyOn(core, 'info'); 83 | jest 84 | .spyOn(ActionUtils, 'fetchData') 85 | .mockImplementationOnce(async () => { 86 | return { 87 | collection_id: 'collection_1', 88 | space_id: 'space_id', 89 | view_ids: ['view_1'] 90 | } as any; 91 | }) 92 | .mockImplementationOnce(async () => { 93 | return { 94 | schema 95 | } as any; 96 | }); 97 | 98 | let http = new HttpClient(); 99 | 100 | jest.spyOn(http, 'post' as any).mockImplementationOnce(async () => { 101 | return { 102 | async resBody() { 103 | return JSON.stringify({ recordMap }); 104 | } 105 | }; 106 | }); 107 | 108 | jest.spyOn(ActionUtils, 'getSchemaEntries').mockImplementationOnce(() => { 109 | return [ 110 | ['category', category_schema_unit], 111 | ['color', { name: 'Color', type: 'text' }], 112 | ['title', { name: 'Name', type: 'title' }], 113 | ['base64', { name: 'Base64', type: 'text' }] 114 | ]; 115 | }); 116 | 117 | jest 118 | .spyOn(ActionUtils, 'modifyRows') 119 | .mockImplementationOnce(() => [block_3, block_2]); 120 | 121 | jest.spyOn(ActionUtils, 'constructCategoriesMap').mockImplementationOnce( 122 | () => 123 | new Map([ 124 | [ 125 | 'Runtime', 126 | { 127 | color: 'teal', 128 | items: [] 129 | } 130 | ], 131 | [ 132 | 'Library', 133 | { 134 | color: 'yellow', 135 | items: [] 136 | } 137 | ] 138 | ]) 139 | ); 140 | const readFileSyncMock = jest 141 | .spyOn(fs, 'readFileSync') 142 | .mockImplementationOnce( 143 | () => 144 | '# Header\nfirst\n\n\nsecond' 145 | ); 146 | jest 147 | .spyOn(ActionUtils, 'checkForSections') 148 | .mockImplementationOnce(() => [2, 3]); 149 | 150 | jest.spyOn(ActionUtils, 'constructNewContents').mockImplementation(() => { 151 | return ['new line 1', 'new line 2']; 152 | }); 153 | const writeFileSyncMock = jest 154 | .spyOn(fs, 'writeFileSync') 155 | .mockImplementationOnce(() => undefined); 156 | jest 157 | .spyOn(ActionUtils, 'commitFile') 158 | .mockImplementationOnce(async () => undefined); 159 | 160 | await action(); 161 | 162 | expect(coreInfo).toHaveBeenNthCalledWith(1, 'Fetched database'); 163 | expect(coreInfo).toHaveBeenNthCalledWith(2, 'Fetched collection'); 164 | expect(coreInfo).toHaveBeenNthCalledWith(3, 'Fetched rows'); 165 | expect(coreInfo).toHaveBeenNthCalledWith( 166 | 4, 167 | `Reading from ${GITHUB_WORKSPACE}/README.md` 168 | ); 169 | expect(coreInfo).toHaveBeenNthCalledWith( 170 | 5, 171 | `Writing to ${GITHUB_WORKSPACE}/README.md` 172 | ); 173 | expect(getInputMock).toHaveBeenNthCalledWith(1, 'token_v2'); 174 | expect(getInputMock).toHaveBeenNthCalledWith(2, 'database_id'); 175 | expect(readFileSyncMock).toHaveBeenCalledWith( 176 | `${GITHUB_WORKSPACE}/README.md`, 177 | 'utf-8' 178 | ); 179 | expect(writeFileSyncMock).toHaveBeenCalledWith( 180 | `${GITHUB_WORKSPACE}/README.md`, 181 | '# Header\nfirst\n\nnew line 1\nnew line 2\n\nsecond', 182 | 'utf-8' 183 | ); 184 | }); 185 | -------------------------------------------------------------------------------- /tests/utils/checkForSections.test.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import { checkForSections } from '../../src/utils/checkForSections'; 3 | 4 | afterEach(() => { 5 | jest.restoreAllMocks(); 6 | }); 7 | 8 | it(`Should return correct start and end index`, () => { 9 | const [startIdx, endIdx] = checkForSections([ 10 | '1', 11 | '', 12 | '2', 13 | '', 14 | '3' 15 | ]); 16 | expect(startIdx).toBe(1); 17 | expect(endIdx).toBe(3); 18 | }); 19 | 20 | it(`Should return correct start and end index`, () => { 21 | const setFailedMock = jest.spyOn(core, 'setFailed'); 22 | 23 | const [startIdx, endIdx] = checkForSections(['1', '2', '3']); 24 | expect(setFailedMock).toHaveBeenNthCalledWith( 25 | 1, 26 | `Couldn't find the comment. Exiting!` 27 | ); 28 | expect(setFailedMock).toHaveBeenNthCalledWith( 29 | 2, 30 | `Couldn't find the comment. Exiting!` 31 | ); 32 | expect(startIdx).toBe(-1); 33 | expect(endIdx).toBe(-1); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/utils/constructCategoriesMap.test.ts: -------------------------------------------------------------------------------- 1 | import { constructCategoriesMap } from '../../src/utils/constructCategoriesMap'; 2 | 3 | it(`Should work`, () => { 4 | const option_1 = { 5 | color: 'blue', 6 | value: 'A' 7 | } as any, 8 | option_2 = { 9 | color: 'yellow', 10 | value: 'C' 11 | } as any, 12 | option_3 = { 13 | color: 'red', 14 | value: 'B' 15 | } as any; 16 | const categories_map = constructCategoriesMap({ 17 | name: 'Options', 18 | options: [option_1, option_2, option_3], 19 | type: 'select' 20 | }); 21 | 22 | expect(Array.from(categories_map.entries())).toStrictEqual([ 23 | [ 24 | 'A', 25 | { 26 | items: [], 27 | ...option_1 28 | } 29 | ], 30 | [ 31 | 'B', 32 | { 33 | items: [], 34 | ...option_3 35 | } 36 | ], 37 | [ 38 | 'C', 39 | { 40 | items: [], 41 | ...option_2 42 | } 43 | ] 44 | ]); 45 | }); 46 | -------------------------------------------------------------------------------- /tests/utils/constructNewContents.test.ts: -------------------------------------------------------------------------------- 1 | import { constructNewContents } from '../../src/utils/constructNewContents'; 2 | 3 | it('Should Work', () => { 4 | const categories_map = new Map([ 5 | [ 6 | 'Tech Tools', 7 | { 8 | items: [ 9 | { 10 | title: [['React']], 11 | color: [['blue']] 12 | }, 13 | { 14 | title: [['Apollo Graphql']] 15 | }, 16 | { 17 | title: [['Terraform']], 18 | base64: [['base64']] 19 | } 20 | ], 21 | color: 'teal' 22 | } 23 | ] 24 | ]) as any; 25 | 26 | const newContents = constructNewContents(categories_map, 'color', 'base64'); 27 | expect(newContents).toStrictEqual([ 28 | '

', 29 | 'React', 30 | 'Apollo Graphql', 31 | 'Terraform', 32 | '
' 33 | ]); 34 | }); 35 | 36 | it('Should throw error if title not present', () => { 37 | const categories_map = new Map([ 38 | [ 39 | 'Tech Tools', 40 | { 41 | items: [{}], 42 | color: 'teal' 43 | } 44 | ] 45 | ]) as any; 46 | 47 | expect(() => 48 | constructNewContents(categories_map, 'color', 'base64') 49 | ).toThrow(); 50 | }); 51 | -------------------------------------------------------------------------------- /tests/utils/fetchData.test.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import { HttpClient } from '@actions/http-client'; 3 | import { fetchData } from '../../src/utils/fetchData'; 4 | 5 | afterEach(() => { 6 | jest.restoreAllMocks(); 7 | }); 8 | 9 | it(`Should fetch data successfully`, async () => { 10 | let http = new HttpClient(); 11 | 12 | const syncRecordValuesMock = jest 13 | .spyOn(http, 'post' as any) 14 | .mockImplementationOnce(async () => { 15 | return { 16 | async readBody() { 17 | return JSON.stringify({ 18 | recordMap: { 19 | block: { 20 | block_1: { 21 | role: 'comment_only', 22 | value: { 23 | id: 'block_1' 24 | } 25 | } 26 | } 27 | } 28 | }); 29 | } 30 | }; 31 | }); 32 | 33 | const data = await fetchData('block_1', 'block', http); 34 | 35 | expect(data).toStrictEqual({ 36 | id: 'block_1' 37 | }); 38 | expect(syncRecordValuesMock).toHaveBeenCalledWith( 39 | `https://www.notion.so/api/v3/syncRecordValues`, 40 | JSON.stringify({ 41 | requests: [ 42 | { 43 | id: 'block_1', 44 | table: 'block', 45 | version: -1 46 | } 47 | ] 48 | }) 49 | ); 50 | }); 51 | 52 | it(`Should not fetch data`, async () => { 53 | let http = new HttpClient(); 54 | const setFailed = jest.spyOn(core, 'setFailed'); 55 | const syncRecordValuesMock = jest 56 | .spyOn(http, 'post' as any) 57 | .mockImplementationOnce(async () => { 58 | return { 59 | async readBody() { 60 | return JSON.stringify({ 61 | recordMap: { 62 | block: { 63 | block_1: { 64 | role: 'comment_only' 65 | } 66 | } as any 67 | } 68 | }); 69 | } 70 | }; 71 | }); 72 | 73 | const data = await fetchData('block_1', 'block', http); 74 | 75 | expect(data).toStrictEqual(undefined); 76 | expect(syncRecordValuesMock).toHaveBeenCalledWith( 77 | `https://www.notion.so/api/v3/syncRecordValues`, 78 | JSON.stringify({ 79 | requests: [ 80 | { 81 | id: 'block_1', 82 | table: 'block', 83 | version: -1 84 | } 85 | ] 86 | }) 87 | ); 88 | expect(setFailed).toHaveBeenCalledWith( 89 | `Either your NOTION_TOKEN_V2 has expired or a block with id:block_1 doesn't exist` 90 | ); 91 | }); 92 | -------------------------------------------------------------------------------- /tests/utils/getSchemaEntries.test.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import { 3 | SelectSchemaUnit, 4 | TextSchemaUnit, 5 | TitleSchemaUnit 6 | } from '@nishans/types'; 7 | import { getSchemaEntries } from '../../src/utils/getSchemaEntries'; 8 | 9 | afterEach(() => { 10 | jest.restoreAllMocks(); 11 | }); 12 | 13 | it(`Should find both color and category entries`, () => { 14 | const color_schema_unit = { 15 | type: 'text', 16 | name: 'Color' 17 | } as TextSchemaUnit, 18 | category_schema_unit = { 19 | type: 'select', 20 | name: 'Category', 21 | options: [] 22 | } as SelectSchemaUnit, 23 | title_schema_unit = { 24 | type: 'title', 25 | name: 'Name', 26 | options: [] 27 | } as TitleSchemaUnit; 28 | const [ 29 | category_schema_entry, 30 | color_schema_entry, 31 | title_schema_entry 32 | ] = getSchemaEntries({ 33 | color: color_schema_unit, 34 | category: category_schema_unit, 35 | title: title_schema_unit 36 | }); 37 | 38 | expect(category_schema_entry).toStrictEqual([ 39 | 'category', 40 | category_schema_unit 41 | ]); 42 | 43 | expect(title_schema_entry).toStrictEqual(['title', title_schema_unit]); 44 | expect(color_schema_entry).toStrictEqual(['color', color_schema_unit]); 45 | }); 46 | 47 | it(`Should not find both color, title, category entries`, () => { 48 | const color_schema_unit = { 49 | type: 'text', 50 | name: 'color' 51 | } as TextSchemaUnit, 52 | category_schema_unit = { 53 | type: 'select', 54 | name: 'category', 55 | options: [] 56 | } as SelectSchemaUnit, 57 | title_schema_unit = { 58 | type: 'title', 59 | name: 'name', 60 | options: [] 61 | } as TitleSchemaUnit; 62 | 63 | const setFailedMock = jest.spyOn(core, 'setFailed'); 64 | 65 | const [ 66 | category_schema_entry, 67 | color_schema_entry, 68 | title_schema_entry 69 | ] = getSchemaEntries({ 70 | color: color_schema_unit, 71 | category: category_schema_unit, 72 | title: title_schema_unit 73 | }); 74 | 75 | expect(setFailedMock).toHaveBeenNthCalledWith( 76 | 1, 77 | "Couldn't find Category named select type column in the database" 78 | ); 79 | expect(setFailedMock).toHaveBeenNthCalledWith( 80 | 2, 81 | "Couldn't find Color named text type column in the database" 82 | ); 83 | expect(setFailedMock).toHaveBeenNthCalledWith( 84 | 3, 85 | "Couldn't find Name named title type column in the database" 86 | ); 87 | expect(category_schema_entry).toStrictEqual(undefined); 88 | expect(color_schema_entry).toStrictEqual(undefined); 89 | expect(title_schema_entry).toStrictEqual(undefined); 90 | }); 91 | -------------------------------------------------------------------------------- /tests/utils/modifyRows.test.ts: -------------------------------------------------------------------------------- 1 | import { modifyRows } from '../../src/utils/modifyRows'; 2 | 3 | it('Should work', () => { 4 | const rows = modifyRows( 5 | { 6 | block: { 7 | block_1: { 8 | role: 'editor', 9 | value: { 10 | properties: { 11 | title: [['A']] 12 | } 13 | } as any 14 | }, 15 | block_2: { 16 | role: 'editor', 17 | value: { 18 | properties: { 19 | title: [['C']] 20 | } 21 | } as any 22 | }, 23 | block_3: { 24 | role: 'editor', 25 | value: { 26 | properties: { 27 | title: [['B']] 28 | } 29 | } as any 30 | } 31 | } 32 | }, 33 | 'block_4' 34 | ); 35 | expect(rows).toStrictEqual([ 36 | { 37 | properties: { 38 | title: [['A']] 39 | } 40 | }, 41 | { 42 | properties: { 43 | title: [['B']] 44 | } 45 | }, 46 | { 47 | properties: { 48 | title: [['C']] 49 | } 50 | } 51 | ]); 52 | }); 53 | -------------------------------------------------------------------------------- /tests/utils/populateCategoriesMapItems.test.ts: -------------------------------------------------------------------------------- 1 | import { IPage } from '@nishans/types'; 2 | import { ICategoryMap } from '../../src/types'; 3 | import { populateCategoriesMapItems } from '../../src/utils/populateCategoriesMapItems'; 4 | 5 | it(`Should work`, () => { 6 | const category_map: ICategoryMap = new Map([ 7 | [ 8 | 'Library', 9 | { 10 | color: 'teal', 11 | items: [] 12 | } 13 | ] 14 | ]); 15 | const rows: IPage[] = [ 16 | { 17 | properties: { 18 | title: [['React']], 19 | category: [['Library']] 20 | } 21 | } as any 22 | ]; 23 | populateCategoriesMapItems(rows, 'category', category_map); 24 | 25 | expect(Array.from(category_map.entries())).toStrictEqual([ 26 | [ 27 | 'Library', 28 | { 29 | color: 'teal', 30 | items: [ 31 | { 32 | title: [['React']], 33 | category: [['Library']] 34 | } 35 | ] 36 | } 37 | ] 38 | ]); 39 | }); 40 | 41 | it(`Should fail if category not provided`, () => { 42 | const category_map: ICategoryMap = new Map([ 43 | [ 44 | 'Library', 45 | { 46 | color: 'teal', 47 | items: [] 48 | } 49 | ] 50 | ]); 51 | const rows: IPage[] = [ 52 | { 53 | properties: { 54 | title: [['React']] 55 | } 56 | } as any 57 | ]; 58 | expect(() => 59 | populateCategoriesMapItems(rows, 'category', category_map) 60 | ).toThrow(); 61 | }); 62 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 4 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 5 | "lib": [ 6 | "es6" 7 | ], 8 | "strict": false, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "outDir": "./build", 12 | "moduleResolution": "node", 13 | "removeComments": true, 14 | "noImplicitAny": true, 15 | "strictNullChecks": true, 16 | "strictFunctionTypes": true, 17 | "noImplicitThis": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noImplicitReturns": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "allowSyntheticDefaultImports": true, 23 | "esModuleInterop": true, 24 | "emitDecoratorMetadata": true, 25 | "experimentalDecorators": true, 26 | "resolveJsonModule": true, 27 | "baseUrl": ".", 28 | "incremental": false 29 | }, 30 | "exclude": [ 31 | "__tests__", 32 | "build", 33 | "node_modules" 34 | ] 35 | } --------------------------------------------------------------------------------