[
  {
    "path": ".gitattributes",
    "content": "*.php text eol=lf\n/.github/ export-ignore\n/docs/ export-ignore\n/test/ export-ignore\n/.gitattributes export-ignore\n/.gitignore export-ignore\n/CHANGELOG.md export-ignore\n/composer.lock export-ignore\n/infection.json export-ignore\n/phpcs.xml export-ignore\n/phpunit.xml export-ignore\n/psalm.xml export-ignore\n/README.md export-ignore\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: [mariosimao]\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: \"[BUG] \"\nlabels: bug\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Code snippet**\nIf applicable, add code snippets to help explain your problem.\n\n**Environment (please complete the following information):**\n - OS: [e.g. iOS]\n - SDK Version [e.g. v1.0.0]\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: \"[FEATURE] \"\nlabels: enhancement\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where the package manifests are located.\n# Please see the documentation for all configuration options:\n# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates\n\nversion: 2\nupdates:\n  - package-ecosystem: \"composer\" # See documentation for possible values\n    directory: \"/\" # Location of package manifests\n    schedule:\n      interval: \"weekly\"\n"
  },
  {
    "path": ".github/workflows/coding-standards.yml",
    "content": "name: \"Check Coding Standards\"\n\non:\n  pull_request:\n  push:\n    branches:\n      - main\n\njobs:\n  coding-standards:\n    name: \"Check Coding Standards\"\n\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v2\n\n      - name: Install PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: \"8.1\"\n          tools: composer:v2, cs2pr\n\n      - name: Install composer dependencies\n        uses: ramsey/composer-install@1.3.0\n        with:\n          dependency-versions: locked\n          composer-options: --no-ansi --no-interaction --no-progress\n\n      - name: Run phpcs checks\n        run: \"composer ci:phpcs\""
  },
  {
    "path": ".github/workflows/deploy.yml",
    "content": "name: Deploy\n\non:\n  push:\n    branches:\n      - main\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 16\n      - run: cd docs && npm ci\n\n      - name: Build\n        run: cd docs && npm run docs:build\n\n      - name: Deploy\n        uses: peaceiris/actions-gh-pages@v3\n        with:\n          github_token: ${{ secrets.GITHUB_TOKEN }}\n          publish_dir: docs/.vitepress/dist\n"
  },
  {
    "path": ".github/workflows/psalm.yml",
    "content": "name: \"Static Analysis (Psalm)\"\n\non:\n  pull_request:\n  push:\n    branches:\n      - main\n\njobs:\n  psalm:\n    name: Psalm\n\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v2\n\n      - name: Install PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: \"8.1\"\n          tools: none\n\n      - name: Install composer dependencies\n        uses: ramsey/composer-install@v3\n        with:\n          dependency-versions: locked\n          composer-options: --no-ansi --no-interaction --no-progress\n\n      - name: Run tests with psalm and send coverage report\n        run: composer ci:psalm"
  },
  {
    "path": ".github/workflows/sponsors.yml",
    "content": "name: Generate Sponsors section on README\non:\n  workflow_dispatch:\n  schedule:\n    - cron: 30 15 * * 0-6\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v2\n\n      - name: Generate Sponsors\n        uses: JamesIves/github-sponsors-readme-action@v1\n        with:\n          token: ${{ secrets.PAT }}\n          file: 'README.md'\n          minimum: 1000\n          \n      - name: Create Pull Request\n        uses: peter-evans/create-pull-request@v5\n"
  },
  {
    "path": ".github/workflows/tests.yml",
    "content": "on:\n  pull_request:\n  push:\n    branches:\n      - main\n\nname: Tests\n\njobs:\n  unit-tests:\n    name: Unit tests (PHPUnit)\n\n    runs-on: ${{ matrix.os }}\n\n    strategy:\n      fail-fast: false\n      matrix:\n        os:\n          - ubuntu-latest\n\n        php-version:\n          - \"8.1\"\n\n        dependencies:\n          # - lowest\n          - highest\n          - locked\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v2\n\n      - name: Install PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: ${{ matrix.php-version }}\n          tools: none\n\n      - name: Install composer dependencies\n        uses: ramsey/composer-install@v3\n        with:\n          dependency-versions: ${{ matrix.dependencies }}\n\n      - name: Run tests with phpunit\n        run: |\n          composer config --no-plugins allow-plugins.infection/extension-installer true\n          composer ci:unit\n\n  # integration-tests:\n  #   name: Integration tests (PHPUnit)\n\n  #   runs-on: ubuntu-latest\n\n  #   steps:\n  #     - name: Checkout\n  #       uses: actions/checkout@v2\n\n  #     - name: Install PHP\n  #       uses: shivammathur/setup-php@v2\n  #       with:\n  #         php-version: \"8.1\"\n  #         tools: none\n\n  #     - name: Install composer dependencies\n  #       uses: ramsey/composer-install@v3\n  #       with:\n  #         dependency-versions: ${{ matrix.locked }}\n  #         composer-options: --no-ansi --no-interaction --no-progress\n\n  #     - name: Run tests with phpunit\n  #       env:\n  #         NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}\n  #         TEST_PAGE_ID: ${{ secrets.TEST_PAGE_ID }}\n  #       run: composer ci:integration\n\n  mutation-tests:\n    name: \"Mutation tests (Infection)\"\n\n    # if: ${{ always() }}\n    # needs: [coverage]\n\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v2\n\n      - name: Install PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: \"8.1\"\n          coverage: \"pcov\"\n          tools: none\n\n      - name: Install composer dependencies\n        uses: ramsey/composer-install@v3\n        with:\n          dependency-versions: locked\n          composer-options: --no-ansi --no-interaction --no-progress\n\n      - name: Run mutation tests with Infection\n        env:\n          NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}\n          TEST_PAGE_ID: ${{ secrets.TEST_PAGE_ID }}\n        run: composer ci:mutation\n\n  coverage:\n    name: Test coverage (PHPUnit)\n\n    # if: ${{ always() }}\n    # needs: [mutation-tests]\n\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v2\n\n      - name: Install PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: \"8.1\"\n          coverage: pcov\n          tools: none\n\n      - name: Install composer dependencies\n        uses: ramsey/composer-install@v3\n        with:\n          dependency-versions: locked\n          composer-options: --no-ansi --no-interaction --no-progress\n\n      - name: Calculate test coverage\n        env:\n          NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}\n          TEST_PAGE_ID: ${{ secrets.TEST_PAGE_ID }}\n        run: composer ci:coverage\n\n      - name: Send code coverage report to Codecov.io\n        uses: codecov/codecov-action@v5\n        env:\n          CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": ".infection\n.phpunit.cache\ncoverage\ndist\ntest.php\nvendor\nnode_modules\n.idea/\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [Unreleased]\n\n## [1.17.0] 2026-03-11\n\n### Fixed\n- Broken API requests after breaking change on archived property (#419)\n\n## [v1.16.0] 2026-01-11\n\n### Added\n- Possibility to use custom Markdown renderers.\n- Full support to file upload.\n\n## [v1.15.0] 2025-09-22\n\n### Fixed\n- Error when updating pages with People properties.\n\n### Build\n- Fix failing CI actions due to outdated dependencies.\n\n## [v1.14.0] 2024-04-22\n\n### Added\n- 'Change' methods to page properties (#342)\n- Description to property metadata (#365)\n\n### Docs\nUpdate documentation website URL (#364)\n\n### Build\n- Bump vite from 3.2.7 to 3.2.8 in /docs (#335)\n- Bump squizlabs/php_codesniffer from 3.8.0 to 3.8.1 (#334)\n- Bump phpunit/phpunit from 10.5.5 to 10.5.7 (#332)\n- Bump vimeo/psalm from 5.18.0 to 5.19.0 (#333)\n- Bump phpunit/phpunit from 10.5.7 to 10.5.10 (#344)\n- Bump squizlabs/php_codesniffer from 3.8.1 to 3.9.0 (#348)\n- Bump vite from 3.2.8 to 3.2.10 in /docs (#363)\n- Bump squizlabs/php_codesniffer from 3.9.0 to 3.9.1 (#361)\n- Bump phpunit/phpunit from 10.5.10 to 10.5.16 (#362)\n- Bump infection/infection from 0.27.9 to 0.27.11 (#359)\n\n## [v1.13.0] 2024-01-10\n\n### Added\n- Support to inline databases (#330)\n\n### Build\n- Bump infection/infection from 0.27.8 to 0.27.9 (#327)\n- Bump squizlabs/php_codesniffer from 3.7.2 to 3.8.0 (#328)\n- Bump phpunit/phpunit from 10.5.3 to 10.5.5 (#329)\n\n## [v1.12.0] 2023-12-31\n\n### Added\n- Add support to tables (#224)\n\n### Fixed\n- Allow DateFilter to filter created_time and last_edited_time\n- Add formula to pages notUpdatableProps (#320)\n- Use null fallback for missing array keys for optional user properties (#324)\n\n### Documentation\n- Fix page property example (#293)\n- Recommend static analysers (#304)\n\n### Build\n- Bump infection/infection from 0.27.2 to 0.27.8 (#309)\n- Bump phpunit/phpunit from 10.3.4 to 10.4.2 (#305)\n- Bump postcss from 8.4.24 to 8.4.31 in /docs (#294)\n- Bump phpunit/phpunit from 10.4.2 to 10.5.3 (#322)\n- Bump vimeo/psalm from 5.15.0 to 5.18.0 (#321)\n- Bump php-http/discovery from 1.19.1 to 1.19.2 (#316)\n- Bump guzzlehttp/guzzle from 7.8.0 to 7.8.1 (#315)\n- Bump brianium/paratest from 7.2.7 to 7.3.1 (#306)\n- Fix tests CI (#325)\n\n## [v1.11.1] 2023-09-25\n\n### Fixed\n- Error when Notion sends an empty response (#289)\n### Build\n- Bump brianium/paratest from 7.2.3 to 7.2.4 (#274)\n- Bump vimeo/psalm from 5.14.0 to 5.14.1 (#275)\n- Bump brianium/paratest from 7.2.4 to 7.2.5 (#277)\n- Bump phpunit/phpunit from 10.3.1 to 10.3.2 (#278)\n- Bump vimeo/psalm from 5.14.1 to 5.15.0 (#279)\n- Bump guzzlehttp/guzzle from 7.7.0 to 7.8.0 (#280)\n- Bump brianium/paratest from 7.2.5 to 7.2.6 (#283)\n- Bump phpunit/phpunit from 10.3.2 to 10.3.3 (#284)\n- Bump phpunit/phpunit from 10.3.3 to 10.3.4 (#286)\n- Bump brianium/paratest from 7.2.6 to 7.2.7 (#288)\n- Bump infection/infection from 0.27.0 to 0.27.2 (#287)\n\n## [v1.11.0] 2023-08-02\n\n### Added\n- UniqueId page and database property (#268)\n### Fixed\n- Do not update Unique ID property (#269)\n- Do not update Rollup properties (#272)\n### Build\n- Bump vimeo/psalm from 5.13.0 to 5.13.1 (#263)\n- Bump phpunit/phpunit from 10.2.2 to 10.2.3 (#262)\n- Bump phpunit/phpunit from 10.2.3 to 10.2.4 (#264)\n- Bump php-http/discovery from 1.19.0 to 1.19.1 (#266)\n- Bump phpunit/phpunit from 10.2.4 to 10.2.6 (#265)\n- Bump brianium/paratest from 7.2.2 to 7.2.3 (#267)\n- Bump vimeo/psalm from 5.13.1 to 5.14.0 (#271)\n\n## [v1.10.0] 2023-06-27\n\n### Added\n- Page properties with empty values (#260)\n\n### Build\n- Bump php-http/discovery from 1.18.1 to 1.19.0 (#254)\n- Bump brianium/paratest from 7.1.4 to 7.2.0 (#255)\n- Bump vimeo/psalm from 5.12.0 to 5.13.0 (#259)\n- Bump brianium/paratest from 7.2.0 to 7.2.2 (#258)\n\n### Documentation\n- Fix typo on `metadata` method (#253)\n\n## [v1.9.0] 2023-06-15\n\n### Added\n- Caption on image block and file objects (#250)\n\n### Fixed\n- Do not update CreatedBy prop on pages (#251)\n\n## [v1.8.1] 2023-06-12\n\n### Fixed\n- Rich Text mention creation (#241)\n- Possible null pointer on RichText (#246)\n- Change rich text URL\n- Only send to API file name when set (#234)\n\n### Build\n- Bump phpunit/phpunit from 10.1.2 to 10.1.3 (#227)\n- Bump guzzlehttp/guzzle from 7.5.1 to 7.6.1 (#226)\n- Bump php-http/discovery from 1.18.0 to 1.18.1 (#228)\n- Bump infection/infection from 0.26.21 to 0.27.0 (#229)\n- Bump guzzlehttp/guzzle from 7.6.1 to 7.7.0 (#230)\n- Bump vimeo/psalm from 5.11.0 to 5.12.0 (#233)\n- Bump vite from 3.1.3 to 3.2.7 in /docs (#236)\n- Bump phpunit/phpunit from 10.1.3 to 10.2.2 (#244)\n\n### Test\n- Fix file-related tests (#245)\n\n### Chore\n- Add sponsors to README (#237)\n\n## [v1.8.0] 2023-05-09\n\n### Added\n- Support block as page and database parent (#214)\n- `number_with_commas` format in Number database properties (#216)\n\n### Fixed\n- API error while updating page file property (#220)\n\n### Build\n- Bump php-http/discovery from 1.15.3 to 1.17.0 (#217)\n- Bump infection/infection from 0.26.20 to 0.26.21 (#218)\n- Bump brianium/paratest from 7.1.3 to 7.1.4 (#221)\n- Bump php-http/discovery from 1.17.0 to 1.18.0 (#222)\n- Bump vimeo/psalm from 5.9.0 to 5.11.0 (#223)\n\n## [1.7.0] 2023-04-26\n\n### Added\n- Support block colors (#209)\n- Render blocks as markdown (#207)\n\n### Fixed\n- API exception on query all pages (#211)\n\n### Build\n- Bump phpunit from 10.0.19 to 10.1.2 (#206)\n- Bump infection from 0.26.19 to 0.26.20 (#204)\n- Bump paratest from 7.1.2 to 7.1.3 (#205)\n- Bump guzzle from 7.5.0 to 7.5.1 (#203)\n\n## [1.6.2]\n\n### Fixed\n- Date filter JSON serialziation (#198)\n\n## [1.6.1]\n\n### Fixed\n- Do not update created time page property. (#187)\n\n## [1.6.0]\n\n### Added\n- This week on date filter for database queries. (#184)\n- Relation filter for database queries. (#185)\n\n## [1.5.0]\n\n### Added\n- Support unknown blocks and page/database properties. Pages and databases with unsuported resources will be loaded without errors. (#173)\n- Page and database properties collection with typed getters. (#179)\n- Search pages and databases.\n\n## [1.4.1]\n\n### Fixed\n- Fix `Page::getProperty()` typo (#170)\n\n### Chore\n- Update dependencies (#171)\n\n## [1.4.0]\n\n### Added\n- Support to comments (#163)\n\n### Fixed\n- Update options without color (#166)\n- Prevent `LastEditedBy` errors on page update (#167)\n\n### Documentation\n- Document `People` page property (#162)\n\n## [1.3.0]\n\n### Added\n- Configuration support for custom options (#155)\n- Retry after `conflict_error` (#155)\n- `changeColor` methods on `StatusOption` and `Status` property (#159)\n\n### Fixed\n- Status page property update (#159)\n\n### Documentation\n- Reflect breaking changes from v1 on the documentation.\n\n## [1.2.0]\n\n### Added\n- Support to nullable page properties (#149)\n  - Properties: `Date`, `Email`, `Number`, `PhoneNumber`, `Select`, `Url`.\n  - New method `isEmpty()` on those properties.\n- Support `Relation` database property (#150)\n\n## [1.1.0]\n\n### Added\n- Add Query::addSort() (#144)\n### Changed\n- Improve PropertyFactory Exception message (#141)\n- Deprecate Query::changeAddedSort() (#144)\n### Documentation\n- Correct documentation for `->change...()` methods (#145)\n### Internal\n- Remove toUpdateArray method from blocks (#136)\n- Increase unit test coverage (#137)\n- Add Intellij Idea default directory .idea to .gitignore (#142)\n- Allow usage of secrets while running tests from forks (#143)\n\n## [1.0.0]\n\n### Added\n- Add database description (#125)\n- Add support to toggleable headings (#126)\n- Add caption to Code block (#127)\n- Add support to Status property (#132)\n- Icon value object instead of `File|Emoji`\n### Changed\n- Unify constructor method names (#131)\n- Require PHP 8.1\n- Enums instead of constants for everything. Example: collor, block type, ...\n- Readonly public properties and removal of getters\n- Many method signatures were changed\n\n## [1.0.0-beta.1]\n\n### Added\n- Add database description (#125)\n- Add support to toggleable headings (#126)\n- Add caption to Code block (#127)\n- Add support to Status property (#132)\n\n### Changed\n- Unify constructor method names (#131)\n\n### Documentation\n- Express psr/http-client dependency on documentation (#128)\n- Document blocks (#129)\n\n## [1.0.0-beta.1]\n\n### Added\n- Icon value object instead of `File|Emoji`\n\n### Changed\n- Require PHP 8.1\n- Enums instead of constants for everything. Example: collor, block type, ...\n- Readonly public properties and removal of getters\n- Many method signatures were changed\n\n## [0.6.2] - 2022-08-19\n### Fixed\n- Missing `Files` page property (#105)\n\n### Documentation\n- How to add and update page properties (#104)\n- How to get page content (#106)\n\n## [0.6.1] - 2022-08-04\n\n### Added\n- Documentation website\n\n### Fixed\n- Method typo ~~`Block::lastEditedType()`~~ `Block::lastEditedTime()`\n\n## [0.6.0] - 2022-07-04\n\n### Added\n\n- Add URL support to RichText objects (#89)\n\n### Fixed\n\n- Wrong object to array conversion\n\n## [0.5.2] - 2022-06-22\n\n### Fixed\n- Handle empty value for select property (#86)\n\n## [0.5.1] - 2022-06-02\n\n### Fixed\n- Add support to internal cover image (#80)\n\n## [0.5.0] - 2022-05-12\n\n### Added\n- Query database (#5 and #75)\n\n## [0.4.0] - 2022-03-24\n\n### Added\n- How to documentation for pages\n- Find block (#58)\n- Update block (#59)\n- Append blocks (#60)\n- Delete block (#61)\n\n### Changed\n- Notion version to `2022-02-22` (#69)\n\n## [0.3.0] - 2021-12-04\n\n### Added\n- Find block children\n- Find block children recursively\n- Link preview block\n- Column and column list blocks\n\n### Changed\n- Blocks `withChildren()` methods renamed to `changeChildren()`\n## [0.2.0] - 2021-11-20\n### Added\n- Breadcrumb block\n- Support discovery of more PSR clients with `php-http/discovery`\n\n### Changed\n- Clients require implementations of `RequestFactoryInterface`\n- Rename `Notion\\Client::createWithPsrClient()` to `createWithPsrImplementations()`\n- Rename `Notion\\Client` to `Notion\\Notion`\n- Rename `Notion\\Databases\\Database::withTitle()` to `withAdvancedTitle()`\n- Use `list<RichText>` instead of `...RichText` on\n  - `Bookmark::withCaption()`\n  - `BulletedListItem::withText()`\n  - `Callout::withText()`\n  - `Code::withText()`\n  - `Heading1::withText()`\n  - `Heading2::withText()`\n  - `Heading3::withText()`\n  - `NumberedListItem::withText()`\n  - `Paragraph::withText()`\n  - `Quote::withText()`\n  - `ToDo::withText()`\n  - `Toggle::withText()`\n  - `Database::withAdvancedTitle()`\n  - `Title::withRichTexts()`\n  - `RichTextProperty::withText()`\n- Use `list<BlockInterface>` instead of `...BlockInterface` on\n  - `BulletedListItem::withChildren()`\n  - `Callout::withChildren()`\n  - `NumberedListItem::withChildren()`\n  - `Paragraph::withChildren()`\n  - `Quote::withChildren()`\n  - `ToDo::withChildren()`\n  - `Toggle::withChildren()`\n  - `Notion\\Pages\\Client::create()`\n- Use `list<SelectOption>` instead of `...SelectOption` on\n  - `Select::withOptions()`\n  - `MultiSelect::withOptions()`\n- Use `list<non-empty-string>` instead of `...string` on\n  - `MultiSelect::fromIds()` and `MultiSelect::fromNames()`\n  - `Relation::create()` and `Relation::withRelations()`\n- Use `list<User>` instead of `...User` on\n  - `People::create()` and `People::withPeople()`\n\n## [0.1.0] - 2021-11-03\n### Added\n- Support to pages, databases and users API.\n- Blocks\n  - bookmark\n  - bulleted list item\n  - callout\n  - child database\n  - child page\n  - code\n  - divider\n  - embed\n  - equation\n  - file\n  - heading 1\n  - heading 2\n  - heading 3\n  - image\n  - numbered list item\n  - paragraph\n  - PDF\n  - quote\n  - table of contents\n  - to do\n  - toggle\n  - video\n\n- Database and Page properties:\n  - checkbox\n  - created by\n  - created time\n  - date\n  - email\n  - files\n  - formula\n  - last edited by\n  - last edited time\n  - multi select\n  - number\n  - people\n  - phone number\n  - rich text\n  - select\n  - title\n  - URL\n\n[0.1.0]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v0.1.0\n[0.2.0]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v0.2.0\n[0.3.0]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v0.3.0\n[0.4.0]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v0.4.0\n[0.5.0]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v0.5.0\n[0.5.1]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v0.5.1\n[0.5.2]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v0.5.2\n[0.6.0]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v0.6.0\n[0.6.1]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v0.6.1\n[0.6.2]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v0.6.2\n[1.0.0-beta.1]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v1.0.0-beta.1\n[1.0.0-beta.2]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v1.0.0-beta.2\n[1.0.0]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v1.0.0\n[1.1.0]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v1.1.0\n[1.2.0]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v1.2.0\n[1.3.0]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v1.3.0\n[1.4.0]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v1.4.0\n[1.4.1]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v1.4.1\n[1.5.0]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v1.5.0\n[1.6.0]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v1.6.0\n[1.6.1]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v1.6.1\n[1.6.2]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v1.6.2\n[1.7.0]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v1.7.0\n[1.8.0]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v1.8.0\n[1.8.1]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v1.8.1\n[1.9.0]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v1.9.0\n[1.10.0]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v1.10.0\n[1.11.0]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v1.11.0\n[1.11.1]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v1.11.1\n[1.12.0]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v1.12.0\n[1.13.0]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v1.13.0\n[1.14.0]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v1.14.0\n[1.15.0]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v1.15.0\n[1.16.0]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v1.16.0\n[1.17.0]: https://github.com/mariosimao/notion-sdk-php/releases/tag/v1.17.0\n[Unreleased]: https://github.com/mariosimao/notion-sdk-php/compare/v1.17.0...HEAD\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022 Mario Simão\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<h1 align=\"center\">notion-sdk-php</h1>\n<p align=\"center\">A complete Notion SDK for PHP developers.</p>\n\n<p align=\"center\">\n<a href=\"https://mario.engineering/notion-sdk-php\">\n    <img src=\"./docs/public/logo.png\" width=\"300\">\n</a>\n</p>\n\n<p align=\"center\">\n    <a href=\"https://app.codecov.io/gh/mariosimao/notion-sdk-php\">\n        <image src=\"https://img.shields.io/codecov/c/github/mariosimao/notion-sdk-php?token=ZKKCWDY4QX\">\n    </a>\n    <a href=\"https://shepherd.dev/github/mariosimao/notion-sdk\">\n        <image src=\"https://shepherd.dev/github/mariosimao/notion-sdk/coverage.svg\">\n    </a>\n    <a href=\"https://developers.notion.com/reference/versioning\">\n        <image src=\"https://img.shields.io/badge/API%20Version-2022--06--28-%23212121\">\n    </a>\n    <a href=\"https://packagist.org/packages/mariosimao/notion-sdk-php\">\n        <image src=\"https://img.shields.io/packagist/php-v/mariosimao/notion-sdk-php?color=%23787CB5\">\n    </a>\n    <a href=\"https://packagist.org/packages/mariosimao/notion-sdk-php\">\n        <image src=\"https://img.shields.io/packagist/dt/mariosimao/notion-sdk-php?color=%23FF8A65\">\n    </a>\n</p>\n\n\n## 📦 Installation\n\nThis project requires PHP 8.1 or higher. To install it with Composer run:\n\n```bash\n$ composer require mariosimao/notion-sdk-php\n```\n\n## 👩‍💻 Basic usage\n\nCreating a page on Notion with the SDK is easy.\n\n```php\nuse Notion\\Blocks\\Heading1;\nuse Notion\\Blocks\\ToDo;\nuse Notion\\Common\\Emoji;\nuse Notion\\Notion;\nuse Notion\\Pages\\Page;\nuse Notion\\Pages\\PageParent;\n\n$notion = Notion::create(\"secret_token\");\n\n$parent = PageParent::page(\"c986d7b0-7051-4f18-b165-cc0b9503ffc2\");\n$page = Page::create($parent)\n            ->changeTitle(\"Shopping list\")\n            ->changeIcon(Emoji::fromString(\"🛒\"));\n\n$content = [\n    Heading1::fromString(\"Supermarket\"),\n    ToDo::fromString(\"Tomato\"),\n    ToDo::fromString(\"Sugar\"),\n    ToDo::fromString(\"Apple\"),\n    ToDo::fromString(\"Milk\"),\n    Heading1::fromString(\"Mall\"),\n    ToDo::fromString(\"Black T-shirt\"),\n];\n\n$page = $notion->pages()->create($page, $content);\n```\n\n## 📄 Documentation\n\nFurther documentation can be found at https://mariosimao.github.io/notion-sdk-php.\n\nThe Notion PHP SDK supports the usage of static analysers. We strongly recommend the usage of\neither [vimeo/psalm](https://github.com/vimeo/psalm) or [phpstan/phpstan](https://github.com/phpstan/phpstan) in combination with this library, to avoid simple mistakes.\n\n## 🏷️ Versioning\n\n[SemVer](semver.org) is followed closely. Minor and patch releases should not introduce breaking changes to the codebase.\n\nAny classes or methods marked as `@internal` are not intended for use outside of this library and are subject to breaking changes at any time, avoid using them.\n\n## 🛠️ Maintenance & Support\nWhen a new minor version (e.g. 1.3 -> 1.4) is released, the previous one (1.3) will continue to receive security and critical bug fixes for at least 3 months.\n\nWhen a new major version is released (e.g. 1.6 -> 2.0), the previous one (1.6) will receive critical bug fixes for at least 3 months and security updates for 6 months after that new release comes out.\n\nThis policy may change in the future and exceptions may be made on a case-by-case basis.\n\n## ❤️ Sponsors\n\nAn special thanks to all sponsors who activelly support the SDK!\n\n<!-- sponsors --><!-- sponsors -->\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nVersions that are currently being supported with security updates.\n\n| Version | Supported          |\n| ------- | ------------------ |\n| 1.0.x   | :white_check_mark: |\n| 0.x.x   | :x:                |\n\n## Reporting a Vulnerability\n\nReport a vulnerability via [GitHub Security Advisories](https://github.com/mariosimao/notion-sdk-php/security/advisories/new).\n"
  },
  {
    "path": "composer.json",
    "content": "{\n    \"name\": \"mariosimao/notion-sdk-php\",\n    \"description\": \"A complete Notion SDK for PHP developers.\",\n    \"type\": \"library\",\n    \"license\": \"MIT\",\n    \"homepage\": \"https://mariosimao.github.io/notion-sdk-php\",\n    \"autoload\": {\n        \"psr-4\": {\n            \"Notion\\\\\": \"src/\"\n        }\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\n            \"Notion\\\\Test\\\\\": \"tests/\"\n        }\n    },\n    \"authors\": [\n        {\n            \"name\": \"Mario Simão\",\n            \"email\": \"mariogsimao@gmail.com\"\n        }\n    ],\n    \"require\": {\n        \"php\": \">=8.1\",\n        \"php-http/discovery\": \"^1.15\",\n        \"php-http/multipart-stream-builder\": \"^1.4\",\n        \"psr/http-client-implementation\": \"^1.0\",\n        \"psr/http-factory-implementation\": \"^1.0\",\n        \"psr/http-message-implementation\": \"^1.0\"\n    },\n    \"require-dev\": {\n        \"brianium/paratest\": \"7.3.2\",\n        \"guzzlehttp/guzzle\": \"^7.5\",\n        \"infection/infection\": \"^0.26.19 || ^0.27.0\",\n        \"phpunit/phpunit\": \"10.5.63\",\n        \"psalm/plugin-phpunit\": \"^0.18.4\",\n        \"squizlabs/php_codesniffer\": \"^3.7\",\n        \"vimeo/psalm\": \"^5.7\"\n    },\n    \"scripts\": {\n        \"ci:phpcs\": \"phpcs -q --report=checkstyle | cs2pr\",\n        \"ci:psalm\": \"psalm --output-format=github --shepherd --stats\",\n        \"ci:coverage\": [\n            \"Composer\\\\Config::disableProcessTimeout\",\n            \"paratest --coverage-clover dist/phpunit/clover.xml\"\n        ],\n        \"ci:unit\": \"paratest --testsuite Unit\",\n        \"ci:integration\": \"paratest --testsuite Integration\",\n        \"ci:mutation\": [\n            \"Composer\\\\Config::disableProcessTimeout\",\n            \"infection --threads=max\"\n        ],\n        \"test\": [\n            \"@test:phpcs\",\n            \"@test:psalm\",\n            \"@test:unit\"\n        ],\n        \"test:phpcs\": \"phpcs\",\n        \"test:psalm\": \"psalm --no-cache\",\n        \"test:unit\": \"phpunit --testsuite Unit\",\n        \"test:integration\": \"paratest --testsuite Integration\",\n        \"test:coverage\": \"paratest --coverage-html dist/phpunit/html && echo \\\"Open the result on your browser: $PWD/dist/phpunit/html/index.html\\\"\"\n    },\n    \"config\": {\n        \"allow-plugins\": {\n            \"infection/extension-installer\": true,\n            \"php-http/discovery\": true\n        },\n        \"sort-packages\": true\n    }\n}\n"
  },
  {
    "path": "docs/.vitepress/config.ts",
    "content": "import fs from 'fs';\nimport { defineConfig } from 'vitepress';\n\nconst removeExtension = (filename: string) => filename.split('.').shift() ?? '';\nconst kebabToSpaced = (kebab: string) => kebab.split('-').join(' ');\nconst capitalizeFirstLetter = (name: string) => name.charAt(0).toUpperCase() + name.slice(1);\n\nconst blockTitle = (filename: string) => {\n    return removeExtension(filename);\n}\n\nconst howToTitle = (filename: string) => {\n    const noExtension = removeExtension(filename);\n    const spaced = kebabToSpaced(noExtension);\n\n    return capitalizeFirstLetter(spaced);\n}\n\nconst blockItems = fs.readdirSync('./blocks')\n    .filter(file => file !== 'index.md')\n    .sort()\n    .map(file => ({\n        text: blockTitle(file),\n        link: `/blocks/${file}`,\n    }));\n\nconst pagePropsItems = fs.readdirSync('./page-properties')\n.filter(file => file !== 'index.md')\n.sort()\n.map(file => ({\n    text: blockTitle(file),\n    link: `/page-properties/${file}`,\n}));\n\nconst howToItems = fs.readdirSync('./how-to')\n    .filter(file => file !== 'index.md')\n    .sort()\n    .map(file => ({\n        text: howToTitle(file),\n        link: `/how-to/${file}`,\n    }));\n\nconst basePath = '/notion-sdk-php/';\n\nexport default defineConfig({\n    base: basePath,\n    title: 'Notion SDK PHP',\n    description: 'A complete Notion SDK for PHP developers.',\n    lang: 'en-US',\n    lastUpdated: true,\n    head: [\n        ['link', { rel: 'apple-touch-icon', sizes: '180x180', href: `${basePath}favicons/apple-touch-icon.png`}],\n        ['link', { rel: 'icon', type: 'image/png', sizes: '32x32', href: `${basePath}favicons/favicon-32x32.png`}],\n        ['link', { rel: 'icon', type: 'image/png', sizes: '16x16', href: `${basePath}favicons/favicon-16x16.png`}],\n        ['link', { rel: 'manifest', href: `${basePath}favicons/site.webmanifest`}],\n        ['link', { rel: 'shortcut icon', href: `${basePath}favicons/favicon.ico`}],\n        ['meta', { name: 'theme-color', content: '#787CB5'}],\n    ],\n    themeConfig: {\n        logo: '/logo.png',\n        algolia: {\n            appId: 'I6RXP3ZUR1',\n            apiKey: 'd4fe90d3f1d686f71865fec455c3ac59',\n            indexName: 'notion-sdk-php',\n        },\n        nav: [\n            { text: 'Documentation', link: '/getting-started' },\n            { text: 'Changelog', link: 'https://github.com/mariosimao/notion-sdk-php/blob/main/CHANGELOG.md'},\n        ],\n        socialLinks: [\n            { icon: 'github', link: 'https://github.com/mariosimao/notion-sdk-php' },\n            { icon: 'twitter', link: 'https://twitter.com/mariogsimao' },\n        ],\n        sidebar: [\n            {\n                text: 'Introduction',\n                items: [\n                    { text: 'Getting started', link: '/getting-started' },\n                ],\n            },\n            {\n                text: 'How to',\n                collapsible: true,\n                collapsed: true,\n                items: [\n                    ...howToItems,\n                ],\n            },\n            {\n                text: 'Blocks',\n                collapsible: true,\n                collapsed: true,\n                items: [\n                    { text: 'Introduction', link: '/blocks/' },\n                    ...blockItems,\n                ],\n            },\n            {\n                text: 'Page properties',\n                collapsible: true,\n                collapsed: true,\n                items: [\n                    { text: 'Introduction', link: '/page-properties/' },\n                    ...pagePropsItems,\n                ],\n            },\n            {\n                text: 'Comments',\n                collapsible: true,\n                collapsed: true,\n                items: [\n                    { text: 'Introduction', link: '/comments/' },\n                ],\n            },\n            {\n                text: 'Search',\n                collapsible: true,\n                collapsed: true,\n                items: [\n                    { text: 'Introduction', link: '/search/' },\n                ],\n            },\n            {\n                text: 'Advanced',\n                collapsible: true,\n                items: [\n                    { text: 'Configuration', link: '/advanced/configuration' },\n                ]\n            }\n        ],\n        footer: {\n            message: 'Released under the MIT License.',\n            copyright: 'Copyright © 2021-present Mario Simão',\n        },\n    },\n});\n"
  },
  {
    "path": "docs/.vitepress/theme/custom.css",
    "content": ":root {\n    --vp-c-brand: #787CB5;\n    --vp-c-brand-light: #B0B3D6;\n    --vp-c-brand-lighter: var(--vp-c-indigo-lighter);\n    --vp-c-brand-dark: #474A8A;\n    --vp-c-brand-darker: var(--vp-c-indigo-darker);\n}\n\n.VPDoc img {\n    border-style: solid;\n    border-width: 0.5px;\n    border-color: rgba(84, 84, 84, 0.65);\n    border-radius: 8px;\n}\n"
  },
  {
    "path": "docs/.vitepress/theme/index.js",
    "content": "import DefaultTheme from 'vitepress/theme'\nimport './custom.css'\n\nexport default DefaultTheme;\n"
  },
  {
    "path": "docs/advanced/configuration.md",
    "content": "# SDK Configuration\n\nThe SDK can be configured with custom options.\n\n```php\n$config = Configuration::create();\n\n$notion = Notion::createFromConfig($config);\n```\n\n## Default values\n\n| Option | Type | Default |\n|--------|------|---------|\n|[`retryOnConflict`](#retry-on-conflict)|bool|`true`|\n|[`retryOnConflictAttempts`](#retry-on-conflict)|int|`1`|\n\n## Retry on conflict\n\nSometimes, the Notion API responds with the following error\n\n```json\n{\n    \"code\": \"conflict_error\",\n    \"message\": \"Conflict occurred while saving. Please try again.\"\n}\n```\n\nThe SDK provides a retry option (enabled by default) and sends the request again\nuntil a success responde or when reaches the maximum number of attempts.\n\n### Enable\n\n```php\n$token = $_ENV[\"NOTION_TOKEN\"];\n\n$retryAttempts = 3;\n$config = Configuration::create($token)\n            ->enableRetryOnConflict($retryAttempts);\n\n$notion = Notion::createFromConfig($config);\n```\n\n### Disable\n\n```php\n$token = $_ENV[\"NOTION_TOKEN\"];\n\n$config = Configuration::create($token)\n            ->disableRetryOnConflict();\n\n$notion = Notion::createFromConfig($config);\n```\n"
  },
  {
    "path": "docs/blocks/Bookmark.md",
    "content": "# Bookmark block\n\n## Create\n\nCreating a bookmark from a URL string:\n```php\n<?php\n\n$bookmark = Bookmark::fromUrl(\"https://notion.so\");\n```\n\n![](../images/bookmark.png)\n\n## Add caption\n\n```php\n$caption = RichText::fromString(\"An awesome bookmark caption\");\n\n$bookmark = Bookmark::fromUrl(\"https://notion.so\")\n                    ->changeCaption($caption);\n```\n\n![](../images/bookmark-caption.png)\n\n## Change URL\n\n```php\n$bookmark = Bookmark::fromUrl(\"https://notion.so\");\n$bookmark = $bookmark->changeUrl(\"https://google.com\");\n\necho $bookmark->url;\n```\n\nOutput:\n```\nhttps://google.com\n```\n\n## Change caption\n\n```php\n$oldCaption = RichText::fromString(\"An awesome bookmark caption\");\n$bookmark = Bookmark::fromUrl(\"https://notion.so\")\n                    ->changeCaption($oldCaption);\n\n$newCaption = RichText::fromString(\"A new caption!\");\n$bookmark = $bookmark->changeCaption($newCaption);\n\necho RichText::multipleToString($bookmark->caption);\n```\n\nOutput:\n```\nA new caption!\n```"
  },
  {
    "path": "docs/blocks/Breadcrumb.md",
    "content": "# Breadcrumb\n\n## Create\n\nCreating a breadcrumb:\n```php\n<?php\n\n$breadcrumb = Breadcrumb::create();\n```\n\n![](../images/breadcrumb.png)\n"
  },
  {
    "path": "docs/blocks/BulletedListItem.md",
    "content": "# Bulleted list item\n\n## Create empty item\n\n```php\n$item = BulletedListItem::create();\n```\n\n## Create from string\n\nBulleted list items can be created from simple strings.\n\n```php\n$item = BulletedListItem::fromString(\"Item content\");\n```\n\n![](../images/bulleted-list-item.jpg)\n\n## Create from `RichText`\n\n```php\n$text = RichText::fromString(\"Item text\")->italic();\n\n$item = BulletedListItem::create()->changeText($text);\n```\n\n![](../images/bulleted-list-item-rich-text.jpg)\n\n## Add text\n\n```php\n$item = BulletedListItem::fromString(\"Item text\");\n$item = $item->addText(\n    RichText::fromString(\" can be extended!\")\n);\n\necho $item->toString();\n```\n\nOutput:\n```\nItem text can be extended!\n```\n## Add child\n\n```php\n$item = BulletedListItem::fromString(\"Item text\");\n\n$item = $item->addChild(\n    Paragraph::fromString(\"A simple child paragraph.\")\n);\n```\n\n![](../images/bulleted-list-item-append-child.jpg)\n\n## Change children\n\n```php\n$item = BulletedListItem::fromString(\"Item text\")\n    ->addChild(Paragraph::fromString(\"Old child\"));\n\n$item = $item->changeChildren(\n    Paragraph::fromString(\"Child paragraph 1\"),\n    Paragraph::fromString(\"Child paragraph 2\"),\n);\n```\n\n![](../images/bulleted-list-item-change-children.jpg)\n\n## Convert to string\n\nGet item content as string\n\n```php\n$item = BulletedListItem::fromString(\"Item text\");\n\necho $item->toString();\n```\n\nOutput:\n\n```\nItem text\n```"
  },
  {
    "path": "docs/blocks/Callout.md",
    "content": "# Callout\n\n## Create\n\nCreating an empty callout:\n\n```php\n$block = Callout::create();\n```\n\n![](../images/callout.png)\n\n## Create from string\n\n```php\n$block = Callout::fromString(\"💡\", \"A brilliant idea\");\n```\n\n![](../images/callout-from-string.png)\n\n## Create from `RichText`\n\n```php\n$block = Callout::create()->changeText(\n    RichText::fromString(\"Rich text \")->italic(),\n    RichText::fromString(\"is\")->underline(),\n    RichText::fromString(\" amazing\")->bold(),\n);\n```\n\n![](../images/callout-rich-text.png)\n\n## Convert to string\n\n```php\n$block = Callout::fromString(\"💡\", \"A brilliant idea\");\n\necho $block->toString();\n```\n\nOutput:\n```\nA brilliant idea\n```\n\n## Change text\n\n```php\n$block = Callout::fromString(\"💡\", \"Old text\");\n\n$block = $block->changeText(\n    RichText::fromString(\"New text\"),\n);\n\necho $block->toString();\n```\n\nOutput:\n\n```\nNew text\n```\n\n## Add text\n\n```php\n$block = Callout::fromString(\"💡\", \"ABC\")\n    ->addText(RichText::fromString(\"123\"));\n\necho $block->toString();\n```\n\nOutput:\n\n```\nABC123\n```\n\n## Change icon\n\n### Emoji\n\n```php\n$block = Callout::fromString(\"😎\", \"A brilliant idea\");\n\n$newIcon = Emoji::fromString(\"💡\");\n$block = $block->changeIcon($newIcon);\n\necho $block->icon->emoji->emoji;\n```\n\nOutput:\n```\n💡\n```\n\n### File\n\n```php\n$block = Callout::fromString(\"😎\", \"A brilliant idea\");\n\n$newIcon = File::createExternal(\n    \"https://cdn-icons-png.flaticon.com/512/648/648675.png\"\n);\n$block = $block->changeIcon($newIcon);\n\necho $block->icon->file->url;\n```\n\nOutput:\n```\nhttps://cdn-icons-png.flaticon.com/512/648/648675.png\n```\n\n## Add child\n\n```php\n$block = Callout::create()\n    ->chageIcon(Emoji::fromString(\"💡\"))\n    ->changeText(\n        RichText::fromString(\"Heraclitus\")->bold(),\n        RichText::fromString(\" once said:\"),\n    )->addChild(\n        Quote::fromString(\"One cannot step twice in the same river\")\n    );\n```\n\n![](../images/callout-children.png)\n\n## Change children\n\n```php\n$block = Callout::fromString(\"💡\", \"A brilliant idea\")\n    ->addChild(\n        Paragraph::fromString(\"Old paragraph\")\n    );\n\n$block = $block->changeChildren(\n    Paragraph::fromString(\"New paragraph 1\"),\n    Paragraph::fromString(\"New paragraph 2\"),\n);\n\necho $block->children[0]->toString() . PHP_EOL;\necho $block->children[1]->toString() . PHP_EOL;\n```\n\nOutput:\n```\nNew paragraph 1\nNew paragraph 2\n```\n"
  },
  {
    "path": "docs/blocks/ChildDatabase.md",
    "content": "# Child database\n\nChild databases can only be added to a page by creating a new database.\n\nHowever, a child database can be retreived from the page content.\n\n```php\n$pageId = \"3f4ch4...\";\n$content = $notion->blocks()->findChildren($pageId);\n\n/** @var ChildDatabase */\n$childDatabase = $content[0];\necho $childDatabase->title;\n```"
  },
  {
    "path": "docs/blocks/ChildPage.md",
    "content": "# Child page\n\nChild pages can only be added to a page by creating a new page.\n\nHowever, a child page can be retreived from the page content.\n\n```php\n$pageId = \"3f4ch4...\";\n$content = $notion->blocks()->findChildren($pageId);\n\n/** @var ChildPage */\n$childPage = $content[0];\necho $childPage->title;\n```"
  },
  {
    "path": "docs/blocks/Code.md",
    "content": "# Code\n\n## Create empty\n\nCreating an empty code block:\n\n```php\n$block = Code::create();\n```\n\n## Create from string\n\n```php\n$text = \"<?php echo 'Hello, world!';\";\n$block = Code::createFromString($text, CodeLanguage::Php);\n```\n\n![](../images/code-rich-text.png)\n\n## Create from `RichText`\n\n```php\n$text = [\n    RichText::fromString(\"<?php\\n\"),\n    RichText::fromString(\"echo 'Hello, world!';\"),\n];\n$block = Code::create($text, CodeLanguage::Php);\n```\n\n![](../images/code-rich-text.png)\n\n## Change language\n\n```php\n$block = Code::createFromString(\"console.log('Hello!');\")->changeLanguage(CodeLanguage::Javascript);\n```\n\n![](../images/code-change-language.png)\n\n## Append text\n\n```php\n$block = Code::create()\n    ->addText(RichText::fromString(\"console.log('Hello!')\"))\n    ->changeLanguage(CodeLangugae::Javascript);\n```\n\n## Change text\n\n```php\n$text = [\n    RichText::fromString(\"<?php\\n\"),\n    RichText::fromString(\"echo 'Hello, world!';\"),\n];\n\n$block = Code::create()\n    ->changeText(...$text)\n    ->changeLanguage(CodeLanguage::Php);\n```"
  },
  {
    "path": "docs/blocks/Column.md",
    "content": "# Column\n\nColumns should be used inside a [ColumnList](./ColumnList) block.\n\nThe block's children will define the content of the column.\n\n## Create\n\n```php\n$column1 = Column::create(\n    Heading1::fromString(\"Column 1\"),\n    Paragraph::fromString(\"This is column 1\"),\n);\n$column2 = Column::create(\n    Heading1::fromString(\"Column 2\"),\n    Paragraph::fromString(\"This is column 2\"),\n);\n\n$columnList = ColumnList::create($column1, $column2);\n```\n\n![](../images/column-create.png)\n\n## Add child block\n\n```php\nColumn::create(\n        Heading1::fromString(\"Column 1\")\n    )->addChild(\n        Paragraph::fromString(\"This is column 1\")\n    );\n```\n\n::: tip\nAll blocks, but `Column` are allowed as children.\n:::\n\n## Change children\n\n```php\nColumn::create()->changeChildren(\n    Paragraph::fromString(\"This is column 1\")\n);\n```\n"
  },
  {
    "path": "docs/blocks/ColumnList.md",
    "content": "# Column list\n\nColumn lists are blocks that contains [Columns](./Column) as children.\n\n## Create\n\n```php\n$text = Paragraph::fromString(\"Column\");\n$col1 = Column::create($text);\n$col2 = Column::create($text);\n$col3 = Column::create($text);\n\n$block = ColumnList::create($col1, $col2, $col3);\n```\n\n## Add column\n\n```php\n$col1 = Column::create();\n$col2 = Column::create();\n$col3 = Column::create();\n\n$block = ColumnList::create($col1, $col2);\n$block = $block->addChild($col3);\n```\n\n## Change columns\n\n```php\n$col1 = Column::create();\n$col2 = Column::create();\n$col3 = Column::create();\n\n$block = ColumnList::create()\n    ->changeColumns($col1, $col2, $col3);\n```"
  },
  {
    "path": "docs/blocks/Divider.md",
    "content": "# Divider\n\n## Create\n\n```php\n$block = Divider::create();\n```\n\n![](../images/divider.png)"
  },
  {
    "path": "docs/blocks/Embed.md",
    "content": "# Embed\n\nEmbed anything: PDFs, Google Docs, Google Maps, Spotify...\n\n## Create\n\n```php\n$url = \"https://goo.gl/maps/t7y33q3qfCBxr5489\";\n$block = Embed::create($url);\n```\n\n![](../images/embed-maps.png)\n"
  },
  {
    "path": "docs/blocks/EquationBlock.md",
    "content": "# Embed\n\nDisplay a standalone math equation.\n\n## Create\n\n```php\n$block = EquationBlock::create(\"\n    |x| = \\\\begin{cases}\n    x, &\\\\quad x \\geq 0 \\\\\\\\\n    -x, &\\\\quad x < 0\n    \\\\end{cases}\n\");\n```\n\n![](../images/equation-block.png)\n\n## Change equation\n\n```php\n$block = EquationBlock::create(\"a^2 + b^2 = c^2\");\n$newEquation = Equation::create(\"E = m * c^2\");\n$block = $block->changeEquation($newEquation);\n```\n"
  },
  {
    "path": "docs/blocks/FileBlock.md",
    "content": "# File\n\nUpload or embed with a link.\n\n## Create\n\n```php\n$file = File::createExternal(\n    \"https://shakespeare.folger.edu/downloads/pdf/hamlet_PDF_FolgerShakespeare.pdf\"\n);\n$block = FileBlock::create($file);\n```\n\n![](../images/file-block.png)\n"
  },
  {
    "path": "docs/blocks/Heading.md",
    "content": "# Heading\n\nSmall, medium and big section heading.\n\n## Create from string\n\n```php\n$blocks = [\n    Heading1::fromString(\"Heading 1\"),\n    Heading2::fromString(\"Heading 2\"),\n    Heading3::fromString(\"Heading 3\"),\n];\n```\n\n![](../images/heading-from-string.png)\n\n## Create from `RichText`\n\n```php\n$block = Heading1::create(\n    RichText::fromString(\"Heading \"),\n    RichText::fromString(\"with \")->italic(),\n    RichText::fromString(\"Rich\")->underline(),\n    RichText::fromString(\"Text\")->bold(),\n);\n```\n\n![](../images/heading-from-rich-text.png)\n\n## Convert to string\n\n```php\n$block = Heading1::create(\n    RichText::fromString(\"Heading \"),\n    RichText::fromString(\"with \")->italic(),\n    RichText::fromString(\"Rich\")->underline(),\n    RichText::fromString(\"Text\")->bold(),\n);\n\necho $block->toString();\n```\n\nOutput:\n```\nHeading with RichText\n```\n\n## Change text\n\n```php\n$block = Heading1::fromString(\"Old heading\");\n$block = $block->changeText(\n    RichText::fromString(\"New \"),\n    RichText::fromString(\"heading\")->bold(),\n);\n\necho $block->toString();\n```\n\nOutput:\n\n```\nNew heading\n```\n\n## Add text\n\n```php\n$block = Heading1::fromString(\"Heading\");\n\n$block = $block->addText(RichText::fromString(\" extended\"));\n\necho $block->toString();\n```\n\nOutput\n\n```\nHeading extended\n```\n"
  },
  {
    "path": "docs/blocks/Image.md",
    "content": "# Image\n\nUpload or embed image with a link.\n\n## Upload\n\nThe Notion API [currently does not support uploading new files](https://developers.notion.com/docs/working-with-files-and-media#uploading-files-and-media-via-the-notion-api).\n\n## Embed with link\n\n```php\n$file = File::createExternal(\"https://www.placecage.com/640/360.png\");\n$block = Image::create($file);\n```\n\n## Change file\n\n```php\n$file = File::createExternal(\"https://www.placecage.com/640/360.png\");\n$block = Image::create($file);\n\n// Add to a Notion page...\n\n$newFile = File::creatExternal(\"https://www.fillmurray.com/640/360.png\");\n$block = $block->changeFile($newFile);\n```"
  },
  {
    "path": "docs/blocks/LinkPreview.md",
    "content": "# Link Preview\n\nThis block cannot be created, only retrieved from the API.\n\n```php\n/** @var LinkPreview $block */\n$block->url;      // Link URL\n$block->metadata; // Block metadata\n```"
  },
  {
    "path": "docs/blocks/NumberedListItem.md",
    "content": "# Numbered list item\n\nList with numbering.\n\n## Create from string\n\n```php\n$blocks = [\n    NumberedListItem::fromString(\"First item\"),\n    NumberedListItem::fromString(\"Second item\"),\n    NumberedListItem::fromString(\"Third item\"),\n];\n```\n\n![](../images/numbered-list-item-from-string.png)\n\n"
  },
  {
    "path": "docs/blocks/Paragraph.md",
    "content": "# Paragraph block\n\n## Working with strings\n\nCreating a paragraph from a simple string:\n```php\n<?php\n\n$paragraph = Paragraph::fromString(\"Simple paragraph.\");\n$paragraph->toString(); // \"Simple paragraph.\"\n```\n\n![](../images/paragraph.png)\n\nOr creating an empty paragraph:\n```php\n<?php\n\n$paragraph = Paragraph::create();\n$paragraph->toString(); // empty string\n```\n\n## Working with `RichText` objects\n\n```php\n<?php\n\n// \"Simple text\" will be bold and italic\n$text = RichText::fromString(\"Simple text\")->bold()->italic();\n\n$paragraph = Paragraph::create()->addText($text);\n```\n\n![](../images/paragraph-rich-text.png)\n\nWhile working with multiple texts:\n\n```php\n$text = [\n    RichText::fromString(\"Paragraphs can be \"),\n    RichText::fromString(\"bold\")->bold(),\n    RichText::fromString(\", \"),\n    RichText::fromString(\"underlined\")->underline(),\n    RichText::fromString(\" and much more!\"),\n];\n\n$paragraph = Paragraph::create()->changeText(...$text);\n```\n\nNote that `changeText()` will replace the text on a new instance of `Paragraph`.\n\n![](../images/paragraph-rich-text-multiple.png)\n"
  },
  {
    "path": "docs/blocks/Pdf.md",
    "content": "# PDF\n\nEmbed a PDF file.\n\n## Create from a link\n\n```php\n$file = File::createExternal(\n    \"https://shakespeare.folger.edu/downloads/pdf/hamlet_PDF_FolgerShakespeare.pdf\"\n);\n$block = Pdf::create($file);\n```\n\n## Change file\n\n```php\n$file = File::createExternal(\"https://example.com/sample1.pdf\");\n$block = Pdf::create($file);\n\n// Add to a Notion page...\n\n$newFile = File::creatExternal(\"https://example.com/sample2.pdf\");\n$block = $block->changeFile($newFile);\n```\n"
  },
  {
    "path": "docs/blocks/Quote.md",
    "content": "# Quote\n\nCapture a quote.\n\n## Create from string\n\n```php\n$block = Quote::fromString(\n    \"The way to get started is to quit talking and begin doing.\"\n);\n```\n\n![](../images/quote-from-string.png)\n"
  },
  {
    "path": "docs/blocks/index.md",
    "content": "# Blocks\n\n## Introduction\n\nBlocks are one of the main objects from the Notion. They are used to compose the\ncontents of a page. All available blocks are listed [bellow](#available-blocks).\n\n## Metadata\n\nAll block objects have the `metadata()` method, witch exposes some metadata.\n\n```php\n$p = Paragraph::fromString(\"Simple paragraph.\");\n\n$p->metadata()->id;                              // a9f03ee5...\n$p->metadata()->createdTime->format(\"Y-m-d\");    // 2022-07-01\n$p->metadata()->lastEditedTime->format(\"Y-m-d\"); // 2022-07-01\n$p->metadata()->inTrash;                         // false\n$p->metadata()->hasChildren;                     // false\n```\n\n## Children\n\nSome blocks additionally support adding or changing children. Children can be\nany other type of block.\n\n```php\n/* Add child block */\n$c = Callout::fromString(\"💡\", \"A brilliant idea\");\n$p = Paragraph::fromString(\"Simple paragraph.\")\n        ->addChild($c);\n\n/* Change children blocks */\n$p = $p->changeChildren(\n    Paragraph::fromString(\"Nested paragraph 1\"),\n    Paragraph::fromString(\"Nested paragraph 2\"),\n);\ncount($p->children); // 2\n\n/* Remove children blocks */\n$p = $p->changeChildren();\n```\n\n## Available blocks\n\n| Block                                  | Support children |\n|----------------------------------------|:----------------:|\n| [Bookmark](./Bookmark)                 | ❌              |\n| [Breadcrumb](./Breadcrumb)             | ❌              |\n| [BulletedListItem](./BulletedListItem) | ✔               |\n| [Callout](./Callout)                   | ✔               |\n| [ChildDatabase](./ChildDatabase)       | ❌              |\n| [ChildPage](./ChildPage)               | ❌              |\n| [Code](./Code)                         | ❌              |\n| [Column](./Column)                     | ✔               |\n| [ColumnList](./ColumnList)             | ✔               |\n| [Divider](./Divider)                   | ❌              |\n| [Embed](./Embed)                       | ❌              |\n| [EquationBlock](./EquationBlock)       | ❌              |\n| [FileBlock](./FileBlock)               | ❌              |\n| [Heading1](./Heading)                  | ✔               |\n| [Heading2](./Heading)                  | ✔               |\n| [Heading3](./Heading)                  | ✔               |\n| [Image](./Image)                       | ❌              |\n| [LinkPreview](./LinkPreview)           | ❌              |\n| [NumberedListItem](./NumberedListItem) | ✔               |\n| [Paragraph](./Paragraph)               | ✔               |\n| [PDF](./Pdf.md)                        | ❌              |\n| [Quote](./Quote.md)                    | ✔               |\n| TableOfContents                        | ❌              |\n| ToDo                                   | ✔               |\n| Toggle                                 | ✔               |\n| Video                                  | ❌              |"
  },
  {
    "path": "docs/comments/index.md",
    "content": "# Comments\n\nIt is possible to comment on pages and blocks.\n\nComment objects have the following fields:\n\n```php\n$comment->id;               // cca26fdc...\n$comment->createdTime;      // 2023-01-18\n$comment->lastEditedTime;   // 2023-01-18\n$comment->userId;           // 3f577044...\n$comment->parent->id;       // 41ef05c4...\n$comment->parent->type;     // ParentType (block, page, database)\n$comment->discussionId;     // 311523ee...\n$comment->text;             // RichText array\n```\n\n## Read comments\n\n```php\n$pageComments = $notion->comments()->list($pageId);\n$blockComments = $notion->comments()->list($blockId);\n\nforeach ($pageComments as $comment) {\n    echo RichText::multipleToString($comment);\n}\n```\n\n## Add page comment\n\n```php\n$text = RichText::fromString(\"A sample page comment.\")\n$comment = Comment::create($pageId, $text);\n\n$notion->comments()->create($comment);\n```\n\n## Add comment on discussion\n\n```php\n$text = RichText::fromString(\"A sample discussion comment.\")\n$comment = Comment::createReply($discussionId, $text);\n\n$notion->comments()->create($comment);\n```\n"
  },
  {
    "path": "docs/getting-started.md",
    "content": "# Getting started\n\nThis section will help you setup a basic Notion application using\n`notion-sdk-php` from ground up. If you already have an existing project, start\nfrom [Step 2](#_2-install-notion-sdk-php).\n\n## 1. Create a new project\n\nCreate and change into a new directory.\n\n```bash\n$ mkdir notion-app && cd notion-app\n```\n\nThen, initialize with Composer.\n\n```bash\n$ composer init\n```\n\n## 2. Install Notion SDK PHP\n\n::: tip\nAn implementation of `psr/http-client` should be previously installed.\n\nFor example:\n```bash\n$ composer require guzzlehttp/guzzle\n```\n:::\n\nAdd `notion-sdk-php` as dependency for the project.\n\n```bash\n$ composer require mariosimao/notion-sdk-php\n```\n\n\n## 3. Get a Notion token\n\nA Notion token will be needed to fully use this library. If you don't have one,\nplease refer to the [Authorization section](https://developers.notion.com/docs/authorization)\non the [Notion API documentation](https://developers.notion.com/).\n\n## 4. Use the SDK\n\nTest if everything is working by listing all users from the Notion workspace.\n\n```php\n<?php\n\nrequire \"vendor/autoload.php\";\n\nuse Notion\\Notion;\n\n$token = \"secret_token\";\n$notion = Notion::create($token);\n\n$users = $notion->users()->findAll();\n\nforeach ($users as $user) {\n    echo $user->name . PHP_EOL;\n}\n```\n"
  },
  {
    "path": "docs/how-to/add-content-to-page.md",
    "content": "# Add content to page\n\n```php\n<?php\n\nuse Notion\\Blocks\\Paragraph;\nuse Notion\\Notion;\n\n$token = $_ENV[\"NOTION_SECRET\"];\n$notion = Notion::create($token);\n\n$pageId = \"c986d7b0-7051-4f18-b165-cc0b9503ffc2\";\n\n$content = [\n    Paragraph::fromString(\"This paragraph will be appended.\"),\n    Paragraph::fromString(\"This other paragraph too!\"),\n];\n\n$notion->blocks()->append($pageId, $content);\n```"
  },
  {
    "path": "docs/how-to/add-row-to-database.md",
    "content": "# Add row to database\n\nDatabase rows on Notion are essentially pages where the parent is the database.\n\n```php\n<?php\n\nuse Notion\\Blocks\\Paragraph;\nuse Notion\\Notion;\n\n$token = $_ENV[\"NOTION_SECRET\"];\n$notion = Notion::create($token);\n\n$databaseId = \"your_database_id_here\";\n$parent = \\Notion\\Pages\\PageParent::database($databaseId);\n\n$title    = \\Notion\\Pages\\Properties\\Title::fromString(\"Superbad\");\n$release  = \\Notion\\Pages\\Properties\\Date::create(new DateTimeImmutable(\"2007-10-19\"));\n$category = \\Notion\\Pages\\Properties\\Select::fromName(\"Comedy\");\n\n$page = \\Notion\\Pages\\Page::create($parent)\n    ->addProperty(\"Title\", $title)\n    ->addProperty(\"Release date\", $release)\n    ->addProperty(\"Category\", $category);\n\n$page = $notion->pages()->create($page);\n```"
  },
  {
    "path": "docs/how-to/create-a-page.md",
    "content": "# Create a page\n\n## Empty page\n\n```php\n<?php\n\nuse Notion\\Notion;\nuse Notion\\Common\\Emoji;\nuse Notion\\Pages\\Page;\nuse Notion\\Pages\\PageParent;\n\n$token = $_ENV[\"NOTION_SECRET\"];\n$notion = Notion::create($token);\n\n$parent = PageParent::page(\"c986d7b0-7051-4f18-b165-cc0b9503ffc2\");\n$page = Page::create($parent)\n            ->changeTitle(\"Empty page\")\n            ->changeIcon(Emoji::fromString(\"⭐\"));\n\n$page = $notion->pages()->create($page);\n```\n\n## Page with content\n\n```php\n<?php\n\nuse Notion\\Blocks\\Heading1;\nuse Notion\\Notion;\nuse Notion\\Blocks\\ToDo;\nuse Notion\\Common\\Emoji;\nuse Notion\\Pages\\Page;\nuse Notion\\Pages\\PageParent;\n\n$token = $_ENV[\"NOTION_SECRET\"];\n$notion = Notion::create($token);\n\n$parent = PageParent::page(\"c986d7b0-7051-4f18-b165-cc0b9503ffc2\");\n$page = Page::create($parent)\n            ->changeTitle(\"Shopping list\")\n            ->changeIcon(Emoji::fromString(\"🛒\"));\n\n$content = [\n    Heading1::fromString(\"Supermarket\"),\n    ToDo::fromString(\"Tomato\"),\n    ToDo::fromString(\"Sugar\"),\n    ToDo::fromString(\"Apple\"),\n    ToDo::fromString(\"Milk\"),\n    Heading1::fromString(\"Mall\"),\n    ToDo::fromString(\"Black T-shirt\"),\n];\n\n$page = $notion->pages()->create($page, $content);\n```\n"
  },
  {
    "path": "docs/how-to/delete-a-page.md",
    "content": "# Delete a page\n\nDeleted pages are moved to trash. It is possible to recover pages in trash.\n\n```php\n<?php\n\nuse Notion\\Notion;\n\n$token = $_ENV[\"NOTION_SECRET\"];\n$notion = Notion::create($token);\n\n$pageId = \"c986d7b0-7051-4f18-b165-cc0b9503ffc2\";\n$page = $notion->pages()->find($pageId);\n$page = $notion->pages()->delete($page);\n\n$page->inTrash; // true\n```"
  },
  {
    "path": "docs/how-to/find-a-page.md",
    "content": "# Find a page\n\nIt is possible to retrieve a page by knowing its ID.\n\n```php\n<?php\n\nuse Notion\\Notion;\n\n$token = $_ENV[\"NOTION_SECRET\"];\n$notion = Notion::create($token);\n\n$pageId = \"c986d7b0-7051-4f18-b165-cc0b9503ffc2\";\n$page = $notion->pages()->find($pageId);\n\necho $page->title()?->toString();\n```\n::: warning\nWhen finding a page, you will not get the content of it, only meta information and properties.\n:::\n\n## Get page content\n\nIn Notion, pages behave like blocks where the content is their children.\n\nYou can fetch page content with\n\n* `$notion->blocks()->findChildren($pageId)` or\n* `$notion->blocks()->findChildrenRecursive($pageId)`\n\nThe second option iterates over the content to find also their children (useful for nested content).\n\n```php\n<?php\n\n$token = $_ENV[\"NOTION_TOKEN\"];\n$notion = \\Notion\\Notion::create($token);\n\n$pageId = \"471373adacbe4247aa4b2ce06ed14026\";\n$content = $notion->blocks()->findChildren($pageId);\n```\n"
  },
  {
    "path": "docs/how-to/list-database-pages.md",
    "content": "# List database pages\n\n```php\n<?php\n\nuse Notion\\Notion;\n\n$token = $_ENV[\"NOTION_SECRET\"];\n$notion = Notion::create($token);\n\n$databaseId = \"c986d7b0-7051-4f18-b165-cc0b9503ffc2\";\n$database = $notion->databases()->find($databaseId);\n\n$pages = $notion->databases()->queryAllPages($database);\n\ncount($pages);\n```\n"
  },
  {
    "path": "docs/how-to/query-database.md",
    "content": "# Query database\n\n```php\n<?php\n\nuse Notion\\Notion;\nuse Notion\\Databases\\Query;\nuse Notion\\Databases\\Query\\CompoundFilter;\nuse Notion\\Databases\\Query\\DateFilter;\nuse Notion\\Databases\\Query\\Sort;\nuse Notion\\Databases\\Query\\TextFilter;\n\n$token = $_ENV[\"NOTION_SECRET\"];\n$notion = Notion::create($token);\n\n$databaseId = \"c986d7b0-7051-4f18-b165-cc0b9503ffc2\";\n$database = $notion->databases()->find($databaseId);\n\n$query = Query::create()\n    ->changeFilter(\n        CompoundFilter::and(\n            DateFilter::createdTime()->pastWeek(),\n            TextFilter::property(\"Name\")->contains(\"John\"),\n        )\n    )\n    ->addSort(Sort::property(\"Name\")->ascending())\n    ->changePageSize(20);\n\n$result = $notion->databases()->query($database, $query);\n\n$pages = $result->pages; // array of Page\n$result->hasMore; // true or false\n$result->nextCursor // cursor ID or null\n```\n"
  },
  {
    "path": "docs/how-to/update-a-page.md",
    "content": "# Update a page\n\n## Update title\n\n```php\n<?php\n\nuse Notion\\Notion;\nuse Notion\\Common\\Emoji;\n\n$token = $_ENV[\"NOTION_SECRET\"];\n$notion = Notion::create($token);\n\n$pageId = \"c986d7b0-7051-4f18-b165-cc0b9503ffc2\";\n$page = $notion->pages()->find($pageId);\n$page = $page->changeTitle(\"New title\")\n             ->changeIcon(Emoji::fromString(🚲));\n\n$notion->pages()->update($page);\n```\n\n## Update properties\n\n```php\n<?php\n\nuse Notion\\Notion;\n\n$token = $_ENV[\"NOTION_SECRET\"];\n$notion = Notion::create($token);\n\n$pageId = \"c986d7b0-7051-4f18-b165-cc0b9503ffc2\";\n$page = $notion->pages()->find($pageId);\n\n$updatedRelease = \\Notion\\Pages\\Properties\\Date::create(new DateTimeImmutable(\"2008-11-04\"));\n$page = $page->addProperty(\"Release date\", $updatedRelease);\n\n$notion->pages()->update($page);\n```\n"
  },
  {
    "path": "docs/index.md",
    "content": "---\nlayout: home\n\ntitleTemplate: false\nlastUpdated: false\n\nhero:\n  name: Notion SDK PHP\n  text: \"🐘 + 🇳 = 💜\"\n  tagline: A complete Notion SDK for PHP developers.\n  image:\n    src: /logo.png\n  actions:\n    - theme: brand\n      text: Get Started\n      link: /getting-started\n    - theme: alt\n      text: View on GitHub\n      link: https://github.com/mariosimao/notion-sdk-php\n\nfeatures:\n  - icon: ✔️\n    title: Everything from Notion API\n    details: The sky is the limit! Support to all blocks, objects and endpoints available.\n  - icon: 💡\n    title: Fully typed\n    details: Stop guessing method names! Enjoy IDE autocompletes on a 100% typed API.\n  - icon: ⛔\n    title: No dependencies\n    details: Goodbye dependency hell! You only need PHP 8.1 and your favorite implementation of PSR-18.\n---\n"
  },
  {
    "path": "docs/package.json",
    "content": "{\n  \"name\": \"notion-sdk-php-docs\",\n  \"description\": \"Notion SDK PHP documentation\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"docs:dev\": \"vitepress dev . --host\",\n    \"docs:build\": \"vitepress build .\",\n    \"docs:serve\": \"vitepress serve .\"\n  },\n  \"author\": \"Mario Simão\",\n  \"license\": \"MIT\",\n  \"devDependencies\": {\n    \"@types/node\": \"18.0.5\",\n    \"vitepress\": \"1.0.0-alpha.16\",\n    \"vue\": \"3.2.37\"\n  }\n}\n"
  },
  {
    "path": "docs/page-properties/People.md",
    "content": "# People\n\nA list of users.\n\n## Get value\n\n```php\n// Find page\n$page = $notion->pages()->find($pageId);\n\n// Get value from column \"Assignee\"\n/** @var \\Notion\\Pages\\Properties\\People $assignees */\n$assignees = $page->getProperty(\"Assignees\");\n\n$users = $assignees->users; // array of Users\nforeach ($users as $user) {\n    echo $user->name;       // e.g. \"Mario Simao\"\n}\n```\n\n## Add people property to a page\n\n```php\n// Get users\n$assignee1 = $notion->users()->find(\"b548b6e7-da76-494b-8ede-1febf4796024\");\n$assignee2 = $notion->users()->find(\"dc1a838b-4d6f-4343-b012-be3a4d9fd0c6\");\n\n// Create property\n$assignees = People::create($assignee1, $assignee2);\n\n// Find page and change property\n$page = $notion->pages()->find($pageId);\n$page = $page->addProperty(\"Assignees\", $assignees);\n\n// Send to Notion\n$notion->pages()->update($page);\n```\n\n## Update values\n```php\n// Get property\n$page = $notion->pages()->find($pageId);\n/** @var \\Notion\\Pages\\Properties\\People $assignees */\n$assignees = $page->getProperty(\"Assignees\");\n\n// Fetch users\n$assignee1 = $notion->users()->find(\"789653af-844b-4643-ae14-082d09cfc1f8\");\n$assignee2 = $notion->users()->find(\"31449c19-1a9b-456c-a63c-1fdc9b6c7c00\");\n$assignee3 = $notion->users()->find(\"d320799c-40d1-46e1-953a-e218f96aeefa\");\n\n// Add user to the list\n$assignees = $assignees->addPerson($assignee3)\n\n// Change the list\n$assignees = $assignees->changePeople($assignee1, $assignee2);\n\n// Remove user from the list\n$assignees = $assignees->removePerson($assignee2->id);\n\n// Update property and send to Notion\n$page = $page->addProperty(\"Assignee\", $assignees);\n$notion->pages()->update($page);\n```\n"
  },
  {
    "path": "docs/page-properties/index.md",
    "content": "# Page properties\n\n## Introduction\n\nA page is made up of page properties that contain data about the page. You can\nuse the Notion SDK to retrieve and update information of a page property.\n\nAccourding to Notion documentation:\n> If a page object’s Parent object is a database, then the property values\n> conform to the database property schema. If a page object is not part of a\n> database, then the only property value available for that page is its title.\n\nAll available properties are listed [bellow](#available-properties).\n\n## Metadata\n\nAll page propety objects have the `metadata()` method, witch exposes\nthe ID and type of the property.\n\n```php\n$property->metadata()->id;  // a9f03ee5...\n$property->metadata()->type // instance of Notion\\Pages\\Property\\PropertyType\n```\n\n## Add page property\n\n```php\nuse Notion\\Pages\\Properties\\Number;\n\n// Create property\n$price = Number::create(59.99);\n\n// Find page\n$pageId = \"249c7266-611a-416a-b2d4-2c7a833b6ac1\";\n$page = $notion->pages()->find($pageId);\n\n// Add property to page\n$page = $page->addProperty(\"Price\", $price);\n\n// Send to Notion\n$notion->pages->update($page);\n```\n\n## Get page property\n```php\n// Find page\n$pageId = \"249c7266-611a-416a-b2d4-2c7a833b6ac1\";\n$page = $notion->pages()->find($pageId);\n\n// Get property\n/** @var \\Notion\\Pages\\Properties\\Number $releaseDate */\n$price = $page->getProperty(\"Price\");\n\n$price->number;             // 59.99\n$price->metadata()->id;     // d7b38593-cb6b-410d-8445-0eac9b774fe0\n$price->metadata()->type;   // PropertyType::Number (enum)\n```\n\n## Update page property\n\n```php\n// Retrieve page\n$pageId = \"249c7266-611a-416a-b2d4-2c7a833b6ac1\";\n$page = $notion->pages()->find($pageId);\n\n// Get property\n/** @var \\Notion\\Pages\\Properties\\Number $releaseDate */\n$price = $page->getProperty(\"Price\");\n\n// Update property\n$price = $price->changeNumber(49.99);\n$page = $page->addProperty(\"Price\", $price);\n\n// Send to Notion\n$notion->pages()->update($page);\n```\n\n## Available properties\n\n- Checkbox\n- CreatedBy\n- CreatedTime\n- Date\n- Email\n- Files\n- Formula\n- LastEditedBy\n- LastEditedTime\n- MultiSelect\n- Number\n- People\n- PhoneNumber\n- Relation\n- RichText\n- Select\n"
  },
  {
    "path": "docs/public/favicons/site.webmanifest",
    "content": "{\"name\":\"Notion SDK PHP\",\"short_name\":\"Notion PHP\",\"icons\":[{\"src\":\"/android-chrome-192x192.png\",\"sizes\":\"192x192\",\"type\":\"image/png\"},{\"src\":\"/android-chrome-512x512.png\",\"sizes\":\"512x512\",\"type\":\"image/png\"}],\"theme_color\":\"#787CB5\",\"display\":\"standalone\"}"
  },
  {
    "path": "docs/search/index.md",
    "content": "# Search\n\n## Introduction\n\nSearches all parent or child pages and databases that have been shared with an\nintegration.\n\nReturns all pages or databases, excluding duplicated linked databases, that have\ntitles that include the query param. If no query param is provided, then the\nresponse contains all pages or databases that have been shared with the\nintegration.\n\n::: warning\nIf you want to search a specific individual database, rather than across all\ndatabases, then [query a database](../how-to/query-database.md) instead.\n:::\n\n## Searching\n\n```php\nuse Notion\\Search\\Query;\n\n$query = Query::title(\"Example\"); // Search by page/database title...\n$query = Query::all();            // ... or search everything\n\n$result = $notion->search()->search($query);\n\n$result->hasMore;    // bool\n$result->nextCursor; // CursorId when hasMore is true\n$result->results;    // array of Page and/or Database objects\n```\n\n## Query options\n\n```php\nuse Notion\\Search\\Query;\n\n$query = Query::title(\"Example\");                                // Page or database title\n$query = $query->filterByPages();                                // Return only pages\n$query = $query->filterByDatabases();                            // Return only databases\n$query = $query->sortByLastEditedTime(SortDirection::Ascending); // Results order\n\n// Pagination\n$query = $query->changePageSize(10);\n$query = $query->changeNextCursor(\"70d73991-7e06-43d9-ad3c-3711213f1235\")\n```"
  },
  {
    "path": "infection.json",
    "content": "{\n    \"$schema\": \"vendor/infection/infection/resources/schema.json\",\n    \"source\": {\n        \"directories\": [\n            \"src\"\n        ]\n    },\n    \"logs\": {\n        \"text\": \".infection/infection.log\",\n        \"summary\": \".infection/summary.log\",\n        \"json\": \".infection/infection-log.json\",\n        \"perMutator\": \".infection/per-mutator.md\"\n    },\n    \"minMsi\": 81,\n    \"minCoveredMsi\": 89,\n    \"mutators\": {\n        \"@default\": true\n    }\n}\n"
  },
  {
    "path": "phpcs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ruleset name=\"notion-sdk-php\">\n    <description>Coding standard for the notion-sdk-php library.</description>\n    <rule ref=\"PSR12\"/>\n\n    <file>src</file>\n    <file>tests</file>\n\n    <rule ref=\"PSR1.Methods.CamelCapsMethodName\">\n        <exclude-pattern>./tests/*</exclude-pattern>\n    </rule>\n</ruleset>\n"
  },
  {
    "path": "phpunit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit\n  xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n  xsi:noNamespaceSchemaLocation=\"https://schema.phpunit.de/10.1/phpunit.xsd\"\n  bootstrap=\"vendor/autoload.php\"\n  beStrictAboutOutputDuringTests=\"true\"\n  beStrictAboutChangesToGlobalState=\"true\"\n  failOnRisky=\"true\"\n  failOnWarning=\"true\"\n  cacheDirectory=\".phpunit.cache\"\n>\n  <testsuites>\n    <testsuite name=\"Unit\">\n      <directory>tests/Unit</directory>\n    </testsuite>\n    <testsuite name=\"Integration\">\n      <directory>tests/Integration</directory>\n    </testsuite>\n  </testsuites>\n  <coverage/>\n  <source>\n    <include>\n      <directory suffix=\".php\">src</directory>\n    </include>\n  </source>\n</phpunit>\n"
  },
  {
    "path": "psalm.xml",
    "content": "<psalm\n    errorLevel=\"1\"\n    resolveFromConfigFile=\"true\"\n    memoizeMethodCallResults=\"true\"\n    findUnusedPsalmSuppress=\"false\"\n    findUnusedCode=\"false\"\n    findUnusedBaselineEntry=\"false\"\n    xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n    xmlns=\"https://getpsalm.org/schema/config\"\n    xsi:schemaLocation=\"https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd\"\n>\n    <projectFiles>\n        <directory name=\"src\"/>\n        <directory name=\"tests\"/>\n        <ignoreFiles>\n            <directory name=\"vendor\"/>\n        </ignoreFiles>\n    </projectFiles>\n\n    <plugins>\n        <pluginClass class=\"Psalm\\PhpUnitPlugin\\Plugin\"/>\n    </plugins>\n</psalm>\n"
  },
  {
    "path": "src/Blocks/BlockFactory.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\nclass BlockFactory\n{\n    /**\n     * @param array{ type: string, ... } $array\n     */\n    public static function fromArray(array $array): BlockInterface\n    {\n        $type = $array[\"type\"];\n\n        return match ($type) {\n            BlockType::Bookmark->value         => Bookmark::fromArray($array),\n            BlockType::Breadcrumb->value       => Breadcrumb::fromArray($array),\n            BlockType::BulletedListItem->value => BulletedListItem::fromArray($array),\n            BlockType::Callout->value          => Callout::fromArray($array),\n            BlockType::ChildDatabase->value    => ChildDatabase::fromArray($array),\n            BlockType::ChildPage->value        => ChildPage::fromArray($array),\n            BlockType::Code->value             => Code::fromArray($array),\n            BlockType::Column->value           => Column::fromArray($array),\n            BlockType::ColumnList->value       => ColumnList::fromArray($array),\n            BlockType::Divider->value          => Divider::fromArray($array),\n            BlockType::Embed->value            => Embed::fromArray($array),\n            BlockType::Equation->value         => EquationBlock::fromArray($array),\n            BlockType::File->value             => FileBlock::fromArray($array),\n            BlockType::Heading1->value         => Heading1::fromArray($array),\n            BlockType::Heading2->value         => Heading2::fromArray($array),\n            BlockType::Heading3->value         => Heading3::fromArray($array),\n            BlockType::Image->value            => Image::fromArray($array),\n            BlockType::LinkPreview->value      => LinkPreview::fromArray($array),\n            BlockType::NumberedListItem->value => NumberedListItem::fromArray($array),\n            BlockType::Paragraph->value        => Paragraph::fromArray($array),\n            BlockType::Pdf->value              => Pdf::fromArray($array),\n            BlockType::Quote->value            => Quote::fromArray($array),\n            BlockType::TableOfContents->value  => TableOfContents::fromArray($array),\n            BlockType::ToDo->value             => ToDo::fromArray($array),\n            BlockType::Toggle->value           => Toggle::fromArray($array),\n            BlockType::Video->value            => Video::fromArray($array),\n            default                            => Unknown::fromArray($array),\n        };\n    }\n}\n"
  },
  {
    "path": "src/Blocks/BlockInterface.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\n/** @psalm-immutable */\ninterface BlockInterface\n{\n    public function metadata(): BlockMetadata;\n    public function addChild(BlockInterface $child): self;\n    public function changeChildren(BlockInterface ...$children): self;\n    public function delete(): self;\n    /**\n     * @deprecated 1.17.0 Use `delete()` instead.\n     * @codeCoverageIgnore\n     */\n    public function archive(): self;\n\n    /** @internal */\n    public static function fromArray(array $array): self;\n    /** @internal */\n    public function toArray(): array;\n}\n"
  },
  {
    "path": "src/Blocks/BlockMetadata.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\nuse DateTimeImmutable;\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Common\\Date;\n\n/**\n * @psalm-type BlockMetadataJson = array{\n *      type: string,\n *      id: string,\n *      created_time: string,\n *      last_edited_time: string,\n *      in_trash: bool,\n *      has_children: bool,\n * }\n *\n * @psalm-immutable\n */\nclass BlockMetadata\n{\n    private function __construct(\n        public readonly string $id,\n        public readonly DateTimeImmutable $createdTime,\n        public readonly DateTimeImmutable $lastEditedTime,\n        public readonly bool $inTrash,\n        public readonly bool $hasChildren,\n        public readonly BlockType $type,\n        private readonly string|null $unknownType = null\n    ) {\n        /** @psalm-suppress DeprecatedProperty */\n        $this->archived = $inTrash;\n    }\n\n    /**\n     * @deprecated 1.17.0 Use `$inTrash` instead.\n     * @codeCoverageIgnore\n     */\n    public readonly bool $archived;\n\n    /** @internal */\n    public static function create(BlockType $type): self\n    {\n        $now = new DateTimeImmutable(\"now\");\n\n        return new self(\"\", $now, $now, false, false, $type);\n    }\n\n    /**\n     * @psalm-param BlockMetadataJson $array\n     *\n     * @internal\n     */\n    public static function fromArray(array $array): self\n    {\n        $type = BlockType::tryFrom($array[\"type\"]) ?? BlockType::Unknown;\n\n        return new self(\n            $array[\"id\"],\n            new DateTimeImmutable($array[\"created_time\"]),\n            new DateTimeImmutable($array[\"last_edited_time\"]),\n            $array[\"in_trash\"],\n            $array[\"has_children\"],\n            $type,\n            $type === BlockType::Unknown ? $array[\"type\"] : null,\n        );\n    }\n\n    /** @internal */\n    public function toArray(): array\n    {\n        $type = $this->type !== BlockType::Unknown ? $this->type->value : $this->unknownType;\n\n        $array = [\n            \"object\"           => \"block\",\n            \"created_time\"     => $this->createdTime->format(Date::FORMAT),\n            \"last_edited_time\" => $this->lastEditedTime->format(Date::FORMAT),\n            \"in_trash\"         => $this->inTrash,\n            \"has_children\"     => $this->hasChildren,\n            \"type\"             => $type,\n        ];\n\n        if ($this->id !== \"\") {\n            $array[\"id\"] = $this->id;\n        }\n\n        return $array;\n    }\n\n    /** @internal */\n    public function delete(): self\n    {\n        return new self(\n            $this->id,\n            $this->createdTime,\n            new DateTimeImmutable(\"now\"),\n            true,\n            $this->hasChildren,\n            $this->type,\n        );\n    }\n\n    /** @internal */\n    public function restore(): self\n    {\n        return new self(\n            $this->id,\n            $this->createdTime,\n            new DateTimeImmutable(\"now\"),\n            false,\n            $this->hasChildren,\n            $this->type,\n        );\n    }\n\n    /** @internal */\n    public function updateHasChildren(bool $hasChildren): self\n    {\n        return new self(\n            $this->id,\n            $this->createdTime,\n            new DateTimeImmutable(\"now\"),\n            $this->inTrash,\n            $hasChildren,\n            $this->type,\n        );\n    }\n\n    public function update(): self\n    {\n        return new self(\n            $this->id,\n            $this->createdTime,\n            new DateTimeImmutable(\"now\"),\n            $this->inTrash,\n            $this->hasChildren,\n            $this->type,\n        );\n    }\n\n    /**\n     * @internal\n     *\n     * @throws BlockException\n     */\n    public function checkType(BlockType $expectedType): void\n    {\n        if ($this->type !== $expectedType) {\n            throw BlockException::wrongType($expectedType);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Blocks/BlockType.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\nenum BlockType: string\n{\n    case Paragraph = \"paragraph\";\n    case Heading1 = \"heading_1\";\n    case Heading2 = \"heading_2\";\n    case Heading3 = \"heading_3\";\n    case Callout = \"callout\";\n    case Quote = \"quote\";\n    case BulletedListItem = \"bulleted_list_item\";\n    case NumberedListItem = \"numbered_list_item\";\n    case ToDo = \"to_do\";\n    case Toggle = \"toggle\";\n    case Code = \"code\";\n    case ChildPage = \"child_page\";\n    case ChildDatabase = \"child_database\";\n    case Embed = \"embed\";\n    case Image = \"image\";\n    case Video = \"video\";\n    case File = \"file\";\n    case Pdf = \"pdf\";\n    case Bookmark = \"bookmark\";\n    case Equation = \"equation\";\n    case Divider = \"divider\";\n    case Table = \"table\";\n    case TableRow = \"table_row\";\n    case TableOfContents = \"table_of_contents\";\n    case Breadcrumb = \"breadcrumb\";\n    case Column = \"column\";\n    case ColumnList = \"column_list\";\n    case LinkPreview = \"link_preview\";\n    case Unknown = \"unknown\";\n}\n"
  },
  {
    "path": "src/Blocks/Bookmark.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Common\\RichText;\n\n/**\n * Bookmark block\n *\n * @psalm-import-type BlockMetadataJson from BlockMetadata\n * @psalm-import-type RichTextJson from \\Notion\\Common\\RichText\n *\n * @psalm-type BookmarkJson = array{\n *      bookmark: array{\n *          url: string,\n *          caption: RichTextJson[],\n *      },\n * }\n *\n * @psalm-immutable\n */\nclass Bookmark implements BlockInterface\n{\n    /** @param RichText[] $caption */\n    private function __construct(\n        private readonly BlockMetadata $metadata,\n        public readonly string $url,\n        public readonly array $caption\n    ) {\n        $metadata->checkType(BlockType::Bookmark);\n    }\n\n    /**\n     * Create a bookmark from a URL\n     */\n    public static function fromUrl(string $url): self\n    {\n        $metadata = BlockMetadata::create(BlockType::Bookmark);\n\n        return new self($metadata, $url, []);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var BlockMetadataJson $array */\n        $metadata = BlockMetadata::fromArray($array);\n\n        /** @psalm-var BookmarkJson $array */\n        $url = $array[\"bookmark\"][\"url\"];\n\n        $caption = array_map(fn($t) => RichText::fromArray($t), $array[\"bookmark\"][\"caption\"]);\n\n        return new self($metadata, $url, $caption);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"bookmark\"] = [\n            \"url\" => $this->url,\n            \"caption\" => array_map(fn(RichText $t) => $t->toArray(), $this->caption),\n        ];\n\n        return $array;\n    }\n\n    public function metadata(): BlockMetadata\n    {\n        return $this->metadata;\n    }\n\n    /** Change bookmark URL */\n    public function changeUrl(string $url): self\n    {\n        return new self($this->metadata, $url, $this->caption);\n    }\n\n    /** Change bookmark caption */\n    public function changeCaption(RichText ...$caption): self\n    {\n        return new self($this->metadata, $this->url, $caption);\n    }\n\n    public function addChild(BlockInterface $child): never\n    {\n        throw BlockException::noChindrenSupport();\n    }\n\n    public function changeChildren(BlockInterface ...$children): never\n    {\n        throw BlockException::noChindrenSupport();\n    }\n\n    public function delete(): BlockInterface\n    {\n        return new self(\n            $this->metadata->delete(),\n            $this->url,\n            $this->caption,\n        );\n    }\n\n    /**\n     * @deprecated 1.17.0 Use `delete()` instead.\n     * @codeCoverageIgnore\n     */\n    public function archive(): BlockInterface\n    {\n        return $this->delete();\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Breadcrumb.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\nuse Notion\\Exceptions\\BlockException;\n\n/**\n * @psalm-import-type BlockMetadataJson from BlockMetadata\n *\n * @psalm-type BreadcrumbJson = array{\n *      breadcrumb: array<empty, empty>\n * }\n *\n * @psalm-immutable\n */\nclass Breadcrumb implements BlockInterface\n{\n    private function __construct(\n        private readonly BlockMetadata $metadata\n    ) {\n        $metadata->checkType(BlockType::Breadcrumb);\n    }\n\n    public static function create(): self\n    {\n        $metadata = BlockMetadata::create(BlockType::Breadcrumb);\n\n        return new self($metadata);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var BlockMetadataJson $array */\n        $metadata = BlockMetadata::fromArray($array);\n\n        return new self($metadata);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"breadcrumb\"] = new \\stdClass();\n\n        return $array;\n    }\n\n    public function metadata(): BlockMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function addChild(BlockInterface $child): never\n    {\n        throw BlockException::noChindrenSupport();\n    }\n\n    public function changeChildren(BlockInterface ...$children): never\n    {\n        throw BlockException::noChindrenSupport();\n    }\n\n    public function delete(): BlockInterface\n    {\n        return new self($this->metadata->delete());\n    }\n\n    /**\n     * @deprecated 1.17.0 Use `delete()` instead.\n     * @codeCoverageIgnore\n     */\n    public function archive(): BlockInterface\n    {\n        return $this->delete();\n    }\n}\n"
  },
  {
    "path": "src/Blocks/BulletedListItem.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\nuse Notion\\Common\\Color;\nuse Notion\\Common\\RichText;\n\n/**\n * Bulleted list item\n *\n * @psalm-import-type BlockMetadataJson from BlockMetadata\n * @psalm-import-type RichTextJson from \\Notion\\Common\\RichText\n *\n * @psalm-type BulletedListItemJson = array{\n *      bulleted_list_item: array{\n *          rich_text: list<RichTextJson>,\n *          color?: string,\n *          children?: list<BlockMetadataJson>,\n *      },\n * }\n *\n * @psalm-immutable\n */\nclass BulletedListItem implements BlockInterface\n{\n    /**\n     * @param RichText[] $text\n     * @param BlockInterface[] $children\n     */\n    private function __construct(\n        private readonly BlockMetadata $metadata,\n        public readonly array $text,\n        public readonly Color $color,\n        public readonly array $children,\n    ) {\n        $this->metadata->checkType(BlockType::BulletedListItem);\n    }\n\n    /**\n     * Create empty bulleted list item\n     */\n    public static function create(): self\n    {\n        $metadata = BlockMetadata::create(BlockType::BulletedListItem);\n\n        return new self($metadata, [], Color::Default, []);\n    }\n\n    /**\n     * Create bulleted list item from a string\n     */\n    public static function fromString(string $content): self\n    {\n        $metadata = BlockMetadata::create(BlockType::BulletedListItem);\n        $text = [ RichText::fromString($content) ];\n\n        return new self($metadata, $text, Color::Default, []);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var BlockMetadataJson $array */\n        $metadata = BlockMetadata::fromArray($array);\n\n        /** @psalm-var BulletedListItemJson $array */\n        $item = $array[\"bulleted_list_item\"];\n\n        $text = array_map(fn($t) => RichText::fromArray($t), $item[\"rich_text\"]);\n\n        $color = Color::tryFrom($item[\"color\"] ?? \"\") ?? Color::Default;\n\n        $children = array_map(fn($b) => BlockFactory::fromArray($b), $item[\"children\"] ?? []);\n\n        return new self($metadata, $text, $color, $children);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"bulleted_list_item\"] = [\n            \"rich_text\" => array_map(fn(RichText $t) => $t->toArray(), $this->text),\n            \"color\"     => $this->color->value,\n            \"children\"  => array_map(fn(BlockInterface $b) => $b->toArray(), $this->children),\n        ];\n\n        return $array;\n    }\n\n    /** Get item content as string */\n    public function toString(): string\n    {\n        return RichText::multipleToString(...$this->text);\n    }\n\n    public function metadata(): BlockMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function changeText(RichText ...$text): self\n    {\n        return new self($this->metadata->update(), $text, $this->color, $this->children);\n    }\n\n    /**\n     * add text to list item\n     */\n    public function addText(RichText $text): self\n    {\n        $texts = $this->text;\n        $texts[] = $text;\n\n        return new self($this->metadata, $texts, $this->color, $this->children);\n    }\n\n    public function changeChildren(BlockInterface ...$children): self\n    {\n        $hasChildren = (count($children) > 0);\n\n        return new self(\n            $this->metadata->updateHasChildren($hasChildren),\n            $this->text,\n            $this->color,\n            $children,\n        );\n    }\n\n    public function addChild(BlockInterface $child): self\n    {\n        $children = $this->children;\n        $children[] = $child;\n\n        return new self(\n            $this->metadata->updateHasChildren(true),\n            $this->text,\n            $this->color,\n            $children,\n        );\n    }\n\n    public function changeColor(Color $color): self\n    {\n        return new self(\n            $this->metadata->update(),\n            $this->text,\n            $color,\n            $this->children,\n        );\n    }\n\n    public function delete(): BlockInterface\n    {\n        return new self(\n            $this->metadata->delete(),\n            $this->text,\n            $this->color,\n            $this->children,\n        );\n    }\n\n    /**\n     * @deprecated 1.17.0 Use `delete()` instead.\n     * @codeCoverageIgnore\n     */\n    public function archive(): BlockInterface\n    {\n        return $this->delete();\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Callout.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\nuse Notion\\Common\\Color;\nuse Notion\\Common\\Emoji;\nuse Notion\\Common\\File;\nuse Notion\\Common\\Icon;\nuse Notion\\Common\\RichText;\n\n/**\n * @psalm-import-type BlockMetadataJson from BlockMetadata\n * @psalm-import-type RichTextJson from \\Notion\\Common\\RichText\n * @psalm-import-type EmojiJson from \\Notion\\Common\\Emoji\n * @psalm-import-type FileJson from \\Notion\\Common\\File\n *\n * @psalm-type CalloutJson = array{\n *      callout: array{\n *          rich_text: list<RichTextJson>,\n *          color?: string,\n *          children?: list<BlockMetadataJson>,\n *          icon: EmojiJson|FileJson,\n *      },\n * }\n *\n * @psalm-immutable\n */\nclass Callout implements BlockInterface\n{\n    /**\n     * @param RichText[] $text\n     * @param BlockInterface[] $children\n     */\n    private function __construct(\n        private readonly BlockMetadata $metadata,\n        public readonly array $text,\n        public readonly Icon $icon,\n        public readonly Color $color,\n        public readonly array $children,\n    ) {\n        $metadata->checkType(BlockType::Callout);\n    }\n\n    public static function create(): self\n    {\n        $metadata = BlockMetadata::create(BlockType::Callout);\n        $icon = Icon::fromEmoji(Emoji::fromString(\"⭐\"));\n\n        return new self($metadata, [], $icon, Color::Default, []);\n    }\n\n    public static function fromString(string $emoji, string $content): self\n    {\n        $metadata = BlockMetadata::create(BlockType::Callout);\n        $text = [ RichText::fromString($content) ];\n        $icon = Icon::fromEmoji(Emoji::fromString($emoji));\n\n        return new self($metadata, $text, $icon, Color::Default, []);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var BlockMetadataJson $array */\n        $metadata = BlockMetadata::fromArray($array);\n\n        /** @psalm-var CalloutJson $array */\n        $callout = $array[\"callout\"];\n\n        $text = array_map(fn($t) => RichText::fromArray($t), $callout[\"rich_text\"]);\n\n        $iconArray = $callout[\"icon\"];\n        if ($iconArray[\"type\"] === \"emoji\") {\n            /** @psalm-var EmojiJson $iconArray */\n            $emoji = Emoji::fromArray($iconArray);\n            $icon = Icon::fromEmoji($emoji);\n        } else {\n            /** @psalm-var FileJson $iconArray */\n            $file = File::fromArray($iconArray);\n            $icon = Icon::fromFile($file);\n        }\n\n        $color = Color::tryFrom($callout[\"color\"] ?? \"\") ?? Color::Default;\n\n        $children = array_map(fn($b) => BlockFactory::fromArray($b), $callout[\"children\"] ?? []);\n\n        return new self($metadata, $text, $icon, $color, $children);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"callout\"] = [\n            \"rich_text\"     => array_map(fn(RichText $t) => $t->toArray(), $this->text),\n            \"icon\"     => $this->icon->toArray(),\n            \"color\"    => $this->color->value,\n            \"children\" => array_map(fn(BlockInterface $b) => $b->toArray(), $this->children),\n        ];\n\n        return $array;\n    }\n\n    public function toString(): string\n    {\n        return RichText::multipleToString(...$this->text);\n    }\n\n    public function metadata(): BlockMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function changeText(RichText ...$text): self\n    {\n        return new self($this->metadata, $text, $this->icon, $this->color, $this->children);\n    }\n\n    public function addText(RichText $text): self\n    {\n        $texts = $this->text;\n        $texts[] = $text;\n\n        return new self($this->metadata, $texts, $this->icon, $this->color, $this->children);\n    }\n\n    public function changeIcon(Emoji|File|Icon $icon): self\n    {\n        if ($icon instanceof Emoji) {\n            $icon = Icon::fromEmoji($icon);\n        }\n\n        if ($icon instanceof File) {\n            $icon = Icon::fromFile($icon);\n        }\n\n        return new self($this->metadata, $this->text, $icon, $this->color, $this->children);\n    }\n\n    public function changeChildren(BlockInterface ...$children): self\n    {\n        $hasChildren = (count($children) > 0);\n\n        return new self(\n            $this->metadata->updateHasChildren($hasChildren),\n            $this->text,\n            $this->icon,\n            $this->color,\n            $children,\n        );\n    }\n\n    public function addChild(BlockInterface $child): self\n    {\n        $children = $this->children;\n        $children[] = $child;\n\n        return new self(\n            $this->metadata->updateHasChildren(true),\n            $this->text,\n            $this->icon,\n            $this->color,\n            $children,\n        );\n    }\n\n    public function changeColor(Color $color): self\n    {\n        return new self(\n            $this->metadata->update(),\n            $this->text,\n            $this->icon,\n            $color,\n            $this->children,\n        );\n    }\n\n    public function delete(): BlockInterface\n    {\n        return new self(\n            $this->metadata->delete(),\n            $this->text,\n            $this->icon,\n            $this->color,\n            $this->children,\n        );\n    }\n\n    /**\n     * @deprecated 1.17.0 Use `delete()` instead.\n     * @codeCoverageIgnore\n     */\n    public function archive(): BlockInterface\n    {\n        return $this->delete();\n    }\n}\n"
  },
  {
    "path": "src/Blocks/ChildDatabase.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\nuse Notion\\Exceptions\\BlockException;\n\n/**\n * @psalm-import-type BlockMetadataJson from BlockMetadata\n *\n * @psalm-type ChildDatabaseJson = array{\n *      child_database: array{ title: string },\n * }\n *\n * @psalm-immutable\n */\nclass ChildDatabase implements BlockInterface\n{\n    private function __construct(\n        private readonly BlockMetadata $metadata,\n        public readonly string $title\n    ) {\n        $metadata->checkType(BlockType::ChildDatabase);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var BlockMetadataJson $array */\n        $metadata = BlockMetadata::fromArray($array);\n\n        /** @psalm-var ChildDatabaseJson $array */\n        $databaseTitle = $array[\"child_database\"][\"title\"];\n\n        return new self($metadata, $databaseTitle);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"child_database\"] = [ \"title\" => $this->title ];\n\n        return $array;\n    }\n\n    public function metadata(): BlockMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function addChild(BlockInterface $child): never\n    {\n        throw BlockException::noChindrenSupport();\n    }\n\n    public function changeChildren(BlockInterface ...$children): never\n    {\n        throw BlockException::noChindrenSupport();\n    }\n\n    public function delete(): BlockInterface\n    {\n        return new self(\n            $this->metadata->delete(),\n            $this->title,\n        );\n    }\n\n    /**\n     * @deprecated 1.17.0 Use `delete()` instead.\n     * @codeCoverageIgnore\n     */\n    public function archive(): BlockInterface\n    {\n        return $this->delete();\n    }\n}\n"
  },
  {
    "path": "src/Blocks/ChildPage.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\nuse Notion\\Exceptions\\BlockException;\n\n/**\n * @psalm-import-type BlockMetadataJson from BlockMetadata\n *\n * @psalm-type ChildPageJson = array{\n *      child_page: array{ title: string },\n * }\n *\n * @psalm-immutable\n */\nclass ChildPage implements BlockInterface\n{\n    private function __construct(\n        private readonly BlockMetadata $metadata,\n        public readonly string $title,\n    ) {\n        $metadata->checkType(BlockType::ChildPage);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var BlockMetadataJson $array */\n        $metadata = BlockMetadata::fromArray($array);\n\n        /** @psalm-var ChildPageJson $array */\n        $pageTitle = $array[\"child_page\"][\"title\"];\n\n        return new self($metadata, $pageTitle);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"child_page\"] = [ \"title\" => $this->title ];\n\n        return $array;\n    }\n\n    public function metadata(): BlockMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function addChild(BlockInterface $child): never\n    {\n        throw BlockException::noChindrenSupport();\n    }\n\n    public function changeChildren(BlockInterface ...$children): never\n    {\n        throw BlockException::noChindrenSupport();\n    }\n\n    public function delete(): BlockInterface\n    {\n        return new self(\n            $this->metadata->delete(),\n            $this->title,\n        );\n    }\n\n    /**\n     * @deprecated 1.17.0 Use `delete()` instead.\n     * @codeCoverageIgnore\n     */\n    public function archive(): BlockInterface\n    {\n        return $this->delete();\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Client.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\nuse Notion\\Blocks\\BlockInterface;\nuse Notion\\Configuration;\nuse Notion\\Infrastructure\\Http;\n\nclass Client\n{\n    /**\n     * @internal Use `\\Notion\\Notion::blocks()` instead\n     */\n    public function __construct(\n        private readonly Configuration $config,\n    ) {\n    }\n\n    public function find(string $blockId): BlockInterface\n    {\n        $url = \"https://api.notion.com/v1/blocks/{$blockId}\";\n        $request = Http::createRequest($url, $this->config);\n\n        /** @var array{ type: string } $body */\n        $body = Http::sendRequest($request, $this->config);\n\n        return BlockFactory::fromArray($body);\n    }\n\n    /** @return BlockInterface[] */\n    public function findChildren(string $blockId): array\n    {\n        $url = \"https://api.notion.com/v1/blocks/{$blockId}/children\";\n        $request = Http::createRequest($url, $this->config);\n\n        /** @var array{ results: list<array{ type: string }> } $body */\n        $body = Http::sendRequest($request, $this->config);\n\n        return array_map(\n            fn(array $blockArray) => BlockFactory::fromArray($blockArray),\n            $body[\"results\"],\n        );\n    }\n\n    /** @return BlockInterface[] */\n    public function findChildrenRecursive(string $blockId): array\n    {\n        $children = $this->findChildren($blockId);\n        return array_map(\n            function (BlockInterface $block) {\n                if ($block->metadata()->hasChildren) {\n                    $blockChildren = $this->findChildrenRecursive($block->metadata()->id);\n                    return $block->changeChildren(...$blockChildren);\n                }\n\n                return $block;\n            },\n            $children\n        );\n    }\n\n    /**\n     * @param BlockInterface[] $blocks\n     *\n     * @return BlockInterface[] Newly created blocks\n     */\n    public function append(string $blockId, array $blocks): array\n    {\n        $data = json_encode([\n            \"children\" => array_map(fn(BlockInterface $b) => $b->toArray(), $blocks),\n        ]);\n\n        $url = \"https://api.notion.com/v1/blocks/{$blockId}/children\";\n        $request = Http::createRequest($url, $this->config)\n            ->withMethod(\"PATCH\")\n            ->withHeader(\"Content-Type\", \"application/json\");\n        $request->getBody()->write($data);\n\n        /** @var array{ results: list<array{ type: string }> } $body */\n        $body = Http::sendRequest($request, $this->config);\n\n        return array_map(\n            fn(array $blockArray): BlockInterface => BlockFactory::fromArray($blockArray),\n            $body[\"results\"],\n        );\n    }\n\n    public function update(BlockInterface $block): BlockInterface\n    {\n        $blockId = $block->metadata()->id;\n        $blockType = $block->metadata()->type->value;\n\n        $data = $block->toArray();\n\n        unset($data[\"type\"]);\n        unset($data[\"id\"]);\n        unset($data[\"created_time\"]);\n        unset($data[\"last_edited_time\"]);\n        unset($data[\"has_children\"]);\n        if (is_array($data[$blockType])) {\n            unset($data[$blockType][\"children\"]);\n        }\n\n        $json = json_encode($data);\n\n        $url = \"https://api.notion.com/v1/blocks/{$blockId}\";\n        $request = Http::createRequest($url, $this->config)\n            ->withMethod(\"PATCH\")\n            ->withHeader(\"Content-Type\", \"application/json\");\n\n        $request->getBody()->write($json);\n\n        /** @var array{ type: string } $body */\n        $body = Http::sendRequest($request, $this->config);\n\n        return BlockFactory::fromArray($body);\n    }\n\n    public function delete(string $blockId): BlockInterface\n    {\n        $url = \"https://api.notion.com/v1/blocks/{$blockId}\";\n        $request = Http::createRequest($url, $this->config)\n            ->withMethod(\"DELETE\");\n\n        /** @var array{ type: string } $body */\n        $body = Http::sendRequest($request, $this->config);\n\n        return BlockFactory::fromArray($body);\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Code.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Common\\RichText;\n\n/**\n * @psalm-import-type BlockMetadataJson from BlockMetadata\n * @psalm-import-type RichTextJson from \\Notion\\Common\\RichText\n *\n * @psalm-type CodeJson = array{\n *      code: array{\n *          rich_text: RichTextJson[],\n *          caption: RichTextJson[],\n *          language: string,\n *      },\n * }\n *\n * @psalm-immutable\n */\nclass Code implements BlockInterface\n{\n    /**\n     * @param RichText[] $text\n     * @param RichText[] $caption\n     */\n    private function __construct(\n        private readonly BlockMetadata $metadata,\n        public readonly array $text,\n        public readonly CodeLanguage $language,\n        public readonly array $caption,\n    ) {\n        $metadata->checkType(BlockType::Code);\n    }\n\n    public static function create(): self\n    {\n        return self::fromText([]);\n    }\n\n    /** @param RichText[] $text */\n    public static function fromText(\n        array $text,\n        CodeLanguage $language = CodeLanguage::PlainText,\n    ): self {\n        $metadata = BlockMetadata::create(BlockType::Code);\n\n        return new self($metadata, $text, $language, []);\n    }\n\n    public static function fromString(\n        string $code,\n        CodeLanguage $language = CodeLanguage::PlainText,\n    ): self {\n        $metadata = BlockMetadata::create(BlockType::Code);\n        $text = [ RichText::fromString($code) ];\n\n        return new self($metadata, $text, $language, []);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var BlockMetadataJson $array */\n        $metadata = BlockMetadata::fromArray($array);\n\n        /** @psalm-var CodeJson $array */\n        $code = $array[\"code\"];\n        $text = array_map(fn($t) => RichText::fromArray($t), $code[\"rich_text\"]);\n        $caption = array_map(fn($t) => RichText::fromArray($t), $code[\"caption\"]);\n        $language = CodeLanguage::from($code[\"language\"]);\n\n        return new self($metadata, $text, $language, $caption);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"code\"] = [\n            \"rich_text\" => array_map(fn(RichText $t) => $t->toArray(), $this->text),\n            \"caption\"   => array_map(fn(RichText $t) => $t->toArray(), $this->caption),\n            \"language\"  => $this->language->value,\n        ];\n\n        return $array;\n    }\n\n    public function toString(): string\n    {\n        return RichText::multipleToString(...$this->text);\n    }\n\n    public function metadata(): BlockMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function changeText(RichText ...$text): self\n    {\n        return new self($this->metadata, $text, $this->language, $this->caption);\n    }\n\n    public function addText(RichText $text): self\n    {\n        $texts = $this->text;\n        $texts[] = $text;\n\n        return new self($this->metadata, $texts, $this->language, $this->caption);\n    }\n\n    public function changeLanguage(CodeLanguage $language): self\n    {\n        return new self($this->metadata, $this->text, $language, $this->caption);\n    }\n\n    public function changeCaption(RichText ...$caption): self\n    {\n        return new self($this->metadata, $this->text, $this->language, $caption);\n    }\n\n    public function addChild(BlockInterface $child): never\n    {\n        throw BlockException::noChindrenSupport();\n    }\n\n    public function changeChildren(BlockInterface ...$children): never\n    {\n        throw BlockException::noChindrenSupport();\n    }\n\n    public function delete(): BlockInterface\n    {\n        return new self(\n            $this->metadata->delete(),\n            $this->text,\n            $this->language,\n            $this->caption,\n        );\n    }\n\n    /**\n     * @deprecated 1.17.0 Use `delete()` instead.\n     * @codeCoverageIgnore\n     */\n    public function archive(): BlockInterface\n    {\n        return $this->delete();\n    }\n}\n"
  },
  {
    "path": "src/Blocks/CodeLanguage.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\nenum CodeLanguage: string\n{\n    case Abap = \"abap\";\n    case Arduino = \"arduino\";\n    case Bash = \"bash\";\n    case Basic = \"basic\";\n    case C = \"c\";\n    case Clojure = \"clojure\";\n    case Coffeescript = \"coffeescript\";\n    case Cpp = \"c++\";\n    case CSharp = \"c#\";\n    case Css = \"css\";\n    case Dart = \"dart\";\n    case Diff = \"diff\";\n    case Docker = \"docker\";\n    case Elixir = \"elixir\";\n    case Elm = \"elm\";\n    case Erlang = \"erlang\";\n    case Flow = \"flow\";\n    case Fortran = \"fortran\";\n    case FSharp = \"f#\";\n    case Gherkin = \"gherkin\";\n    case Glsl = \"glsl\";\n    case Go = \"go\";\n    case Graphql = \"graphql\";\n    case Groovy = \"groovy\";\n    case Haskell = \"haskell\";\n    case Html = \"html\";\n    case Java = \"java\";\n    case Javascript = \"javascript\";\n    case Json = \"json\";\n    case Julia = \"julia\";\n    case Kotlin = \"kotlin\";\n    case Latex = \"latex\";\n    case Less = \"less\";\n    case Lisp = \"lisp\";\n    case Livescript = \"livescript\";\n    case Lua = \"lua\";\n    case Makefile = \"makefile\";\n    case Markdown = \"markdown\";\n    case Markup = \"markup\";\n    case Matlab = \"matlab\";\n    case Mermaid = \"mermaid\";\n    case Nix = \"nix\";\n    case ObjectiveC = \"objective-c\";\n    case Ocaml = \"ocaml\";\n    case Pascal = \"pascal\";\n    case Perl = \"perl\";\n    case Php = \"php\";\n    case PlainText = \"plain text\";\n    case Powershell = \"powershell\";\n    case Prolog = \"prolog\";\n    case Protobuf = \"protobuf\";\n    case Python = \"python\";\n    case R = \"r\";\n    case Reason = \"reason\";\n    case Ruby = \"ruby\";\n    case Rust = \"rust\";\n    case Sass = \"sass\";\n    case Scala = \"scala\";\n    case Scheme = \"scheme\";\n    case Scss = \"scss\";\n    case Shell = \"shell\";\n    case Sql = \"sql\";\n    case Swift = \"swift\";\n    case Typescript = \"typescript\";\n    case VbNet = \"vb.net\";\n    case Verilog = \"verilog\";\n    case Vhdl = \"vhdl\";\n    case VisualBasic = \"visual basic\";\n    case Webassembly = \"webassembly\";\n    case Xml = \"xml\";\n    case Yaml = \"yaml\";\n}\n"
  },
  {
    "path": "src/Blocks/Column.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\nuse Notion\\Exceptions\\ColumnException;\n\n/**\n * @psalm-import-type BlockMetadataJson from BlockMetadata\n *\n * @psalm-type ColumnJson = array{\n *     column: array {\n *         children: list<BlockMetadataJson>\n *     },\n * }\n *\n * @psalm-immutable\n */\nclass Column implements BlockInterface\n{\n    /** @param BlockInterface[] $children */\n    private function __construct(\n        private readonly BlockMetadata $block,\n        public readonly array $children,\n    ) {\n        foreach ($children as $child) {\n            if ($child->metadata()->type === BlockType::Column) {\n                throw ColumnException::columnInsideColumn();\n            }\n        }\n    }\n\n    public static function create(BlockInterface ...$children): self\n    {\n        $block = BlockMetadata::create(BlockType::Column);\n\n        return new self($block, $children);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var BlockMetadataJson $array */\n        $block = BlockMetadata::fromArray($array);\n\n        /** @psalm-var ColumnJson $array */\n        $rawChildren = $array[\"column\"][\"children\"] ?? [];\n        $children = array_map(fn($child) => BlockFactory::fromArray($child), $rawChildren);\n\n        return new self($block, $children);\n    }\n\n    public function addChild(BlockInterface $child): self\n    {\n        return new self($this->block, [...$this->children, $child]);\n    }\n\n    public function changeChildren(BlockInterface ...$children): self\n    {\n        return new self($this->block, $children);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata()->toArray();\n\n        $array[\"column\"] = [\n            \"children\" => array_map(fn ($child) => $child->toArray(), $this->children),\n        ];\n\n        return $array;\n    }\n\n    public function metadata(): BlockMetadata\n    {\n        return $this->block;\n    }\n\n    public function delete(): BlockInterface\n    {\n        return new self(\n            $this->block->delete(),\n            $this->children,\n        );\n    }\n\n    /**\n     * @deprecated 1.17.0 Use `delete()` instead.\n     * @codeCoverageIgnore\n     */\n    public function archive(): BlockInterface\n    {\n        return $this->delete();\n    }\n}\n"
  },
  {
    "path": "src/Blocks/ColumnList.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\nuse Notion\\Exceptions\\ColumnListException;\n\n/**\n * @psalm-import-type BlockMetadataJson from BlockMetadata\n *\n * @psalm-type ColumnListJson = array{\n *     column_list: array{\n *         children: list<BlockMetadataJson>\n *     },\n * }\n *\n * @psalm-immutable\n */\nclass ColumnList implements BlockInterface\n{\n    /** @param Column[] $columns */\n    private function __construct(\n        private readonly BlockMetadata $metadata,\n        public readonly array $columns,\n    ) {\n        $metadata->checkType(BlockType::ColumnList);\n    }\n\n    public static function create(Column ...$columns): self\n    {\n        $metadata = BlockMetadata::create(BlockType::ColumnList);\n\n        return new self($metadata, $columns);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var BlockMetadataJson $array */\n        $metadata = BlockMetadata::fromArray($array);\n\n        /** @psalm-var ColumnListJson $array */\n        $rawColumns = $array[\"column_list\"][\"children\"] ?? [];\n        /** @var Column[] $columns */\n        $columns = array_map(fn($child) => BlockFactory::fromArray($child), $rawColumns);\n\n        return new self($metadata, $columns);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"column_list\"] = [\n            \"children\" => array_map(fn (Column $c) => $c->toArray(), $this->columns),\n        ];\n\n        return $array;\n    }\n\n    public function metadata(): BlockMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function addChild(BlockInterface $child): self\n    {\n        if ($child->metadata()->type !== BlockType::Column) {\n            throw ColumnListException::childNotColumn();\n        }\n\n        /** @var Column $child */\n        return new self($this->metadata(), [...$this->columns, $child]);\n    }\n\n    public function changeChildren(BlockInterface ...$children): self\n    {\n        foreach ($children as $child) {\n            if ($child->metadata()->type !== BlockType::Column) {\n                throw ColumnListException::childNotColumn();\n            }\n        }\n\n        /** @var Column[] $children */\n        return new self($this->metadata(), $children);\n    }\n\n    public function delete(): BlockInterface\n    {\n        return new self(\n            $this->metadata->delete(),\n            $this->columns,\n        );\n    }\n\n    /**\n     * @deprecated 1.17.0 Use `delete()` instead.\n     * @codeCoverageIgnore\n     */\n    public function archive(): BlockInterface\n    {\n        return $this->delete();\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Divider.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\nuse Notion\\Exceptions\\BlockException;\n\n/**\n * @psalm-import-type BlockMetadataJson from BlockMetadata\n *\n * @psalm-type DividerJson = array{\n *      divider: array<empty, empty>\n * }\n * @psalm-immutable\n */\nclass Divider implements BlockInterface\n{\n    private function __construct(\n        private readonly BlockMetadata $metadata,\n    ) {\n        $metadata->checkType(BlockType::Divider);\n    }\n\n    public static function create(): self\n    {\n        $metadata = BlockMetadata::create(BlockType::Divider);\n\n        return new self($metadata);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var BlockMetadataJson $array */\n        $metadata = BlockMetadata::fromArray($array);\n\n        return new self($metadata);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"divider\"] = new \\stdClass();\n\n        return $array;\n    }\n\n    public function metadata(): BlockMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function addChild(BlockInterface $child): never\n    {\n        throw BlockException::noChindrenSupport();\n    }\n\n    public function changeChildren(BlockInterface ...$children): never\n    {\n        throw BlockException::noChindrenSupport();\n    }\n\n    public function delete(): BlockInterface\n    {\n        return new self(\n            $this->metadata->delete(),\n        );\n    }\n\n    /**\n     * @deprecated 1.17.0 Use `delete()` instead.\n     * @codeCoverageIgnore\n     */\n    public function archive(): BlockInterface\n    {\n        return $this->delete();\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Embed.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\nuse Notion\\Exceptions\\BlockException;\n\n/**\n * @psalm-import-type BlockMetadataJson from BlockMetadata\n *\n * @psalm-type EmbedJson = array{\n *      embed: array{ url: string },\n * }\n *\n * @psalm-immutable\n */\nclass Embed implements BlockInterface\n{\n    private function __construct(\n        private readonly BlockMetadata $metadata,\n        public readonly string $url,\n    ) {\n        $metadata->checkType(BlockType::Embed);\n    }\n\n    public static function fromUrl(string $url = \"\"): self\n    {\n        $metadata = BlockMetadata::create(BlockType::Embed);\n\n        return new self($metadata, $url);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var BlockMetadataJson $array */\n        $metadata = BlockMetadata::fromArray($array);\n\n        /** @psalm-var EmbedJson $array */\n        $url = $array[\"embed\"][\"url\"];\n\n        return new self($metadata, $url);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"embed\"] = [ \"url\" => $this->url ];\n\n        return $array;\n    }\n\n    public function metadata(): BlockMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function changeUrl(string $url): self\n    {\n        return new self($this->metadata, $url);\n    }\n\n    public function addChild(BlockInterface $child): never\n    {\n        throw BlockException::noChindrenSupport();\n    }\n\n    public function changeChildren(BlockInterface ...$children): never\n    {\n        throw BlockException::noChindrenSupport();\n    }\n\n    public function delete(): BlockInterface\n    {\n        return new self(\n            $this->metadata->delete(),\n            $this->url,\n        );\n    }\n\n    /**\n     * @deprecated 1.17.0 Use `delete()` instead.\n     * @codeCoverageIgnore\n     */\n    public function archive(): BlockInterface\n    {\n        return $this->delete();\n    }\n}\n"
  },
  {
    "path": "src/Blocks/EquationBlock.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Common\\Equation;\n\n/**\n * @psalm-import-type BlockMetadataJson from BlockMetadata\n * @psalm-import-type EquationJson from \\Notion\\Common\\Equation\n *\n * @psalm-type EquationBlockMetadataJson = array{\n *      equation: EquationJson,\n * }\n *\n * @psalm-immutable\n */\nclass EquationBlock implements BlockInterface\n{\n    private function __construct(\n        private readonly BlockMetadata $metadata,\n        public readonly Equation $equation\n    ) {\n        $metadata->checkType(BlockType::Equation);\n    }\n\n    public static function fromString(string $expression = \"\"): self\n    {\n        $block = BlockMetadata::create(BlockType::Equation);\n        $equation = Equation::fromString($expression);\n\n        return new self($block, $equation);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var BlockMetadataJson $array */\n        $block = BlockMetadata::fromArray($array);\n\n        /** @psalm-var EquationBlockMetadataJson $array */\n        $equation = Equation::fromArray($array[\"equation\"]);\n\n        return new self($block, $equation);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"equation\"] = $this->equation->toArray();\n\n        return $array;\n    }\n\n    public function metadata(): BlockMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function changeEquation(Equation $equation): self\n    {\n        return new self($this->metadata, $equation);\n    }\n\n    public function addChild(BlockInterface $child): never\n    {\n        throw BlockException::noChindrenSupport();\n    }\n\n    public function changeChildren(BlockInterface ...$children): never\n    {\n        throw BlockException::noChindrenSupport();\n    }\n\n    public function delete(): BlockInterface\n    {\n        return new self(\n            $this->metadata->delete(),\n            $this->equation,\n        );\n    }\n\n    /**\n     * @deprecated 1.17.0 Use `delete()` instead.\n     * @codeCoverageIgnore\n     */\n    public function archive(): BlockInterface\n    {\n        return $this->delete();\n    }\n}\n"
  },
  {
    "path": "src/Blocks/FileBlock.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Common\\File;\n\n/**\n * @psalm-import-type BlockMetadataJson from BlockMetadata\n * @psalm-import-type FileJson from \\Notion\\Common\\File\n *\n * @psalm-type FileBlockMetadataJson = array{ file: FileJson }\n *\n * @psalm-immutable\n */\nclass FileBlock implements BlockInterface\n{\n    private function __construct(\n        private readonly BlockMetadata $metadata,\n        private readonly File $file,\n    ) {\n        $metadata->checkType(BlockType::File);\n    }\n\n    public static function fromFile(File $file): self\n    {\n        $metadata = BlockMetadata::create(BlockType::File);\n\n        return new self($metadata, $file);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var BlockMetadataJson $array */\n        $metadata = BlockMetadata::fromArray($array);\n\n        /** @psalm-var FileBlockMetadataJson $array */\n        $file = File::fromArray($array[\"file\"]);\n\n        return new self($metadata, $file);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"file\"] = $this->file->toArray();\n\n        return $array;\n    }\n\n    public function metadata(): BlockMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function file(): File\n    {\n        return $this->file;\n    }\n\n    public function changeFile(File $file): self\n    {\n        return new self($this->metadata, $file);\n    }\n\n    public function addChild(BlockInterface $child): never\n    {\n        throw BlockException::noChindrenSupport();\n    }\n\n    public function changeChildren(BlockInterface ...$children): never\n    {\n        throw BlockException::noChindrenSupport();\n    }\n\n    public function delete(): BlockInterface\n    {\n        return new self(\n            $this->metadata->delete(),\n            $this->file,\n        );\n    }\n\n    /**\n     * @deprecated 1.17.0 Use `delete()` instead.\n     * @codeCoverageIgnore\n     */\n    public function archive(): BlockInterface\n    {\n        return $this->delete();\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Heading1.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\nuse Notion\\Common\\Color;\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Common\\RichText;\nuse Notion\\Exceptions\\HeadingException;\n\n/**\n * @psalm-import-type BlockMetadataJson from BlockMetadata\n * @psalm-import-type RichTextJson from \\Notion\\Common\\RichText\n *\n * @psalm-type Heading1Json = array{\n *      heading_1: array{\n *          rich_text: RichTextJson[],\n *          is_toggleable: bool,\n *          color?: string,\n *          children?: BlockMetadataJson[]\n *      },\n * }\n *\n * @psalm-immutable\n */\nclass Heading1 implements BlockInterface\n{\n    /**\n     * @param RichText[] $text\n     * @param BlockInterface[]|null $children\n     */\n    private function __construct(\n        private readonly BlockMetadata $metadata,\n        public readonly array $text,\n        public readonly bool $isToggleable,\n        public readonly Color $color,\n        public readonly array|null $children,\n    ) {\n        $metadata->checkType(BlockType::Heading1);\n    }\n\n    public static function fromText(RichText ...$text): self\n    {\n        $block = BlockMetadata::create(BlockType::Heading1);\n\n        return new self($block, $text, false, Color::Default, []);\n    }\n\n    public static function fromString(string $content): self\n    {\n        $block = BlockMetadata::create(BlockType::Heading1);\n        $text = [ RichText::fromString($content) ];\n\n        return new self($block, $text, false, Color::Default, []);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var BlockMetadataJson $array */\n        $block = BlockMetadata::fromArray($array);\n\n        /** @psalm-var Heading1Json $array */\n        $heading = $array[\"heading_1\"];\n\n        $text = array_map(fn($t) => RichText::fromArray($t), $heading[\"rich_text\"]);\n\n        $isToggleable = $heading[\"is_toggleable\"];\n\n        $color = Color::tryFrom($heading[\"color\"] ?? \"\") ?? Color::Default;\n\n        $children = null;\n        if ($isToggleable) {\n            $children = array_map(fn($b) => BlockFactory::fromArray($b), $heading[\"children\"] ?? []);\n        }\n\n        return new self($block, $text, $isToggleable, $color, $children);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"heading_1\"] = [\n            \"rich_text\" => array_map(fn(RichText $t) => $t->toArray(), $this->text),\n            \"is_toggleable\" => $this->isToggleable,\n            \"color\" => $this->color->value,\n            \"children\" => array_map(fn($b) => $b->toArray(), $this->children ?? [])\n        ];\n\n        return $array;\n    }\n\n    public function toString(): string\n    {\n        $string = \"\";\n        foreach ($this->text as $richText) {\n            $string = $string . $richText->plainText;\n        }\n\n        return $string;\n    }\n\n    public function metadata(): BlockMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function changeText(RichText ...$text): self\n    {\n        return new self($this->metadata, $text, $this->isToggleable, $this->color, $this->children);\n    }\n\n    public function addText(RichText $text): self\n    {\n        $texts = $this->text;\n        $texts[] = $text;\n\n        return new self($this->metadata, $texts, $this->isToggleable, $this->color, $this->children);\n    }\n\n    public function toggllify(): self\n    {\n        return new self($this->metadata, $this->text, true, $this->color, []);\n    }\n\n    public function untogglify(): self\n    {\n        if (!empty($this->children)) {\n            throw HeadingException::untogglifyWithChildren();\n        }\n\n        return new self($this->metadata, $this->text, false, $this->color, null);\n    }\n\n    public function changeColor(Color $color): self\n    {\n        return new self(\n            $this->metadata->update(),\n            $this->text,\n            $this->isToggleable,\n            $color,\n            $this->children,\n        );\n    }\n\n    public function addChild(BlockInterface $child): self\n    {\n        if (!$this->isToggleable) {\n            throw BlockException::noChindrenSupport();\n        }\n\n        $children = $this->children ? [...$this->children, $child] : [$child];\n        return new self(\n            $this->metadata,\n            $this->text,\n            $this->isToggleable,\n            $this->color,\n            $children,\n        );\n    }\n\n    public function changeChildren(BlockInterface ...$children): self\n    {\n        if (!$this->isToggleable) {\n            throw BlockException::noChindrenSupport();\n        }\n\n        return new self($this->metadata, $this->text, $this->isToggleable, $this->color, $children);\n    }\n\n    public function delete(): BlockInterface\n    {\n        return new self(\n            $this->metadata->delete(),\n            $this->text,\n            $this->isToggleable,\n            $this->color,\n            $this->children,\n        );\n    }\n\n    /**\n     * @deprecated 1.17.0 Use `delete()` instead.\n     * @codeCoverageIgnore\n     */\n    public function archive(): BlockInterface\n    {\n        return $this->delete();\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Heading2.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\nuse Notion\\Common\\Color;\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Common\\RichText;\nuse Notion\\Exceptions\\HeadingException;\n\n/**\n * @psalm-import-type BlockMetadataJson from BlockMetadata\n * @psalm-import-type RichTextJson from \\Notion\\Common\\RichText\n *\n * @psalm-type Heading2Json = array{\n *      heading_2: array{\n *          rich_text: RichTextJson[],\n *          is_toggleable: bool,\n *          color?: string,\n *          children?: BlockMetadataJson[]\n *      },\n * }\n *\n * @psalm-immutable\n */\nclass Heading2 implements BlockInterface\n{\n    /**\n     * @param RichText[] $text\n     * @param BlockInterface[]|null $children\n     */\n    private function __construct(\n        private readonly BlockMetadata $metadata,\n        public readonly array $text,\n        public readonly bool $isToggleable,\n        public readonly Color $color,\n        public readonly array|null $children,\n    ) {\n        $metadata->checkType(BlockType::Heading2);\n    }\n\n    public static function fromText(RichText ...$text): self\n    {\n        $block = BlockMetadata::create(BlockType::Heading2);\n\n        return new self($block, $text, false, Color::Default, []);\n    }\n\n    public static function fromString(string $content): self\n    {\n        $block = BlockMetadata::create(BlockType::Heading2);\n        $text = [ RichText::fromString($content) ];\n\n        return new self($block, $text, false, Color::Default, []);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var BlockMetadataJson $array */\n        $block = BlockMetadata::fromArray($array);\n\n        /** @psalm-var Heading2Json $array */\n        $heading = $array[\"heading_2\"];\n\n        $text = array_map(fn($t) => RichText::fromArray($t), $heading[\"rich_text\"]);\n\n        $isToggleable = $heading[\"is_toggleable\"];\n\n        $color = Color::tryFrom($heading[\"color\"] ?? \"\") ?? Color::Default;\n\n        $children = null;\n        if ($isToggleable) {\n            $children = array_map(fn($b) => BlockFactory::fromArray($b), $heading[\"children\"] ?? []);\n        }\n\n        return new self($block, $text, $isToggleable, $color, $children);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"heading_2\"] = [\n            \"rich_text\" => array_map(fn(RichText $t) => $t->toArray(), $this->text),\n            \"is_toggleable\" => $this->isToggleable,\n            \"color\" => $this->color->value,\n            \"children\" => array_map(fn($b) => $b->toArray(), $this->children ?? [])\n        ];\n\n        return $array;\n    }\n\n    public function toString(): string\n    {\n        $string = \"\";\n        foreach ($this->text as $richText) {\n            $string = $string . $richText->plainText;\n        }\n\n        return $string;\n    }\n\n    public function metadata(): BlockMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function changeText(RichText ...$text): self\n    {\n        return new self($this->metadata, $text, $this->isToggleable, $this->color, $this->children);\n    }\n\n    public function addText(RichText $text): self\n    {\n        $texts = $this->text;\n        $texts[] = $text;\n\n        return new self($this->metadata, $texts, $this->isToggleable, $this->color, $this->children);\n    }\n\n    public function toggllify(): self\n    {\n        return new self($this->metadata, $this->text, true, $this->color, []);\n    }\n\n    public function untogglify(): self\n    {\n        if (!empty($this->children)) {\n            throw HeadingException::untogglifyWithChildren();\n        }\n\n        return new self($this->metadata, $this->text, false, $this->color, null);\n    }\n\n    public function changeColor(Color $color): self\n    {\n        return new self(\n            $this->metadata->update(),\n            $this->text,\n            $this->isToggleable,\n            $color,\n            $this->children,\n        );\n    }\n\n    public function addChild(BlockInterface $child): self\n    {\n        if (!$this->isToggleable) {\n            throw BlockException::noChindrenSupport();\n        }\n\n        $children = $this->children ? [...$this->children, $child] : [$child];\n        return new self(\n            $this->metadata,\n            $this->text,\n            $this->isToggleable,\n            $this->color,\n            $children,\n        );\n    }\n\n    public function changeChildren(BlockInterface ...$children): self\n    {\n        if (!$this->isToggleable) {\n            throw BlockException::noChindrenSupport();\n        }\n\n        return new self($this->metadata, $this->text, $this->isToggleable, $this->color, $children);\n    }\n\n    public function delete(): BlockInterface\n    {\n        return new self(\n            $this->metadata->delete(),\n            $this->text,\n            $this->isToggleable,\n            $this->color,\n            $this->children,\n        );\n    }\n\n    /**\n     * @deprecated 1.17.0 Use `delete()` instead.\n     * @codeCoverageIgnore\n     */\n    public function archive(): BlockInterface\n    {\n        return $this->delete();\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Heading3.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\nuse Notion\\Common\\Color;\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Common\\RichText;\nuse Notion\\Exceptions\\HeadingException;\n\n/**\n * @psalm-import-type BlockMetadataJson from BlockMetadata\n * @psalm-import-type RichTextJson from \\Notion\\Common\\RichText\n *\n * @psalm-type Heading3Json = array{\n *      heading_3: array{\n *          rich_text: RichTextJson[],\n *          is_toggleable: bool,\n *          color?: string,\n *          children?: BlockMetadataJson[]\n *      },\n * }\n *\n * @psalm-immutable\n */\nclass Heading3 implements BlockInterface\n{\n    /**\n     * @param RichText[] $text\n     * @param BlockInterface[]|null $children\n     */\n    private function __construct(\n        private readonly BlockMetadata $metadata,\n        public readonly array $text,\n        public readonly bool $isToggleable,\n        public readonly Color $color,\n        public readonly array|null $children,\n    ) {\n        $metadata->checkType(BlockType::Heading3);\n    }\n\n    public static function fromText(RichText ...$text): self\n    {\n        $block = BlockMetadata::create(BlockType::Heading3);\n\n        return new self($block, $text, false, Color::Default, []);\n    }\n\n    public static function fromString(string $content): self\n    {\n        $block = BlockMetadata::create(BlockType::Heading3);\n        $text = [ RichText::fromString($content) ];\n\n        return new self($block, $text, false, Color::Default, []);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var BlockMetadataJson $array */\n        $block = BlockMetadata::fromArray($array);\n\n        /** @psalm-var Heading3Json $array */\n        $heading = $array[\"heading_3\"];\n\n        $text = array_map(fn($t) => RichText::fromArray($t), $heading[\"rich_text\"]);\n\n        $isToggleable = $heading[\"is_toggleable\"];\n\n        $color = Color::tryFrom($heading[\"color\"] ?? \"\") ?? Color::Default;\n\n        $children = null;\n        if ($isToggleable) {\n            $children = array_map(fn($b) => BlockFactory::fromArray($b), $heading[\"children\"] ?? []);\n        }\n\n        return new self($block, $text, $isToggleable, $color, $children);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"heading_3\"] = [\n            \"rich_text\" => array_map(fn(RichText $t) => $t->toArray(), $this->text),\n            \"is_toggleable\" => $this->isToggleable,\n            \"color\" => $this->color->value,\n            \"children\" => array_map(fn($b) => $b->toArray(), $this->children ?? [])\n        ];\n\n        return $array;\n    }\n\n    public function toString(): string\n    {\n        $string = \"\";\n        foreach ($this->text as $richText) {\n            $string = $string . $richText->plainText;\n        }\n\n        return $string;\n    }\n\n    public function metadata(): BlockMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function changeText(RichText ...$text): self\n    {\n        return new self($this->metadata, $text, $this->isToggleable, $this->color, $this->children);\n    }\n\n    public function addText(RichText $text): self\n    {\n        $texts = $this->text;\n        $texts[] = $text;\n\n        return new self($this->metadata, $texts, $this->isToggleable, $this->color, $this->children);\n    }\n\n    public function toggllify(): self\n    {\n        return new self($this->metadata, $this->text, true, $this->color, []);\n    }\n\n    public function untogglify(): self\n    {\n        if (!empty($this->children)) {\n            throw HeadingException::untogglifyWithChildren();\n        }\n\n        return new self($this->metadata, $this->text, false, $this->color, null);\n    }\n\n    public function changeColor(Color $color): self\n    {\n        return new self(\n            $this->metadata->update(),\n            $this->text,\n            $this->isToggleable,\n            $color,\n            $this->children,\n        );\n    }\n\n    public function addChild(BlockInterface $child): self\n    {\n        if (!$this->isToggleable) {\n            throw BlockException::noChindrenSupport();\n        }\n\n        $children = $this->children ? [...$this->children, $child] : [$child];\n        return new self(\n            $this->metadata,\n            $this->text,\n            $this->isToggleable,\n            $this->color,\n            $children,\n        );\n    }\n\n    public function changeChildren(BlockInterface ...$children): self\n    {\n        if (!$this->isToggleable) {\n            throw BlockException::noChindrenSupport();\n        }\n\n        return new self($this->metadata, $this->text, $this->isToggleable, $this->color, $children);\n    }\n\n    public function delete(): BlockInterface\n    {\n        return new self(\n            $this->metadata->delete(),\n            $this->text,\n            $this->isToggleable,\n            $this->color,\n            $this->children,\n        );\n    }\n\n    /**\n     * @deprecated 1.17.0 Use `delete()` instead.\n     * @codeCoverageIgnore\n     */\n    public function archive(): BlockInterface\n    {\n        return $this->delete();\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Image.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Common\\File;\nuse Notion\\Common\\RichText;\n\n/**\n * @psalm-import-type BlockMetadataJson from BlockMetadata\n * @psalm-import-type FileJson from \\Notion\\Common\\File\n *\n * @psalm-type ImageJson = array{ image: FileJson }\n *\n * @psalm-immutable\n */\nclass Image implements BlockInterface\n{\n    private function __construct(\n        private readonly BlockMetadata $metadata,\n        public readonly File $file,\n    ) {\n        $metadata->checkType(BlockType::Image);\n    }\n\n    public static function fromFile(File $file): self\n    {\n        $block = BlockMetadata::create(BlockType::Image);\n\n        return new self($block, $file);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var BlockMetadataJson $array */\n        $block = BlockMetadata::fromArray($array);\n\n        /** @psalm-var ImageJson $array */\n        $file = File::fromArray($array[\"image\"]);\n\n        return new self($block, $file);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"image\"] = $this->file->toArray();\n\n        return $array;\n    }\n\n    public function metadata(): BlockMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function changeFile(File $file): self\n    {\n        return new self($this->metadata, $file);\n    }\n\n    public function changeCaption(RichText ...$caption): self\n    {\n        return new self($this->metadata, $this->file->changeCaption(...$caption));\n    }\n\n    public function addChild(BlockInterface $child): never\n    {\n        throw BlockException::noChindrenSupport();\n    }\n\n    public function changeChildren(BlockInterface ...$children): never\n    {\n        throw BlockException::noChindrenSupport();\n    }\n\n    public function delete(): BlockInterface\n    {\n        return new self(\n            $this->metadata->delete(),\n            $this->file,\n        );\n    }\n\n    /**\n     * @deprecated 1.17.0 Use `delete()` instead.\n     * @codeCoverageIgnore\n     */\n    public function archive(): BlockInterface\n    {\n        return $this->delete();\n    }\n}\n"
  },
  {
    "path": "src/Blocks/LinkPreview.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\nuse Notion\\Exceptions\\BlockException;\n\n/**\n * Link Preview block.\n *\n * This block cannot be created, only retrieved by the API.\n *\n * @psalm-import-type BlockMetadataJson from BlockMetadata\n *\n * @psalm-type LinkPreviewJson = array{\n *      link_preview: array{ url: string },\n * }\n *\n * @psalm-immutable\n */\nclass LinkPreview implements BlockInterface\n{\n    private function __construct(\n        private readonly BlockMetadata $metadata,\n        public readonly string $url\n    ) {\n        $metadata->checkType(BlockType::LinkPreview);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var BlockMetadataJson $array */\n        $block = BlockMetadata::fromArray($array);\n\n        /** @psalm-var LinkPreviewJson $array */\n        $url = $array[\"link_preview\"][\"url\"];\n\n        return new self($block, $url);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"link_preview\"] = [ \"url\" => $this->url ];\n\n        return $array;\n    }\n\n    public function metadata(): BlockMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function addChild(BlockInterface $child): never\n    {\n        throw BlockException::noChindrenSupport();\n    }\n\n    public function changeChildren(BlockInterface ...$children): never\n    {\n        throw BlockException::noChindrenSupport();\n    }\n\n    public function delete(): BlockInterface\n    {\n        return new self(\n            $this->metadata->delete(),\n            $this->url,\n        );\n    }\n\n    /**\n     * @deprecated 1.17.0 Use `delete()` instead.\n     * @codeCoverageIgnore\n     */\n    public function archive(): BlockInterface\n    {\n        return $this->delete();\n    }\n}\n"
  },
  {
    "path": "src/Blocks/NumberedListItem.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\nuse Notion\\Common\\Color;\nuse Notion\\Common\\RichText;\n\n/**\n * @psalm-import-type BlockMetadataJson from BlockMetadata\n * @psalm-import-type RichTextJson from \\Notion\\Common\\RichText\n *\n * @psalm-type NumberedListItemJson = array{\n *      numbered_list_item: array{\n *          rich_text: list<RichTextJson>,\n *          color?: string,\n *          children?: list<BlockMetadataJson>,\n *      },\n * }\n *\n * @psalm-immutable\n */\nclass NumberedListItem implements BlockInterface\n{\n    /**\n     * @param RichText[] $text\n     * @param \\Notion\\Blocks\\BlockInterface[] $children\n     */\n    private function __construct(\n        private readonly BlockMetadata $metadata,\n        public readonly array $text,\n        public readonly Color $color,\n        public readonly array $children,\n    ) {\n        $metadata->checkType(BlockType::NumberedListItem);\n    }\n\n    public static function create(): self\n    {\n        $block = BlockMetadata::create(BlockType::NumberedListItem);\n\n        return new self($block, [], Color::Default, []);\n    }\n\n    public static function fromString(string $content): self\n    {\n        $block = BlockMetadata::create(BlockType::NumberedListItem);\n        $text = [ RichText::fromString($content) ];\n\n        return new self($block, $text, Color::Default, []);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var BlockMetadataJson $array */\n        $block = BlockMetadata::fromArray($array);\n\n        /** @psalm-var NumberedListItemJson $array */\n        $item = $array[\"numbered_list_item\"];\n\n        $text = array_map(fn($t) => RichText::fromArray($t), $item[\"rich_text\"]);\n\n        $color = Color::tryFrom($item[\"color\"] ?? \"\") ?? Color::Default;\n\n        $children = array_map(fn($b) => BlockFactory::fromArray($b), $item[\"children\"] ?? []);\n\n        return new self($block, $text, $color, $children);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"numbered_list_item\"] = [\n            \"rich_text\" => array_map(fn(RichText $t) => $t->toArray(), $this->text),\n            \"color\"     => $this->color->value,\n            \"children\"  => array_map(fn(BlockInterface $b) => $b->toArray(), $this->children),\n        ];\n\n        return $array;\n    }\n\n    public function toString(): string\n    {\n        return RichText::multipleToString(...$this->text);\n    }\n\n    public function metadata(): BlockMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function changeText(RichText ...$text): self\n    {\n        return new self($this->metadata->update(), $text, $this->color, $this->children);\n    }\n\n    public function addText(RichText $text): self\n    {\n        $texts = $this->text;\n        $texts[] = $text;\n\n        return new self($this->metadata->update(), $texts, $this->color, $this->children);\n    }\n\n    public function addChild(BlockInterface $child): self\n    {\n        $children = $this->children;\n        $children[] = $child;\n\n        return new self(\n            $this->metadata->updateHasChildren(true),\n            $this->text,\n            $this->color,\n            $children,\n        );\n    }\n\n    public function changeChildren(BlockInterface ...$children): self\n    {\n        $hasChildren = (count($children) > 0);\n\n        return new self(\n            $this->metadata->updateHasChildren($hasChildren),\n            $this->text,\n            $this->color,\n            $children,\n        );\n    }\n\n    public function changeColor(Color $color): self\n    {\n        return new self(\n            $this->metadata->update(),\n            $this->text,\n            $color,\n            $this->children,\n        );\n    }\n\n    public function delete(): BlockInterface\n    {\n        return new self(\n            $this->metadata->delete(),\n            $this->text,\n            $this->color,\n            $this->children,\n        );\n    }\n\n    /**\n     * @deprecated 1.17.0 Use `delete()` instead.\n     * @codeCoverageIgnore\n     */\n    public function archive(): BlockInterface\n    {\n        return $this->delete();\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Paragraph.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\nuse Notion\\Common\\Color;\nuse Notion\\Common\\RichText;\n\n/**\n * @psalm-import-type BlockMetadataJson from BlockMetadata\n * @psalm-import-type RichTextJson from \\Notion\\Common\\RichText\n *\n * @psalm-type ParagraphJson = array{\n *      paragraph: array{\n *          rich_text: list<RichTextJson>,\n *          children?: list<BlockMetadataJson>,\n *          color?: string,\n *      },\n * }\n *\n * @psalm-immutable\n */\nclass Paragraph implements BlockInterface\n{\n    /**\n     * @param RichText[] $text\n     * @param BlockInterface[] $children\n     */\n    private function __construct(\n        private readonly BlockMetadata $metadata,\n        public readonly array $text,\n        public readonly array $children,\n        public readonly Color $color,\n    ) {\n        $metadata->checkType(BlockType::Paragraph);\n    }\n\n    public static function create(): self\n    {\n        $block = BlockMetadata::create(BlockType::Paragraph);\n\n        return new self($block, [], [], Color::Default);\n    }\n\n    public static function fromString(string $content): self\n    {\n        $block = BlockMetadata::create(BlockType::Paragraph);\n        $text = [ RichText::fromString($content) ];\n\n        return new self($block, $text, [], Color::Default);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var BlockMetadataJson $array */\n        $block = BlockMetadata::fromArray($array);\n\n        /** @psalm-var ParagraphJson $array */\n        $paragraph = $array[\"paragraph\"];\n\n        $text = array_map(fn($t) => RichText::fromArray($t), $paragraph[\"rich_text\"]);\n\n        $children = array_map(fn($b) => BlockFactory::fromArray($b), $paragraph[\"children\"] ?? []);\n\n        $color = Color::tryFrom($paragraph[\"color\"] ?? \"\") ?? Color::Default;\n\n        return new self($block, $text, $children, $color);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"paragraph\"] = [\n            \"rich_text\" => array_map(fn(RichText $t) => $t->toArray(), $this->text),\n            \"children\"  => array_map(fn(BlockInterface $b) => $b->toArray(), $this->children),\n            \"color\"     => $this->color->value,\n        ];\n\n        return $array;\n    }\n\n    public function toString(): string\n    {\n        $string = \"\";\n        foreach ($this->text as $richText) {\n            $string = $string . $richText->plainText;\n        }\n\n        return $string;\n    }\n\n    public function metadata(): BlockMetadata\n    {\n        return $this->metadata;\n    }\n\n    /** @param RichText[] $text */\n    public function changeText(array $text): self\n    {\n        return new self($this->metadata, $text, $this->children, $this->color);\n    }\n\n    public function addText(RichText $text): self\n    {\n        $texts = $this->text;\n        $texts[] = $text;\n\n        return new self($this->metadata, $texts, $this->children, $this->color);\n    }\n\n    public function changeChildren(BlockInterface ...$children): self\n    {\n        $hasChildren = (count($children) > 0);\n\n        return new self(\n            $this->metadata->updateHasChildren($hasChildren),\n            $this->text,\n            $children,\n            $this->color,\n        );\n    }\n\n    public function addChild(BlockInterface $child): self\n    {\n        $children = $this->children;\n        $children[] = $child;\n\n        return new self(\n            $this->metadata->updateHasChildren(true),\n            $this->text,\n            $children,\n            $this->color,\n        );\n    }\n\n    public function changeColor(Color $color): self\n    {\n        return new self(\n            $this->metadata->update(),\n            $this->text,\n            $this->children,\n            $color,\n        );\n    }\n\n    public function delete(): BlockInterface\n    {\n        return new self(\n            $this->metadata->delete(),\n            $this->text,\n            $this->children,\n            $this->color,\n        );\n    }\n\n    /**\n     * @deprecated 1.17.0 Use `delete()` instead.\n     * @codeCoverageIgnore\n     */\n    public function archive(): BlockInterface\n    {\n        return $this->delete();\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Pdf.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Common\\File;\n\n/**\n * @psalm-import-type BlockMetadataJson from BlockMetadata\n * @psalm-import-type FileJson from \\Notion\\Common\\File\n *\n * @psalm-type PdfJson = array{ pdf: FileJson }\n *\n * @psalm-immutable\n */\nclass Pdf implements BlockInterface\n{\n    private function __construct(\n        private readonly BlockMetadata $metadata,\n        public readonly File $file\n    ) {\n        $metadata->checkType(BlockType::Pdf);\n    }\n\n    public static function fromFile(File $file): self\n    {\n        $block = BlockMetadata::create(BlockType::Pdf);\n\n        return new self($block, $file);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var BlockMetadataJson $array */\n        $block = BlockMetadata::fromArray($array);\n\n        /** @psalm-var PdfJson $array */\n        $file = File::fromArray($array[\"pdf\"]);\n\n        return new self($block, $file);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"pdf\"] = $this->file->toArray();\n\n        return $array;\n    }\n\n    public function metadata(): BlockMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function changeFile(File $file): self\n    {\n        return new self($this->metadata, $file);\n    }\n\n    public function addChild(BlockInterface $child): never\n    {\n        throw BlockException::noChindrenSupport();\n    }\n\n    public function changeChildren(BlockInterface ...$children): never\n    {\n        throw BlockException::noChindrenSupport();\n    }\n\n    public function delete(): BlockInterface\n    {\n        return new self(\n            $this->metadata->delete(),\n            $this->file,\n        );\n    }\n\n    /**\n     * @deprecated 1.17.0 Use `delete()` instead.\n     * @codeCoverageIgnore\n     */\n    public function archive(): BlockInterface\n    {\n        return $this->delete();\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Quote.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\nuse Notion\\Common\\Color;\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Common\\RichText;\n\n/**\n * @psalm-import-type BlockMetadataJson from BlockMetadata\n * @psalm-import-type RichTextJson from \\Notion\\Common\\RichText\n *\n * @psalm-type QuoteJson = array{\n *      quote: array{\n *          rich_text: list<RichTextJson>,\n *          color?: string,\n *          children: list<BlockMetadataJson>,\n *      },\n * }\n *\n * @psalm-immutable\n */\nclass Quote implements BlockInterface\n{\n    /**\n     * @param RichText[] $text\n     * @param BlockInterface[] $children\n     */\n    private function __construct(\n        private readonly BlockMetadata $metadata,\n        public readonly array $text,\n        public readonly Color $color,\n        public readonly array $children,\n    ) {\n        $metadata->checkType(BlockType::Quote);\n    }\n\n    public static function create(): self\n    {\n        $block = BlockMetadata::create(BlockType::Quote);\n\n        return new self($block, [], Color::Default, []);\n    }\n\n    public static function fromString(string $content): self\n    {\n        $block = BlockMetadata::create(BlockType::Quote);\n        $text = [ RichText::fromString($content) ];\n\n        return new self($block, $text, Color::Default, []);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var BlockMetadataJson $array */\n        $block = BlockMetadata::fromArray($array);\n\n        /** @psalm-var QuoteJson $array */\n        $quote = $array[\"quote\"];\n\n        $text = array_map(fn($t) => RichText::fromArray($t), $quote[\"rich_text\"]);\n\n        $color = Color::tryFrom($quote[\"color\"] ?? \"\") ?? Color::Default;\n\n        $children = array_map(fn($b) => BlockFactory::fromArray($b), $quote[\"children\"] ?? []);\n\n        return new self($block, $text, $color, $children);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"quote\"] = [\n            \"rich_text\"     => array_map(fn(RichText $t) => $t->toArray(), $this->text),\n            \"color\"    => $this->color->value,\n            \"children\" => array_map(fn(BlockInterface $b) => $b->toArray(), $this->children),\n        ];\n\n        return $array;\n    }\n\n    public function toString(): string\n    {\n        $string = \"\";\n        foreach ($this->text as $richText) {\n            $string = $string . $richText->plainText;\n        }\n\n        return $string;\n    }\n\n    public function metadata(): BlockMetadata\n    {\n        return $this->metadata;\n    }\n\n    /** @param RichText[] $text */\n    public function changeText(array $text): self\n    {\n        return new self($this->metadata, $text, $this->color, $this->children);\n    }\n\n    public function addText(RichText $text): self\n    {\n        $texts = $this->text;\n        $texts[] = $text;\n\n        return new self($this->metadata, $texts, $this->color, $this->children);\n    }\n\n    public function changeChildren(BlockInterface ...$children): self\n    {\n        $hasChildren = (count($children) > 0);\n\n        return new self(\n            $this->metadata->updateHasChildren($hasChildren),\n            $this->text,\n            $this->color,\n            $children,\n        );\n    }\n\n    public function addChild(BlockInterface $child): self\n    {\n        $children = $this->children;\n        $children[] = $child;\n\n        return new self(\n            $this->metadata->updateHasChildren(true),\n            $this->text,\n            $this->color,\n            $children,\n        );\n    }\n\n    public function changeColor(Color $color): self\n    {\n        return new self(\n            $this->metadata->update(),\n            $this->text,\n            $color,\n            $this->children,\n        );\n    }\n\n    public function delete(): BlockInterface\n    {\n        return new self(\n            $this->metadata->delete(),\n            $this->text,\n            $this->color,\n            $this->children,\n        );\n    }\n\n    /**\n     * @deprecated 1.17.0 Use `delete()` instead.\n     * @codeCoverageIgnore\n     */\n    public function archive(): BlockInterface\n    {\n        return $this->delete();\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Renderer/BlockRendererInterface.php",
    "content": "<?php\n\nnamespace Notion\\Blocks\\Renderer;\n\nuse Notion\\Blocks\\BlockInterface;\n\ninterface BlockRendererInterface\n{\n    public static function render(BlockInterface $block, int $depth = 0): string;\n}\n"
  },
  {
    "path": "src/Blocks/Renderer/Markdown/BookmarkRenderer.php",
    "content": "<?php\n\nnamespace Notion\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\BlockInterface;\nuse Notion\\Blocks\\Bookmark;\nuse Notion\\Blocks\\Renderer\\BlockRendererInterface;\nuse Notion\\Blocks\\Renderer\\MarkdownRenderer;\n\nfinal class BookmarkRenderer implements BlockRendererInterface\n{\n    public static function render(BlockInterface $block, int $depth = 0): string\n    {\n        if (!$block instanceof Bookmark) {\n            return \"\";\n        }\n\n        $url = $block->url;\n        return MarkdownRenderer::ident(\"<{$url}>\", $depth);\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Renderer/Markdown/BreadcrumbRenderer.php",
    "content": "<?php\n\nnamespace Notion\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\BlockInterface;\nuse Notion\\Blocks\\Breadcrumb;\nuse Notion\\Blocks\\Renderer\\BlockRendererInterface;\nuse Notion\\Blocks\\Renderer\\MarkdownRenderer;\n\nfinal class BreadcrumbRenderer implements BlockRendererInterface\n{\n    public static function render(BlockInterface $block, int $depth = 0): string\n    {\n        if (!$block instanceof Breadcrumb) {\n            return \"\";\n        }\n\n        return MarkdownRenderer::ident(\"[Breadcrumb]\", $depth);\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Renderer/Markdown/BulletedListItemRenderer.php",
    "content": "<?php\n\nnamespace Notion\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\BlockInterface;\nuse Notion\\Blocks\\BulletedListItem;\nuse Notion\\Blocks\\Renderer\\BlockRendererInterface;\nuse Notion\\Blocks\\Renderer\\MarkdownRenderer;\n\nfinal class BulletedListItemRenderer implements BlockRendererInterface\n{\n    public static function render(BlockInterface $block, int $depth = 0): string\n    {\n        if (!$block instanceof BulletedListItem) {\n            return \"\";\n        }\n\n        $main = RichTextRenderer::render(...$block->text);\n        $markdown = MarkdownRenderer::ident(\"- {$main}\", $depth);\n\n        foreach ($block->children as $child) {\n            $markdown .= \"\\n\" . MarkdownRenderer::renderBlock($child, $depth + 1);\n        }\n\n        return $markdown;\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Renderer/Markdown/CalloutRenderer.php",
    "content": "<?php\n\nnamespace Notion\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\BlockInterface;\nuse Notion\\Blocks\\Callout;\nuse Notion\\Blocks\\Renderer\\BlockRendererInterface;\nuse Notion\\Blocks\\Renderer\\MarkdownRenderer;\n\nfinal class CalloutRenderer implements BlockRendererInterface\n{\n    public static function render(BlockInterface $block, int $depth = 0): string\n    {\n        if (!$block instanceof Callout) {\n            return \"\";\n        }\n\n        $emoji = $block->icon->isEmoji() ? $block->icon->emoji->toString() . \" \" : \"\";\n        $text = RichTextRenderer::render(...$block->text);\n\n        $markdown = MarkdownRenderer::ident(\"> {$emoji}{$text}\", $depth);\n\n        foreach ($block->children as $child) {\n            $markdown .= \"\\n>\\n> \" . MarkdownRenderer::renderBlock($child, $depth);\n        }\n\n        return $markdown;\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Renderer/Markdown/ChildDatabaseRenderer.php",
    "content": "<?php\n\nnamespace Notion\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\BlockInterface;\nuse Notion\\Blocks\\ChildDatabase;\nuse Notion\\Blocks\\Renderer\\BlockRendererInterface;\nuse Notion\\Blocks\\Renderer\\MarkdownRenderer;\n\nfinal class ChildDatabaseRenderer implements BlockRendererInterface\n{\n    public static function render(BlockInterface $block, int $depth = 0): string\n    {\n        if (!$block instanceof ChildDatabase) {\n            return \"\";\n        }\n\n        return MarkdownRenderer::ident($block->title, $depth);\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Renderer/Markdown/ChildPageRenderer.php",
    "content": "<?php\n\nnamespace Notion\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\BlockInterface;\nuse Notion\\Blocks\\ChildPage;\nuse Notion\\Blocks\\Renderer\\BlockRendererInterface;\nuse Notion\\Blocks\\Renderer\\MarkdownRenderer;\n\nfinal class ChildPageRenderer implements BlockRendererInterface\n{\n    public static function render(BlockInterface $block, int $depth = 0): string\n    {\n        if (!$block instanceof ChildPage) {\n            return \"\";\n        }\n\n        return MarkdownRenderer::ident($block->title, $depth);\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Renderer/Markdown/CodeRenderer.php",
    "content": "<?php\n\nnamespace Notion\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\BlockInterface;\nuse Notion\\Blocks\\Code;\nuse Notion\\Blocks\\Renderer\\BlockRendererInterface;\nuse Notion\\Blocks\\Renderer\\MarkdownRenderer;\n\nfinal class CodeRenderer implements BlockRendererInterface\n{\n    public static function render(BlockInterface $block, int $depth = 0): string\n    {\n        if (!$block instanceof Code) {\n            return \"\";\n        }\n\n        $language = $block->language->value;\n        $code = RichTextRenderer::render(...$block->text);\n        $markdown = \"```{$language}\\n{$code}\\n```\";\n\n        return MarkdownRenderer::ident($markdown, $depth);\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Renderer/Markdown/ColumnListRenderer.php",
    "content": "<?php\n\nnamespace Notion\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\BlockInterface;\nuse Notion\\Blocks\\ColumnList;\nuse Notion\\Blocks\\Renderer\\BlockRendererInterface;\nuse Notion\\Blocks\\Renderer\\MarkdownRenderer;\n\nfinal class ColumnListRenderer implements BlockRendererInterface\n{\n    public static function render(BlockInterface $block, int $depth = 0): string\n    {\n        if (!$block instanceof ColumnList) {\n            return \"\";\n        }\n\n        $markdown = \"\";\n        $isFirst = true;\n        foreach ($block->columns as $child) {\n            $newLine = $isFirst ? \"\" : \"\\n\\n\";\n            $markdown .= $newLine . MarkdownRenderer::renderBlock($child, $depth);\n            $isFirst = false;\n        }\n\n        return $markdown;\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Renderer/Markdown/ColumnRenderer.php",
    "content": "<?php\n\nnamespace Notion\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\BlockInterface;\nuse Notion\\Blocks\\Column;\nuse Notion\\Blocks\\Renderer\\BlockRendererInterface;\nuse Notion\\Blocks\\Renderer\\MarkdownRenderer;\n\nfinal class ColumnRenderer implements BlockRendererInterface\n{\n    public static function render(BlockInterface $block, int $depth = 0): string\n    {\n        if (!$block instanceof Column) {\n            return \"\";\n        }\n\n        $markdown = \"\";\n        $isFirst = true;\n        foreach ($block->children as $child) {\n            $newLine = $isFirst ? \"\" : \"\\n\\n\";\n            $markdown .= $newLine . MarkdownRenderer::renderBlock($child, $depth);\n            $isFirst = false;\n        }\n\n        return $markdown;\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Renderer/Markdown/DividerRenderer.php",
    "content": "<?php\n\nnamespace Notion\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\BlockInterface;\nuse Notion\\Blocks\\Divider;\nuse Notion\\Blocks\\Renderer\\BlockRendererInterface;\nuse Notion\\Blocks\\Renderer\\MarkdownRenderer;\n\nfinal class DividerRenderer implements BlockRendererInterface\n{\n    public static function render(BlockInterface $block, int $depth = 0): string\n    {\n        if (!$block instanceof Divider) {\n            return \"\";\n        }\n\n        return MarkdownRenderer::ident(\"---\", $depth);\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Renderer/Markdown/EmbedRenderer.php",
    "content": "<?php\n\nnamespace Notion\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\BlockInterface;\nuse Notion\\Blocks\\Embed;\nuse Notion\\Blocks\\Renderer\\BlockRendererInterface;\nuse Notion\\Blocks\\Renderer\\MarkdownRenderer;\n\nfinal class EmbedRenderer implements BlockRendererInterface\n{\n    public static function render(BlockInterface $block, int $depth = 0): string\n    {\n        if (!$block instanceof Embed) {\n            return \"\";\n        }\n\n        return MarkdownRenderer::ident($block->url, $depth);\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Renderer/Markdown/EquationRenderer.php",
    "content": "<?php\n\nnamespace Notion\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\BlockInterface;\nuse Notion\\Blocks\\EquationBlock;\nuse Notion\\Blocks\\Renderer\\BlockRendererInterface;\nuse Notion\\Blocks\\Renderer\\MarkdownRenderer;\n\nfinal class EquationRenderer implements BlockRendererInterface\n{\n    public static function render(BlockInterface $block, int $depth = 0): string\n    {\n        if (!$block instanceof EquationBlock) {\n            return \"\";\n        }\n\n        $equation = $block->equation->expression;\n        return MarkdownRenderer::ident(\"$$ {$equation} $$\", $depth);\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Renderer/Markdown/FileRenderer.php",
    "content": "<?php\n\nnamespace Notion\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\BlockInterface;\nuse Notion\\Blocks\\FileBlock;\nuse Notion\\Blocks\\Renderer\\BlockRendererInterface;\nuse Notion\\Blocks\\Renderer\\MarkdownRenderer;\n\nfinal class FileRenderer implements BlockRendererInterface\n{\n    public static function render(BlockInterface $block, int $depth = 0): string\n    {\n        if (!$block instanceof FileBlock) {\n            return \"\";\n        }\n\n        if ($block->file()->url === null) {\n            return \"\";\n        }\n\n        return MarkdownRenderer::ident($block->file()->url, $depth);\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Renderer/Markdown/Heading1Renderer.php",
    "content": "<?php\n\nnamespace Notion\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\BlockInterface;\nuse Notion\\Blocks\\Heading1;\nuse Notion\\Blocks\\Renderer\\BlockRendererInterface;\nuse Notion\\Blocks\\Renderer\\MarkdownRenderer;\n\nfinal class Heading1Renderer implements BlockRendererInterface\n{\n    public static function render(BlockInterface $block, int $depth = 0): string\n    {\n        if (!$block instanceof Heading1) {\n            return \"\";\n        }\n\n        $main = RichTextRenderer::render(...$block->text);\n        return MarkdownRenderer::ident(\"# {$main}\", $depth);\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Renderer/Markdown/Heading2Renderer.php",
    "content": "<?php\n\nnamespace Notion\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\BlockInterface;\nuse Notion\\Blocks\\Heading2;\nuse Notion\\Blocks\\Renderer\\BlockRendererInterface;\nuse Notion\\Blocks\\Renderer\\MarkdownRenderer;\n\nfinal class Heading2Renderer implements BlockRendererInterface\n{\n    public static function render(BlockInterface $block, int $depth = 0): string\n    {\n        if (!$block instanceof Heading2) {\n            return \"\";\n        }\n\n        $main = RichTextRenderer::render(...$block->text);\n        return MarkdownRenderer::ident(\"## {$main}\", $depth);\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Renderer/Markdown/Heading3Renderer.php",
    "content": "<?php\n\nnamespace Notion\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\BlockInterface;\nuse Notion\\Blocks\\Heading3;\nuse Notion\\Blocks\\Renderer\\BlockRendererInterface;\nuse Notion\\Blocks\\Renderer\\MarkdownRenderer;\n\nfinal class Heading3Renderer implements BlockRendererInterface\n{\n    public static function render(BlockInterface $block, int $depth = 0): string\n    {\n        if (!$block instanceof Heading3) {\n            return \"\";\n        }\n\n        $main = RichTextRenderer::render(...$block->text);\n        return MarkdownRenderer::ident(\"### {$main}\", $depth);\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Renderer/Markdown/ImageRenderer.php",
    "content": "<?php\n\nnamespace Notion\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\BlockInterface;\nuse Notion\\Blocks\\Image;\nuse Notion\\Blocks\\Renderer\\BlockRendererInterface;\nuse Notion\\Blocks\\Renderer\\MarkdownRenderer;\n\nfinal class ImageRenderer implements BlockRendererInterface\n{\n    public static function render(BlockInterface $block, int $depth = 0): string\n    {\n        if (!$block instanceof Image) {\n            return \"\";\n        }\n\n        $url = $block->file->url;\n        return MarkdownRenderer::ident(\"![]({$url})\", $depth);\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Renderer/Markdown/LinkPreviewRenderer.php",
    "content": "<?php\n\nnamespace Notion\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\BlockInterface;\nuse Notion\\Blocks\\LinkPreview;\nuse Notion\\Blocks\\Renderer\\BlockRendererInterface;\nuse Notion\\Blocks\\Renderer\\MarkdownRenderer;\n\nfinal class LinkPreviewRenderer implements BlockRendererInterface\n{\n    public static function render(BlockInterface $block, int $depth = 0): string\n    {\n        if (!$block instanceof LinkPreview) {\n            return \"\";\n        }\n\n        return MarkdownRenderer::ident($block->url, $depth);\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Renderer/Markdown/NumberedListItemRenderer.php",
    "content": "<?php\n\nnamespace Notion\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\BlockInterface;\nuse Notion\\Blocks\\NumberedListItem;\nuse Notion\\Blocks\\Renderer\\BlockRendererInterface;\nuse Notion\\Blocks\\Renderer\\MarkdownRenderer;\n\nfinal class NumberedListItemRenderer implements BlockRendererInterface\n{\n    public static function render(BlockInterface $block, int $depth = 0): string\n    {\n        if (!$block instanceof NumberedListItem) {\n            return \"\";\n        }\n\n        $main = RichTextRenderer::render(...$block->text);\n        $markdown = MarkdownRenderer::ident(\"1. {$main}\", $depth);\n\n        foreach ($block->children as $child) {\n            $markdown .= \"\\n\" . MarkdownRenderer::renderBlock($child, $depth + 1);\n        }\n\n        return $markdown;\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Renderer/Markdown/ParagraphRenderer.php",
    "content": "<?php\n\nnamespace Notion\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\BlockInterface;\nuse Notion\\Blocks\\Paragraph;\nuse Notion\\Blocks\\Renderer\\BlockRendererInterface;\nuse Notion\\Blocks\\Renderer\\MarkdownRenderer;\n\nfinal class ParagraphRenderer implements BlockRendererInterface\n{\n    public static function render(BlockInterface $block, int $depth = 0): string\n    {\n        if (!$block instanceof Paragraph) {\n            return \"\";\n        }\n\n        $text = RichTextRenderer::render(...$block->text);\n        $markdown = MarkdownRenderer::ident($text . \"\\n\", $depth);\n\n        foreach ($block->children as $child) {\n            $markdown .= \"\\n\\n\" . MarkdownRenderer::renderBlock($child, $depth + 1);\n        }\n\n        return $markdown;\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Renderer/Markdown/PdfRenderer.php",
    "content": "<?php\n\nnamespace Notion\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\BlockInterface;\nuse Notion\\Blocks\\Pdf;\nuse Notion\\Blocks\\Renderer\\BlockRendererInterface;\nuse Notion\\Blocks\\Renderer\\MarkdownRenderer;\n\nfinal class PdfRenderer implements BlockRendererInterface\n{\n    public static function render(BlockInterface $block, int $depth = 0): string\n    {\n        if (!$block instanceof Pdf) {\n            return \"\";\n        }\n\n        if ($block->file->url === null) {\n            return \"\";\n        }\n\n        return MarkdownRenderer::ident($block->file->url, $depth);\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Renderer/Markdown/QuoteRenderer.php",
    "content": "<?php\n\nnamespace Notion\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\BlockInterface;\nuse Notion\\Blocks\\Quote;\nuse Notion\\Blocks\\Renderer\\BlockRendererInterface;\nuse Notion\\Blocks\\Renderer\\MarkdownRenderer;\n\nfinal class QuoteRenderer implements BlockRendererInterface\n{\n    public static function render(BlockInterface $block, int $depth = 0): string\n    {\n        if (!$block instanceof Quote) {\n            return \"\";\n        }\n\n        $text = RichTextRenderer::render(...$block->text);\n\n        $markdown = MarkdownRenderer::ident(\"> {$text}\", $depth);\n\n        foreach ($block->children as $child) {\n            $markdown .= \"\\n>\\n> \" . MarkdownRenderer::renderBlock($child, $depth);\n        }\n\n        return $markdown;\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Renderer/Markdown/RichTextRenderer.php",
    "content": "<?php\n\nnamespace Notion\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Common\\RichText;\n\nfinal class RichTextRenderer\n{\n    public static function render(RichText ...$text): string\n    {\n        $result = \"\";\n        foreach ($text as $t) {\n            $markdown = $t->plainText;\n\n            if ($t->isEquation()) {\n                $markdown = \"\\${$markdown}\\$\";\n            }\n            if ($t->annotations->isCode) {\n                $markdown = \"`{$markdown}`\";\n            }\n            if ($t->annotations->isBold) {\n                $markdown = self::around($markdown, \"**\");\n            }\n            if ($t->annotations->isItalic) {\n                $markdown = self::around($markdown, \"*\");\n            }\n            if ($t->annotations->isStrikeThrough) {\n                $markdown = self::around($markdown, \"~~\");\n            }\n            if ($t->annotations->isUnderline) {\n                $markdown = \"<u>{$markdown}</u>\";\n            }\n\n            if ($t->href !== null) {\n                $markdown = \"[{$markdown}]({$t->href})\";\n            }\n\n            $result = \"{$result}{$markdown}\";\n        }\n\n        return $result;\n    }\n\n    private static function around(string $text, string $around): string\n    {\n        preg_match(\"/^(\\s*)/\", $text, $leftSpace);\n        preg_match(\"/(\\s*)$/\", $text, $righSpace);\n\n        $text = trim($text);\n\n        return \"{$leftSpace[0]}{$around}{$text}{$around}{$righSpace[0]}\";\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Renderer/Markdown/TableOfContentsRenderer.php",
    "content": "<?php\n\nnamespace Notion\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\BlockInterface;\nuse Notion\\Blocks\\TableOfContents;\nuse Notion\\Blocks\\Renderer\\BlockRendererInterface;\nuse Notion\\Blocks\\Renderer\\MarkdownRenderer;\n\nfinal class TableOfContentsRenderer implements BlockRendererInterface\n{\n    public static function render(BlockInterface $block, int $depth = 0): string\n    {\n        if (!$block instanceof TableOfContents) {\n            return \"\";\n        }\n\n        return MarkdownRenderer::ident(\"[TableOfContents]\", $depth);\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Renderer/Markdown/ToDoRenderer.php",
    "content": "<?php\n\nnamespace Notion\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\BlockInterface;\nuse Notion\\Blocks\\ToDo;\nuse Notion\\Blocks\\Renderer\\BlockRendererInterface;\nuse Notion\\Blocks\\Renderer\\MarkdownRenderer;\n\nfinal class ToDoRenderer implements BlockRendererInterface\n{\n    public static function render(BlockInterface $block, int $depth = 0): string\n    {\n        if (!$block instanceof ToDo) {\n            return \"\";\n        }\n\n        $text = RichTextRenderer::render(...$block->text);\n        $check = $block->checked ? \"x\" : \" \";\n        $markdown = MarkdownRenderer::ident(\"- [{$check}] {$text}\", $depth);\n\n        foreach ($block->children as $child) {\n            $markdown .= \"\\n\" . MarkdownRenderer::renderBlock($child, $depth + 1);\n        }\n\n        return $markdown;\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Renderer/Markdown/ToggleRenderer.php",
    "content": "<?php\n\nnamespace Notion\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\BlockInterface;\nuse Notion\\Blocks\\Toggle;\nuse Notion\\Blocks\\Renderer\\BlockRendererInterface;\nuse Notion\\Blocks\\Renderer\\MarkdownRenderer;\nuse Notion\\Common\\RichText;\n\nfinal class ToggleRenderer implements BlockRendererInterface\n{\n    public static function render(BlockInterface $block, int $depth = 0): string\n    {\n        if (!$block instanceof Toggle) {\n            return \"\";\n        }\n\n        $text = RichText::multipleToString(...$block->text);\n\n        $markdown = MarkdownRenderer::ident(\"<details>\", $depth);\n\n        $markdown .= MarkdownRenderer::ident(\"\\n<summary>{$text}</summary>\", $depth);\n        foreach ($block->children as $child) {\n            $markdown .= \"\\n\\n\" . MarkdownRenderer::renderBlock($child, $depth);\n        }\n\n        $markdown .= MarkdownRenderer::ident(\"</details>\", $depth);\n\n        return $markdown;\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Renderer/Markdown/VideoRenderer.php",
    "content": "<?php\n\nnamespace Notion\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\BlockInterface;\nuse Notion\\Blocks\\Video;\nuse Notion\\Blocks\\Renderer\\BlockRendererInterface;\nuse Notion\\Blocks\\Renderer\\MarkdownRenderer;\n\nfinal class VideoRenderer implements BlockRendererInterface\n{\n    public static function render(BlockInterface $block, int $depth = 0): string\n    {\n        if (!$block instanceof Video) {\n            return \"\";\n        }\n\n        $url = $block->file->url;\n        return MarkdownRenderer::ident(\"![]({$url})\", $depth);\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Renderer/MarkdownRenderer.php",
    "content": "<?php\n\nnamespace Notion\\Blocks\\Renderer;\n\nuse Notion\\Blocks\\BlockInterface;\nuse Notion\\Blocks\\BlockType;\nuse Notion\\Blocks\\Renderer\\Markdown;\n\nclass MarkdownRenderer implements RendererInterface\n{\n    public static function render(BlockInterface ...$blocks): string\n    {\n        $markdown = \"\";\n        foreach ($blocks as $block) {\n            $markdown = $markdown . self::renderBlock($block, 0) . \"\\n\";\n        }\n\n        return $markdown;\n    }\n\n    /**\n     * Render blocks with custom renderers.\n     *\n     * @param array<value-of<BlockType>, BlockRendererInterface> $overrides\n     * @param BlockInterface ...$blocks\n     *\n     * @return string\n     */\n    public static function renderWithOverrides(\n        array $overrides,\n        BlockInterface ...$blocks,\n    ): string {\n        $markdown = \"\";\n        foreach ($blocks as $block) {\n            $markdown = $markdown . self::renderBlock($block, 0, $overrides) . \"\\n\";\n        }\n\n        return $markdown;\n    }\n\n    /**\n     * Undocumented function\n     *\n     * @param BlockInterface $block\n     * @param int $depth\n     * @param array<value-of<BlockType>, BlockRendererInterface> $overrides\n     *\n     * @return string\n     */\n    public static function renderBlock(\n        BlockInterface $block,\n        int $depth = 0,\n        array $overrides = []\n    ): string {\n        if (array_key_exists($block->metadata()->type->value, $overrides)) {\n            $renderer = $overrides[$block->metadata()->type->value];\n            return $renderer::render($block, $depth);\n        }\n\n        return match ($block->metadata()->type) {\n            BlockType::Bookmark         => Markdown\\BookmarkRenderer::render($block, $depth),\n            BlockType::Breadcrumb       => Markdown\\BreadcrumbRenderer::render($block, $depth),\n            BlockType::BulletedListItem => Markdown\\BulletedListItemRenderer::render($block, $depth),\n            BlockType::Callout          => Markdown\\CalloutRenderer::render($block, $depth),\n            BlockType::ChildDatabase    => Markdown\\ChildDatabaseRenderer::render($block, $depth),\n            BlockType::ChildPage        => Markdown\\ChildPageRenderer::render($block, $depth),\n            BlockType::Code             => Markdown\\CodeRenderer::render($block, $depth),\n            BlockType::Column           => Markdown\\ColumnRenderer::render($block, $depth),\n            BlockType::ColumnList       => Markdown\\ColumnListRenderer::render($block, $depth),\n            BlockType::Divider          => Markdown\\DividerRenderer::render($block, $depth),\n            BlockType::Embed            => Markdown\\EmbedRenderer::render($block, $depth),\n            BlockType::Equation         => Markdown\\EquationRenderer::render($block, $depth),\n            BlockType::File             => Markdown\\FileRenderer::render($block, $depth),\n            BlockType::Heading1         => Markdown\\Heading1Renderer::render($block, $depth),\n            BlockType::Heading2         => Markdown\\Heading2Renderer::render($block, $depth),\n            BlockType::Heading3         => Markdown\\Heading3Renderer::render($block, $depth),\n            BlockType::Image            => Markdown\\ImageRenderer::render($block, $depth),\n            BlockType::LinkPreview      => Markdown\\LinkPreviewRenderer::render($block, $depth),\n            BlockType::NumberedListItem => Markdown\\NumberedListItemRenderer::render($block, $depth),\n            BlockType::Paragraph        => Markdown\\ParagraphRenderer::render($block, $depth),\n            BlockType::Pdf              => Markdown\\PdfRenderer::render($block, $depth),\n            BlockType::Quote            => Markdown\\QuoteRenderer::render($block, $depth),\n            BlockType::TableOfContents  => Markdown\\TableOfContentsRenderer::render($block, $depth),\n            BlockType::ToDo             => Markdown\\ToDoRenderer::render($block, $depth),\n            BlockType::Toggle           => Markdown\\ToggleRenderer::render($block, $depth),\n            BlockType::Video            => Markdown\\VideoRenderer::render($block, $depth),\n            default                     => \"\",\n        };\n    }\n\n    public static function ident(string $text, int $depth): string\n    {\n        $lines = array_map(\n            function (string $line) use ($depth): string {\n                if (strlen($line) == 0) {\n                    return $line;\n                }\n\n                $padding = str_repeat(\" \", $depth * 2);\n                return $padding . $line;\n            },\n            explode(\"\\n\", $text),\n        );\n\n        return implode(\"\\n\", $lines);\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Renderer/RendererInterface.php",
    "content": "<?php\n\nnamespace Notion\\Blocks\\Renderer;\n\nuse Notion\\Blocks\\BlockInterface;\n\ninterface RendererInterface\n{\n    public static function render(BlockInterface ...$blocks): string;\n\n    public static function renderBlock(BlockInterface $block, int $depth = 0): string;\n}\n"
  },
  {
    "path": "src/Blocks/Table.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\nuse Notion\\Exceptions\\BlockException;\n\n/**\n * @psalm-import-type BlockMetadataJson from BlockMetadata\n * @psalm-import-type RichTextJson from \\Notion\\Common\\RichText\n *\n * @psalm-type TableJson = array{\n *      table: array{\n *          table_width: int,\n *          has_column_header: bool,\n *          has_row_header: bool,\n *          children: list<BlockMetadataJson>,\n *      },\n * }\n *\n * @psalm-immutable\n */\nclass Table implements BlockInterface\n{\n    /** @param TableRow[] $rows */\n    private function __construct(\n        private readonly BlockMetadata $metadata,\n        public readonly int $tableWidth,\n        public readonly bool $hasColumnHeader,\n        public readonly bool $hasRowHeader,\n        public readonly array $rows,\n    ) {\n        $metadata->checkType(BlockType::Table);\n    }\n\n    public static function create(): self\n    {\n        $block = BlockMetadata::create(BlockType::Table);\n\n        return new self($block, 1, false, false, []);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var BlockMetadataJson $array */\n        $block = BlockMetadata::fromArray($array);\n\n        /** @psalm-var TableJson $array */\n        $table = $array[\"table\"];\n\n        $tableWidth = $table[\"table_width\"];\n        $hasColumnHeader = $table[\"has_column_header\"];\n        $hasRowHeader = $table[\"has_row_header\"];\n        $rows = array_map(fn(array $row) => TableRow::fromArray($row), $table[\"children\"]);\n\n        return new self($block, $tableWidth, $hasColumnHeader, $hasRowHeader, $rows);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"table\"] = [\n            \"table_width\"       => $this->tableWidth,\n            \"has_column_header\" => $this->hasColumnHeader,\n            \"has_row_header\"    => $this->hasRowHeader,\n            \"children\"          => array_map(fn(TableRow $row) => $row->toArray(), $this->rows),\n        ];\n\n        return $array;\n    }\n\n    public function metadata(): BlockMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function changeWidth(int $tableWidth): self\n    {\n        return new self(\n            $this->metadata->update(),\n            $tableWidth,\n            $this->hasColumnHeader,\n            $this->hasRowHeader,\n            $this->rows,\n        );\n    }\n\n    public function enableColumnHeader(): self\n    {\n        return new self(\n            $this->metadata->update(),\n            $this->tableWidth,\n            true,\n            $this->hasRowHeader,\n            $this->rows,\n        );\n    }\n\n    public function disableColumnHeader(): self\n    {\n        return new self(\n            $this->metadata->update(),\n            $this->tableWidth,\n            false,\n            $this->hasRowHeader,\n            $this->rows,\n        );\n    }\n\n    public function enableRowHeader(): self\n    {\n        return new self(\n            $this->metadata->update(),\n            $this->tableWidth,\n            $this->hasColumnHeader,\n            true,\n            $this->rows,\n        );\n    }\n\n    public function disableRowHeader(): self\n    {\n        return new self(\n            $this->metadata->update(),\n            $this->tableWidth,\n            $this->hasColumnHeader,\n            false,\n            $this->rows,\n        );\n    }\n\n    public function changeRows(TableRow ...$rows): self\n    {\n        $hasChildren = (count($rows) > 0);\n\n        return new self(\n            $this->metadata->updateHasChildren($hasChildren),\n            $this->tableWidth,\n            $this->hasColumnHeader,\n            $this->hasRowHeader,\n            $rows,\n        );\n    }\n\n    public function addRow(TableRow $row): self\n    {\n        $rows = $this->rows;\n        $rows[] = $row;\n\n        return new self(\n            $this->metadata->updateHasChildren(true),\n            $this->tableWidth,\n            $this->hasColumnHeader,\n            $this->hasRowHeader,\n            $rows,\n        );\n    }\n\n    public function changeChildren(BlockInterface ...$children): self\n    {\n        foreach ($children as $child) {\n            if ($child::class !== TableRow::class) {\n                throw BlockException::wrongType(BlockType::TableRow);\n            }\n        }\n\n        /** @psalm-var TableRow[] $children */\n        return $this->changeRows(...$children);\n    }\n\n    public function addChild(BlockInterface $child): self\n    {\n        if ($child::class !== TableRow::class) {\n            throw BlockException::wrongType(BlockType::TableRow);\n        }\n\n        /** @psalm-var TableRow $child */\n        return $this->addRow($child);\n    }\n\n    public function delete(): BlockInterface\n    {\n        return new self(\n            $this->metadata->delete(),\n            $this->tableWidth,\n            $this->hasColumnHeader,\n            $this->hasRowHeader,\n            $this->rows,\n        );\n    }\n\n    /**\n     * @deprecated 1.17.0 Use `delete()` instead.\n     * @codeCoverageIgnore\n     */\n    public function archive(): BlockInterface\n    {\n        return $this->delete();\n    }\n}\n"
  },
  {
    "path": "src/Blocks/TableOfContents.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\nuse Notion\\Common\\Color;\nuse Notion\\Exceptions\\BlockException;\n\n/**\n * @psalm-import-type BlockMetadataJson from BlockMetadata\n *\n * @psalm-type TableOfContentsJson = array{\n *      table_of_contents: array{\n *          color?: string\n *      }\n * }\n *\n * @psalm-immutable\n */\nclass TableOfContents implements BlockInterface\n{\n    private function __construct(\n        private readonly BlockMetadata $metadata,\n        public readonly Color $color,\n    ) {\n        $metadata->checkType(BlockType::TableOfContents);\n    }\n\n    public static function create(): self\n    {\n        $block = BlockMetadata::create(BlockType::TableOfContents);\n\n        return new self($block, Color::Default);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var BlockMetadataJson $array */\n        $block = BlockMetadata::fromArray($array);\n\n        /** @psalm-var TableOfContentsJson $array */\n        $toc = $array[\"table_of_contents\"];\n\n        $color = Color::tryFrom($toc[\"color\"] ?? \"\") ?? Color::Default;\n\n        return new self($block, $color);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"table_of_contents\"] = new \\stdClass();\n\n        return $array;\n    }\n\n    public function metadata(): BlockMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function addChild(BlockInterface $child): never\n    {\n        throw BlockException::noChindrenSupport();\n    }\n\n    public function changeChildren(BlockInterface ...$children): never\n    {\n        throw BlockException::noChindrenSupport();\n    }\n\n    public function changeColor(Color $color): self\n    {\n        return new self(\n            $this->metadata->update(),\n            $color,\n        );\n    }\n\n    public function delete(): BlockInterface\n    {\n        return new self(\n            $this->metadata->delete(),\n            $this->color,\n        );\n    }\n\n    /**\n     * @deprecated 1.17.0 Use `delete()` instead.\n     * @codeCoverageIgnore\n     */\n    public function archive(): BlockInterface\n    {\n        return $this->delete();\n    }\n}\n"
  },
  {
    "path": "src/Blocks/TableRow.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\nuse Notion\\Common\\RichText;\nuse Notion\\Exceptions\\BlockException;\n\n/**\n * @psalm-import-type BlockMetadataJson from BlockMetadata\n * @psalm-import-type RichTextJson from \\Notion\\Common\\RichText\n *\n * @psalm-type TableRowJson = array{\n *      table_row: array{\n *          cells: list<list<RichTextJson>>\n *      },\n * }\n *\n * @psalm-immutable\n */\nclass TableRow implements BlockInterface\n{\n    /** @param RichText[][] $cells */\n    private function __construct(\n        private readonly BlockMetadata $metadata,\n        public readonly array $cells,\n    ) {\n        $metadata->checkType(BlockType::TableRow);\n    }\n\n    public static function create(): self\n    {\n        $block = BlockMetadata::create(BlockType::TableRow);\n\n        return new self($block, []);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var BlockMetadataJson $array */\n        $metadata = BlockMetadata::fromArray($array);\n\n        /** @psalm-var TableRowJson $array */\n        $cells = array_map(\n            fn(array $cell) => array_map(fn(array $text) => RichText::fromArray($text), $cell),\n            $array[\"table_row\"][\"cells\"],\n        );\n\n        return new self($metadata, $cells);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"table_row\"] = [\n            \"cells\" => array_map(\n                fn(array $c) => array_map(fn(RichText $t) => $t->toArray(), $c),\n                $this->cells\n            ),\n        ];\n\n        return $array;\n    }\n\n    public function addCell(RichText ...$cell): self\n    {\n        $cells = $this->cells;\n        $cells[] = $cell;\n\n        return new self($this->metadata, $cells);\n    }\n\n    public function metadata(): BlockMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function changeChildren(BlockInterface ...$children): self\n    {\n        throw BlockException::noChindrenSupport();\n    }\n\n    public function addChild(BlockInterface $child): self\n    {\n        throw BlockException::noChindrenSupport();\n    }\n\n    public function delete(): BlockInterface\n    {\n        return new self(\n            $this->metadata->delete(),\n            $this->cells,\n        );\n    }\n\n    /**\n     * @deprecated 1.17.0 Use `delete()` instead.\n     * @codeCoverageIgnore\n     */\n    public function archive(): BlockInterface\n    {\n        return $this->delete();\n    }\n}\n"
  },
  {
    "path": "src/Blocks/ToDo.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\nuse Notion\\Common\\Color;\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Common\\RichText;\n\n/**\n * @psalm-import-type BlockMetadataJson from BlockMetadata\n * @psalm-import-type RichTextJson from \\Notion\\Common\\RichText\n *\n * @psalm-type ToDoJson = array{\n *      to_do: array{\n *          checked: bool,\n *          rich_text: list<RichTextJson>,\n *          color?: string,\n *          children?: list<BlockMetadataJson>,\n *      },\n * }\n *\n * @psalm-immutable\n */\nclass ToDo implements BlockInterface\n{\n    /**\n     * @param RichText[] $text\n     * @param BlockInterface[] $children\n     */\n    private function __construct(\n        private readonly BlockMetadata $metadata,\n        public readonly array $text,\n        public readonly bool $checked,\n        public readonly Color $color,\n        public readonly array $children,\n    ) {\n        $metadata->checkType(BlockType::ToDo);\n    }\n\n    public static function create(): self\n    {\n        $block = BlockMetadata::create(BlockType::ToDo);\n\n        return new self($block, [], false, Color::Default, []);\n    }\n\n    public static function fromString(string $content): self\n    {\n        $block = BlockMetadata::create(BlockType::ToDo);\n        $text = [ RichText::fromString($content) ];\n\n        return new self($block, $text, false, Color::Default, []);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var BlockMetadataJson $array */\n        $block = BlockMetadata::fromArray($array);\n\n        /** @psalm-var ToDoJson $array */\n        $todo = $array[\"to_do\"];\n\n        $text = array_map(fn($t) => RichText::fromArray($t), $todo[\"rich_text\"]);\n\n        $checked = $todo[\"checked\"];\n\n        $color = Color::tryFrom($todo[\"color\"] ?? \"\") ?? Color::Default;\n\n        $children = array_map(fn($b) => BlockFactory::fromArray($b), $todo[\"children\"] ?? []);\n\n        return new self($block, $text, $checked, $color, $children);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"to_do\"] = [\n            \"rich_text\" => array_map(fn(RichText $t) => $t->toArray(), $this->text),\n            \"checked\"   => $this->checked,\n            \"color\"     => $this->color->value,\n            \"children\"  => array_map(fn(BlockInterface $b) => $b->toArray(), $this->children),\n        ];\n\n        return $array;\n    }\n\n    public function toString(): string\n    {\n        $string = \"\";\n        foreach ($this->text as $richText) {\n            $string = $string . $richText->plainText;\n        }\n\n        return $string;\n    }\n\n    public function metadata(): BlockMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function changeText(RichText ...$text): self\n    {\n        return new self($this->metadata, $text, $this->checked, $this->color, $this->children);\n    }\n\n    public function addText(RichText $text): self\n    {\n        $texts = $this->text;\n        $texts[] = $text;\n\n        return new self($this->metadata, $texts, $this->checked, $this->color, $this->children);\n    }\n\n    public function check(): self\n    {\n        return new self($this->metadata, $this->text, true, $this->color, $this->children);\n    }\n\n    public function uncheck(): self\n    {\n        return new self($this->metadata, $this->text, false, $this->color, $this->children);\n    }\n\n    public function changeChildren(BlockInterface ...$children): self\n    {\n        $hasChildren = (count($children) > 0);\n\n        return new self(\n            $this->metadata->updateHasChildren($hasChildren),\n            $this->text,\n            $this->checked,\n            $this->color,\n            $children,\n        );\n    }\n\n    public function addChild(BlockInterface $child): self\n    {\n        $children = $this->children;\n        $children[] = $child;\n\n        return new self(\n            $this->metadata->updateHasChildren(true),\n            $this->text,\n            $this->checked,\n            $this->color,\n            $children,\n        );\n    }\n\n    public function changeColor(Color $color): self\n    {\n        return new self(\n            $this->metadata->update(),\n            $this->text,\n            $this->checked,\n            $color,\n            $this->children,\n        );\n    }\n\n    public function delete(): BlockInterface\n    {\n        return new self(\n            $this->metadata->delete(),\n            $this->text,\n            $this->checked,\n            $this->color,\n            $this->children,\n        );\n    }\n\n    /**\n     * @deprecated 1.17.0 Use `delete()` instead.\n     * @codeCoverageIgnore\n     */\n    public function archive(): BlockInterface\n    {\n        return $this->delete();\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Toggle.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\nuse Notion\\Common\\Color;\nuse Notion\\Common\\RichText;\n\n/**\n * @psalm-import-type BlockMetadataJson from BlockMetadata\n * @psalm-import-type RichTextJson from \\Notion\\Common\\RichText\n *\n * @psalm-type ToggleJson = array{\n *      toggle: array{\n *          rich_text: list<RichTextJson>,\n *          color?: string,\n *          children?: list<BlockMetadataJson>,\n *      },\n * }\n *\n * @psalm-immutable\n */\nclass Toggle implements BlockInterface\n{\n    /**\n     * @param RichText[] $text\n     * @param BlockInterface[] $children\n     */\n    private function __construct(\n        private readonly BlockMetadata $metadata,\n        public readonly array $text,\n        public readonly Color $color,\n        public readonly array $children,\n    ) {\n        $metadata->checkType(BlockType::Toggle);\n    }\n\n    public static function createEmpty(): self\n    {\n        $block = BlockMetadata::create(BlockType::Toggle);\n\n        return new self($block, [], Color::Default, []);\n    }\n\n    public static function fromString(string $content): self\n    {\n        $block = BlockMetadata::create(BlockType::Toggle);\n        $text = [ RichText::fromString($content) ];\n\n        return new self($block, $text, Color::Default, []);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var BlockMetadataJson $array */\n        $block = BlockMetadata::fromArray($array);\n\n        /** @psalm-var ToggleJson $array */\n        $toggle = $array[\"toggle\"];\n\n        $text = array_map(fn($t) => RichText::fromArray($t), $toggle[\"rich_text\"]);\n\n        $color = Color::tryFrom($toggle[\"color\"] ?? \"\") ?? Color::Default;\n\n        $children = array_map(fn($b) => BlockFactory::fromArray($b), $toggle[\"children\"] ?? []);\n\n        return new self($block, $text, $color, $children);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"toggle\"] = [\n            \"rich_text\" => array_map(fn(RichText $t) => $t->toArray(), $this->text),\n            \"color\"     => $this->color->value,\n            \"children\"  => array_map(fn(BlockInterface $b) => $b->toArray(), $this->children),\n        ];\n\n        return $array;\n    }\n\n    public function toString(): string\n    {\n        $string = \"\";\n        foreach ($this->text as $richText) {\n            $string = $string . $richText->plainText;\n        }\n\n        return $string;\n    }\n\n    public function metadata(): BlockMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function changeText(RichText ...$text): self\n    {\n        return new self($this->metadata, $text, $this->color, $this->children);\n    }\n\n    public function addText(RichText $text): self\n    {\n        $texts = $this->text;\n        $texts[] = $text;\n\n        return new self($this->metadata, $texts, $this->color, $this->children);\n    }\n\n    public function changeChildren(BlockInterface ...$children): self\n    {\n        $hasChildren = (count($children) > 0);\n\n        return new self(\n            $this->metadata->updateHasChildren($hasChildren),\n            $this->text,\n            $this->color,\n            $children,\n        );\n    }\n\n    public function addChild(BlockInterface $child): self\n    {\n        $children = $this->children;\n        $children[] = $child;\n\n        return new self(\n            $this->metadata->updateHasChildren(true),\n            $this->text,\n            $this->color,\n            $children,\n        );\n    }\n\n    public function changeColor(Color $color): self\n    {\n        return new self(\n            $this->metadata->update(),\n            $this->text,\n            $color,\n            $this->children,\n        );\n    }\n\n    public function delete(): BlockInterface\n    {\n        return new self(\n            $this->metadata->delete(),\n            $this->text,\n            $this->color,\n            $this->children,\n        );\n    }\n\n    /**\n     * @deprecated 1.17.0 Use `delete()` instead.\n     * @codeCoverageIgnore\n     */\n    public function archive(): BlockInterface\n    {\n        return $this->delete();\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Unknown.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\n/**\n * An unknown block not implemented yet by the SDK.\n *\n * @psalm-import-type BlockMetadataJson from BlockMetadata\n *\n * @psalm-immutable\n */\nclass Unknown implements BlockInterface\n{\n    private function __construct(\n        private readonly BlockMetadata $metadata,\n        private readonly array $data,\n    ) {\n    }\n\n    public function metadata(): BlockMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function addChild(BlockInterface $child): self\n    {\n        /** @var array{ children?: array } */\n        $children = $this->data[\"children\"] ?? [];\n        $children[] = $child->toArray();\n\n        $data = $this->data;\n        $data[\"children\"] = $children;\n\n        return new self(\n            $this->metadata->updateHasChildren(true),\n            $data,\n        );\n    }\n\n    public function changeChildren(BlockInterface ...$children): self\n    {\n        $data = $this->data;\n        $data[\"children\"] = array_map(fn (BlockInterface $b) => $b->toArray(), $children);\n\n        $hasChildren = (count($children) > 0);\n\n        return new self(\n            $this->metadata->updateHasChildren($hasChildren),\n            $data,\n        );\n    }\n\n    public function delete(): self\n    {\n        $metadata = $this->metadata()->delete();\n\n        $data = array_merge($this->data, $metadata->toArray());\n\n        return new self($metadata, $data);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var BlockMetadataJson $array */\n        $metadata = BlockMetadata::fromArray($array);\n\n        return new self($metadata, $array);\n    }\n\n    public function toArray(): array\n    {\n        return $this->data;\n    }\n\n    /**\n     * @deprecated 1.17.0 Use `delete()` instead.\n     * @codeCoverageIgnore\n     */\n    public function archive(): BlockInterface\n    {\n        return $this->delete();\n    }\n}\n"
  },
  {
    "path": "src/Blocks/Video.php",
    "content": "<?php\n\nnamespace Notion\\Blocks;\n\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Common\\File;\n\n/**\n * @psalm-import-type BlockMetadataJson from BlockMetadata\n * @psalm-import-type FileJson from \\Notion\\Common\\File\n *\n * @psalm-type VideoJson = array{ video: FileJson }\n *\n * @psalm-immutable\n */\nclass Video implements BlockInterface\n{\n    private function __construct(\n        private readonly BlockMetadata $metadata,\n        public readonly File $file,\n    ) {\n        $metadata->checkType(BlockType::Video);\n    }\n\n    public static function fromFile(File $file): self\n    {\n        $block = BlockMetadata::create(BlockType::Video);\n\n        return new self($block, $file);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var BlockMetadataJson $array */\n        $block = BlockMetadata::fromArray($array);\n\n        /** @psalm-var VideoJson $array */\n        $file = File::fromArray($array[\"video\"]);\n\n        return new self($block, $file);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"video\"] = $this->file->toArray();\n\n        return $array;\n    }\n\n    public function metadata(): BlockMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function changeFile(File $file): self\n    {\n        return new self($this->metadata, $file);\n    }\n\n    public function addChild(BlockInterface $child): never\n    {\n        throw BlockException::noChindrenSupport();\n    }\n\n    public function changeChildren(BlockInterface ...$children): never\n    {\n        throw BlockException::noChindrenSupport();\n    }\n\n    public function delete(): BlockInterface\n    {\n        return new self(\n            $this->metadata->delete(),\n            $this->file,\n        );\n    }\n\n    /**\n     * @deprecated 1.17.0 Use `delete()` instead.\n     * @codeCoverageIgnore\n     */\n    public function archive(): BlockInterface\n    {\n        return $this->delete();\n    }\n}\n"
  },
  {
    "path": "src/Comments/Client.php",
    "content": "<?php\n\nnamespace Notion\\Comments;\n\nuse Notion\\Configuration;\nuse Notion\\Infrastructure\\Http;\n\n/**\n * @psalm-import-type CommentJson from \\Notion\\Comments\\Comment\n */\nclass Client\n{\n    /**\n     * @internal Use `\\Notion\\Notion::comments()` instead\n     */\n    public function __construct(\n        private readonly Configuration $config,\n    ) {\n    }\n\n    /**\n     * List comments from a page\n     *\n     * @param string $id Page or Block ID\n     *\n     * @return Comment[]\n     */\n    public function list(string $id): array\n    {\n        $url = \"https://api.notion.com/v1/comments?block_id={$id}\";\n        $request = Http::createRequest($url, $this->config);\n\n        /** @psalm-var array{ results: CommentJson[] } $body */\n        $body = Http::sendRequest($request, $this->config);\n\n        return array_map(fn($c) => Comment::fromArray($c), $body[\"results\"]);\n    }\n\n    public function create(Comment $comment): Comment\n    {\n        $data = $comment->toArray();\n        unset($data[\"id\"]);\n        unset($data[\"created_time\"]);\n        unset($data[\"last_edited_time\"]);\n        unset($data[\"created_by\"]);\n\n        $json = json_encode($data);\n\n        $url = \"https://api.notion.com/v1/comments\";\n        $request = Http::createRequest($url, $this->config)\n            ->withMethod(\"POST\")\n            ->withHeader(\"Content-Type\", \"application/json\");\n        $request->getBody()->write($json);\n\n        /** @psalm-var CommentJson $body */\n        $body = Http::sendRequest($request, $this->config);\n\n        return Comment::fromArray($body);\n    }\n\n    // public function find(string $userId): User\n    // {\n    //     $url = \"https://api.notion.com/v1/users/{$userId}\";\n    //     $request = Http::createRequest($url, $this->config);\n\n    //     /** @psalm-var UserJson $body */\n    //     $body = Http::sendRequest($request, $this->config);\n\n    //     return User::fromArray($body);\n    // }\n\n    // /**\n    //  * @return User[]\n    //  */\n    // public function findAll(): array\n    // {\n    //     $url = \"https://api.notion.com/v1/users\";\n    //     $request = Http::createRequest($url, $this->config);\n\n    //     /** @var array{ results: UserJson[] } $body */\n    //     $body = Http::sendRequest($request, $this->config);\n\n    //     return array_map(\n    //         function (array $userData): User {\n    //             return User::fromArray($userData);\n    //         },\n    //         $body[\"results\"],\n    //     );\n    // }\n\n    // public function me(): User\n    // {\n    //     $url = \"https://api.notion.com/v1/users/me\";\n    //     $request = Http::createRequest($url, $this->config);\n\n    //     /** @psalm-var UserJson $body */\n    //     $body = Http::sendRequest($request, $this->config);\n\n    //     return User::fromArray($body);\n    // }\n}\n"
  },
  {
    "path": "src/Comments/Comment.php",
    "content": "<?php\n\nnamespace Notion\\Comments;\n\nuse DateTimeImmutable;\nuse Notion\\Common\\Date;\nuse Notion\\Common\\ParentBlock;\nuse Notion\\Common\\RichText;\n\n/**\n * @psalm-import-type ParentJson from \\Notion\\Common\\ParentBlock\n * @psalm-import-type RichTextJson from \\Notion\\Common\\RichText\n *\n * @psalm-type CommentJson = array{\n *      id: string,\n *      parent?: ParentJson,\n *      discussion_id?: string,\n *      created_time: string,\n *      last_edited_time: string,\n *      created_by: array{\n *          object: \"user\",\n *          id: string\n *      },\n *      rich_text: RichTextJson[]\n * }\n */\nclass Comment\n{\n    /** @param RichText[] $text */\n    private function __construct(\n        public readonly string $id,\n        public readonly ParentBlock|null $parent,\n        public readonly string|null $discussionId,\n        public readonly DateTimeImmutable $createdTime,\n        public readonly DateTimeImmutable $lastEditedTime,\n        public readonly string $userId,\n        public readonly array $text,\n    ) {\n    }\n\n    public static function create(string $pageId, RichText ...$text): self\n    {\n        $createdTime = $lastEditedTime = new DateTimeImmutable(\"now\");\n\n        $id = $userId = \"\";\n        $parent = ParentBlock::page($pageId);\n        $discussionId = null;\n\n        return new self($id, $parent, $discussionId, $createdTime, $lastEditedTime, $userId, $text);\n    }\n\n    public static function createReply(string $discussionId, RichText ...$text): self\n    {\n        $createdTime = $lastEditedTime = new DateTimeImmutable(\"now\");\n\n        $id = $userId = \"\";\n        $parent = null;\n\n        return new self($id, $parent, $discussionId, $createdTime, $lastEditedTime, $userId, $text);\n    }\n\n    /** @psalm-param CommentJson $array */\n    public static function fromArray(array $array): self\n    {\n        $id = $array[\"id\"];\n        $parent = !empty($array[\"parent\"]) ? ParentBlock::fromArray($array[\"parent\"]) : null;\n        $discussionId = $array[\"discussion_id\"] ?? null;\n        $createdTime = new DateTimeImmutable($array[\"created_time\"]);\n        $lastEditedTime = new DateTimeImmutable($array[\"last_edited_time\"]);\n        $userId = $array[\"created_by\"][\"id\"];\n        $text = array_map(fn ($t) => RichText::fromArray($t), $array[\"rich_text\"]);\n\n        return new self($id, $parent, $discussionId, $createdTime, $lastEditedTime, $userId, $text);\n    }\n\n    public function toArray(): array\n    {\n        $array = [\n            \"id\" => $this->id,\n            \"created_time\" => $this->createdTime->format(Date::FORMAT),\n            \"last_edited_time\" => $this->lastEditedTime->format(Date::FORMAT),\n            \"created_by\" => [\n                \"object\" => \"user\",\n                \"id\" => $this->userId,\n            ],\n            \"rich_text\" => array_map(fn (RichText $t) => $t->toArray(), $this->text),\n        ];\n\n        if ($this->parent !== null) {\n            $array[\"parent\"] = $this->parent->toArray();\n        }\n\n        if ($this->discussionId !== null) {\n            $array[\"discussion_id\"] = $this->discussionId;\n        }\n\n        return $array;\n    }\n}\n"
  },
  {
    "path": "src/Common/Annotations.php",
    "content": "<?php\n\nnamespace Notion\\Common;\n\n/** @psalm-type AnnotationsJson = array{\n *      bold: bool,\n *      italic: bool,\n *      strikethrough: bool,\n *      underline: bool,\n *      code: bool,\n *      color: string,\n * }\n *\n * @psalm-immutable\n */\nclass Annotations\n{\n    private function __construct(\n        public readonly bool $isBold,\n        public readonly bool $isItalic,\n        public readonly bool $isStrikeThrough,\n        public readonly bool $isUnderline,\n        public readonly bool $isCode,\n        public readonly Color $color,\n    ) {\n    }\n\n    /** @psalm-mutation-free */\n    public static function create(): self\n    {\n        return new self(false, false, false, false, false, Color::Default);\n    }\n\n    /**\n     * @param AnnotationsJson $array\n     *\n     * @internal\n    */\n    public static function fromArray(array $array): self\n    {\n        return new self(\n            $array[\"bold\"],\n            $array[\"italic\"],\n            $array[\"strikethrough\"],\n            $array[\"underline\"],\n            $array[\"code\"],\n            Color::from($array[\"color\"]),\n        );\n    }\n\n    public function toArray(): array\n    {\n        return [\n            \"bold\"          => $this->isBold,\n            \"italic\"        => $this->isItalic,\n            \"strikethrough\" => $this->isStrikeThrough,\n            \"underline\"     => $this->isUnderline,\n            \"code\"          => $this->isCode,\n            \"color\"         => $this->color->value,\n        ];\n    }\n\n    public function bold(bool $bold = true): self\n    {\n        return new self(\n            $bold,\n            $this->isItalic,\n            $this->isStrikeThrough,\n            $this->isUnderline,\n            $this->isCode,\n            $this->color,\n        );\n    }\n\n    public function italic(bool $italic = true): self\n    {\n        return new self(\n            $this->isBold,\n            $italic,\n            $this->isStrikeThrough,\n            $this->isUnderline,\n            $this->isCode,\n            $this->color,\n        );\n    }\n\n    public function strikeThrough(bool $strikeThrough = true): self\n    {\n        return new self(\n            $this->isBold,\n            $this->isItalic,\n            $strikeThrough,\n            $this->isUnderline,\n            $this->isCode,\n            $this->color,\n        );\n    }\n\n    public function underline(bool $underline = true): self\n    {\n        return new self(\n            $this->isBold,\n            $this->isItalic,\n            $this->isStrikeThrough,\n            $underline,\n            $this->isCode,\n            $this->color,\n        );\n    }\n\n    public function code(bool $code = true): self\n    {\n        return new self(\n            $this->isBold,\n            $this->isItalic,\n            $this->isStrikeThrough,\n            $this->isUnderline,\n            $code,\n            $this->color,\n        );\n    }\n\n    public function changeColor(Color $color): self\n    {\n        return new self(\n            $this->isBold,\n            $this->isItalic,\n            $this->isStrikeThrough,\n            $this->isUnderline,\n            $this->isCode,\n            $color,\n        );\n    }\n}\n"
  },
  {
    "path": "src/Common/Color.php",
    "content": "<?php\n\nnamespace Notion\\Common;\n\nenum Color: string\n{\n    case Default = \"default\";\n    case Gray = \"gray\";\n    case Brown = \"brown\";\n    case Orange = \"orange\";\n    case Yellow = \"yellow\";\n    case Green = \"green\";\n    case Blue = \"blue\";\n    case Purple = \"purple\";\n    case Pink = \"pink\";\n    case Red = \"red\";\n    case GrayBackground = \"gray_background\";\n    case BrownBackground = \"brown_background\";\n    case OrangeBackground = \"orange_background\";\n    case YellowBackground = \"yellow_background\";\n    case GreenBackground = \"green_background\";\n    case BlueBackground = \"blue_background\";\n    case PurpleBackground = \"purple_background\";\n    case PinkBackground = \"pink_background\";\n    case RedBackground = \"red_background\";\n}\n"
  },
  {
    "path": "src/Common/Date.php",
    "content": "<?php\n\nnamespace Notion\\Common;\n\nuse DateTimeImmutable;\n\n/**\n * @psalm-type DateJson = array{ start: string, end?: string|null }\n *\n * @psalm-immutable\n */\nclass Date\n{\n    public const FORMAT = \"Y-m-d\\TH:i:s.up\";\n\n    private function __construct(\n        public readonly DateTimeImmutable $start,\n        public readonly DateTimeImmutable|null $end,\n    ) {\n    }\n\n    public static function create(DateTimeImmutable $date): self\n    {\n        return new self($date, null);\n    }\n\n    public static function createRange(\n        DateTimeImmutable $start,\n        DateTimeImmutable $end,\n    ): self {\n        return new self($start, $end);\n    }\n\n    public static function now(): self\n    {\n        return self::create(new DateTimeImmutable(\"now\"));\n    }\n\n    /**\n     * @param DateJson $array\n     *\n     * @internal\n     */\n    public static function fromArray(array $array): self\n    {\n        $start = new DateTimeImmutable($array[\"start\"]);\n        $end = isset($array[\"end\"]) ? new DateTimeImmutable($array[\"end\"]) : null;\n\n        return new self($start, $end);\n    }\n\n    public function toArray(): array\n    {\n        return [\n            \"start\" => $this->start->format(self::FORMAT),\n            \"end\"   => $this->end?->format(self::FORMAT),\n        ];\n    }\n\n    public function isRange(): bool\n    {\n        return $this->end !== null;\n    }\n\n    public function changeStart(DateTimeImmutable $start): self\n    {\n        return new self($start, $this->end);\n    }\n\n    public function changeEnd(DateTimeImmutable $end): self\n    {\n        return new self($this->start, $end);\n    }\n\n    public function removeEnd(): self\n    {\n        return new self($this->start, null);\n    }\n}\n"
  },
  {
    "path": "src/Common/Emoji.php",
    "content": "<?php\n\nnamespace Notion\\Common;\n\n/**\n * @psalm-type EmojiJson = array{ type: \"emoji\", emoji: string }\n *\n * @psalm-immutable\n */\nclass Emoji\n{\n    private function __construct(\n        public readonly string $emoji,\n    ) {\n    }\n\n    public static function fromString(string $emoji): self\n    {\n        return new self($emoji);\n    }\n\n    /** @param EmojiJson $array */\n    public static function fromArray(array $array): self\n    {\n        return new self($array[\"emoji\"]);\n    }\n\n    public function toArray(): array\n    {\n        return [\n            \"type\"  => \"emoji\",\n            \"emoji\" => $this->emoji,\n        ];\n    }\n\n    public function toString(): string\n    {\n        return $this->emoji;\n    }\n}\n"
  },
  {
    "path": "src/Common/Equation.php",
    "content": "<?php\n\nnamespace Notion\\Common;\n\n/**\n * @psalm-type EquationJson = array{ expression: string }\n *\n * @psalm-immutable\n */\nclass Equation\n{\n    private function __construct(\n        public readonly string $expression\n    ) {\n    }\n\n    public static function fromString(string $expression): self\n    {\n        return new self($expression);\n    }\n\n    /**\n     * @param EquationJson $array\n     *\n     * @internal\n     */\n    public static function fromArray(array $array): self\n    {\n        return new self($array[\"expression\"]);\n    }\n\n    public function toArray(): array\n    {\n        return [\n            \"expression\" => $this->expression,\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Common/File.php",
    "content": "<?php\n\nnamespace Notion\\Common;\n\nuse DateTimeImmutable;\n\n/**\n * @psalm-import-type RichTextJson from \\Notion\\Common\\RichText\n *\n * @psalm-type FileJson = array{\n *      type: \"external\"|\"file\"|\"file_upload\",\n *      file?: array{ url: string, expiry_time: string },\n *      external?: array{ url: string },\n *      file_upload?: array{ id: string },\n *      name?: string,\n *      caption?: list<RichTextJson>\n * }\n *\n * @psalm-immutable\n */\nclass File\n{\n    /** @param RichText[] $caption */\n    private function __construct(\n        public readonly FileType $type,\n        public readonly string|null $url,\n        public readonly string|null $fileId,\n        public readonly DateTimeImmutable|null $expiryTime,\n        public readonly string|null $name,\n        public readonly array $caption,\n    ) {\n    }\n\n    public static function createExternal(string $url): self\n    {\n        return new self(FileType::External, $url, null, null, null, []);\n    }\n\n    public static function createInternal(\n        string $url,\n        DateTimeImmutable|null $expiryTime = null\n    ): self {\n        return new self(FileType::Internal, $url, null, $expiryTime, null, []);\n    }\n\n    public static function createFileUpload(string $fileId): self\n    {\n        return new self(FileType::FileUpload, null, $fileId, null, null, []);\n    }\n\n    /**\n     * @param FileJson $array\n     *\n     * @internal\n     */\n    public static function fromArray(array $array): self\n    {\n        $type = $array[\"type\"];\n\n        $file = $array[$type] ?? [];\n\n        return new self(\n            FileType::from($type),\n            $file[\"url\"] ?? \"\",\n            $file[\"id\"] ?? \"\",\n            isset($file[\"expiry_time\"]) ? new DateTimeImmutable($file[\"expiry_time\"]) : null,\n            $array[\"name\"] ?? null,\n            isset($array[\"caption\"]) ? array_map(fn($c) => RichText::fromArray($c), $array[\"caption\"]) : [],\n        );\n    }\n\n    public function toArray(): array\n    {\n        $array = [];\n        $type = $this->type;\n\n        if ($type === FileType::Internal) {\n            $array = [\n                \"type\" => \"file\",\n                \"file\" => [\n                    \"url\" => $this->url,\n                    \"expiry_time\" => $this->expiryTime?->format(Date::FORMAT),\n                ],\n            ];\n        }\n\n        if ($type === FileType::FileUpload) {\n            $array = [\n                \"type\" => \"file_upload\",\n                \"file_upload\" => [ \"id\" => $this->fileId ],\n            ];\n        }\n\n        if ($type === FileType::External) {\n            $array = [\n                \"type\" => \"external\",\n                \"external\" => [ \"url\" => $this->url ],\n            ];\n        }\n\n        if ($this->name !== null) {\n            $array[\"name\"] = $this->name;\n        }\n\n        if (count($this->caption) > 0) {\n            $array[\"caption\"] = array_map(fn($t) => $t->toArray(), $this->caption);\n        }\n\n        return $array;\n    }\n\n    public function isExternal(): bool\n    {\n        return $this->type === FileType::External;\n    }\n\n    public function isInternal(): bool\n    {\n        return $this->type === FileType::Internal;\n    }\n\n    public function isFileUpload(): bool\n    {\n        return $this->type === FileType::FileUpload;\n    }\n\n    public function changeUrl(string $url): self\n    {\n        return new self($this->type, $url, null, $this->expiryTime, $this->name, $this->caption);\n    }\n\n    public function changeFileUploadId(string $fileId): self\n    {\n        return new self($this->type, null, $fileId, $this->expiryTime, $this->name, $this->caption);\n    }\n\n    public function changeName(string $name): self\n    {\n        return new self($this->type, $this->url, $this->fileId, $this->expiryTime, $name, $this->caption);\n    }\n\n    public function changeCaption(RichText ...$caption): self\n    {\n        return new self($this->type, $this->url, $this->fileId, $this->expiryTime, $this->name, $caption);\n    }\n}\n"
  },
  {
    "path": "src/Common/FileType.php",
    "content": "<?php\n\nnamespace Notion\\Common;\n\nenum FileType: string\n{\n    case Internal = \"file\";\n    case External = \"external\";\n    case FileUpload = \"file_upload\";\n}\n"
  },
  {
    "path": "src/Common/Icon.php",
    "content": "<?php\n\nnamespace Notion\\Common;\n\nuse Notion\\Exceptions\\IconException;\n\nclass Icon\n{\n    private function __construct(\n        public readonly Emoji|null $emoji,\n        public readonly File|null $file,\n    ) {\n        if ($emoji === null && $file === null) {\n            throw IconException::bothNull();\n        }\n\n        if ($emoji !== null && $file !== null) {\n            throw IconException::bothSet();\n        }\n    }\n\n    /** @psalm-mutation-free */\n    public static function fromEmoji(Emoji $emoji): self\n    {\n        return new self($emoji, null);\n    }\n\n    /** @psalm-mutation-free */\n    public static function fromFile(File $file): self\n    {\n        return new self(null, $file);\n    }\n\n    /** @psalm-mutation-free */\n    public function toArray(): array\n    {\n        if ($this->emoji !== null) {\n            return $this->emoji->toArray();\n        }\n\n        if ($this->file !== null) {\n            return $this->file->toArray();\n        }\n\n        return [];\n    }\n\n    /**\n     * @psalm-assert-if-true Emoji $this->emoji\n     */\n    public function isEmoji(): bool\n    {\n        return $this->emoji !== null;\n    }\n\n    /**\n     * @psalm-assert-if-true File $this->file\n     */\n    public function isFile(): bool\n    {\n        return $this->file !== null;\n    }\n}\n"
  },
  {
    "path": "src/Common/Mention.php",
    "content": "<?php\n\nnamespace Notion\\Common;\n\nuse Notion\\Users\\User;\n\n/**\n * @psalm-import-type UserJson from \\Notion\\Users\\User\n * @psalm-import-type DateJson from Date\n *\n * @psalm-type MentionJson = array{\n *      type: \"page\"|\"database\"|\"user\"|\"date\",\n *      page?: array{ id: string },\n *      database?: array{ id: string },\n *      user?: UserJson,\n *      date?: DateJson,\n * }\n *\n * @psalm-immutable\n */\nclass Mention\n{\n    private function __construct(\n        public readonly MentionType $type,\n        public readonly string|null $pageId,\n        public readonly string|null $databaseId,\n        public readonly User|null $user,\n        public readonly Date|null $date,\n    ) {\n    }\n\n    public static function page(string $pageId): self\n    {\n        return new self(MentionType::Page, $pageId, null, null, null);\n    }\n\n    public static function database(string $databaseId): self\n    {\n        return new self(MentionType::Database, null, $databaseId, null, null);\n    }\n\n    public static function user(User $user): self\n    {\n        return new self(MentionType::User, null, null, $user, null);\n    }\n\n    public static function date(Date $date): self\n    {\n        return new self(MentionType::Date, null, null, null, $date);\n    }\n\n    /**\n     * @psalm-param MentionJson $array\n     *\n     * @internal\n     */\n    public static function fromArray(array $array): self\n    {\n        $type = MentionType::from($array[\"type\"]);\n\n        $pageId = array_key_exists(\"page\", $array) ? $array[\"page\"][\"id\"] : null;\n        $databaseId = array_key_exists(\"database\", $array) ? $array[\"database\"][\"id\"] : null;\n        $user = array_key_exists(\"user\", $array) ? User::fromArray($array[\"user\"]) : null;\n        $date = array_key_exists(\"date\", $array) ? Date::fromArray($array[\"date\"]) : null;\n\n        return new self($type, $pageId, $databaseId, $user, $date);\n    }\n\n    public function toArray(): array\n    {\n        $array = [ \"type\" => $this->type->value ];\n\n        if ($this->isPage()) {\n            $array[\"page\"] = [ \"id\" => $this->pageId ];\n        }\n        if ($this->isDatabase()) {\n            $array[\"database\"] = [ \"id\" => $this->databaseId ];\n        }\n        if ($this->isUser()) {\n            $array[\"user\"] = $this->user->toArray();\n        }\n        if ($this->isDate()) {\n            $array[\"date\"] = $this->date->toArray();\n        }\n\n        return $array;\n    }\n\n    /**\n     * @psalm-assert-if-true string $this->pageId\n     */\n    public function isPage(): bool\n    {\n        return $this->type === MentionType::Page;\n    }\n\n    /**\n     * @psalm-assert-if-true string $this->databaseId\n     */\n    public function isDatabase(): bool\n    {\n        return $this->type === MentionType::Database;\n    }\n\n    /**\n     * @psalm-assert-if-true User $this->user\n     */\n    public function isUser(): bool\n    {\n        return $this->type === MentionType::User;\n    }\n\n    /**\n     * @psalm-assert-if-true Date $this->date\n     */\n    public function isDate(): bool\n    {\n        return $this->type === MentionType::Date;\n    }\n}\n"
  },
  {
    "path": "src/Common/MentionType.php",
    "content": "<?php\n\nnamespace Notion\\Common;\n\nenum MentionType: string\n{\n    case Page = \"page\";\n    case Database = \"database\";\n    case User = \"user\";\n    case Date = \"date\";\n}\n"
  },
  {
    "path": "src/Common/ParentBlock.php",
    "content": "<?php\n\nnamespace Notion\\Common;\n\n/**\n * @psalm-type ParentJson = array{\n *      type: string,\n *      database_id?: string,\n *      page_id?: string,\n *      block_id?: string,\n *      workspace?: true\n * }\n */\nclass ParentBlock\n{\n    private function __construct(\n        public readonly ParentType $type,\n        public readonly string|null $id\n    ) {\n    }\n\n    public static function database(string $databaseId): self\n    {\n        return new self(ParentType::Database, $databaseId);\n    }\n\n    public static function page(string $pageId): self\n    {\n        return new self(ParentType::Page, $pageId);\n    }\n\n    public static function block(string $blockId): self\n    {\n        return new self(ParentType::Block, $blockId);\n    }\n\n    public static function workspace(): self\n    {\n        return new self(ParentType::Workspace, null);\n    }\n\n    /** @psalm-param ParentJson $array */\n    public static function fromArray(array $array): self\n    {\n        $type = ParentType::from($array[\"type\"]);\n\n        $id = match ($type) {\n            ParentType::Database => $array[\"database_id\"] ?? \"\",\n            ParentType::Page => $array[\"page_id\"] ?? \"\",\n            ParentType::Block => $array[\"block_id\"] ?? \"\",\n            ParentType::Workspace => null,\n        };\n\n        return new self($type, $id);\n    }\n\n    public function toArray(): array\n    {\n        $array = [\n            \"type\" => $this->type->value,\n        ];\n\n        if ($this->type === ParentType::Database) {\n            $array[\"database_id\"] = $this->id;\n        }\n\n        if ($this->type === ParentType::Block) {\n            $array[\"block_id\"] = $this->id;\n        }\n\n        if ($this->type === ParentType::Page) {\n            $array[\"page_id\"] = $this->id;\n        }\n\n        if ($this->type === ParentType::Workspace) {\n            $array[\"workspace\"] = true;\n        }\n\n        return $array;\n    }\n}\n"
  },
  {
    "path": "src/Common/ParentType.php",
    "content": "<?php\n\nnamespace Notion\\Common;\n\nenum ParentType: string\n{\n    case Database = \"database_id\";\n    case Page = \"page_id\";\n    case Block = \"block_id\";\n    case Workspace = \"workspace\";\n}\n"
  },
  {
    "path": "src/Common/RichText.php",
    "content": "<?php\n\nnamespace Notion\\Common;\n\n/**\n * @psalm-import-type AnnotationsJson from Annotations\n * @psalm-import-type TextJson from Text\n * @psalm-import-type MentionJson from Mention\n * @psalm-import-type EquationJson from Equation\n *\n * @psalm-type RichTextJson = array{\n *      plain_text: string,\n *      href: string|null,\n *      annotations: AnnotationsJson,\n *      type: \"text\"|\"mention\"|\"equation\",\n *      text?: TextJson,\n *      mention?: MentionJson,\n *      equation?: EquationJson,\n * }\n *\n * @psalm-immutable\n */\nclass RichText\n{\n    private function __construct(\n        public readonly string $plainText,\n        public readonly string|null $href,\n        public readonly Annotations $annotations,\n        public readonly RichTextType $type,\n        public readonly Text|null $text,\n        public readonly Mention|null $mention,\n        public readonly Equation|null $equation,\n    ) {\n    }\n\n    /** @psalm-mutation-free */\n    public static function fromString(string $content): self\n    {\n        $text = Text::fromString($content);\n\n        return self::fromText($text);\n    }\n\n    public static function createLink(string $content, string $url): self\n    {\n        $text = Text::fromString($content)->changeUrl($url);\n\n        return self::fromText($text);\n    }\n\n    /** @psalm-mutation-free */\n    public static function fromText(Text $text): self\n    {\n        $annotations = Annotations::create();\n\n        return new self(\n            $text->content,\n            $text->url,\n            $annotations,\n            RichTextType::Text,\n            $text,\n            null,\n            null\n        );\n    }\n\n    public static function fromEquation(Equation $equation): self\n    {\n        $annotations = Annotations::create();\n\n        return new self(\n            $equation->expression,\n            null,\n            $annotations,\n            RichTextType::Equation,\n            null,\n            null,\n            $equation\n        );\n    }\n\n    public static function fromMention(Mention $mention): self\n    {\n        $annotations = Annotations::create();\n\n        return new self(\n            \"\",\n            null,\n            $annotations,\n            RichTextType::Mention,\n            null,\n            $mention,\n            null,\n        );\n    }\n\n    public static function newLine(): self\n    {\n        return self::fromString(\"\\n\");\n    }\n\n    /**\n     * @psalm-param RichTextJson $array\n     *\n     * @internal\n     */\n    public static function fromArray(array $array): self\n    {\n        return new self(\n            $array[\"plain_text\"],\n            isset($array[\"href\"]) ? $array[\"href\"] : null,\n            Annotations::fromArray($array[\"annotations\"]),\n            RichTextType::from($array[\"type\"]),\n            array_key_exists(\"text\", $array) ? Text::fromArray($array[\"text\"]) : null,\n            array_key_exists(\"mention\", $array) ? Mention::fromArray($array[\"mention\"]) : null,\n            array_key_exists(\"equation\", $array) ? Equation::fromArray($array[\"equation\"]) : null,\n        );\n    }\n\n    public function toString(): string\n    {\n        return $this->plainText;\n    }\n\n    public function toArray(): array\n    {\n        $array = [\n            \"plain_text\"  => $this->plainText,\n            \"href\"        => $this->href,\n            \"annotations\" => $this->annotations->toArray(),\n            \"type\"        => $this->type->value,\n        ];\n\n        if ($this->isText()) {\n            $array[\"text\"] = $this->text->toArray();\n        }\n\n        if ($this->isMention()) {\n            $array[\"mention\"] = $this->mention->toArray();\n        }\n\n        if ($this->isEquation()) {\n            $array[\"equation\"] = $this->equation->toArray();\n        }\n\n        return $array;\n    }\n\n    /**\n     * @psalm-assert-if-true Text $this->text\n     */\n    public function isText(): bool\n    {\n        return $this->type === RichTextType::Text;\n    }\n\n    /**\n     * @psalm-assert-if-true Mention $this->mention\n     */\n    public function isMention(): bool\n    {\n        return $this->type === RichTextType::Mention;\n    }\n\n    /**\n     * @psalm-assert-if-true Equation $this->equation\n     */\n    public function isEquation(): bool\n    {\n        return $this->type === RichTextType::Equation;\n    }\n\n    public function changeHref(string $href): self\n    {\n        return new self(\n            $this->plainText,\n            $href,\n            $this->annotations,\n            $this->type,\n            $this->text?->changeUrl($href),\n            $this->mention,\n            $this->equation,\n        );\n    }\n\n    public function changeAnnotations(Annotations $annotations): self\n    {\n        return new self(\n            $this->plainText,\n            $this->href,\n            $annotations,\n            $this->type,\n            $this->text,\n            $this->mention,\n            $this->equation,\n        );\n    }\n\n    public function bold(bool $bold = true): self\n    {\n        $annotations = $this->annotations->bold($bold);\n\n        return $this->changeAnnotations($annotations);\n    }\n\n    public function italic(bool $italic = true): self\n    {\n        $annotations = $this->annotations->italic($italic);\n\n        return $this->changeAnnotations($annotations);\n    }\n\n    public function strikeThrough(bool $strikeThrough = true): self\n    {\n        $annotations = $this->annotations->strikeThrough($strikeThrough);\n\n        return $this->changeAnnotations($annotations);\n    }\n\n    public function underline(bool $underline = true): self\n    {\n        $annotations = $this->annotations->underline($underline);\n\n        return $this->changeAnnotations($annotations);\n    }\n\n    public function code(bool $code = true): self\n    {\n        $annotations = $this->annotations->code($code);\n\n        return $this->changeAnnotations($annotations);\n    }\n\n    public function color(Color $color): self\n    {\n        $annotations = $this->annotations->changeColor($color);\n\n        return $this->changeAnnotations($annotations);\n    }\n\n    /** @psalm-mutation-free */\n    public static function multipleToString(self ...$richText): string\n    {\n        $string = \"\";\n        foreach ($richText as $singleRichText) {\n            $string = $string . $singleRichText->toString();\n        }\n\n        return $string;\n    }\n}\n"
  },
  {
    "path": "src/Common/RichTextType.php",
    "content": "<?php\n\nnamespace Notion\\Common;\n\nenum RichTextType: string\n{\n    case Equation = \"equation\";\n    case Mention = \"mention\";\n    case Text = \"text\";\n}\n"
  },
  {
    "path": "src/Common/Text.php",
    "content": "<?php\n\nnamespace Notion\\Common;\n\n/**\n * @psalm-type TextJson = array{ content: string, link?: array{ url: string } }\n *\n * @psalm-immutable\n */\nclass Text\n{\n    private function __construct(\n        public readonly string $content,\n        public readonly string|null $url,\n    ) {\n    }\n\n    /** @psalm-mutation-free */\n    public static function fromString(string $content): self\n    {\n        return new self($content, null);\n    }\n\n    /**\n     * @param TextJson $array\n     *\n     * @internal\n     */\n    public static function fromArray(array $array): self\n    {\n        $url = isset($array[\"link\"]) ? $array[\"link\"][\"url\"] : null;\n\n        return new self($array[\"content\"], $url);\n    }\n\n    public function toArray(): array\n    {\n        $array = [ \"content\" => $this->content ];\n        if ($this->url !== null) {\n            $array[\"link\"] = [ \"url\" => $this->url ];\n        }\n\n        return $array;\n    }\n\n    public function changeContent(string $content): self\n    {\n        return new self($content, $this->url);\n    }\n\n    public function changeUrl(string $url): self\n    {\n        return new self($this->content, $url);\n    }\n\n    public function removeUrl(): self\n    {\n        return new self($this->content, null);\n    }\n}\n"
  },
  {
    "path": "src/Configuration.php",
    "content": "<?php\n\nnamespace Notion;\n\nuse Http\\Discovery\\Psr17FactoryDiscovery;\nuse Http\\Discovery\\Psr18ClientDiscovery;\nuse Psr\\Http\\Client\\ClientInterface;\nuse Psr\\Http\\Message\\RequestFactoryInterface;\n\n/**\n * Notion SDK configuration\n *\n * @psalm-type ConfigProperties = array{\n *     token: string,\n *     version: string,\n *     httpClient: ClientInterface,\n *     requestFactory: RequestFactoryInterface,\n *     retryOnConflict: bool,\n *     retryOnConflictAttempts: int,\n *     ...\n * }\n *\n * @psalm-immutable\n */\nclass Configuration\n{\n    private function __construct(\n        public readonly string $token,\n        public readonly string $version,\n        public readonly ClientInterface $httpClient,\n        public readonly RequestFactoryInterface $requestFactory,\n        public readonly bool $retryOnConflict,\n        public readonly int $retryOnConflictAttempts,\n    ) {\n    }\n\n    public static function create(string $token): self\n    {\n        return new self(\n            token: $token,\n            version: Notion::API_VERSION,\n            httpClient: Psr18ClientDiscovery::find(),\n            requestFactory: Psr17FactoryDiscovery::findRequestFactory(),\n            retryOnConflict: true,\n            retryOnConflictAttempts: 3,\n        );\n    }\n\n    public static function createFromPsrImplementations(\n        string $token,\n        ClientInterface $httpClient,\n        RequestFactoryInterface $requestFactory,\n    ): self {\n        return new self(\n            token: $token,\n            version: Notion::API_VERSION,\n            httpClient: $httpClient,\n            requestFactory: $requestFactory,\n            retryOnConflict: true,\n            retryOnConflictAttempts: 3,\n        );\n    }\n\n    /**\n     * Retry operations when the Notion API responds with conflict error.\n     *\n     * @param int $attempts Number of attempts\n     */\n    public function enableRetryOnConflict(int $attempts = 1): self\n    {\n        $properties = $this->properties();\n        $properties[\"retryOnConflict\"] = true;\n        $properties[\"retryOnConflictAttempts\"] = $attempts;\n\n        return new self(...$properties);\n    }\n\n    public function disableRetryOnConflict(): self\n    {\n        $properties = $this->properties();\n        $properties[\"retryOnConflict\"] = false;\n        $properties[\"retryOnConflictAttempts\"] = 0;\n\n        return new self(...$properties);\n    }\n\n    /** @psalm-return ConfigProperties */\n    private function properties(): array\n    {\n        return get_object_vars($this);\n    }\n}\n"
  },
  {
    "path": "src/Databases/Client.php",
    "content": "<?php\n\nnamespace Notion\\Databases;\n\nuse Notion\\Configuration;\nuse Notion\\Databases\\Query\\Result;\nuse Notion\\Databases\\Query\\Sort;\nuse Notion\\Infrastructure\\Http;\nuse Notion\\Pages\\Page;\n\n/**\n * @psalm-import-type DatabaseJson from Database\n * @psalm-import-type QueryResultJson from Result\n */\nclass Client\n{\n    /**\n     * @internal Use `\\Notion\\Notion::databases()` instead\n     */\n    public function __construct(\n        private readonly Configuration $config,\n    ) {\n    }\n\n    public function find(string $databaseId): Database\n    {\n        $url = \"https://api.notion.com/v1/databases/{$databaseId}\";\n        $request = Http::createRequest($url, $this->config);\n\n        /** @psalm-var DatabaseJson $body */\n        $body = Http::sendRequest($request, $this->config);\n\n        return Database::fromArray($body);\n    }\n\n    public function create(Database $database): Database\n    {\n        $data = $database->toArray();\n        unset($data[\"id\"]);\n\n        $url = \"https://api.notion.com/v1/databases\";\n        $request = Http::createRequest($url, $this->config)\n            ->withMethod(\"POST\")\n            ->withHeader(\"Content-Type\", \"application/json\");\n        $request->getBody()->write(json_encode($data));\n\n        /** @psalm-var DatabaseJson $body */\n        $body = Http::sendRequest($request, $this->config);\n\n        return Database::fromArray($body);\n    }\n\n    public function update(Database $database): Database\n    {\n        $data = $database->toArray();\n        unset($data[\"parent\"]);\n        unset($data[\"created_time\"]);\n        unset($data[\"last_edited_time\"]);\n\n        $databaseId = $database->id;\n        $url = \"https://api.notion.com/v1/databases/{$databaseId}\";\n        $request = Http::createRequest($url, $this->config)\n            ->withMethod(\"PATCH\")\n            ->withHeader(\"Content-Type\", \"application/json\");\n\n        $request->getBody()->write(json_encode($data));\n\n        /** @psalm-var DatabaseJson $body */\n        $body = Http::sendRequest($request, $this->config);\n\n        return Database::fromArray($body);\n    }\n\n    public function delete(Database $database): void\n    {\n        $databaseId = $database->id;\n        $url = \"https://api.notion.com/v1/blocks/{$databaseId}\";\n        $request = Http::createRequest($url, $this->config)\n            ->withMethod(\"DELETE\");\n\n        Http::sendRequest($request, $this->config);\n    }\n\n    public function query(Database $database, Query $query): Result\n    {\n        $data = $query->toArray();\n\n        $databaseId = $database->id;\n        $url = \"https://api.notion.com/v1/databases/{$databaseId}/query\";\n        $request = Http::createRequest($url, $this->config)\n            ->withMethod(\"POST\")\n            ->withHeader(\"Content-Type\", \"application/json\");\n\n        $request->getBody()->write(json_encode($data));\n\n        /** @psalm-var QueryResultJson $body */\n        $body = Http::sendRequest($request, $this->config);\n\n        return Result::fromArray($body);\n    }\n\n    /**\n     * @param Sort[] $sorts\n     *\n     * @return Page[]\n     */\n    public function queryAllPages(Database $database, array $sorts = []): array\n    {\n        $query = Query::create()\n                    ->changeSorts(...$sorts)\n                    ->changePageSize(Query::MAX_PAGE_SIZE);\n\n        $pages = [];\n        $startCursor = null;\n        $hasMore = true;\n\n        while ($hasMore) {\n            if ($startCursor !== null) {\n                $query = $query->changeStartCursor($startCursor);\n            }\n\n            $result = $this->query($database, $query);\n\n            $pages = array_merge($pages, $result->pages);\n            $hasMore = $result->hasMore;\n            $startCursor = $result->nextCursor;\n        }\n\n        return $pages;\n    }\n}\n"
  },
  {
    "path": "src/Databases/Database.php",
    "content": "<?php\n\nnamespace Notion\\Databases;\n\nuse DateTimeImmutable;\nuse Notion\\Common\\Date;\nuse Notion\\Common\\Emoji;\nuse Notion\\Common\\File;\nuse Notion\\Common\\Icon;\nuse Notion\\Common\\RichText;\nuse Notion\\Databases\\Properties\\PropertyCollection;\nuse Notion\\Databases\\Properties\\PropertyFactory;\nuse Notion\\Databases\\Properties\\PropertyInterface;\nuse Notion\\Databases\\Properties\\Status;\nuse Notion\\Databases\\Properties\\Title;\nuse Notion\\Exceptions\\DatabaseException;\n\n/**\n * @psalm-import-type EmojiJson from \\Notion\\Common\\Emoji\n * @psalm-import-type FileJson from \\Notion\\Common\\File\n * @psalm-import-type RichTextJson from \\Notion\\Common\\RichText\n * @psalm-import-type PropertyMetadataJson from \\Notion\\Databases\\Properties\\PropertyMetadata\n * @psalm-import-type DatabaseParentJson from DatabaseParent\n *\n * @psalm-type DatabaseJson = array{\n *      object: \"database\",\n *      id: string,\n *      created_time: string,\n *      last_edited_time: string,\n *      title: RichTextJson[],\n *      description: RichTextJson[],\n *      icon: EmojiJson|FileJson|null,\n *      cover: FileJson|null,\n *      properties: array<string, PropertyMetadataJson>,\n *      parent: DatabaseParentJson,\n *      url: string,\n *      is_inline: bool,\n * }\n *\n * @psalm-immutable\n */\nclass Database\n{\n    /**\n     * @param RichText[] $title\n     * @param RichText[] $description\n     * @param array<string, PropertyInterface> $properties\n     */\n    private function __construct(\n        public readonly string $id,\n        public readonly DateTimeImmutable $createdTime,\n        public readonly DateTimeImmutable $lastEditedTime,\n        public readonly array $title,\n        public readonly array $description,\n        public readonly Icon|null $icon,\n        public readonly File|null $cover,\n        public readonly array $properties,\n        public readonly DatabaseParent $parent,\n        public readonly string $url,\n        public readonly bool $isInline,\n    ) {\n        if ($cover !== null && $cover->isInternal()) {\n            throw DatabaseException::internalCover();\n        }\n\n        if (!$this->hasTitleProperty($properties)) {\n            throw DatabaseException::noTitleProperty();\n        }\n    }\n\n    public static function create(DatabaseParent $parent): self\n    {\n        $now = new DateTimeImmutable(\"now\");\n\n        return new self(\n            \"\",\n            $now,\n            $now,\n            [],\n            [],\n            null,\n            null,\n            [ \"Title\" => Title::create() ],\n            $parent,\n            \"\",\n            false,\n        );\n    }\n\n    /**\n     * @param DatabaseJson $array\n     *\n     * @internal\n     */\n    public static function fromArray(array $array): self\n    {\n        $title = array_map(\n            function (array $richTextArray): RichText {\n                return RichText::fromArray($richTextArray);\n            },\n            $array[\"title\"],\n        );\n        $description = array_map(\n            function (array $descriptionArray): RichText {\n                return RichText::fromArray($descriptionArray);\n            },\n            $array[\"description\"] ?? [],\n        );\n\n        $icon = null;\n        if (is_array($array[\"icon\"])) {\n            $iconArray = $array[\"icon\"];\n            $iconType = $iconArray[\"type\"];\n\n            if ($iconType === \"emoji\") {\n                /** @psalm-var EmojiJson $iconArray */\n                $emoji = Emoji::fromArray($iconArray);\n                $icon = Icon::fromEmoji($emoji);\n            }\n\n            if ($iconType === \"file\" || $iconType === \"external\") {\n                /** @psalm-var FileJson $iconArray */\n                $file = File::fromArray($iconArray);\n                $icon = Icon::fromFile($file);\n            }\n        }\n\n        $cover = isset($array[\"cover\"]) ? File::fromArray($array[\"cover\"]) : null;\n\n        $parent = DatabaseParent::fromArray($array[\"parent\"]);\n\n        $properties = [];\n        foreach ($array[\"properties\"] as $propertyName => $propertyArray) {\n            $properties[$propertyName] = PropertyFactory::fromArray($propertyArray);\n        }\n\n        return new self(\n            $array[\"id\"],\n            new DateTimeImmutable($array[\"created_time\"]),\n            new DateTimeImmutable($array[\"last_edited_time\"]),\n            $title,\n            $description,\n            $icon,\n            $cover,\n            $properties,\n            $parent,\n            $array[\"url\"],\n            $array[\"is_inline\"],\n        );\n    }\n\n    public function toArray(): array\n    {\n        return [\n            \"object\"           => \"database\",\n            \"id\"               => $this->id,\n            \"created_time\"     => $this->createdTime->format(Date::FORMAT),\n            \"last_edited_time\" => $this->lastEditedTime->format(Date::FORMAT),\n            \"title\"            => array_map(fn(RichText $t) => $t->toArray(), $this->title),\n            \"description\"      => array_map(fn(RichText $t) => $t->toArray(), $this->description),\n            \"icon\"             => $this->icon?->toArray(),\n            \"cover\"            => $this->cover?->toArray(),\n            \"properties\"       => $this->propertiesToArray(),\n            \"parent\"           => $this->parent->toArray(),\n            \"url\"              => $this->url,\n            \"is_inline\"        => $this->isInline,\n        ];\n    }\n\n    /**\n     * @psalm-assert-if-false null $this->icon\n     */\n    public function hasIcon(): bool\n    {\n        return $this->icon !== null;\n    }\n\n    public function changeTitle(string $title): self\n    {\n        return new self(\n            $this->id,\n            $this->createdTime,\n            $this->lastEditedTime,\n            [ RichText::fromString($title) ],\n            $this->description,\n            $this->icon,\n            $this->cover,\n            $this->properties,\n            $this->parent,\n            $this->url,\n            $this->isInline,\n        );\n    }\n\n    public function changeAdvancedTitle(RichText ...$title): self\n    {\n        return new self(\n            $this->id,\n            $this->createdTime,\n            $this->lastEditedTime,\n            $title,\n            $this->description,\n            $this->icon,\n            $this->cover,\n            $this->properties,\n            $this->parent,\n            $this->url,\n            $this->isInline,\n        );\n    }\n\n    public function changeIcon(Emoji|File|Icon $icon): self\n    {\n        if ($icon instanceof Emoji) {\n            $icon = Icon::fromEmoji($icon);\n        }\n\n        if ($icon instanceof File) {\n            $icon = Icon::fromFile($icon);\n        }\n\n        return new self(\n            $this->id,\n            $this->createdTime,\n            $this->lastEditedTime,\n            $this->title,\n            $this->description,\n            $icon,\n            $this->cover,\n            $this->properties,\n            $this->parent,\n            $this->url,\n            $this->isInline,\n        );\n    }\n\n    public function removeIcon(): self\n    {\n        return new self(\n            $this->id,\n            $this->createdTime,\n            $this->lastEditedTime,\n            $this->title,\n            $this->description,\n            null,\n            $this->cover,\n            $this->properties,\n            $this->parent,\n            $this->url,\n            $this->isInline,\n        );\n    }\n\n    public function changeCover(File $cover): self\n    {\n        return new self(\n            $this->id,\n            $this->createdTime,\n            $this->lastEditedTime,\n            $this->title,\n            $this->description,\n            $this->icon,\n            $cover,\n            $this->properties,\n            $this->parent,\n            $this->url,\n            $this->isInline,\n        );\n    }\n\n    public function removeCover(): self\n    {\n        return new self(\n            $this->id,\n            $this->createdTime,\n            $this->lastEditedTime,\n            $this->title,\n            $this->description,\n            $this->icon,\n            null,\n            $this->properties,\n            $this->parent,\n            $this->url,\n            $this->isInline,\n        );\n    }\n\n    public function properties(): PropertyCollection\n    {\n        return PropertyCollection::create(...$this->properties);\n    }\n\n    public function addProperty(PropertyInterface $property): self\n    {\n        return new self(\n            $this->id,\n            $this->createdTime,\n            $this->lastEditedTime,\n            $this->title,\n            $this->description,\n            $this->icon,\n            $this->cover,\n            $this->properties()->add($property)->getAll(),\n            $this->parent,\n            $this->url,\n            $this->isInline,\n        );\n    }\n\n    public function removePropertyByName(string $propertyName): self\n    {\n        return new self(\n            $this->id,\n            $this->createdTime,\n            $this->lastEditedTime,\n            $this->title,\n            $this->description,\n            $this->icon,\n            $this->cover,\n            $this->properties()->remove($propertyName)->getAll(),\n            $this->parent,\n            $this->url,\n            $this->isInline,\n        );\n    }\n\n    public function changeProperty(PropertyInterface $property): self\n    {\n        return new self(\n            $this->id,\n            $this->createdTime,\n            $this->lastEditedTime,\n            $this->title,\n            $this->description,\n            $this->icon,\n            $this->cover,\n            $this->properties()->change($property)->getAll(),\n            $this->parent,\n            $this->url,\n            $this->isInline,\n        );\n    }\n\n    /** @param array<string, PropertyInterface> $properties */\n    public function changeProperties(array $properties): self\n    {\n        return new self(\n            $this->id,\n            $this->createdTime,\n            $this->lastEditedTime,\n            $this->title,\n            $this->description,\n            $this->icon,\n            $this->cover,\n            PropertyCollection::create(...$properties)->getAll(),\n            $this->parent,\n            $this->url,\n            $this->isInline,\n        );\n    }\n\n    public function changeParent(DatabaseParent $parent): self\n    {\n        return new self(\n            $this->id,\n            $this->createdTime,\n            $this->lastEditedTime,\n            $this->title,\n            $this->description,\n            $this->icon,\n            $this->cover,\n            $this->properties,\n            $parent,\n            $this->url,\n            $this->isInline,\n        );\n    }\n\n    public function enableInline(): self\n    {\n        return new self(\n            $this->id,\n            $this->createdTime,\n            $this->lastEditedTime,\n            $this->title,\n            $this->description,\n            $this->icon,\n            $this->cover,\n            $this->properties,\n            $this->parent,\n            $this->url,\n            true,\n        );\n    }\n\n    public function disableInline(): self\n    {\n        return new self(\n            $this->id,\n            $this->createdTime,\n            $this->lastEditedTime,\n            $this->title,\n            $this->description,\n            $this->icon,\n            $this->cover,\n            $this->properties,\n            $this->parent,\n            $this->url,\n            false,\n        );\n    }\n\n    /** @param array<string, PropertyInterface> $properties */\n    private function hasTitleProperty(array $properties): bool\n    {\n        foreach ($properties as $property) {\n            if ($property instanceof Title) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    private function propertiesToArray(): array\n    {\n        $array = [];\n\n        $properties = $this->properties;\n        foreach ($properties as $name => $property) {\n            if ($property instanceof Status) {\n                continue;\n            }\n            $array[$name] = $property->toArray();\n        }\n\n        return $array;\n    }\n}\n"
  },
  {
    "path": "src/Databases/DatabaseParent.php",
    "content": "<?php\n\nnamespace Notion\\Databases;\n\n/**\n * @psalm-type DatabaseParentJson = array{\n *      type: \"page_id\"|\"workspace\"|\"block_id\",\n *      page_id?: string,\n *      workspace?: true,\n *      block_id?: string,\n * }\n *\n * @psalm-immutable\n */\nclass DatabaseParent\n{\n    private function __construct(\n        public readonly DatabaseParentType $type,\n        public readonly string|null $id,\n    ) {\n    }\n\n    public static function page(string $pageId): self\n    {\n        return new self(DatabaseParentType::Page, $pageId);\n    }\n\n    public static function workspace(): self\n    {\n        return new self(DatabaseParentType::Workspace, null);\n    }\n\n    public static function block(string $blockId): self\n    {\n        return new self(DatabaseParentType::Block, $blockId);\n    }\n\n    /**\n     * @param DatabaseParentJson $array\n     *\n     * @internal\n     */\n    public static function fromArray(array $array): self\n    {\n        $type = DatabaseParentType::from($array[\"type\"]);\n\n        $id = $array[\"page_id\"] ?? $array[\"block_id\"] ?? null;\n\n        return new self($type, $id);\n    }\n\n    public function toArray(): array\n    {\n        $array = [];\n\n        if ($this->isPage()) {\n            $array[\"page_id\"] = $this->id;\n        }\n        if ($this->isWorkspace()) {\n            $array[\"workspace\"] = true;\n        }\n        if ($this->isBlock()) {\n            $array[\"block_id\"] = $this->id;\n        }\n\n        return $array;\n    }\n\n    public function isPage(): bool\n    {\n        return $this->type === DatabaseParentType::Page;\n    }\n\n    public function isWorkspace(): bool\n    {\n        return $this->type === DatabaseParentType::Workspace;\n    }\n\n    public function isBlock(): bool\n    {\n        return $this->type === DatabaseParentType::Block;\n    }\n}\n"
  },
  {
    "path": "src/Databases/DatabaseParentType.php",
    "content": "<?php\n\nnamespace Notion\\Databases;\n\nenum DatabaseParentType: string\n{\n    case Page = \"page_id\";\n    case Workspace = \"workspace\";\n    case Block = \"block_id\";\n}\n"
  },
  {
    "path": "src/Databases/Properties/Checkbox.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Properties;\n\n/**\n * @psalm-type CheckboxJson = array{\n *      id: string,\n *      name: string,\n *      type: \"checkbox\",\n *      checkbox: array<empty, empty>,\n *      description?: string,\n * }\n *\n * @psalm-immutable\n */\nclass Checkbox implements PropertyInterface\n{\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n    ) {\n    }\n\n    public static function create(string $propertyName = \"Checkbox\"): self\n    {\n        $metadata = PropertyMetadata::create(\"\", $propertyName, PropertyType::Checkbox);\n\n        return new self($metadata);\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var CheckboxJson $array */\n        $property = PropertyMetadata::fromArray($array);\n\n        return new self($property);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n        $array[\"checkbox\"] = new \\stdClass();\n\n        return $array;\n    }\n}\n"
  },
  {
    "path": "src/Databases/Properties/CreatedBy.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Properties;\n\n/**\n * @psalm-type CreatedByJson = array{\n *      id: string,\n *      name: string,\n *      type: \"created_by\",\n *      created_by: array<empty, empty>,\n *      description?: string,\n * }\n *\n * @psalm-immutable\n */\nclass CreatedBy implements PropertyInterface\n{\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n    ) {\n    }\n\n    public static function create(string $propertyName = \"CreatedBy\"): self\n    {\n        $property = PropertyMetadata::create(\"\", $propertyName, PropertyType::CreatedBy);\n\n        return new self($property);\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var CreatedByJson $array */\n        $property = PropertyMetadata::fromArray($array);\n\n        return new self($property);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n        $array[\"created_by\"] = new \\stdClass();\n\n        return $array;\n    }\n}\n"
  },
  {
    "path": "src/Databases/Properties/CreatedTime.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Properties;\n\n/**\n * @psalm-type CreatedTimeJson = array{\n *      id: string,\n *      name: string,\n *      type: \"created_time\",\n *      created_time: array<empty, empty>,\n *      description?: string,\n * }\n *\n * @psalm-immutable\n */\nclass CreatedTime implements PropertyInterface\n{\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n    ) {\n    }\n\n    public static function create(string $propertyName = \"CreatedTime\"): self\n    {\n        $property = PropertyMetadata::create(\"\", $propertyName, PropertyType::CreatedTime);\n\n        return new self($property);\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var CreatedTimeJson $array */\n        $property = PropertyMetadata::fromArray($array);\n\n        return new self($property);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n        $array[\"created_time\"] = new \\stdClass();\n\n        return $array;\n    }\n}\n"
  },
  {
    "path": "src/Databases/Properties/Date.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Properties;\n\n/**\n * @psalm-type DateJson = array{\n *      id: string,\n *      name: string,\n *      type: \"date\",\n *      date: array<empty, empty>,\n *      description?: string,\n * }\n *\n * @psalm-immutable\n */\nclass Date implements PropertyInterface\n{\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n    ) {\n    }\n\n    public static function create(string $propertyName = \"Date\"): self\n    {\n        $property = PropertyMetadata::create(\"\", $propertyName, PropertyType::Date);\n\n        return new self($property);\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var DateJson $array */\n        $property = PropertyMetadata::fromArray($array);\n\n        return new self($property);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n        $array[\"date\"] = new \\stdClass();\n\n        return $array;\n    }\n}\n"
  },
  {
    "path": "src/Databases/Properties/Email.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Properties;\n\n/**\n * @psalm-type EmailJson = array{\n *      id: string,\n *      name: string,\n *      type: \"email\",\n *      email: array<empty, empty>,\n *      description?: string,\n * }\n *\n * @psalm-immutable\n */\nclass Email implements PropertyInterface\n{\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n    ) {\n    }\n\n    public static function create(string $propertyName = \"Email\"): self\n    {\n        $metadata = PropertyMetadata::create(\"\", $propertyName, PropertyType::Email);\n\n        return new self($metadata);\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var EmailJson $array */\n        $metadata = PropertyMetadata::fromArray($array);\n\n        return new self($metadata);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n        $array[\"email\"] = new \\stdClass();\n\n        return $array;\n    }\n}\n"
  },
  {
    "path": "src/Databases/Properties/Files.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Properties;\n\n/**\n * @psalm-type FilesJson = array{\n *      id: string,\n *      name: string,\n *      type: \"files\",\n *      file: array<empty, empty>,\n *      description?: string,\n * }\n *\n * @psalm-immutable\n */\nclass Files implements PropertyInterface\n{\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n    ) {\n    }\n\n    public static function create(string $propertyName = \"Files\"): self\n    {\n        $metadata = PropertyMetadata::create(\"\", $propertyName, PropertyType::Files);\n\n        return new self($metadata);\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var FilesJson $array */\n        $metadata = PropertyMetadata::fromArray($array);\n\n        return new self($metadata);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n        $array[\"files\"] = new \\stdClass();\n\n        return $array;\n    }\n}\n"
  },
  {
    "path": "src/Databases/Properties/Formula.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Properties;\n\n/**\n * @psalm-type FormulaJson = array{\n *      id: string,\n *      name: string,\n *      type: \"formula\",\n *      formula: array{ expression: string },\n *      description?: string,\n * }\n *\n * @psalm-immutable\n */\nclass Formula implements PropertyInterface\n{\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n        public readonly string $expression\n    ) {\n    }\n\n    public static function create(string $propertyName = \"Formula\", string $expression = \"\"): self\n    {\n        $property = PropertyMetadata::create(\"\", $propertyName, PropertyType::Formula);\n\n        return new self($property, $expression);\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function changeExpression(string $expression): self\n    {\n        return new self($this->metadata, $expression);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var FormulaJson $array */\n        $property = PropertyMetadata::fromArray($array);\n        $expression = $array[\"formula\"][\"expression\"];\n\n        return new self($property, $expression);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n        $array[\"formula\"] = [\n            \"expression\" => $this->expression,\n        ];\n\n        return $array;\n    }\n}\n"
  },
  {
    "path": "src/Databases/Properties/LastEditedBy.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Properties;\n\n/**\n * @psalm-type LastEditedByJson = array{\n *      id: string,\n *      name: string,\n *      type: \"last_edited_by\",\n *      last_edited_by: array<empty, empty>,\n *      description?: string,\n * }\n *\n * @psalm-immutable\n */\nclass LastEditedBy implements PropertyInterface\n{\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n    ) {\n    }\n\n    public static function create(string $propertyName = \"LastEditedBy\"): self\n    {\n        $metadata = PropertyMetadata::create(\"\", $propertyName, PropertyType::LastEditedBy);\n\n        return new self($metadata);\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var LastEditedByJson $array */\n        $metadata = PropertyMetadata::fromArray($array);\n\n        return new self($metadata);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n        $array[\"last_edited_by\"] = new \\stdClass();\n\n        return $array;\n    }\n}\n"
  },
  {
    "path": "src/Databases/Properties/LastEditedTime.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Properties;\n\n/**\n * @psalm-type LastEditedTimeJson = array{\n *      id: string,\n *      name: string,\n *      type: \"last_edited_time\",\n *      last_edited_time: array<empty, empty>,\n *      description?: string,\n * }\n *\n * @psalm-immutable\n */\nclass LastEditedTime implements PropertyInterface\n{\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n    ) {\n    }\n\n    public static function create(string $propertyName = \"LastEditedTime\"): self\n    {\n        $metadata = PropertyMetadata::create(\"\", $propertyName, PropertyType::LastEditedTime);\n\n        return new self($metadata);\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var LastEditedTimeJson $array */\n        $metadata = PropertyMetadata::fromArray($array);\n\n        return new self($metadata);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n        $array[\"last_edited_time\"] = new \\stdClass();\n\n        return $array;\n    }\n}\n"
  },
  {
    "path": "src/Databases/Properties/MultiSelect.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Properties;\n\n/**\n * @psalm-import-type SelectOptionJson from SelectOption\n *\n * @psalm-type MultiSelectJson = array{\n *      id: string,\n *      name: string,\n *      type: \"multi_select\",\n *      multi_select: array{\n *          options: list<SelectOptionJson>\n *      },\n *      description?: string,\n * }\n *\n * @psalm-immutable\n */\nclass MultiSelect implements PropertyInterface\n{\n    /** @param SelectOption[] $options */\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n        public readonly array $options,\n    ) {\n    }\n\n    /** @param SelectOption[] $options */\n    public static function create(string $propertyName = \"Multi Select\", array $options = []): self\n    {\n        $property = PropertyMetadata::create(\"\", $propertyName, PropertyType::MultiSelect);\n\n        return new self($property, $options);\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function changeOptions(SelectOption ...$options): self\n    {\n        return new self($this->metadata, $options);\n    }\n\n    public function addOption(SelectOption $option): self\n    {\n        $options = $this->options;\n        $options[] = $option;\n\n        return new self($this->metadata, $options);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var MultiSelectJson $array */\n        $property = PropertyMetadata::fromArray($array);\n        $options = array_map(\n            fn($option) => SelectOption::fromArray($option),\n            $array[\"multi_select\"][\"options\"],\n        );\n\n        return new self($property, $options);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n        $array[\"multi_select\"] = [\n            \"options\" => array_map(fn(SelectOption $o) => $o->toArray(), $this->options),\n        ];\n\n        return $array;\n    }\n}\n"
  },
  {
    "path": "src/Databases/Properties/Number.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Properties;\n\n/**\n * @psalm-type NumberJson = array{\n *      id: string,\n *      name: string,\n *      type: \"number\",\n *      number: array{ format: string },\n *      description?: string,\n * }\n *\n * @psalm-immutable\n */\nclass Number implements PropertyInterface\n{\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n        public readonly NumberFormat $format,\n    ) {\n    }\n\n    public static function create(\n        string $propertyName = \"Number\",\n        NumberFormat $format = NumberFormat::Number,\n    ): self {\n        $property = PropertyMetadata::create(\"\", $propertyName, PropertyType::Number);\n\n        return new self($property, $format);\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function changeFormat(NumberFormat $format): self\n    {\n        return new self($this->metadata, $format);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var NumberJson $array */\n        $property = PropertyMetadata::fromArray($array);\n        $format = NumberFormat::from($array[\"number\"][\"format\"]);\n\n        return new self($property, $format);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n        $array[\"number\"] = [ \"format\" => $this->format->value ];\n\n        return $array;\n    }\n}\n"
  },
  {
    "path": "src/Databases/Properties/NumberFormat.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Properties;\n\nenum NumberFormat: string\n{\n    case Number = \"number\";\n    case NumberChangeCommas = \"number_change_commas\";\n    case NumberWithCommas = \"number_with_commas\";\n    case Percent = \"percent\";\n    case Dollar = \"dollar\";\n    case CanadianDollar = \"canadian_dollar\";\n    case Euro = \"euro\";\n    case Pound = \"pound\";\n    case Yen = \"yen\";\n    case Ruble = \"ruble\";\n    case Rupee = \"rupee\";\n    case Won = \"won\";\n    case Yuan = \"yuan\";\n    case Real = \"real\";\n    case Lira = \"lira\";\n    case Rupiah = \"rupiah\";\n    case Franc = \"franc\";\n    case HongKongDollar = \"hong_kong_dollar\";\n    case NewZealandDollar = \"new_zealand_dollar\";\n    case Krona = \"krona\";\n    case NorwegianKrone = \"norwegian_krone\";\n    case MexicanPeso = \"mexican_peso\";\n    case Rand = \"rand\";\n    case NewTaiwanDollar = \"new_taiwan_dollar\";\n    case DanishKrone = \"danish_krone\";\n    case Zloty = \"zloty\";\n    case Baht = \"baht\";\n    case Forint = \"forint\";\n    case Koruna = \"koruna\";\n    case Shekel = \"shekel\";\n    case ChileanPeso = \"chilean_peso\";\n    case PhilippinePeso = \"philippine_peso\";\n    case Dirham = \"dirham\";\n    case ColombianPeso = \"colombian_peso\";\n    case Riyal = \"riyal\";\n    case Ringgit = \"ringgit\";\n    case Leu = \"leu\";\n}\n"
  },
  {
    "path": "src/Databases/Properties/People.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Properties;\n\n/**\n * @psalm-type PeopleJson = array{\n *      id: string,\n *      name: string,\n *      type: \"people\",\n *      people: array<empty, empty>,\n *      description?: string,\n * }\n *\n * @psalm-immutable\n */\nclass People implements PropertyInterface\n{\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n    ) {\n    }\n\n    public static function create(string $propertyName = \"People\"): self\n    {\n        $metadata = PropertyMetadata::create(\"\", $propertyName, PropertyType::People);\n\n        return new self($metadata);\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var PeopleJson $array */\n        $metadata = PropertyMetadata::fromArray($array);\n\n        return new self($metadata);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n        $array[\"people\"] = new \\stdClass();\n\n        return $array;\n    }\n}\n"
  },
  {
    "path": "src/Databases/Properties/PhoneNumber.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Properties;\n\n/**\n * @psalm-type PhoneNumberJson = array{\n *      id: string,\n *      name: string,\n *      type: \"phone_number\",\n *      phone_number: array<empty, empty>,\n *      description?: string,\n * }\n *\n * @psalm-immutable\n */\nclass PhoneNumber implements PropertyInterface\n{\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n    ) {\n    }\n\n    public static function create(string $propertyName = \"PhoneNumber\"): self\n    {\n        $metadata = PropertyMetadata::create(\"\", $propertyName, PropertyType::PhoneNumber);\n\n        return new self($metadata);\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var PhoneNumberJson $array */\n        $metadata = PropertyMetadata::fromArray($array);\n\n        return new self($metadata);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n        $array[\"phone_number\"] = new \\stdClass();\n\n        return $array;\n    }\n}\n"
  },
  {
    "path": "src/Databases/Properties/PropertyCollection.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Properties;\n\n/** @psalm-immutable */\nfinal class PropertyCollection\n{\n    /** @param array<string, PropertyInterface> $properties */\n    private function __construct(\n        private readonly array $properties\n    ) {\n    }\n\n    /**\n     * @psalm-mutation-free\n     */\n    public static function create(PropertyInterface ...$properties): self\n    {\n        $props = [];\n        foreach ($properties as $property) {\n            $props[$property->metadata()->name] = $property;\n        }\n\n        return new self($props);\n    }\n\n    public function add(PropertyInterface $property): self\n    {\n        $properties = $this->properties;\n        $properties[$property->metadata()->name] = $property;\n\n        return new self($properties);\n    }\n\n    public function change(PropertyInterface $property): self\n    {\n        return $this->add($property);\n    }\n\n    public function remove(string $propertyName): self\n    {\n        $properties = $this->properties;\n        unset($properties[$propertyName]);\n\n        return new self($properties);\n    }\n\n    public function get(string $propertyName): PropertyInterface\n    {\n        if (array_key_exists($propertyName, $this->properties)) {\n            return $this->properties[$propertyName];\n        }\n\n        throw new \\Exception(\"Property '{$propertyName}' not found\");\n    }\n\n    public function getById(string $propertyId): PropertyInterface\n    {\n        foreach ($this->properties as $property) {\n            if ($property->metadata()->id === $propertyId) {\n                return $property;\n            }\n        }\n\n        throw new \\Exception(\"Property '{$propertyId}' not found.\");\n    }\n\n    /** @return array<string, PropertyInterface> */\n    public function getAll(): array\n    {\n        return $this->properties;\n    }\n\n    public function getCheckbox(string $propertyName): Checkbox\n    {\n        return $this->getTyped($propertyName, Checkbox::class);\n    }\n\n    public function getCheckboxById(string $propertyId): Checkbox\n    {\n        return $this->getTypedById($propertyId, Checkbox::class);\n    }\n\n    public function getCreatedBy(string $propertyName): CreatedBy\n    {\n        return $this->getTyped($propertyName, CreatedBy::class);\n    }\n\n    public function getCreatedByById(string $propertyId): CreatedBy\n    {\n        return $this->getTypedById($propertyId, CreatedBy::class);\n    }\n\n    public function getCreatedTime(string $propertyName): CreatedTime\n    {\n        return $this->getTyped($propertyName, CreatedTime::class);\n    }\n\n    public function getCreatedTimeById(string $propertyId): CreatedTime\n    {\n        return $this->getTypedById($propertyId, CreatedTime::class);\n    }\n\n    public function getDate(string $propertyName): Date\n    {\n        return $this->getTyped($propertyName, Date::class);\n    }\n\n    public function getDateById(string $propertyId): Date\n    {\n        return $this->getTypedById($propertyId, Date::class);\n    }\n\n    public function getEmail(string $propertyName): Email\n    {\n        return $this->getTyped($propertyName, Email::class);\n    }\n\n    public function getEmailById(string $propertyId): Email\n    {\n        return $this->getTypedById($propertyId, Email::class);\n    }\n\n    public function getFiles(string $propertyName): Files\n    {\n        return $this->getTyped($propertyName, Files::class);\n    }\n\n    public function getFilesById(string $propertyId): Files\n    {\n        return $this->getTypedById($propertyId, Files::class);\n    }\n\n    public function getFormula(string $propertyName): Formula\n    {\n        return $this->getTyped($propertyName, Formula::class);\n    }\n\n    public function getFormulaById(string $propertyId): Formula\n    {\n        return $this->getTypedById($propertyId, Formula::class);\n    }\n\n    public function getLastEditedBy(string $propertyName): LastEditedBy\n    {\n        return $this->getTyped($propertyName, LastEditedBy::class);\n    }\n\n    public function getLastEditedByById(string $propertyId): LastEditedBy\n    {\n        return $this->getTypedById($propertyId, LastEditedBy::class);\n    }\n\n    public function getLastEditedTime(string $propertyName): LastEditedTime\n    {\n        return $this->getTyped($propertyName, LastEditedTime::class);\n    }\n\n    public function getLastEditedTimeById(string $propertyId): LastEditedTime\n    {\n        return $this->getTypedById($propertyId, LastEditedTime::class);\n    }\n\n    public function getMultiSelect(string $propertyName): MultiSelect\n    {\n        return $this->getTyped($propertyName, MultiSelect::class);\n    }\n\n    public function getMultiSelectById(string $propertyId): MultiSelect\n    {\n        return $this->getTypedById($propertyId, MultiSelect::class);\n    }\n\n    public function getNumber(string $propertyName): Number\n    {\n        return $this->getTyped($propertyName, Number::class);\n    }\n\n    public function getNumberById(string $propertyId): Number\n    {\n        return $this->getTypedById($propertyId, Number::class);\n    }\n\n    public function getPeople(string $propertyName): People\n    {\n        return $this->getTyped($propertyName, People::class);\n    }\n\n    public function getPeopleById(string $propertyId): People\n    {\n        return $this->getTypedById($propertyId, People::class);\n    }\n\n    public function getPhoneNumber(string $propertyName): PhoneNumber\n    {\n        return $this->getTyped($propertyName, PhoneNumber::class);\n    }\n\n    public function getPhoneNumberById(string $propertyId): PhoneNumber\n    {\n        return $this->getTypedById($propertyId, PhoneNumber::class);\n    }\n\n    public function getRelation(string $propertyName): Relation\n    {\n        return $this->getTyped($propertyName, Relation::class);\n    }\n\n    public function getRelationById(string $propertyId): Relation\n    {\n        return $this->getTypedById($propertyId, Relation::class);\n    }\n\n    public function getRichText(string $propertyName): RichTextProperty\n    {\n        return $this->getTyped($propertyName, RichTextProperty::class);\n    }\n\n    public function getRichTextById(string $propertyId): RichTextProperty\n    {\n        return $this->getTypedById($propertyId, RichTextProperty::class);\n    }\n\n    public function getSelect(string $propertyName): Select\n    {\n        return $this->getTyped($propertyName, Select::class);\n    }\n\n    public function getSelectById(string $propertyId): Select\n    {\n        return $this->getTypedById($propertyId, Select::class);\n    }\n\n    public function getStatus(string $propertyName): Status\n    {\n        return $this->getTyped($propertyName, Status::class);\n    }\n\n    public function getStatusById(string $propertyId): Status\n    {\n        return $this->getTypedById($propertyId, Status::class);\n    }\n\n    public function getTitle(string $propertyName): Title\n    {\n        return $this->getTyped($propertyName, Title::class);\n    }\n\n    public function getTitleById(string $propertyId): Title\n    {\n        return $this->getTypedById($propertyId, Title::class);\n    }\n\n    public function getUniqueId(string $propertyName): UniqueId\n    {\n        return $this->getTyped($propertyName, UniqueId::class);\n    }\n\n    public function getUniqueIdById(string $propertyName): UniqueId\n    {\n        return $this->getTypedById($propertyName, UniqueId::class);\n    }\n\n    public function getUrl(string $propertyName): Url\n    {\n        return $this->getTyped($propertyName, Url::class);\n    }\n\n    public function getUrlById(string $propertyName): Url\n    {\n        return $this->getTypedById($propertyName, Url::class);\n    }\n\n    /**\n     * @template T of PropertyInterface\n     * @psalm-param class-string<T> $propertyType\n     *\n     * @psalm-return T\n     */\n    private function getTyped(string $propertyName, string $propertyType): PropertyInterface\n    {\n        $property = $this->get($propertyName);\n\n        if ($property::class !== $propertyType) {\n            throw new \\TypeError(\"Property '{$propertyName}' is not of type {$propertyType}.\");\n        }\n\n        /** @psalm-var T $property */\n        return $property;\n    }\n\n    /**\n     * @template T of PropertyInterface\n     * @psalm-param class-string<T> $propertyType\n     *\n     * @psalm-return T\n     */\n    private function getTypedById(string $propertyId, string $propertyType): PropertyInterface\n    {\n        $property = $this->getById($propertyId);\n\n        if ($property::class !== $propertyType) {\n            throw new \\TypeError(\"Property with ID '{$propertyId}' is not of type {$propertyType}.\");\n        }\n\n        /** @psalm-var T $property */\n        return $property;\n    }\n}\n"
  },
  {
    "path": "src/Databases/Properties/PropertyFactory.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Properties;\n\nclass PropertyFactory\n{\n    /**\n     * @param array{ type: string, ... } $array\n     */\n    public static function fromArray(array $array): PropertyInterface\n    {\n        $type = PropertyType::tryFrom($array[\"type\"]);\n\n        return match ($type) {\n            PropertyType::Checkbox       => Checkbox::fromArray($array),\n            PropertyType::CreatedBy      => CreatedBy::fromArray($array),\n            PropertyType::CreatedTime    => CreatedTime::fromArray($array),\n            PropertyType::Date           => Date::fromArray($array),\n            PropertyType::Email          => Email::fromArray($array),\n            PropertyType::Files          => Files::fromArray($array),\n            PropertyType::Formula        => Formula::fromArray($array),\n            PropertyType::LastEditedBy   => LastEditedBy::fromArray($array),\n            PropertyType::LastEditedTime => LastEditedTime::fromArray($array),\n            PropertyType::MultiSelect    => MultiSelect::fromArray($array),\n            PropertyType::Number         => Number::fromArray($array),\n            PropertyType::People         => People::fromArray($array),\n            PropertyType::PhoneNumber    => PhoneNumber::fromArray($array),\n            PropertyType::Relation       => Relation::fromArray($array),\n            PropertyType::RichText       => RichTextProperty::fromArray($array),\n            PropertyType::Select         => Select::fromArray($array),\n            PropertyType::Status         => Status::fromArray($array),\n            PropertyType::Title          => Title::fromArray($array),\n            PropertyType::UniqueId       => UniqueId::fromArray($array),\n            PropertyType::Url            => Url::fromArray($array),\n            default                      => Unknown::fromArray($array),\n        };\n    }\n}\n"
  },
  {
    "path": "src/Databases/Properties/PropertyInterface.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Properties;\n\n/** @psalm-immutable */\ninterface PropertyInterface\n{\n    /** @internal */\n    public static function fromArray(array $array): self;\n    /** @internal */\n    public function toArray(): array;\n\n    public function metadata(): PropertyMetadata;\n}\n"
  },
  {
    "path": "src/Databases/Properties/PropertyMetadata.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Properties;\n\n/**\n * @psalm-type PropertyMetadataJson = array{ id: string, name: string, type: string, description?: string, ... }\n *\n * @psalm-immutable\n */\nclass PropertyMetadata\n{\n    private function __construct(\n        public readonly string $id,\n        public readonly string $name,\n        public readonly PropertyType $type,\n        private readonly string|null $unknownType = null,\n        public readonly string|null $description = null,\n    ) {\n    }\n\n    public static function create(string $id, string $name, PropertyType $type, ?string $description = null): self\n    {\n        return new self($id, $name, $type, description: $description);\n    }\n\n    /**\n     * @param PropertyMetadataJson $array\n     *\n     * @internal\n     */\n    public static function fromArray(array $array): self\n    {\n        $type = PropertyType::tryFrom($array[\"type\"]) ?? PropertyType::Unknown;\n\n        return new self(\n            $array[\"id\"],\n            $array[\"name\"],\n            $type,\n            $type === PropertyType::Unknown ? $array[\"type\"] : null,\n            $array[\"description\"] ?? null,\n        );\n    }\n\n    public function toArray(): array\n    {\n        $type = $this->type !== PropertyType::Unknown ? $this->type->value : $this->unknownType;\n\n        return [\n            \"id\"   => $this->id,\n            \"name\" => $this->name,\n            \"type\" => $type,\n            ...($this->description !== null ? [\n                \"description\" => $this->description,\n            ] : []),\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Databases/Properties/PropertyType.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Properties;\n\nenum PropertyType: string\n{\n    case Checkbox = \"checkbox\";\n    case CreatedBy = \"created_by\";\n    case CreatedTime = \"created_time\";\n    case Date = \"date\";\n    case Email = \"email\";\n    case Files = \"files\";\n    case Formula = \"formula\";\n    case LastEditedBy = \"last_edited_by\";\n    case LastEditedTime = \"last_edited_time\";\n    case MultiSelect = \"multi_select\";\n    case Number = \"number\";\n    case People = \"people\";\n    case PhoneNumber = \"phone_number\";\n    case Relation = \"relation\";\n    case RichText = \"rich_text\";\n    case Rollup = \"rollup\";\n    case Select = \"select\";\n    case Status = \"status\";\n    case Title = \"title\";\n    case Url = \"url\";\n    case UniqueId = \"unique_id\";\n    case Unknown = \"unknown\";\n}\n"
  },
  {
    "path": "src/Databases/Properties/Relation.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Properties;\n\nuse Notion\\Exceptions\\RelationException;\n\n/**\n * @psalm-type RelationJson = array{\n *      id: string,\n *      name: string,\n *      type: \"relation\",\n *      relation: array{\n *          database_id: string,\n *          type: string,\n *          single_property?: array<empty, empty>,\n *          dual_property?: array{\n *              synced_property_name: string,\n *              synced_property_id: string\n *          }\n *      },\n *      description?: string,\n * }\n *\n * @psalm-immutable\n */\nclass Relation implements PropertyInterface\n{\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n        public readonly string $databaseId,\n        public readonly RelationType $type,\n        public readonly string|null $syncedPropertyName,\n        public readonly string|null $syncedPropertyId,\n    ) {\n        if ($type === RelationType::DualProperty && $syncedPropertyName === null) {\n            throw RelationException::emptySyncedPropertyName();\n        }\n\n        if ($type === RelationType::DualProperty && $syncedPropertyId === null) {\n            throw RelationException::emptySyncedPropertyId();\n        }\n    }\n\n    public static function createUnidirectional(string $propertyName, string $databaseId): self\n    {\n        $metadata = PropertyMetadata::create(\"\", $propertyName, PropertyType::Relation);\n        $type = RelationType::SingleProperty;\n\n        return new self($metadata, $databaseId, $type, null, null);\n    }\n\n    public static function createBidirectional(\n        string $propertyName,\n        string $databaseId,\n        string $syncedPropertyName,\n        string $syncedPropertyId,\n    ): self {\n        $metadata = PropertyMetadata::create(\"\", $propertyName, PropertyType::Relation);\n        $type = RelationType::DualProperty;\n\n        return new self($metadata, $databaseId, $type, $syncedPropertyName, $syncedPropertyId);\n    }\n\n    public function changeToUnidirectional(): self\n    {\n        $newType = RelationType::SingleProperty;\n\n        return new self($this->metadata(), $this->databaseId, $newType, null, null);\n    }\n\n    public function changeToBidirectional(string $syncedPropertyName, string $syncedPropertyId): self\n    {\n        $newType = RelationType::DualProperty;\n\n        return new self(\n            $this->metadata(),\n            $this->databaseId,\n            $newType,\n            $syncedPropertyName,\n            $syncedPropertyId\n        );\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var RelationJson $array */\n        $metadata = PropertyMetadata::fromArray($array);\n\n        $databaseId = $array[\"relation\"][\"database_id\"];\n        $type = RelationType::from($array[\"relation\"][\"type\"]);\n\n        $syncedPropertyName = null;\n        $syncedPropertyId = null;\n        if ($type === RelationType::DualProperty) {\n            $syncedPropertyName = $array[\"relation\"][\"dual_property\"][\"synced_property_name\"] ?? null;\n            $syncedPropertyId = $array[\"relation\"][\"dual_property\"][\"synced_property_id\"] ?? null;\n        }\n\n        return new self($metadata, $databaseId, $type, $syncedPropertyName, $syncedPropertyId);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n        $relation = [\n            \"database_id\" => $this->databaseId,\n            \"type\" => $this->type->value,\n        ];\n\n        if ($this->isUniderectional()) {\n            $relation[\"single_property\"] = new \\stdClass();\n        }\n\n        if ($this->isBiderectional()) {\n            $relation[\"dual_property\"] = [\n                \"synced_property_name\" => $this->syncedPropertyName,\n                \"synced_property_id\"   => $this->syncedPropertyId,\n            ];\n        }\n\n        $array[\"relation\"] = $relation;\n\n        return $array;\n    }\n\n    public function isUniderectional(): bool\n    {\n        return $this->type === RelationType::SingleProperty;\n    }\n\n    public function isBiderectional(): bool\n    {\n        return $this->type === RelationType::DualProperty;\n    }\n}\n"
  },
  {
    "path": "src/Databases/Properties/RelationType.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Properties;\n\nenum RelationType: string\n{\n    case SingleProperty = \"single_property\";\n    case DualProperty = \"dual_property\";\n}\n"
  },
  {
    "path": "src/Databases/Properties/RichTextProperty.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Properties;\n\n/**\n * @psalm-type RichTextJson = array{\n *      id: string,\n *      name: string,\n *      type: \"rich_text\",\n *      rich_text: array<empty, empty>,\n *      description?: string,\n * }\n *\n * @psalm-immutable\n */\nclass RichTextProperty implements PropertyInterface\n{\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n    ) {\n    }\n\n    public static function create(string $propertyName = \"Text\"): self\n    {\n        $metadata = PropertyMetadata::create(\"\", $propertyName, PropertyType::RichText);\n\n        return new self($metadata);\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var RichTextJson $array */\n        $metadata = PropertyMetadata::fromArray($array);\n\n        return new self($metadata);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n        $array[\"rich_text\"] = new \\stdClass();\n\n        return $array;\n    }\n}\n"
  },
  {
    "path": "src/Databases/Properties/Select.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Properties;\n\n/**\n * @psalm-import-type SelectOptionJson from SelectOption\n *\n * @psalm-type SelectJson = array{\n *      id: string,\n *      name: string,\n *      type: \"select\",\n *      select: array{\n *          options: list<SelectOptionJson>\n *      },\n *      description?: string,\n * }\n *\n * @psalm-immutable\n */\nclass Select implements PropertyInterface\n{\n    /** @param SelectOption[] $options */\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n        public readonly array $options\n    ) {\n    }\n\n    /** @param SelectOption[] $options */\n    public static function create(string $propertyName = \"Select\", array $options = []): self\n    {\n        $property = PropertyMetadata::create(\"\", $propertyName, PropertyType::Select);\n\n        return new self($property, $options);\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function changeOptions(SelectOption ...$options): self\n    {\n        return new self($this->metadata, $options);\n    }\n\n    public function addOption(SelectOption $option): self\n    {\n        $options = $this->options;\n        $options[] = $option;\n\n        return new self($this->metadata, $options);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var SelectJson $array */\n        $property = PropertyMetadata::fromArray($array);\n        $options = array_map(\n            function (array $option): SelectOption {\n                return SelectOption::fromArray($option);\n            },\n            $array[\"select\"][\"options\"],\n        );\n\n        return new self($property, $options);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n        $array[\"select\"] = [\n            \"options\" => array_map(fn(SelectOption $o) => $o->toArray(), $this->options),\n        ];\n\n        return $array;\n    }\n}\n"
  },
  {
    "path": "src/Databases/Properties/SelectOption.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Properties;\n\nuse Notion\\Common\\Color;\n\n/**\n * @psalm-type SelectOptionJson = array{\n *      id?: string,\n *      name?: string,\n *      color?: string,\n *      description?: string,\n * }\n *\n * @psalm-immutable\n */\nclass SelectOption\n{\n    private function __construct(\n        public readonly string|null $id,\n        public readonly string|null $name,\n        public readonly Color|null $color,\n    ) {\n    }\n\n    public static function fromId(string $id): self\n    {\n        return new self($id, null, null);\n    }\n\n    public static function fromName(string $name): self\n    {\n        return new self(null, $name, null);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @var SelectOptionJson $array */\n\n        $id = $array[\"id\"] ?? null;\n        $name = $array[\"name\"] ?? null;\n        $color = Color::tryFrom($array[\"color\"] ?? \"\");\n\n        return new self($id, $name, $color);\n    }\n\n    public function toArray(): array\n    {\n        $option = [];\n\n        if ($this->name !== null) {\n            $option[\"name\"] = $this->name;\n        }\n        if ($this->id !== null) {\n            $option[\"id\"] = $this->id;\n        }\n        if ($this->color !== null) {\n            $option[\"color\"] = $this->color->value;\n        }\n\n        return $option;\n    }\n\n    public function changeName(string $name): self\n    {\n        return new self($this->id, $name, $this->color);\n    }\n\n    public function changeColor(Color $color): self\n    {\n        return new self($this->id, $this->name, $color);\n    }\n}\n"
  },
  {
    "path": "src/Databases/Properties/Status.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Properties;\n\n/**\n * @psalm-import-type StatusGroupJson from StatusGroup\n * @psalm-import-type StatusOptionJson from StatusOption\n *\n * @psalm-type StatusJson = array{\n *      id: string,\n *      name: string,\n *      type: \"status\",\n *      status: array{\n *          options: StatusOptionJson[],\n *          groups: StatusGroupJson[]\n *      },\n *      description?: string,\n * }\n *\n * @psalm-immutable\n */\nclass Status implements PropertyInterface\n{\n    /**\n     * @param StatusOption[] $options\n     * @param StatusGroup[] $groups\n     */\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n        public readonly array $options,\n        public readonly array $groups\n    ) {\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var StatusJson $array */\n        $property = PropertyMetadata::fromArray($array);\n        $options = array_map(\n            function (array $option): StatusOption {\n                return StatusOption::fromArray($option);\n            },\n            $array[\"status\"][\"options\"],\n        );\n        $groups = array_map(\n            function (array $group): StatusGroup {\n                return StatusGroup::fromArray($group);\n            },\n            $array[\"status\"][\"groups\"],\n        );\n\n        return new self($property, $options, $groups);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n        $array[\"status\"] = [\n            \"options\" => array_map(fn(StatusOption $o) => $o->toArray(), $this->options),\n            \"groups\" => array_map(fn(StatusGroup $g) => $g->toArray(), $this->groups),\n        ];\n\n        return $array;\n    }\n}\n"
  },
  {
    "path": "src/Databases/Properties/StatusGroup.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Properties;\n\nuse Notion\\Common\\Color;\n\n/**\n * @psalm-type StatusGroupJson = array{\n *      id?: string,\n *      name?: string,\n *      color: string,\n *      option_ids: string[],\n *      description?: string,\n * }\n *\n * @psalm-immutable\n */\nclass StatusGroup\n{\n    /** @param string[] $optionIds */\n    private function __construct(\n        public readonly string $id,\n        public readonly string $name,\n        public readonly Color $color,\n        public array $optionIds,\n    ) {\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @var StatusGroupJson $array */\n        $id = $array[\"id\"] ?? \"\";\n        $name = $array[\"name\"] ?? \"\";\n        $color = Color::tryFrom($array[\"color\"]) ?? Color::Default;\n        $optionIds = $array[\"option_ids\"];\n\n        return new self($id, $name, $color, $optionIds);\n    }\n\n    public function toArray(): array\n    {\n        return [\n            \"id\"         => $this->id,\n            \"name\"       => $this->name,\n            \"color\"      => $this->color->value,\n            \"option_ids\" => $this->optionIds,\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Databases/Properties/StatusOption.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Properties;\n\nuse Notion\\Common\\Color;\n\n/**\n * @psalm-type StatusOptionJson = array{\n *      id?: string,\n *      name?: string,\n *      color?: string,\n *      description?: string,\n * }\n *\n * @psalm-immutable\n */\nclass StatusOption\n{\n    private function __construct(\n        public readonly string|null $id,\n        public readonly string|null $name,\n        public readonly Color|null $color,\n    ) {\n    }\n\n    public static function fromId(string $id): self\n    {\n        return new self($id, null, null);\n    }\n\n    public static function fromName(string $name): self\n    {\n        return new self(null, $name, null);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @var StatusOptionJson $array */\n\n        $id = $array[\"id\"] ?? null;\n        $name = $array[\"name\"] ?? null;\n        $color = Color::tryFrom($array[\"color\"] ?? \"\");\n\n        return new self($id, $name, $color);\n    }\n\n    public function changeColor(Color $color): self\n    {\n        return new self($this->id, $this->name, $color);\n    }\n\n    public function toArray(): array\n    {\n        $option = [];\n\n        if ($this->name !== null) {\n            $option[\"name\"] = $this->name;\n        }\n        if ($this->id !== null) {\n            $option[\"id\"] = $this->id;\n        }\n        if ($this->color !== null) {\n            $option[\"color\"] = $this->color->value;\n        }\n\n        return $option;\n    }\n\n    public function changeName(string $name): self\n    {\n        return new self($this->id, $name, $this->color);\n    }\n}\n"
  },
  {
    "path": "src/Databases/Properties/Title.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Properties;\n\n/**\n * @psalm-type TitleJson = array{\n *      id: \"title\",\n *      name: string,\n *      type: \"title\",\n *      title: array<empty, empty>,\n *      description?: string,\n * }\n *\n * @psalm-immutable\n */\nclass Title implements PropertyInterface\n{\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n    ) {\n    }\n\n    public static function create(string $propertyName = \"Title\"): self\n    {\n        $metadata = PropertyMetadata::create(\"\", $propertyName, PropertyType::Title);\n\n        return new self($metadata);\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var TitleJson $array */\n        $metadata = PropertyMetadata::fromArray($array);\n\n        return new self($metadata);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n        $array[\"title\"] = new \\stdClass();\n\n        return $array;\n    }\n}\n"
  },
  {
    "path": "src/Databases/Properties/UniqueId.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Properties;\n\n/**\n * @psalm-type UniqueIdJson = array{\n *      id: string,\n *      name: string,\n *      type: \"uniqueId\",\n *      unique_id: \\stdClass,\n *      description?: string,\n * }\n *\n * @psalm-immutable\n */\nclass UniqueId implements PropertyInterface\n{\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n    ) {\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var UniqueIdJson $array */\n        $metadata = PropertyMetadata::fromArray($array);\n\n        return new self($metadata);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n        $array[\"unique_id\"] = new \\stdClass();\n\n        return $array;\n    }\n}\n"
  },
  {
    "path": "src/Databases/Properties/Unknown.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Properties;\n\n/**\n * @psalm-type PropertyJson = array{\n *      id: string,\n *      name: string,\n *      type: string,\n *      description?: string,\n * }\n *\n * @psalm-immutable\n */\nclass Unknown implements PropertyInterface\n{\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n        private readonly array $data,\n    ) {\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var PropertyJson $array */\n        $metdata = PropertyMetadata::fromArray($array);\n\n        return new self($metdata, $array);\n    }\n\n    public function toArray(): array\n    {\n        return $this->data;\n    }\n}\n"
  },
  {
    "path": "src/Databases/Properties/Url.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Properties;\n\n/**\n * @psalm-type UrlJson = array{\n *      id: string,\n *      name: string,\n *      type: \"url\",\n *      url: array<empty, empty>,\n *      description?: string,\n * }\n *\n * @psalm-immutable\n */\nclass Url implements PropertyInterface\n{\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n    ) {\n    }\n\n    public static function create(string $propertyName = \"Url\"): self\n    {\n        $metadata = PropertyMetadata::create(\"\", $propertyName, PropertyType::Url);\n\n        return new self($metadata);\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var UrlJson $array */\n        $metadata = PropertyMetadata::fromArray($array);\n\n        return new self($metadata);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n        $array[\"url\"] = new \\stdClass();\n\n        return $array;\n    }\n}\n"
  },
  {
    "path": "src/Databases/Query/CheckboxFilter.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Query;\n\n/** @psalm-immutable */\nclass CheckboxFilter implements Filter, Condition\n{\n    private static array $validOperators = [\n        Operator::Equals,\n        Operator::DoesNotEqual,\n    ];\n\n    private function __construct(\n        private readonly string $propertyName,\n        private readonly Operator $operator,\n        private readonly bool $value,\n    ) {\n        if (!in_array($operator, self::$validOperators)) {\n            throw new \\Exception(\"Invalid operator\");\n        }\n    }\n\n    public static function property(string $propertyName): self\n    {\n        return new self($propertyName, Operator::Equals, true);\n    }\n\n    public function propertyType(): string\n    {\n        return \"property\";\n    }\n\n    public function propertyName(): string\n    {\n        return $this->propertyName;\n    }\n\n    public function operator(): Operator\n    {\n        return $this->operator;\n    }\n\n    public function value(): bool\n    {\n        return $this->value;\n    }\n\n    public function toArray(): array\n    {\n        return [\n            $this->propertyType() => $this->propertyName,\n            \"checkbox\" => [\n                $this->operator->value => $this->value\n            ],\n        ];\n    }\n\n    public function equals(bool $value): self\n    {\n        return new self($this->propertyName, Operator::Equals, $value);\n    }\n\n    public function doesNotEqual(bool $value): self\n    {\n        return new self($this->propertyName, Operator::DoesNotEqual, $value);\n    }\n}\n"
  },
  {
    "path": "src/Databases/Query/CompoundFilter.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Query;\n\n/** @psalm-immutable */\nclass CompoundFilter implements Filter\n{\n    private const TYPE_AND = \"and\";\n    private const TYPE_OR  = \"or\";\n\n    /** @var self::TYPE_* */\n    private string $type;\n    /** @var Filter[] */\n    private array $filters;\n\n    /** @param self::TYPE_* $type */\n    private function __construct(string $type, Filter ...$filters)\n    {\n        $this->type = $type;\n        $this->filters = $filters;\n    }\n\n    public static function and(Filter ...$filters): self\n    {\n        return new self(self::TYPE_AND, ...$filters);\n    }\n\n    public static function or(Filter ...$filters): self\n    {\n        return new self(self::TYPE_OR, ...$filters);\n    }\n\n    public function toArray(): array\n    {\n        return [\n            $this->type => array_map(fn (Filter $f) => $f->toArray(), $this->filters)\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Databases/Query/Condition.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Query;\n\ninterface Condition\n{\n    public function propertyType(): string;\n    public function propertyName(): string;\n    public function operator(): Operator;\n    public function value(): mixed;\n}\n"
  },
  {
    "path": "src/Databases/Query/DateFilter.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Query;\n\nuse stdClass;\n\n/** @psalm-immutable */\nclass DateFilter implements Filter, Condition\n{\n    private const TYPE_PROPERTY = \"property\";\n    private const TYPE_TIMESTAMP = \"timestamp\";\n\n    private static array $validOperators = [\n        Operator::Equals,\n        Operator::Before,\n        Operator::After,\n        Operator::OnOrBefore,\n        Operator::IsEmpty,\n        Operator::IsNotEmpty,\n        Operator::OnOrAfter,\n        Operator::PastWeek,\n        Operator::PastMonth,\n        Operator::PastYear,\n        Operator::NextWeek,\n        Operator::NextMonth,\n        Operator::NextYear,\n        Operator::ThisWeek,\n    ];\n\n    /**\n     * @psalm-param self::TYPE_* $propertyType\n     */\n    private function __construct(\n        private readonly string $propertyType,\n        private readonly string $propertyName,\n        private readonly Operator $operator,\n        private readonly string|bool|array|stdClass $value,\n    ) {\n        if (!in_array($operator, self::$validOperators)) {\n            throw new \\Exception(\"Invalid operator\");\n        }\n    }\n\n    public static function property(string $propertyName): self\n    {\n        return new self(\n            self::TYPE_PROPERTY,\n            $propertyName,\n            Operator::IsNotEmpty,\n            true\n        );\n    }\n\n    public static function createdTime(): self\n    {\n        return new self(\n            self::TYPE_TIMESTAMP,\n            \"created_time\",\n            Operator::IsNotEmpty,\n            true\n        );\n    }\n\n    public static function lastEditedTime(): self\n    {\n        return new self(\n            self::TYPE_TIMESTAMP,\n            \"last_edited_time\",\n            Operator::IsNotEmpty,\n            true\n        );\n    }\n\n    public function propertyType(): string\n    {\n        return $this->propertyType;\n    }\n\n    public function propertyName(): string\n    {\n        return $this->propertyName;\n    }\n\n    public function operator(): Operator\n    {\n        return $this->operator;\n    }\n\n    public function value(): string|bool|array|stdClass\n    {\n        return $this->value;\n    }\n\n    public function toArray(): array\n    {\n        $type = $this->propertyType === self::TYPE_PROPERTY ? \"date\" : $this->propertyName;\n\n        return [\n            $this->propertyType() => $this->propertyName,\n            $type => [\n                $this->operator->value => $this->value\n            ],\n        ];\n    }\n\n    public function equals(string $value): self\n    {\n        return new self($this->propertyType, $this->propertyName, Operator::Equals, $value);\n    }\n\n    public function before(string $value): self\n    {\n        return new self($this->propertyType, $this->propertyName, Operator::Before, $value);\n    }\n\n    public function after(string $value): self\n    {\n        return new self($this->propertyType, $this->propertyName, Operator::After, $value);\n    }\n\n    public function onOrBefore(string $value): self\n    {\n        return new self($this->propertyType, $this->propertyName, Operator::OnOrBefore, $value);\n    }\n\n    public function isEmpty(): self\n    {\n        return new self($this->propertyType, $this->propertyName, Operator::IsEmpty, true);\n    }\n\n    public function isNotEmpty(): self\n    {\n        return new self($this->propertyType, $this->propertyName, Operator::IsNotEmpty, true);\n    }\n\n    public function onOrAfter(string $value): self\n    {\n        return new self($this->propertyType, $this->propertyName, Operator::OnOrAfter, $value);\n    }\n\n    public function pastWeek(): self\n    {\n        return new self($this->propertyType, $this->propertyName, Operator::PastWeek, new stdClass());\n    }\n\n    public function pastMonth(): self\n    {\n        return new self($this->propertyType, $this->propertyName, Operator::PastMonth, new stdClass());\n    }\n\n    public function pastYear(): self\n    {\n        return new self($this->propertyType, $this->propertyName, Operator::PastYear, new stdClass());\n    }\n\n    public function nextWeek(): self\n    {\n        return new self($this->propertyType, $this->propertyName, Operator::NextWeek, new stdClass());\n    }\n\n    public function nextMonth(): self\n    {\n        return new self($this->propertyType, $this->propertyName, Operator::NextMonth, new stdClass());\n    }\n\n    public function nextYear(): self\n    {\n        return new self($this->propertyType, $this->propertyName, Operator::NextYear, new stdClass());\n    }\n\n    public function thisWeek(): self\n    {\n        return new self($this->propertyType, $this->propertyName, Operator::ThisWeek, new stdClass());\n    }\n}\n"
  },
  {
    "path": "src/Databases/Query/Filter.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Query;\n\n/** @psalm-immutable */\ninterface Filter\n{\n    public function toArray(): array;\n}\n"
  },
  {
    "path": "src/Databases/Query/MultiSelectFilter.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Query;\n\n/** @psalm-immutable */\nclass MultiSelectFilter implements Filter, Condition\n{\n    private static array $validOperators = [\n        Operator::Contains,\n        Operator::DoesNotContain,\n        Operator::IsEmpty,\n        Operator::IsNotEmpty,\n    ];\n\n    private function __construct(\n        private readonly string $propertyName,\n        private readonly Operator $operator,\n        private readonly string|bool $value,\n    ) {\n        if (!in_array($operator, self::$validOperators)) {\n            throw new \\Exception(\"Invalid operator\");\n        }\n    }\n\n    public static function property(string $propertyName): self\n    {\n        return new self(\n            $propertyName,\n            Operator::IsNotEmpty,\n            true\n        );\n    }\n\n    public function propertyType(): string\n    {\n        return \"property\";\n    }\n\n    public function propertyName(): string\n    {\n        return $this->propertyName;\n    }\n\n    public function operator(): Operator\n    {\n        return $this->operator;\n    }\n\n    public function value(): string|bool\n    {\n        return $this->value;\n    }\n\n    public function toArray(): array\n    {\n        return [\n            $this->propertyType() => $this->propertyName,\n            \"multi_select\" => [\n                $this->operator->value => $this->value\n            ],\n        ];\n    }\n\n    public function contains(string $value): self\n    {\n        return new self($this->propertyName, Operator::Contains, $value);\n    }\n\n    public function doesNotContain(string $value): self\n    {\n        return new self($this->propertyName, Operator::DoesNotContain, $value);\n    }\n\n    public function isEmpty(): self\n    {\n        return new self($this->propertyName, Operator::IsEmpty, true);\n    }\n\n    public function isNotEmpty(): self\n    {\n        return new self($this->propertyName, Operator::IsNotEmpty, true);\n    }\n}\n"
  },
  {
    "path": "src/Databases/Query/NumberFilter.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Query;\n\n/** @psalm-immutable */\nclass NumberFilter implements Filter, Condition\n{\n    private static array $validOperators = [\n        Operator::Equals,\n        Operator::DoesNotEqual,\n        Operator::GreaterThan,\n        Operator::LessThan,\n        Operator::GreaterThanOrEqualTo,\n        Operator::LessThanOrEqualTo,\n        Operator::IsEmpty,\n        Operator::IsNotEmpty,\n    ];\n\n\n    private function __construct(\n        private readonly string $propertyName,\n        private readonly Operator $operator,\n        private readonly int|float|bool $value,\n    ) {\n        if (!in_array($operator, self::$validOperators)) {\n            throw new \\Exception(\"Invalid operator\");\n        }\n    }\n\n    public static function property(string $propertyName): self\n    {\n        return new self(\n            $propertyName,\n            Operator::IsNotEmpty,\n            true\n        );\n    }\n\n    public function propertyType(): string\n    {\n        return \"property\";\n    }\n\n    public function propertyName(): string\n    {\n        return $this->propertyName;\n    }\n\n    public function operator(): Operator\n    {\n        return $this->operator;\n    }\n\n    public function value(): int|float|bool\n    {\n        return $this->value;\n    }\n\n    public function toArray(): array\n    {\n        return [\n            $this->propertyType() => $this->propertyName,\n            \"number\"   => [\n                $this->operator->value => $this->value\n            ],\n        ];\n    }\n\n    public function equals(int|float $value): self\n    {\n        return new self($this->propertyName, Operator::Equals, $value);\n    }\n\n    public function doesNotEqual(int|float $value): self\n    {\n        return new self($this->propertyName, Operator::DoesNotEqual, $value);\n    }\n\n    public function greaterThan(int|float $value): self\n    {\n        return new self($this->propertyName, Operator::GreaterThan, $value);\n    }\n\n    public function lessThan(int|float $value): self\n    {\n        return new self($this->propertyName, Operator::LessThan, $value);\n    }\n\n    public function greaterThanOrEqualTo(int|float $value): self\n    {\n        return new self($this->propertyName, Operator::GreaterThanOrEqualTo, $value);\n    }\n\n    public function lessThanOrEqualTo(int|float $value): self\n    {\n        return new self($this->propertyName, Operator::LessThanOrEqualTo, $value);\n    }\n\n    public function isEmpty(): self\n    {\n        return new self($this->propertyName, Operator::IsEmpty, true);\n    }\n\n    public function isNotEmpty(): self\n    {\n        return new self($this->propertyName, Operator::IsNotEmpty, true);\n    }\n}\n"
  },
  {
    "path": "src/Databases/Query/Operator.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Query;\n\nenum Operator: string\n{\n    case After = \"after\";\n    case Before = \"before\";\n    case Contains = \"contains\";\n    case DoesNotContain = \"does_not_contain\";\n    case DoesNotEqual = \"does_not_equal\";\n    case EndsWith = \"ends_with\";\n    case Equals = \"equals\";\n    case GreaterThan = \"greater_than\";\n    case GreaterThanOrEqualTo = \"greater_than_or_equal_to\";\n    case IsEmpty = \"is_empty\";\n    case IsNotEmpty = \"is_not_empty\";\n    case LessThan = \"less_than\";\n    case LessThanOrEqualTo = \"less_than_or_equal_to\";\n    case NextMonth = \"next_month\";\n    case NextWeek = \"next_week\";\n    case NextYear = \"next_year\";\n    case OnOrAfter = \"on_or_after\";\n    case OnOrBefore = \"on_or_before\";\n    case PastMonth = \"past_month\";\n    case PastWeek = \"past_week\";\n    case PastYear = \"past_year\";\n    case StartsWith = \"starts_with\";\n    case ThisWeek = \"this_week\";\n}\n"
  },
  {
    "path": "src/Databases/Query/PeopleFilter.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Query;\n\n/** @psalm-immutable */\nclass PeopleFilter implements Filter, Condition\n{\n    private static array $validOperators = [\n        Operator::Contains,\n        Operator::DoesNotContain,\n        Operator::IsEmpty,\n        Operator::IsNotEmpty,\n    ];\n\n    private function __construct(\n        private readonly string $propertyName,\n        private readonly Operator $operator,\n        private readonly string|bool $value,\n    ) {\n        if (!in_array($operator, self::$validOperators)) {\n            throw new \\Exception(\"Invalid operator\");\n        }\n    }\n\n    public static function property(string $propertyName): self\n    {\n        return new self(\n            $propertyName,\n            Operator::IsNotEmpty,\n            true\n        );\n    }\n\n    public static function createdBy(): self\n    {\n        return new self(\n            \"created_by\",\n            Operator::IsNotEmpty,\n            true\n        );\n    }\n\n    public static function lastEditedBy(): self\n    {\n        return new self(\n            \"last_edited_by\",\n            Operator::IsNotEmpty,\n            true\n        );\n    }\n\n    public function propertyType(): string\n    {\n        return \"property\";\n    }\n\n    public function propertyName(): string\n    {\n        return $this->propertyName;\n    }\n\n    public function operator(): Operator\n    {\n        return $this->operator;\n    }\n\n    public function value(): string|bool\n    {\n        return $this->value;\n    }\n\n    public function toArray(): array\n    {\n        return [\n            $this->propertyType() => $this->propertyName,\n            \"people\" => [\n                $this->operator->value => $this->value\n            ],\n        ];\n    }\n\n    public function contains(string $userId): self\n    {\n        return new self($this->propertyName, Operator::Contains, $userId);\n    }\n\n    public function doesNotContain(string $userId): self\n    {\n        return new self($this->propertyName, Operator::DoesNotContain, $userId);\n    }\n\n    public function isEmpty(): self\n    {\n        return new self($this->propertyName, Operator::IsEmpty, true);\n    }\n\n    public function isNotEmpty(): self\n    {\n        return new self($this->propertyName, Operator::IsNotEmpty, true);\n    }\n}\n"
  },
  {
    "path": "src/Databases/Query/RelationFilter.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Query;\n\n/** @psalm-immutable */\nclass RelationFilter implements Filter, Condition\n{\n    private static array $validOperators = [\n        Operator::Contains,\n        Operator::DoesNotContain,\n        Operator::IsEmpty,\n        Operator::IsNotEmpty,\n    ];\n\n\n    private function __construct(\n        private readonly string $propertyName,\n        private readonly Operator $operator,\n        private readonly string|bool $value,\n    ) {\n        if (!in_array($operator, self::$validOperators)) {\n            throw new \\Exception(\"Invalid operator\");\n        }\n    }\n\n    public static function property(string $propertyName): self\n    {\n        return new self(\n            $propertyName,\n            Operator::IsNotEmpty,\n            true\n        );\n    }\n\n    /** @return \"property\" */\n    public function propertyType(): string\n    {\n        return 'property';\n    }\n\n    public function propertyName(): string\n    {\n        return $this->propertyName;\n    }\n\n    public function operator(): Operator\n    {\n        return $this->operator;\n    }\n\n    public function value(): string|bool\n    {\n        return $this->value;\n    }\n\n    public function toArray(): array\n    {\n        return [\n            $this->propertyType() => $this->propertyName,\n            \"relation\" => [\n                $this->operator->value => $this->value\n            ],\n        ];\n    }\n\n    public function contains(string $value): self\n    {\n        return new self($this->propertyName, Operator::Contains, $value);\n    }\n\n    public function doesNotContain(string $value): self\n    {\n        return new self($this->propertyName, Operator::DoesNotContain, $value);\n    }\n\n    public function isEmpty(): self\n    {\n        return new self($this->propertyName, Operator::IsEmpty, true);\n    }\n\n    public function isNotEmpty(): self\n    {\n        return new self($this->propertyName, Operator::IsNotEmpty, true);\n    }\n}\n"
  },
  {
    "path": "src/Databases/Query/Result.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Query;\n\nuse Notion\\Pages\\Page;\n\n/**\n * Database query result\n *\n * @psalm-type QueryResultJson = array{\n *      results: PageJson[],\n *      has_more: bool,\n *      next_cursor: string|null\n * }\n *\n * @psalm-import-type PageJson from \\Notion\\Pages\\Page\n * @psalm-immutable\n */\nclass Result\n{\n    /** @param Page[] $pages */\n    private function __construct(\n        public readonly array $pages,\n        public readonly bool $hasMore,\n        public readonly string|null $nextCursor\n    ) {\n    }\n\n    /** @param QueryResultJson $array */\n    public static function fromArray(array $array): self\n    {\n        $pages = array_map(\n            function (array $pageArray): Page {\n                return Page::fromArray($pageArray);\n            },\n            $array[\"results\"],\n        );\n\n        return new self($pages, $array[\"has_more\"], $array[\"next_cursor\"]);\n    }\n}\n"
  },
  {
    "path": "src/Databases/Query/SelectFilter.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Query;\n\n/** @psalm-immutable */\nclass SelectFilter implements Filter, Condition\n{\n    private static array $validOperators = [\n        Operator::Equals,\n        Operator::DoesNotEqual,\n        Operator::IsEmpty,\n        Operator::IsNotEmpty,\n    ];\n\n    private function __construct(\n        private readonly string $propertyName,\n        private readonly Operator $operator,\n        private readonly string|bool $value,\n    ) {\n        if (!in_array($operator, self::$validOperators)) {\n            throw new \\Exception(\"Invalid operator\");\n        }\n    }\n\n    public static function property(string $propertyName): self\n    {\n        return new self(\n            $propertyName,\n            Operator::IsNotEmpty,\n            true\n        );\n    }\n\n    /** @return \"property\" */\n    public function propertyType(): string\n    {\n        return \"property\";\n    }\n\n    public function propertyName(): string\n    {\n        return $this->propertyName;\n    }\n\n    public function operator(): Operator\n    {\n        return $this->operator;\n    }\n\n    public function value(): string|bool\n    {\n        return $this->value;\n    }\n\n    public function toArray(): array\n    {\n        return [\n            $this->propertyType() => $this->propertyName,\n            \"select\"   => [\n                $this->operator->value => $this->value\n            ],\n        ];\n    }\n\n    public function equals(string $value): self\n    {\n        return new self($this->propertyName, Operator::Equals, $value);\n    }\n\n    public function doesNotEqual(string $value): self\n    {\n        return new self($this->propertyName, Operator::DoesNotEqual, $value);\n    }\n\n    public function isEmpty(): self\n    {\n        return new self($this->propertyName, Operator::IsEmpty, true);\n    }\n\n    public function isNotEmpty(): self\n    {\n        return new self($this->propertyName, Operator::IsNotEmpty, true);\n    }\n}\n"
  },
  {
    "path": "src/Databases/Query/Sort.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Query;\n\n/**\n * @psalm-immutable\n */\nclass Sort\n{\n    private const TYPE_PROPERTY = \"property\";\n    private const TYPE_TIMESTAMP = \"timestamp\";\n\n    private const ORDER_ASCENDING = \"ascending\";\n    private const ORDER_DESCENDING = \"descending\";\n\n    /**\n     * @psalm-param self::TYPE_* $type\n     * @psalm-param self::ORDER_* $direction\n     */\n    private function __construct(\n        private readonly string $type,\n        private readonly string $propertyName,\n        private readonly string $direction,\n    ) {\n    }\n\n    public static function property(string $propertyName): self\n    {\n        return new self(self::TYPE_PROPERTY, $propertyName, self::ORDER_ASCENDING);\n    }\n\n    public static function createdTime(): self\n    {\n        return new self(self::TYPE_TIMESTAMP, \"created_time\", self::ORDER_ASCENDING);\n    }\n\n    public static function lastEditedTime(): self\n    {\n        return new self(self::TYPE_TIMESTAMP, \"last_edited_time\", self::ORDER_ASCENDING);\n    }\n\n    public function ascending(): self\n    {\n        return new self($this->type, $this->propertyName, self::ORDER_ASCENDING);\n    }\n\n    public function descending(): self\n    {\n        return new self($this->type, $this->propertyName, self::ORDER_DESCENDING);\n    }\n\n    public function toArray(): array\n    {\n        return [\n            $this->type => $this->propertyName,\n            \"direction\" => $this->direction,\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Databases/Query/StatusFilter.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Query;\n\n/** @psalm-immutable */\nclass StatusFilter implements Filter, Condition\n{\n    private static array $validOperators = [\n        Operator::Equals,\n        Operator::DoesNotEqual,\n        Operator::IsEmpty,\n        Operator::IsNotEmpty,\n    ];\n\n    private function __construct(\n        private readonly string $propertyName,\n        private readonly Operator $operator,\n        private readonly string|bool $value,\n    ) {\n        if (!in_array($operator, self::$validOperators)) {\n            throw new \\Exception(\"Invalid operator\");\n        }\n    }\n\n    public static function property(string $propertyName): self\n    {\n        return new self(\n            $propertyName,\n            Operator::IsNotEmpty,\n            true\n        );\n    }\n\n    /** @return \"property\" */\n    public function propertyType(): string\n    {\n        return \"property\";\n    }\n\n    public function propertyName(): string\n    {\n        return $this->propertyName;\n    }\n\n    public function operator(): Operator\n    {\n        return $this->operator;\n    }\n\n    public function value(): string|bool\n    {\n        return $this->value;\n    }\n\n    public function toArray(): array\n    {\n        return [\n            $this->propertyType() => $this->propertyName,\n            \"status\"   => [\n                $this->operator->value => $this->value\n            ],\n        ];\n    }\n\n    public function equals(string $value): self\n    {\n        return new self($this->propertyName, Operator::Equals, $value);\n    }\n\n    public function doesNotEqual(string $value): self\n    {\n        return new self($this->propertyName, Operator::DoesNotEqual, $value);\n    }\n\n    public function isEmpty(): self\n    {\n        return new self($this->propertyName, Operator::IsEmpty, true);\n    }\n\n    public function isNotEmpty(): self\n    {\n        return new self($this->propertyName, Operator::IsNotEmpty, true);\n    }\n}\n"
  },
  {
    "path": "src/Databases/Query/TextFilter.php",
    "content": "<?php\n\nnamespace Notion\\Databases\\Query;\n\n/** @psalm-immutable */\nclass TextFilter implements Filter, Condition\n{\n    private static array $validOperators = [\n        Operator::Equals,\n        Operator::DoesNotEqual,\n        Operator::Contains,\n        Operator::DoesNotContain,\n        Operator::StartsWith,\n        Operator::EndsWith,\n        Operator::IsEmpty,\n        Operator::IsNotEmpty,\n    ];\n\n    private function __construct(\n        private readonly string $propertyName,\n        private readonly Operator $operator,\n        private readonly string|bool $value,\n    ) {\n        if (!in_array($operator, self::$validOperators)) {\n            throw new \\Exception(\"Invalid operator\");\n        }\n    }\n\n    public static function property(string $propertyName): self\n    {\n        return new self(\n            $propertyName,\n            Operator::Contains,\n            \"\"\n        );\n    }\n\n    public function propertyType(): string\n    {\n        return \"property\";\n    }\n\n    public function propertyName(): string\n    {\n        return $this->propertyName;\n    }\n\n    public function operator(): Operator\n    {\n        return $this->operator;\n    }\n\n    public function value(): string|bool\n    {\n        return $this->value;\n    }\n\n    public function toArray(): array\n    {\n        return [\n            $this->propertyType() => $this->propertyName,\n            \"rich_text\" => [\n                $this->operator->value => $this->value\n            ],\n        ];\n    }\n\n    public function equals(string $value): self\n    {\n        return new self($this->propertyName, Operator::Equals, $value);\n    }\n\n    public function doesNotEqual(string $value): self\n    {\n        return new self($this->propertyName, Operator::DoesNotEqual, $value);\n    }\n\n    public function contains(string $value): self\n    {\n        return new self($this->propertyName, Operator::Contains, $value);\n    }\n\n    public function doesNotContain(string $value): self\n    {\n        return new self($this->propertyName, Operator::DoesNotContain, $value);\n    }\n\n    public function startsWith(string $value): self\n    {\n        return new self($this->propertyName, Operator::StartsWith, $value);\n    }\n\n    public function endsWith(string $value): self\n    {\n        return new self($this->propertyName, Operator::EndsWith, $value);\n    }\n\n    public function isEmpty(): self\n    {\n        return new self($this->propertyName, Operator::IsEmpty, true);\n    }\n\n    public function isNotEmpty(): self\n    {\n        return new self($this->propertyName, Operator::IsNotEmpty, true);\n    }\n}\n"
  },
  {
    "path": "src/Databases/Query.php",
    "content": "<?php\n\nnamespace Notion\\Databases;\n\nuse Exception;\nuse Notion\\Databases\\Query\\Filter;\nuse Notion\\Databases\\Query\\Sort;\n\n/** @psalm-immutable */\nclass Query\n{\n    public const MAX_PAGE_SIZE = 100;\n\n    /** @param Sort[] $sorts */\n    private function __construct(\n        public readonly Filter|null $filter,\n        public readonly array $sorts,\n        public readonly string|null $startCursor,\n        public readonly int $pageSize,\n    ) {\n    }\n\n    public static function create(): self\n    {\n        return new self(null, [], null, self::MAX_PAGE_SIZE);\n    }\n\n    public function changeFilter(Filter $filter): self\n    {\n        return new self($filter, $this->sorts, $this->startCursor, $this->pageSize);\n    }\n\n    /** Add new sort with lowest priority */\n    public function addSort(Sort $sort): self\n    {\n        $sorts = $this->sorts;\n        $sorts[] = $sort;\n\n        return new self($this->filter, $sorts, $this->startCursor, $this->pageSize);\n    }\n\n    /**\n     * @deprecated 1.1.0 This method will be removed in future versions. Use 'addSort' instead.\n     * @see \\Notion\\Databases\\Query::addSort()\n     */\n    public function changeAddedSort(Sort $sort): self\n    {\n        return $this->addSort($sort);\n    }\n\n    /** Replace all sorts */\n    public function changeSorts(Sort ...$sorts): self\n    {\n        return new self($this->filter, $sorts, $this->startCursor, $this->pageSize);\n    }\n\n    public function changeStartCursor(string $startCursor): self\n    {\n        return new self($this->filter, $this->sorts, $startCursor, $this->pageSize);\n    }\n\n    public function changePageSize(int $pageSize): self\n    {\n        if ($pageSize < 0 || $pageSize > self::MAX_PAGE_SIZE) {\n            throw new Exception(\"Maximum page size: \" . self::MAX_PAGE_SIZE);\n        }\n\n        return new self($this->filter, $this->sorts, $this->startCursor, $pageSize);\n    }\n\n    public function toArray(): array\n    {\n        $array = [\n            \"sorts\"     => array_map(fn (Sort $s) => $s->toArray(), $this->sorts),\n            \"page_size\" => $this->pageSize,\n        ];\n\n        if ($this->filter !== null) {\n            $array[\"filter\"] = $this->filter->toArray();\n        }\n\n        if ($this->startCursor !== null) {\n            $array[\"start_cursor\"] = $this->startCursor;\n        }\n\n        return $array;\n    }\n}\n"
  },
  {
    "path": "src/Exceptions/ApiException.php",
    "content": "<?php\n\nnamespace Notion\\Exceptions;\n\nuse Psr\\Http\\Message\\ResponseInterface;\n\n/**\n * Exception from Notion API\n */\nclass ApiException extends NotionException\n{\n    public readonly string $notionCode;\n    public readonly ResponseInterface $response;\n\n    final public function __construct(\n        string $message,\n        string $notionCode,\n        ResponseInterface $response,\n    ) {\n        $this->notionCode = $notionCode;\n        $this->response = $response;\n\n        parent::__construct($message);\n    }\n\n    final public static function fromResponse(ResponseInterface $response): static\n    {\n        /** @var array{ message: string, code: string}|false|null $body */\n        $body = json_decode((string) $response->getBody(), true);\n\n        if ($body === null || $body === false) {\n            return new static(\"\", \"\", $response);\n        }\n\n        return match ($body[\"code\"]) {\n            \"conflict_error\" => new ConflictException($body[\"message\"], $body[\"code\"], $response),\n            default          => new static($body[\"message\"], $body[\"code\"], $response),\n        };\n    }\n\n    /**\n     * @deprecated 1.3.0 This method will be removed in future versions. Use 'notionCode' property.\n     */\n    final public function getNotionCode(): string\n    {\n        return $this->notionCode;\n    }\n}\n"
  },
  {
    "path": "src/Exceptions/BlockException.php",
    "content": "<?php\n\nnamespace Notion\\Exceptions;\n\nuse Notion\\Blocks\\BlockType;\nuse Notion\\Exceptions\\NotionException;\n\nclass BlockException extends NotionException\n{\n    public static function wrongType(BlockType $expectedType): self\n    {\n        return new self(\"Block must be of type '{$expectedType->value}'\");\n    }\n\n    public static function noChindrenSupport(): self\n    {\n        return new self(\"This block does not support children.\");\n    }\n}\n"
  },
  {
    "path": "src/Exceptions/ColumnException.php",
    "content": "<?php\n\nnamespace Notion\\Exceptions;\n\nclass ColumnException extends BlockException\n{\n    public static function columnInsideColumn(): self\n    {\n        return new self(\"Columns should not contain other columns.\");\n    }\n}\n"
  },
  {
    "path": "src/Exceptions/ColumnListException.php",
    "content": "<?php\n\nnamespace Notion\\Exceptions;\n\nclass ColumnListException extends BlockException\n{\n    public static function childNotColumn(): self\n    {\n        return new self(\"Column lists accept only columns as children.\");\n    }\n}\n"
  },
  {
    "path": "src/Exceptions/ConflictException.php",
    "content": "<?php\n\nnamespace Notion\\Exceptions;\n\nclass ConflictException extends ApiException\n{\n}\n"
  },
  {
    "path": "src/Exceptions/DatabaseException.php",
    "content": "<?php\n\nnamespace Notion\\Exceptions;\n\nclass DatabaseException extends NotionException\n{\n    public static function internalCover(): self\n    {\n        return new self(\"Internal cover image is not supported.\");\n    }\n\n    public static function noTitleProperty(): self\n    {\n        return new self(\"A database must have a title property.\");\n    }\n}\n"
  },
  {
    "path": "src/Exceptions/FileUploadException.php",
    "content": "<?php\n\nnamespace Notion\\Exceptions;\n\nclass FileUploadException extends NotionException\n{\n    public static function fileDoesNotExist(string $filePath): self\n    {\n        return new self(\"File {$filePath} does not exist.\");\n    }\n\n    public static function fileIsNotReadable(string $filePath): self\n    {\n        return new self(\"File {$filePath} is not readable.\");\n    }\n\n    public static function fileSizeCouldNotBeDetermined(string $filePath): self\n    {\n        return new self(\"The size of file {$filePath} could not be determined.\");\n    }\n\n    public static function couldNotGetFileContent(string $filePath): self\n    {\n        return new self(\"Could not get the contents of file {$filePath}.\");\n    }\n\n    public static function couldNotOpenFileForReading(string $filePath): self\n    {\n        return new self(\"Could not open file {$filePath} for reading.\");\n    }\n\n    public static function couldNotReadChunkFromFile(string $filePath, int $partNumber): self\n    {\n        return new self(\"Could not read chunk {$partNumber} from file {$filePath}.\");\n    }\n}\n"
  },
  {
    "path": "src/Exceptions/HeadingException.php",
    "content": "<?php\n\nnamespace Notion\\Exceptions;\n\nclass HeadingException extends BlockException\n{\n    public static function untogglifyWithChildren(): self\n    {\n        return new self(\"Heading cannot be un-togglified with children. Please remove child blocks\");\n    }\n}\n"
  },
  {
    "path": "src/Exceptions/IconException.php",
    "content": "<?php\n\nnamespace Notion\\Exceptions;\n\nclass IconException extends NotionException\n{\n    public static function bothNull(): self\n    {\n        return new self(\"Icon must be either emoji or file, not both null.\");\n    }\n\n    public static function bothSet(): self\n    {\n        return new self(\"Icon must be either emoji or file, not both.\");\n    }\n}\n"
  },
  {
    "path": "src/Exceptions/NotionException.php",
    "content": "<?php\n\nnamespace Notion\\Exceptions;\n\n/**\n * Library parent exception\n *\n * All other exceptions from this library are derived from this class.\n */\nclass NotionException extends \\Exception\n{\n}\n"
  },
  {
    "path": "src/Exceptions/RelationException.php",
    "content": "<?php\n\nnamespace Notion\\Exceptions;\n\nclass RelationException extends NotionException\n{\n    public static function emptySyncedPropertyName(): self\n    {\n        return new self(\"Bidirectional relations must provide 'synced property name'.\");\n    }\n\n    public static function emptySyncedPropertyId(): self\n    {\n        return new self(\"Bidirectional relations must provide 'synced property ID'.\");\n    }\n}\n"
  },
  {
    "path": "src/FileUploads/Client.php",
    "content": "<?php\n\nnamespace Notion\\FileUploads;\n\nuse Http\\Discovery\\Psr17FactoryDiscovery;\nuse Http\\Message\\MultipartStream\\MultipartStreamBuilder;\nuse Notion\\Configuration;\nuse Notion\\Exceptions\\FileUploadException;\nuse Notion\\Infrastructure\\Http;\n\n/**\n * @psalm-import-type FileUploadJson from FileUpload\n */\nclass Client\n{\n    private const SINGLE_PART_MAX_SIZE = 20 * 1024 * 1024; // 20 MB\n    private const CHUNK_SIZE = 10 * 1024 * 1024; // 10 MB\n    private const MAX_PAGE_SIZE = 100;\n\n    /**\n     * @internal Use `\\Notion\\Notion::pages()` instead\n     */\n    public function __construct(\n        private readonly Configuration $config,\n    ) {\n    }\n\n    public function find(string $fileUploadId): FileUpload\n    {\n        $url = \"https://api.notion.com/v1/file_uploads/{$fileUploadId}\";\n        $request = Http::createRequest($url, $this->config)\n            ->withMethod(\"GET\");\n\n        /** @psalm-var FileUploadJson $body */\n        $body = Http::sendRequest($request, $this->config);\n\n        return FileUpload::fromArray($body);\n    }\n\n    /**\n     * @return array{\n     *      results: FileUpload[],\n     *      nextCursor: string|null,\n     *      hasMore: bool,\n     * }\n     */\n    public function list(FileUploadStatus|null $status = null, string|null $cursor = null): array\n    {\n        $uriFactory = Psr17FactoryDiscovery::findUriFactory();\n        $uri = $uriFactory->createUri(\"https://api.notion.com/v1/file_uploads\");\n\n        $queryParams = [ \"page_size\" => self::MAX_PAGE_SIZE ];\n        if ($status !== null) {\n            $queryParams[\"status\"] = $status->value;\n        }\n        if ($cursor !== null) {\n            $queryParams[\"start_cursor\"] = $cursor;\n        }\n        $uri->withQuery(http_build_query($queryParams));\n\n        $request = Http::createRequest((string) $uri, $this->config)\n            ->withMethod(\"GET\");\n\n        /** @psalm-var array{results: FileUploadJson[], next_cursor: string|null, has_more: bool} $body */\n        $body = Http::sendRequest($request, $this->config);\n\n        $results = array_map(\n            fn (array $item): FileUpload => FileUpload::fromArray($item),\n            $body[\"results\"]\n        );\n        return [\n            \"results\" => $results,\n            \"nextCursor\" => $body[\"next_cursor\"],\n            \"hasMore\" => $body[\"has_more\"],\n        ];\n    }\n\n    /**\n     * @return FileUpload[]\n     */\n    public function findAll(FileUploadStatus|null $status = null): array\n    {\n        $fileUploads = [];\n        $startCursor = null;\n        $hasMore = true;\n\n        while ($hasMore) {\n            $result = $this->list($status, $startCursor);\n\n            $fileUploads = array_merge($fileUploads, $result[\"results\"]);\n            $hasMore = $result[\"hasMore\"];\n            $startCursor = $result[\"nextCursor\"];\n        }\n\n        return $fileUploads;\n    }\n\n    public function upload(string $filePath, string|null $filenameOnNotion = null): FileUpload\n    {\n        if (!file_exists($filePath)) {\n            throw FileUploadException::fileDoesNotExist($filePath);\n        }\n\n        if (!is_readable($filePath)) {\n            throw FileUploadException::fileIsNotReadable($filePath);\n        }\n\n        $filename = $filenameOnNotion ?? basename($filePath);\n\n        $fileSize = filesize($filePath);\n        if ($fileSize === false) {\n            throw FileUploadException::fileSizeCouldNotBeDetermined($filePath);\n        }\n\n        if ($fileSize < self::SINGLE_PART_MAX_SIZE) {\n            $fileUpload = $this->createSinglePart();\n\n            $content = file_get_contents($filePath);\n            if ($content === false) {\n                throw FileUploadException::couldNotGetFileContent($filePath);\n            }\n\n            $fileUpload = $this->send($fileUpload->id, $content, $filename, null);\n            return $fileUpload;\n        }\n\n        $contentType = mime_content_type($filePath) ?: \"application/octet-stream\";\n        $numberOfParts = (int) ceil($fileSize / self::CHUNK_SIZE);\n        $fileUpload = $this->createMultiPart($filename, $contentType, $numberOfParts);\n        foreach ($this->chunksGenerator($filePath) as $partNumber => $chunk) {\n            $this->sendMultiPart($fileUpload->id, $chunk, $filename, $partNumber);\n        }\n\n        $fileUpload = $this->complete($fileUpload->id);\n\n        return $fileUpload;\n    }\n\n    public function createSinglePart(string|null $filename = null): FileUpload\n    {\n        $body = [\n            \"mode\" => Mode::SinglePart->value,\n        ];\n        if ($filename !== null) {\n            $body[\"filename\"] = $filename;\n        }\n\n        return $this->create($body);\n    }\n\n    public function createMultiPart(string $filename, string $contentType, int $numberOfParts): FileUpload\n    {\n        $body = [\n            \"mode\" => Mode::MultiPart->value,\n            \"filename\" => $filename,\n            \"content_type\" => $contentType,\n            \"number_of_parts\" => $numberOfParts,\n        ];\n\n        return $this->create($body);\n    }\n\n    public function createExternalUrl(string $filename, string $externalUrl): FileUpload\n    {\n        $body = [\n            \"mode\" => Mode::ExternalUrl->value,\n            \"filename\" => $filename,\n            \"external_url\" => $externalUrl,\n        ];\n\n        return $this->create($body);\n    }\n\n    public function sendSinglePart(string $fileUploadId, string $filename, string $content): FileUpload\n    {\n        return $this->send($fileUploadId, $content, $filename, null);\n    }\n\n    public function sendMultiPart(string $fileUploadId, string $content, string $filename, int $partNumber): FileUpload\n    {\n        return $this->send($fileUploadId, $content, $filename, $partNumber);\n    }\n\n    public function complete(string $fileUploadId): FileUpload\n    {\n        $url = \"https://api.notion.com/v1/file_uploads/{$fileUploadId}/complete\";\n        $request = Http::createRequest($url, $this->config)\n            ->withMethod(\"POST\");\n\n        /** @psalm-var FileUploadJson $body */\n        $body = Http::sendRequest($request, $this->config);\n\n        return FileUpload::fromArray($body);\n    }\n\n    private function create(array $requestBody): FileUpload\n    {\n        $data = json_encode($requestBody);\n\n        $url = \"https://api.notion.com/v1/file_uploads\";\n        $request = Http::createRequest($url, $this->config)\n            ->withMethod(\"POST\")\n            ->withHeader(\"Content-Type\", \"application/json\");\n        $request->getBody()->write($data);\n\n        /** @psalm-var FileUploadJson $responseBody */\n        $responseBody = Http::sendRequest($request, $this->config);\n\n        return FileUpload::fromArray($responseBody);\n    }\n\n    private function send(\n        string $fileUploadId,\n        string $content,\n        string $filename,\n        int|null $partNumber\n    ): FileUpload {\n        $streamFactory = Psr17FactoryDiscovery::findStreamFactory();\n        $builder = new MultipartStreamBuilder($streamFactory);\n\n        $builder->addResource(\"file\", $content, [ \"filename\" => $filename ]);\n\n        if ($partNumber !== null) {\n            $builder->addResource(\"part_number\", (string) $partNumber);\n        }\n\n        $multipartStream = $builder->build();\n        $boundary = $builder->getBoundary();\n\n        $url = \"https://api.notion.com/v1/file_uploads/{$fileUploadId}/send\";\n        $request = Http::createRequest($url, $this->config)\n            ->withMethod(\"POST\")\n            ->withHeader(\"Content-Type\", \"multipart/form-data; boundary={$boundary}\")\n            ->withBody($multipartStream);\n\n        /** @psalm-var FileUploadJson $body */\n        $body = Http::sendRequest($request, $this->config);\n\n        return FileUpload::fromArray($body);\n    }\n\n    /**\n     * @return \\Generator<int, string>\n     */\n    private function chunksGenerator(string $filePath): \\Generator\n    {\n        $handle = fopen($filePath, \"rb\");\n        if ($handle === false) {\n            throw FileUploadException::couldNotOpenFileForReading($filePath);\n        }\n\n        $partNumber = 1;\n        while (!feof($handle)) {\n            $buffer = fread($handle, self::CHUNK_SIZE);\n            if ($buffer === false) {\n                throw FileUploadException::couldNotReadChunkFromFile($filePath, $partNumber);\n            }\n\n            yield $partNumber => $buffer;\n            $partNumber++;\n        }\n\n        fclose($handle);\n    }\n}\n"
  },
  {
    "path": "src/FileUploads/FileUpload.php",
    "content": "<?php\n\nnamespace Notion\\FileUploads;\n\nuse DateTimeImmutable;\n\n/**\n * @psalm-type FileUploadJson = array{\n *     object: \"file_upload\",\n *     id: string,\n *     created_time: string,\n *     last_edited_time: string,\n *     expiry_time: string|null,\n *     status: \"pending\"|\"uploaded\"|\"expired\"|\"failed\",\n *     filename: string|null,\n *     content_type: string|null,\n *     content_length: int|null,\n *     upload_url?: string,\n *     complete_url?: string,\n *     file_import_result?: string,\n * }\n */\nclass FileUpload\n{\n    private function __construct(\n        public readonly string $id,\n        public readonly DateTimeImmutable $createdTime,\n        public readonly DateTimeImmutable $lastEditedTime,\n        public readonly DateTimeImmutable|null $expiryTime,\n        public readonly FileUploadStatus $status,\n        public readonly string|null $filename,\n        public readonly string|null $contentType,\n        public readonly int|null $contentLength,\n        public readonly string|null $uploadUrl,\n        public readonly string|null $completeUrl,\n        public readonly string|null $fileImportResult,\n    ) {\n    }\n\n    /**\n     * @param FileUploadJson $array\n     *\n     * @internal\n     */\n    public static function fromArray(array $array): self\n    {\n        return new self(\n            $array[\"id\"],\n            new DateTimeImmutable($array[\"created_time\"]),\n            new DateTimeImmutable($array[\"last_edited_time\"]),\n            isset($array[\"expiry_time\"]) ? new DateTimeImmutable($array[\"expiry_time\"]) : null,\n            FileUploadStatus::from($array[\"status\"]),\n            $array[\"filename\"] ?? null,\n            $array[\"content_type\"] ?? null,\n            $array[\"content_length\"] ?? null,\n            $array[\"upload_url\"] ?? null,\n            $array[\"complete_url\"] ?? null,\n            $array[\"file_import_result\"] ?? null,\n        );\n    }\n\n    public function isAttached(): bool\n    {\n        return $this->status == FileUploadStatus::Uploaded &&\n            $this->expiryTime !== null;\n    }\n}\n"
  },
  {
    "path": "src/FileUploads/FileUploadStatus.php",
    "content": "<?php\n\nnamespace Notion\\FileUploads;\n\nenum FileUploadStatus: string\n{\n    case Pending = 'pending';\n    case Uploaded = 'uploaded';\n    case Expired = 'expired';\n    case Failed = 'failed';\n}\n"
  },
  {
    "path": "src/FileUploads/Mode.php",
    "content": "<?php\n\nnamespace Notion\\FileUploads;\n\nenum Mode: string\n{\n    case SinglePart = \"single_part\";\n    case MultiPart = \"multi_part\";\n    case ExternalUrl = \"external_url\";\n}\n"
  },
  {
    "path": "src/Infrastructure/Http.php",
    "content": "<?php\n\nnamespace Notion\\Infrastructure;\n\nuse Notion\\Configuration;\nuse Notion\\Exceptions\\ApiException;\nuse Notion\\Exceptions\\ConflictException;\nuse Psr\\Http\\Message\\RequestInterface;\nuse Psr\\Http\\Message\\ResponseInterface;\n\nclass Http\n{\n    public static function parseBody(ResponseInterface $response): array\n    {\n        /** @var array */\n        $body = json_decode((string) $response->getBody(), true);\n\n        if ($response->getStatusCode() >= 400) {\n            throw ApiException::fromResponse($response);\n        }\n\n        return $body;\n    }\n\n    public static function createRequest(string $uri, Configuration $config): RequestInterface\n    {\n        return $config->requestFactory\n            ->createRequest(\"GET\", $uri)\n            ->withHeader(\"Authorization\", \"Bearer {$config->token}\")\n            ->withHeader(\"Notion-Version\", $config->version);\n    }\n\n    public static function sendRequest(\n        RequestInterface $request,\n        Configuration $config,\n        int $currentAttempt = 0,\n    ): array {\n        $response = $config->httpClient->sendRequest($request);\n\n        try {\n            $body = self::parseBody($response);\n        } catch (ConflictException $e) {\n            if (\n                !$config->retryOnConflict ||\n                $currentAttempt >= $config->retryOnConflictAttempts\n            ) {\n                throw $e;\n            }\n\n            // Try again\n            return self::sendRequest($request, $config, $currentAttempt + 1);\n        }\n\n        return $body;\n    }\n}\n"
  },
  {
    "path": "src/Notion.php",
    "content": "<?php\n\nnamespace Notion;\n\nuse Notion\\Blocks\\Client as BlocksClient;\nuse Notion\\Comments\\Client as CommentsClient;\nuse Notion\\Databases\\Client as DatabasesClient;\nuse Notion\\Pages\\Client as PagesClient;\nuse Notion\\Search\\Client as SearchClient;\nuse Notion\\Users\\Client as UsersClient;\nuse Notion\\FileUploads\\Client as FileUploadsClient;\nuse Psr\\Http\\Client\\ClientInterface;\nuse Psr\\Http\\Message\\RequestFactoryInterface;\n\nclass Notion\n{\n    public const API_VERSION = \"2022-06-28\";\n\n    private function __construct(\n        private readonly Configuration $configuration,\n    ) {\n    }\n\n    public static function create(string $token): self\n    {\n        $configuration = Configuration::create($token);\n\n        return new self($configuration);\n    }\n\n    public static function createFromConfig(Configuration $config): self\n    {\n        return new self($config);\n    }\n\n    public static function createWithPsrImplementations(\n        ClientInterface $psrClient,\n        RequestFactoryInterface $requestFactory,\n        string $token,\n    ): self {\n        $configuration = Configuration::createFromPsrImplementations(\n            $token,\n            $psrClient,\n            $requestFactory,\n        );\n\n        return new self($configuration);\n    }\n\n    public function users(): UsersClient\n    {\n        return new UsersClient($this->configuration);\n    }\n\n    public function pages(): PagesClient\n    {\n        return new PagesClient($this->configuration);\n    }\n\n    public function databases(): DatabasesClient\n    {\n        return new DatabasesClient($this->configuration);\n    }\n\n    public function blocks(): BlocksClient\n    {\n        return new BlocksClient($this->configuration);\n    }\n\n    public function comments(): CommentsClient\n    {\n        return new CommentsClient($this->configuration);\n    }\n\n    public function search(): SearchClient\n    {\n        return new SearchClient($this->configuration);\n    }\n\n    public function fileUploads(): FileUploadsClient\n    {\n        return new FileUploadsClient($this->configuration);\n    }\n}\n"
  },
  {
    "path": "src/Pages/Client.php",
    "content": "<?php\n\nnamespace Notion\\Pages;\n\nuse Notion\\Blocks\\BlockInterface;\nuse Notion\\Configuration;\nuse Notion\\Infrastructure\\Http;\nuse Notion\\Pages\\Properties\\CreatedBy;\nuse Notion\\Pages\\Properties\\CreatedTime;\nuse Notion\\Pages\\Properties\\Formula;\nuse Notion\\Pages\\Properties\\LastEditedBy;\nuse Notion\\Pages\\Properties\\LastEditedTime;\nuse Notion\\Pages\\Properties\\PropertyInterface;\nuse Notion\\Pages\\Properties\\PropertyType;\nuse Notion\\Pages\\Properties\\UniqueId;\n\n/**\n * @psalm-import-type PageJson from Page\n */\nclass Client\n{\n    /**\n     * @internal Use `\\Notion\\Notion::pages()` instead\n     */\n    public function __construct(\n        private readonly Configuration $config,\n    ) {\n    }\n\n    public function find(string $pageId): Page\n    {\n        $url = \"https://api.notion.com/v1/pages/{$pageId}\";\n        $request = Http::createRequest($url, $this->config);\n\n        /** @psalm-var PageJson $body */\n        $body = Http::sendRequest($request, $this->config);\n\n        return Page::fromArray($body);\n    }\n\n    /** @param list<BlockInterface> $content */\n    public function create(Page $page, array $content = []): Page\n    {\n        $data = json_encode([\n            \"in_trash\" => $page->inTrash,\n            \"icon\" => $page->icon?->toArray(),\n            \"cover\" => $page->cover?->toArray(),\n            \"properties\" => array_map(fn(PropertyInterface $p) => $p->toArray(), $page->properties),\n            \"parent\" => $page->parent->toArray(),\n            \"children\" => array_map(fn(BlockInterface $b) => $b->toArray(), $content),\n        ]);\n\n        $url = \"https://api.notion.com/v1/pages\";\n        $request = Http::createRequest($url, $this->config)\n            ->withMethod(\"POST\")\n            ->withHeader(\"Content-Type\", \"application/json\");\n        $request->getBody()->write($data);\n\n        /** @psalm-var PageJson $body */\n        $body = Http::sendRequest($request, $this->config);\n\n        return Page::fromArray($body);\n    }\n\n    public function update(Page $page): Page\n    {\n        $notUpdatableProps = [\n            PropertyType::CreatedBy,\n            PropertyType::CreatedTime,\n            PropertyType::Formula,\n            PropertyType::LastEditedBy,\n            PropertyType::LastEditedTime,\n            PropertyType::Rollup,\n            PropertyType::UniqueId,\n        ];\n        $updatableProps = array_filter(\n            $page->properties,\n            function (PropertyInterface $p) use ($notUpdatableProps) {\n                return (!in_array($p->metadata()->type, $notUpdatableProps));\n            }\n        );\n\n        $data = json_encode([\n            \"in_trash\" => $page->inTrash,\n            \"icon\" => $page->icon?->toArray(),\n            \"cover\" => $page->cover?->toArray(),\n            \"properties\" => array_map(fn(PropertyInterface $p) => $p->toArray(), $updatableProps),\n            \"parent\" => $page->parent->toArray(),\n        ]);\n\n        $pageId = $page->id;\n        $url = \"https://api.notion.com/v1/pages/{$pageId}\";\n        $request = Http::createRequest($url, $this->config)\n            ->withMethod(\"PATCH\")\n            ->withHeader(\"Content-Type\", \"application/json\");\n        $request->getBody()->write($data);\n\n        /** @psalm-var PageJson $body */\n        $body = Http::sendRequest($request, $this->config);\n\n        return Page::fromArray($body);\n    }\n\n    public function delete(Page $page): Page\n    {\n        $archivedPage = $page->delete();\n\n        return $this->update($archivedPage);\n    }\n}\n"
  },
  {
    "path": "src/Pages/Page.php",
    "content": "<?php\n\nnamespace Notion\\Pages;\n\nuse DateTimeImmutable;\nuse Notion\\Common\\Date;\nuse Notion\\Common\\Emoji;\nuse Notion\\Common\\File;\nuse Notion\\Common\\Icon;\nuse Notion\\Pages\\Properties\\PropertyCollection;\nuse Notion\\Pages\\Properties\\PropertyFactory;\nuse Notion\\Pages\\Properties\\PropertyInterface;\nuse Notion\\Pages\\Properties\\Title;\n\n/**\n * @psalm-import-type EmojiJson from \\Notion\\Common\\Emoji\n * @psalm-import-type FileJson from \\Notion\\Common\\File\n * @psalm-import-type PropertyMetadataJson from \\Notion\\Pages\\Properties\\PropertyMetadata\n * @psalm-import-type PageParentJson from PageParent\n *\n * @psalm-type PageJson = array{\n *      object: \"page\",\n *      id: string,\n *      created_time: string,\n *      last_edited_time: string,\n *      in_trash: bool,\n *      icon: EmojiJson|FileJson|null,\n *      cover: FileJson|null,\n *      properties: array<string, PropertyMetadataJson>,\n *      parent: PageParentJson,\n *      url: string,\n * }\n *\n * @psalm-immutable\n */\nclass Page\n{\n    /**\n     * @param array<string, PropertyInterface> $properties\n     */\n    private function __construct(\n        public readonly string $id,\n        public readonly DateTimeImmutable $createdTime,\n        public readonly DateTimeImmutable $lastEditedTime,\n        public readonly bool $inTrash,\n        public readonly Icon|null $icon,\n        public readonly File|null $cover,\n        public readonly array $properties,\n        public readonly PageParent $parent,\n        public readonly string $url\n    ) {\n        /** @psalm-suppress DeprecatedProperty */\n        $this->archived = $inTrash;\n    }\n\n    /**\n     * @deprecated 1.17.0 Use `$inTrash` instead.\n     * @codeCoverageIgnore\n     */\n    public readonly bool $archived;\n\n    public static function create(PageParent $parent): self\n    {\n        $now = new DateTimeImmutable(\"now\");\n\n        return new self(\"\", $now, $now, false, null, null, [], $parent, \"\");\n    }\n\n\n    /**\n     * @param PageJson $array\n     *\n     * @internal\n     */\n    public static function fromArray(array $array): self\n    {\n        $icon = null;\n        if (is_array($array[\"icon\"])) {\n            $iconArray = $array[\"icon\"];\n            $iconType = $iconArray[\"type\"];\n\n            if ($iconType === \"emoji\") {\n                /** @psalm-var EmojiJson $iconArray */\n                $emoji = Emoji::fromArray($iconArray);\n                $icon = Icon::fromEmoji($emoji);\n            }\n\n            if ($iconType === \"file\" || $iconType === \"external\") {\n                /** @psalm-var FileJson $iconArray */\n                $file = File::fromArray($iconArray);\n                $icon = Icon::fromFile($file);\n            }\n        }\n\n        $cover = isset($array[\"cover\"]) ? File::fromArray($array[\"cover\"]) : null;\n\n        $parent = PageParent::fromArray($array[\"parent\"]);\n\n        $properties = [];\n        foreach ($array[\"properties\"] as $propertyName => $propertyArray) {\n            $properties[$propertyName] = PropertyFactory::fromArray($propertyArray);\n        }\n\n        return new self(\n            $array[\"id\"],\n            new DateTimeImmutable($array[\"created_time\"]),\n            new DateTimeImmutable($array[\"last_edited_time\"]),\n            $array[\"in_trash\"],\n            $icon,\n            $cover,\n            $properties,\n            $parent,\n            $array[\"url\"],\n        );\n    }\n\n    public function toArray(): array\n    {\n        return [\n            \"object\"           => \"page\",\n            \"id\"               => $this->id,\n            \"created_time\"     => $this->createdTime->format(Date::FORMAT),\n            \"last_edited_time\" => $this->lastEditedTime->format(Date::FORMAT),\n            \"in_trash\"         => $this->inTrash,\n            \"icon\"             => $this->icon?->toArray(),\n            \"cover\"            => $this->cover?->toArray(),\n            \"properties\"       => array_map(fn($p) => $p->toArray(), $this->properties),\n            \"parent\"           => $this->parent->toArray(),\n            \"url\"              => $this->url,\n        ];\n    }\n\n    /**\n     * @psalm-assert-if-false null $this->icon\n     */\n    public function hasIcon(): bool\n    {\n        return $this->icon !== null;\n    }\n\n    public function delete(): self\n    {\n        return new self(\n            $this->id,\n            $this->createdTime,\n            $this->lastEditedTime,\n            true,\n            $this->icon,\n            $this->cover,\n            $this->properties,\n            $this->parent,\n            $this->url,\n        );\n    }\n\n    /**\n     * @deprecated 1.17.0 Use `delete()` instead.\n     * @codeCoverageIgnore\n     */\n    public function archive(): self\n    {\n        return $this->delete();\n    }\n\n    public function restore(): self\n    {\n        return new self(\n            $this->id,\n            $this->createdTime,\n            $this->lastEditedTime,\n            false,\n            $this->icon,\n            $this->cover,\n            $this->properties,\n            $this->parent,\n            $this->url,\n        );\n    }\n\n    /**\n     * @deprecated 1.17.0 Use `restore()` instead.\n     * @codeCoverageIgnore\n     */\n    public function unarchive(): self\n    {\n        return $this->restore();\n    }\n\n    public function changeIcon(Emoji|File|Icon $icon): self\n    {\n        if ($icon instanceof Emoji) {\n            $icon = Icon::fromEmoji($icon);\n        }\n\n        if ($icon instanceof File) {\n            $icon = Icon::fromFile($icon);\n        }\n\n        return new self(\n            $this->id,\n            $this->createdTime,\n            $this->lastEditedTime,\n            $this->inTrash,\n            $icon,\n            $this->cover,\n            $this->properties,\n            $this->parent,\n            $this->url,\n        );\n    }\n\n    public function removeIcon(): self\n    {\n        return new self(\n            $this->id,\n            $this->createdTime,\n            $this->lastEditedTime,\n            $this->inTrash,\n            null,\n            $this->cover,\n            $this->properties,\n            $this->parent,\n            $this->url,\n        );\n    }\n\n    public function changeCover(File $cover): self\n    {\n        return new self(\n            $this->id,\n            $this->createdTime,\n            $this->lastEditedTime,\n            $this->inTrash,\n            $this->icon,\n            $cover,\n            $this->properties,\n            $this->parent,\n            $this->url,\n        );\n    }\n\n    public function removeCover(): self\n    {\n        return new self(\n            $this->id,\n            $this->createdTime,\n            $this->lastEditedTime,\n            $this->inTrash,\n            $this->icon,\n            null,\n            $this->properties,\n            $this->parent,\n            $this->url,\n        );\n    }\n\n    public function properties(): PropertyCollection\n    {\n        return PropertyCollection::create($this->properties);\n    }\n\n    public function getProperty(string $propertyName): PropertyInterface\n    {\n        return $this->properties()->get($propertyName);\n    }\n\n    /** @deprecated 1.4.0 Typo. Use `getProperty()` instead. */\n    public function getProprety(string $propertyName): PropertyInterface\n    {\n        return $this->getProperty($propertyName);\n    }\n\n    public function addProperty(string $name, PropertyInterface $property): self\n    {\n        return new self(\n            $this->id,\n            $this->createdTime,\n            $this->lastEditedTime,\n            $this->inTrash,\n            $this->icon,\n            $this->cover,\n            $this->properties()->add($name, $property)->getAll(),\n            $this->parent,\n            $this->url,\n        );\n    }\n\n    /** @param array<string, PropertyInterface> $properties */\n    public function changeProperties(array $properties): self\n    {\n        return new self(\n            $this->id,\n            $this->createdTime,\n            $this->lastEditedTime,\n            $this->inTrash,\n            $this->icon,\n            $this->cover,\n            PropertyCollection::create($properties)->getAll(),\n            $this->parent,\n            $this->url,\n        );\n    }\n\n    public function changeTitle(string $title): self\n    {\n        $property = Title::fromString($title);\n        $key = $this->properties()->titleKey();\n\n        return $this->addProperty($key, $property);\n    }\n\n    public function title(): Title|null\n    {\n        return $this->properties()->title();\n    }\n\n    public function changeParent(PageParent $parent): self\n    {\n        return new self(\n            $this->id,\n            $this->createdTime,\n            $this->lastEditedTime,\n            $this->inTrash,\n            $this->icon,\n            $this->cover,\n            $this->properties,\n            $parent,\n            $this->url,\n        );\n    }\n}\n"
  },
  {
    "path": "src/Pages/PageParent.php",
    "content": "<?php\n\nnamespace Notion\\Pages;\n\n/**\n * @psalm-type PageParentJson = array{\n *      type: \"page_id\"|\"database_id\"|\"workspace\"|\"block_id\",\n *      page_id?: string,\n *      database_id?: string,\n *      workspace?: true,\n *      block_id?: string,\n * }\n *\n * @psalm-immutable\n */\nclass PageParent\n{\n    private function __construct(\n        public readonly PageParentType $type,\n        public readonly string|null $id,\n    ) {\n    }\n\n    public static function database(string $databaseId): self\n    {\n        return new self(PageParentType::Database, $databaseId);\n    }\n\n    public static function page(string $pageId): self\n    {\n        return new self(PageParentType::Page, $pageId);\n    }\n\n    public static function workspace(): self\n    {\n        return new self(PageParentType::Workspace, null);\n    }\n\n    public static function block(string $blockId): self\n    {\n        return new self(PageParentType::Block, $blockId);\n    }\n\n    /**\n     * @psalm-param PageParentJson $array\n     *\n     * @internal\n     */\n    public static function fromArray(array $array): self\n    {\n        $type = PageParentType::from($array[\"type\"]);\n\n        $id = $array[\"page_id\"] ?? $array[\"database_id\"] ?? $array[\"block_id\"] ?? null;\n\n        return new self($type, $id);\n    }\n\n    public function toArray(): array\n    {\n        $array = [];\n\n        if ($this->isDatabase()) {\n            $array[\"database_id\"] = $this->id;\n        }\n        if ($this->isPage()) {\n            $array[\"page_id\"] = $this->id;\n        }\n        if ($this->isWorkspace()) {\n            $array[\"workspace\"] = true;\n        }\n        if ($this->isBlock()) {\n            $array[\"block_id\"] = $this->id;\n        }\n\n        return $array;\n    }\n\n    public function isDatabase(): bool\n    {\n        return $this->type === PageParentType::Database;\n    }\n\n    public function isPage(): bool\n    {\n        return $this->type === PageParentType::Page;\n    }\n\n    public function isWorkspace(): bool\n    {\n        return $this->type === PageParentType::Workspace;\n    }\n\n    public function isBlock(): bool\n    {\n        return $this->type === PageParentType::Block;\n    }\n}\n"
  },
  {
    "path": "src/Pages/PageParentType.php",
    "content": "<?php\n\nnamespace Notion\\Pages;\n\nenum PageParentType: string\n{\n    case Page = \"page_id\";\n    case Database = \"database_id\";\n    case Workspace = \"workspace\";\n    case Block = \"block_id\";\n}\n"
  },
  {
    "path": "src/Pages/Properties/Checkbox.php",
    "content": "<?php\n\nnamespace Notion\\Pages\\Properties;\n\n/**\n * @psalm-type CheckboxJson = array{\n *      id: string,\n *      type: \"checkbox\",\n *      checkbox: bool,\n * }\n *\n * @psalm-immutable\n */\nclass Checkbox implements PropertyInterface\n{\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n        public readonly bool $checked,\n    ) {\n    }\n\n    public static function createChecked(): self\n    {\n        $property = PropertyMetadata::create(\"\", PropertyType::Checkbox);\n\n        return new self($property, true);\n    }\n\n    public static function createUnchecked(): self\n    {\n        $property = PropertyMetadata::create(\"\", PropertyType::Checkbox);\n\n        return new self($property, false);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var CheckboxJson $array */\n\n        $property = PropertyMetadata::fromArray($array);\n\n        $checked = $array[\"checkbox\"];\n\n        return new self($property, $checked);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"checkbox\"] = $this->checked;\n\n        return $array;\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function check(): self\n    {\n        return new self($this->metadata, true);\n    }\n\n    public function uncheck(): self\n    {\n        return new self($this->metadata, false);\n    }\n}\n"
  },
  {
    "path": "src/Pages/Properties/CreatedBy.php",
    "content": "<?php\n\nnamespace Notion\\Pages\\Properties;\n\nuse Notion\\Users\\User;\n\n/**\n * @psalm-import-type UserJson from \\Notion\\Users\\User\n *\n * @psalm-type CreatedByJson = array{\n *      id: string,\n *      type: \"created_by\",\n *      \"created_by\": UserJson,\n * }\n *\n * @psalm-immutable\n */\nclass CreatedBy implements PropertyInterface\n{\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n        public readonly User $user,\n    ) {\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var CreatedByJson $array */\n        $property = PropertyMetadata::fromArray($array);\n        $user = User::fromArray($array[\"created_by\"]);\n\n        return new self($property, $user);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"created_by\"] = $this->user->toArray();\n\n        return $array;\n    }\n}\n"
  },
  {
    "path": "src/Pages/Properties/CreatedTime.php",
    "content": "<?php\n\nnamespace Notion\\Pages\\Properties;\n\nuse DateTimeImmutable;\nuse Notion\\Common\\Date;\n\n/**\n * @psalm-type CreatedTimeJson = array{\n *      id: string,\n *      type: \"created_time\",\n *      created_time: string,\n * }\n *\n * @psalm-immutable\n */\nclass CreatedTime implements PropertyInterface\n{\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n        public readonly DateTimeImmutable $time,\n    ) {\n    }\n\n    public static function create(DateTimeImmutable $time): self\n    {\n        $property = PropertyMetadata::create(\"\", PropertyType::CreatedTime);\n\n        return new self($property, $time);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var CreatedTimeJson $array */\n\n        $property = PropertyMetadata::fromArray($array);\n\n        $time = new DateTimeImmutable($array[\"created_time\"]);\n\n        return new self($property, $time);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"created_time\"] = $this->time->format(Date::FORMAT);\n\n        return $array;\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function changeTime(DateTimeImmutable $time): self\n    {\n        return new self($this->metadata, $time);\n    }\n}\n"
  },
  {
    "path": "src/Pages/Properties/Date.php",
    "content": "<?php\n\nnamespace Notion\\Pages\\Properties;\n\nuse DateTimeImmutable;\nuse Notion\\Common\\Date as CommonDate;\n\n/**\n * @psalm-type DateJson = array{\n *      id: string,\n *      type: \"date\",\n *      date: array{\n *          start: string,\n *          end?: string,\n *      }|null,\n * }\n *\n * @psalm-immutable\n */\nclass Date implements PropertyInterface\n{\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n        public readonly CommonDate|null $date,\n    ) {\n    }\n\n    public static function create(DateTimeImmutable $date): self\n    {\n        $property = PropertyMetadata::create(\"\", PropertyType::Date);\n\n        return new self($property, CommonDate::create($date));\n    }\n\n    public static function createRange(DateTimeImmutable $start, DateTimeImmutable $end): self\n    {\n        $property = PropertyMetadata::create(\"\", PropertyType::Date);\n\n        return new self($property, CommonDate::createRange($start, $end));\n    }\n\n    public static function createEmpty(): self\n    {\n        $metadata = PropertyMetadata::create(\"\", PropertyType::Date);\n\n        return new self($metadata, null);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var DateJson $array */\n\n        $property = PropertyMetadata::fromArray($array);\n\n        $date = $array[\"date\"] !== null ? CommonDate::fromArray($array[\"date\"]) : null;\n\n        return new self($property, $date);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"date\"] = $this->date?->toArray();\n\n        return $array;\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function changeDate(CommonDate $date): self\n    {\n        return new self($this->metadata, $date);\n    }\n\n    public function changeStart(DateTimeImmutable $start): self\n    {\n        return new self($this->metadata, $this->date?->changeStart($start));\n    }\n\n    public function changeEnd(DateTimeImmutable $end): self\n    {\n        return new self($this->metadata, $this->date?->changeEnd($end));\n    }\n\n    public function removeEnd(): self\n    {\n        return new self($this->metadata, $this->date?->removeEnd());\n    }\n\n    public function clear(): self\n    {\n        return new self($this->metadata, null);\n    }\n\n    public function start(): DateTimeImmutable|null\n    {\n        return $this->date?->start;\n    }\n\n    public function end(): DateTimeImmutable|null\n    {\n        return $this->date?->end;\n    }\n\n    public function isRange(): bool\n    {\n        if ($this->date === null) {\n            return false;\n        }\n\n        return $this->date->isRange();\n    }\n\n    public function isEmpty(): bool\n    {\n        return $this->date === null;\n    }\n}\n"
  },
  {
    "path": "src/Pages/Properties/Email.php",
    "content": "<?php\n\nnamespace Notion\\Pages\\Properties;\n\n/**\n * @psalm-type EmailJson = array{\n *      id: string,\n *      type: \"email\",\n *      email: string|null,\n * }\n *\n * @psalm-immutable\n */\nclass Email implements PropertyInterface\n{\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n        public readonly string|null $email,\n    ) {\n    }\n\n    public static function create(string $email): self\n    {\n        $property = PropertyMetadata::create(\"\", PropertyType::Email);\n\n        return new self($property, $email);\n    }\n\n    public static function createEmpty(): self\n    {\n        $property = PropertyMetadata::create(\"\", PropertyType::Email);\n\n        return new self($property, null);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var EmailJson $array */\n\n        $property = PropertyMetadata::fromArray($array);\n\n        $email = $array[\"email\"];\n\n        return new self($property, $email);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"email\"] = $this->email;\n\n        return $array;\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function changeEmail(string $email): self\n    {\n        return new self($this->metadata, $email);\n    }\n\n    public function clear(): self\n    {\n        return new self($this->metadata, null);\n    }\n\n    public function isEmpty(): bool\n    {\n        return $this->email === null;\n    }\n}\n"
  },
  {
    "path": "src/Pages/Properties/Files.php",
    "content": "<?php\n\nnamespace Notion\\Pages\\Properties;\n\nuse Notion\\Common\\File;\n\n/**\n * @psalm-import-type FileJson from \\Notion\\Common\\File\n *\n * @psalm-type FilesJson = array{\n *      id: string,\n *      type: \"files\",\n *      files: FileJson[],\n * }\n *\n * @psalm-immutable\n */\nclass Files implements PropertyInterface\n{\n    /** @param File[] $files */\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n        public readonly array $files,\n    ) {\n    }\n\n    public static function create(File ...$files): self\n    {\n        $property = PropertyMetadata::create(\"\", PropertyType::Files);\n\n        $files = array_map(function (File $f): File {\n            if ($f->name === null) {\n                $f = $f->changeName(\"File\");\n            }\n\n            return $f;\n        }, $files);\n\n        return new self($property, $files);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var FilesJson $array */\n        $property = PropertyMetadata::fromArray($array);\n\n        $files = array_map(fn($f) => File::fromArray($f), $array[\"files\"]);\n\n        return new self($property, $files);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"files\"] = array_map(fn($f) => $f->toArray(), $this->files);\n\n        return $array;\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function changeFiles(File ...$files): self\n    {\n        return new self($this->metadata, $files);\n    }\n\n    public function addFile(File $file): self\n    {\n        $files = array_merge($this->files, [$file]);\n\n        return new self($this->metadata, $files);\n    }\n}\n"
  },
  {
    "path": "src/Pages/Properties/Formula.php",
    "content": "<?php\n\nnamespace Notion\\Pages\\Properties;\n\nuse DateTimeImmutable;\nuse Notion\\Common\\Date;\n\n/**\n * @psalm-type FormulaJson = array{\n *      id: string,\n *      type: \"formula\",\n *      formula: array{\n *          type: \"string\"|\"number\"|\"boolean\"|\"date\",\n *          string?: string,\n *          number?: int|float,\n *          'boolean'?: bool,\n *          date?: array{\n *              start: string,\n *              end: string|null\n *          }\n *      }\n * }\n *\n * @psalm-immutable\n */\nclass Formula implements PropertyInterface\n{\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n        public readonly FormulaType $type,\n        public readonly string|null $string,\n        public readonly int|float|null $number,\n        public readonly bool|null $boolean,\n        public readonly Date|null $date,\n    ) {\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var FormulaJson $array */\n        $metadata = PropertyMetadata::fromArray($array);\n\n        $formula = $array[\"formula\"];\n        $type = FormulaType::from($formula[\"type\"]);\n\n        $string = $formula[\"string\"] ?? null;\n        $number = $formula[\"number\"] ?? null;\n        $boolean = isset($formula[\"boolean\"]) ? $formula[\"boolean\"] : null;\n\n        $date = null;\n        if (isset($formula[\"date\"])) {\n            $date = Date::fromArray($formula[\"date\"]);\n        }\n\n        return new self($metadata, $type, $string, $number, $boolean, $date);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"formula\"] = [ \"type\" => $this->type->value ];\n\n        switch ($this->type) {\n            case FormulaType::String:\n                $array[\"formula\"][\"string\"] = $this->string;\n                break;\n            case FormulaType::Number:\n                $array[\"formula\"][\"number\"] = $this->number;\n                break;\n            case FormulaType::Boolean:\n                $array[\"formula\"][\"boolean\"] = $this->boolean;\n                break;\n            case FormulaType::Date:\n                $array[\"formula\"][\"date\"] = $this->date?->toArray();\n                break;\n        }\n\n        return $array;\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n}\n"
  },
  {
    "path": "src/Pages/Properties/FormulaType.php",
    "content": "<?php\n\nnamespace Notion\\Pages\\Properties;\n\nenum FormulaType: string\n{\n    case String = \"string\";\n    case Number = \"number\";\n    case Boolean = \"boolean\";\n    case Date = \"date\";\n}\n"
  },
  {
    "path": "src/Pages/Properties/LastEditedBy.php",
    "content": "<?php\n\nnamespace Notion\\Pages\\Properties;\n\nuse Notion\\Users\\User;\n\n/**\n * @psalm-import-type UserJson from \\Notion\\Users\\User\n *\n * @psalm-type LastEditedByJson = array{\n *      id: string,\n *      type: \"last_edited_by\",\n *      \"last_edited_by\": UserJson,\n * }\n *\n * @psalm-immutable\n */\nclass LastEditedBy implements PropertyInterface\n{\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n        public readonly User $user\n    ) {\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var LastEditedByJson $array */\n\n        $property = PropertyMetadata::fromArray($array);\n        $user = User::fromArray($array[\"last_edited_by\"]);\n\n        return new self($property, $user);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"last_edited_by\"] = $this->user->toArray();\n\n        return $array;\n    }\n}\n"
  },
  {
    "path": "src/Pages/Properties/LastEditedTime.php",
    "content": "<?php\n\nnamespace Notion\\Pages\\Properties;\n\nuse DateTimeImmutable;\nuse Notion\\Common\\Date;\n\n/**\n * @psalm-type LastEditedTimeJson = array{\n *      id: string,\n *      type: \"last_edited_time\",\n *      last_edited_time: string,\n * }\n *\n * @psalm-immutable\n */\nclass LastEditedTime implements PropertyInterface\n{\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n        public readonly DateTimeImmutable $time,\n    ) {\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var LastEditedTimeJson $array */\n\n        $metadata = PropertyMetadata::fromArray($array);\n\n        $time = new DateTimeImmutable($array[\"last_edited_time\"]);\n\n        return new self($metadata, $time);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"last_edited_time\"] = $this->time->format(Date::FORMAT);\n\n        return $array;\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n}\n"
  },
  {
    "path": "src/Pages/Properties/MultiSelect.php",
    "content": "<?php\n\nnamespace Notion\\Pages\\Properties;\n\nuse Notion\\Databases\\Properties\\SelectOption;\n\n/**\n * @psalm-import-type SelectOptionJson from SelectOption\n *\n * @psalm-type MultiSelectJson = array{\n *      id: string,\n *      type: \"multi_select\",\n *      multi_select: SelectOptionJson[],\n * }\n *\n * @psalm-immutable\n */\nclass MultiSelect implements PropertyInterface\n{\n    /** @param SelectOption[] $options */\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n        public readonly array $options\n    ) {\n    }\n\n    public static function fromIds(string ...$ids): self\n    {\n        $metadata = PropertyMetadata::create(\"\", PropertyType::MultiSelect);\n        $options = array_map(fn(string $id) => SelectOption::fromId($id), $ids);\n\n        return new self($metadata, $options);\n    }\n\n    public static function fromNames(string ...$names): self\n    {\n        $metadata = PropertyMetadata::create(\"\", PropertyType::MultiSelect);\n        $options = array_map(fn(string $name) => SelectOption::fromName($name), $names);\n\n        return new self($metadata, $options);\n    }\n\n    public static function fromOptions(SelectOption ...$options): self\n    {\n        $metadata = PropertyMetadata::create(\"\", PropertyType::MultiSelect);\n\n        return new self($metadata, $options);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var MultiSelectJson $array */\n        $metadata = PropertyMetadata::fromArray($array);\n\n        $options = array_map(fn(array $option) => SelectOption::fromArray($option), $array[\"multi_select\"]);\n\n        return new self($metadata, $options);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n        $array[\"multi_select\"] = array_map(fn (SelectOption $option) => $option->toArray(), $this->options);\n\n        return $array;\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function addOption(SelectOption $option): self\n    {\n        $options = $this->options;\n        $options[] = $option;\n\n        return new self($this->metadata, $options);\n    }\n\n    public function removeOption(string $optionId): self\n    {\n        return new self(\n            $this->metadata,\n            array_filter($this->options, fn (SelectOption $o) => $o->id !== $optionId),\n        );\n    }\n\n    public function changeOptions(SelectOption ...$options): self\n    {\n        return new self($this->metadata, $options);\n    }\n}\n"
  },
  {
    "path": "src/Pages/Properties/Number.php",
    "content": "<?php\n\nnamespace Notion\\Pages\\Properties;\n\n/**\n * @psalm-type NumberJson = array{\n *      id: string,\n *      type: \"number\",\n *      number: int|float|null,\n * }\n *\n * @psalm-immutable\n */\nclass Number implements PropertyInterface\n{\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n        public readonly int|float|null $number\n    ) {\n    }\n\n    public static function create(int|float $number): self\n    {\n        $property = PropertyMetadata::create(\"\", PropertyType::Number);\n\n        return new self($property, $number);\n    }\n\n    public static function createEmpty(): self\n    {\n        $property = PropertyMetadata::create(\"\", PropertyType::Number);\n\n        return new self($property, null);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var NumberJson $array */\n\n        $property = PropertyMetadata::fromArray($array);\n\n        $number = $array[\"number\"];\n\n        return new self($property, $number);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"number\"] = $this->number;\n\n        return $array;\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function changeNumber(int|float $number): self\n    {\n        return new self($this->metadata, $number);\n    }\n\n    public function clear(): self\n    {\n        return new self($this->metadata, null);\n    }\n\n    public function isEmpty(): bool\n    {\n        return $this->number === null;\n    }\n}\n"
  },
  {
    "path": "src/Pages/Properties/People.php",
    "content": "<?php\n\nnamespace Notion\\Pages\\Properties;\n\nuse Notion\\Users\\User;\n\n/**\n * @psalm-import-type UserJson from \\Notion\\Users\\User\n *\n * @psalm-type PeopleJson = array{\n *      id: string,\n *      type: \"people\",\n *      people: UserJson[],\n * }\n *\n * @psalm-immutable\n */\nclass People implements PropertyInterface\n{\n    /** @param User[] $users */\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n        public readonly array $users\n    ) {\n    }\n\n    public static function create(User ...$users): self\n    {\n        $property = PropertyMetadata::create(\"\", PropertyType::People);\n\n        return new self($property, $users);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var PeopleJson $array */\n\n        $property = PropertyMetadata::fromArray($array);\n\n        $users = array_map(\n            function (array $userArray): User {\n                return User::fromArray($userArray);\n            },\n            $array[\"people\"],\n        );\n\n        return new self($property, $users);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"people\"] = array_map(\n            function (User $user): array {\n                return [ \"id\" => $user->id ];\n            },\n            $this->users,\n        );\n\n        return $array;\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function changePeople(User ...$users): self\n    {\n        return new self($this->metadata, $users);\n    }\n\n    public function addPerson(User $user): self\n    {\n        $users = $this->users;\n        $users[] = $user;\n\n        return new self($this->metadata, $users);\n    }\n\n    public function removePerson(string $userId): self\n    {\n        return new self(\n            $this->metadata,\n            array_filter($this->users, fn (User $u) => $u->id !== $userId),\n        );\n    }\n}\n"
  },
  {
    "path": "src/Pages/Properties/PhoneNumber.php",
    "content": "<?php\n\nnamespace Notion\\Pages\\Properties;\n\n/**\n * @psalm-type PhoneNumberJson = array{\n *      id: string,\n *      type: \"phone_number\",\n *      phone_number: string|null,\n * }\n *\n * @psalm-immutable\n */\nclass PhoneNumber implements PropertyInterface\n{\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n        public readonly string|null $phone\n    ) {\n    }\n\n    public static function create(string $phone): self\n    {\n        $property = PropertyMetadata::create(\"\", PropertyType::PhoneNumber);\n\n        return new self($property, $phone);\n    }\n\n    public static function createEmpty(): self\n    {\n        $property = PropertyMetadata::create(\"\", PropertyType::PhoneNumber);\n\n        return new self($property, null);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var PhoneNumberJson $array */\n\n        $property = PropertyMetadata::fromArray($array);\n\n        $phone = $array[\"phone_number\"];\n\n        return new self($property, $phone);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"phone_number\"] = $this->phone;\n\n        return $array;\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function changePhone(string $phone): self\n    {\n        return new self($this->metadata, $phone);\n    }\n\n    public function clear(): self\n    {\n        return new self($this->metadata, null);\n    }\n\n    public function isEmpty(): bool\n    {\n        return $this->phone === null;\n    }\n}\n"
  },
  {
    "path": "src/Pages/Properties/PropertyCollection.php",
    "content": "<?php\n\nnamespace Notion\\Pages\\Properties;\n\n/** @psalm-immutable */\nfinal class PropertyCollection\n{\n    /**\n     * @param array<string, PropertyInterface> $properties\n     */\n    private function __construct(\n        private readonly array $properties,\n    ) {\n    }\n\n    /**\n     * @param array<string, PropertyInterface> $properties\n     *\n     * @psalm-mutation-free\n     */\n    public static function create(array $properties): self\n    {\n        return new self($properties);\n    }\n\n    public function add(string $propertyName, PropertyInterface $property): self\n    {\n        $properties = $this->properties;\n        $properties[$propertyName] = $property;\n\n        return new self($properties);\n    }\n\n    public function change(string $propertyName, PropertyInterface $property): self\n    {\n        return $this->add($propertyName, $property);\n    }\n\n    public function get(string $propertyName): PropertyInterface\n    {\n        if (array_key_exists($propertyName, $this->properties)) {\n            return $this->properties[$propertyName];\n        }\n\n        throw new \\Exception(\"Property '{$propertyName}' not found\");\n    }\n\n    public function getById(string $propertyId): PropertyInterface\n    {\n        foreach ($this->properties as $property) {\n            if ($property->metadata()->id === $propertyId) {\n                return $property;\n            }\n        }\n\n        throw new \\Exception(\"Property '{$propertyId}' not found.\");\n    }\n\n    /** @return array<string, PropertyInterface> */\n    public function getAll(): array\n    {\n        return $this->properties;\n    }\n\n    public function title(): Title|null\n    {\n        foreach ($this->properties as $property) {\n            if ($property::class === Title::class) {\n                return $property;\n            }\n        }\n\n        return null;\n    }\n\n    public function titleKey(): string\n    {\n        foreach ($this->properties as $key => $property) {\n            if ($property::class === Title::class) {\n                return $key;\n            }\n        }\n\n        return \"title\";\n    }\n\n    public function getCheckbox(string $propertyName): Checkbox\n    {\n        return $this->getTyped($propertyName, Checkbox::class);\n    }\n\n    public function getCheckboxById(string $propertyId): Checkbox\n    {\n        return $this->getTypedById($propertyId, Checkbox::class);\n    }\n\n    public function getCreatedBy(string $propertyName): CreatedBy\n    {\n        return $this->getTyped($propertyName, CreatedBy::class);\n    }\n\n    public function getCreatedByById(string $propertyId): CreatedBy\n    {\n        return $this->getTypedById($propertyId, CreatedBy::class);\n    }\n\n    public function getCreatedTime(string $propertyName): CreatedTime\n    {\n        return $this->getTyped($propertyName, CreatedTime::class);\n    }\n\n    public function getCreatedTimeById(string $propertyId): CreatedTime\n    {\n        return $this->getTypedById($propertyId, CreatedTime::class);\n    }\n\n    public function getDate(string $propertyName): Date\n    {\n        return $this->getTyped($propertyName, Date::class);\n    }\n\n    public function getDateById(string $propertyId): Date\n    {\n        return $this->getTypedById($propertyId, Date::class);\n    }\n\n    public function getEmail(string $propertyName): Email\n    {\n        return $this->getTyped($propertyName, Email::class);\n    }\n\n    public function getEmailById(string $propertyId): Email\n    {\n        return $this->getTypedById($propertyId, Email::class);\n    }\n\n    public function getFiles(string $propertyName): Files\n    {\n        return $this->getTyped($propertyName, Files::class);\n    }\n\n    public function getFilesById(string $propertyId): Files\n    {\n        return $this->getTypedById($propertyId, Files::class);\n    }\n\n    public function getFormula(string $propertyName): Formula\n    {\n        return $this->getTyped($propertyName, Formula::class);\n    }\n\n    public function getFormulaById(string $propertyId): Formula\n    {\n        return $this->getTypedById($propertyId, Formula::class);\n    }\n\n    public function getLastEditedBy(string $propertyName): LastEditedBy\n    {\n        return $this->getTyped($propertyName, LastEditedBy::class);\n    }\n\n    public function getLastEditedByById(string $propertyId): LastEditedBy\n    {\n        return $this->getTypedById($propertyId, LastEditedBy::class);\n    }\n\n    public function getLastEditedTime(string $propertyName): LastEditedTime\n    {\n        return $this->getTyped($propertyName, LastEditedTime::class);\n    }\n\n    public function getLastEditedTimeById(string $propertyId): LastEditedTime\n    {\n        return $this->getTypedById($propertyId, LastEditedTime::class);\n    }\n\n    public function getMultiSelect(string $propertyName): MultiSelect\n    {\n        return $this->getTyped($propertyName, MultiSelect::class);\n    }\n\n    public function getMultiSelectById(string $propertyId): MultiSelect\n    {\n        return $this->getTypedById($propertyId, MultiSelect::class);\n    }\n\n    public function getNumber(string $propertyName): Number\n    {\n        return $this->getTyped($propertyName, Number::class);\n    }\n\n    public function getNumberById(string $propertyId): Number\n    {\n        return $this->getTypedById($propertyId, Number::class);\n    }\n\n    public function getPeople(string $propertyName): People\n    {\n        return $this->getTyped($propertyName, People::class);\n    }\n\n    public function getPeopleById(string $propertyId): People\n    {\n        return $this->getTypedById($propertyId, People::class);\n    }\n\n    public function getPhoneNumber(string $propertyName): PhoneNumber\n    {\n        return $this->getTyped($propertyName, PhoneNumber::class);\n    }\n\n    public function getPhoneNumberById(string $propertyId): PhoneNumber\n    {\n        return $this->getTypedById($propertyId, PhoneNumber::class);\n    }\n\n    public function getRelation(string $propertyName): Relation\n    {\n        return $this->getTyped($propertyName, Relation::class);\n    }\n\n    public function getRelationById(string $propertyId): Relation\n    {\n        return $this->getTypedById($propertyId, Relation::class);\n    }\n\n    public function getRichText(string $propertyName): RichTextProperty\n    {\n        return $this->getTyped($propertyName, RichTextProperty::class);\n    }\n\n    public function getRichTextById(string $propertyId): RichTextProperty\n    {\n        return $this->getTypedById($propertyId, RichTextProperty::class);\n    }\n\n    public function getSelect(string $propertyName): Select\n    {\n        return $this->getTyped($propertyName, Select::class);\n    }\n\n    public function getSelectById(string $propertyId): Select\n    {\n        return $this->getTypedById($propertyId, Select::class);\n    }\n\n    public function getStatus(string $propertyName): Status\n    {\n        return $this->getTyped($propertyName, Status::class);\n    }\n\n    public function getStatusById(string $propertyId): Status\n    {\n        return $this->getTypedById($propertyId, Status::class);\n    }\n\n    public function getUniqueId(string $propertyName): UniqueId\n    {\n        return $this->getTyped($propertyName, UniqueId::class);\n    }\n\n    public function getUniqueIdById(string $propertyId): UniqueId\n    {\n        return $this->getTypedById($propertyId, UniqueId::class);\n    }\n\n    public function getUrl(string $propertyName): Url\n    {\n        return $this->getTyped($propertyName, Url::class);\n    }\n\n    public function getUrlById(string $propertyId): Url\n    {\n        return $this->getTypedById($propertyId, Url::class);\n    }\n\n    /**\n     * @template T of PropertyInterface\n     * @psalm-param class-string<T> $propertyType\n     *\n     * @psalm-return T\n     */\n    private function getTyped(string $propertyName, string $propertyType): PropertyInterface\n    {\n        $property = $this->get($propertyName);\n\n        if ($property::class !== $propertyType) {\n            throw new \\TypeError(\"Property '{$propertyName}' is not of type {$propertyType}.\");\n        }\n\n        /** @psalm-var T $property */\n        return $property;\n    }\n\n    /**\n     * @template T of PropertyInterface\n     * @psalm-param class-string<T> $propertyType\n     *\n     * @psalm-return T\n     */\n    private function getTypedById(string $propertyId, string $propertyType): PropertyInterface\n    {\n        $property = $this->getById($propertyId);\n\n        if ($property::class !== $propertyType) {\n            throw new \\TypeError(\"Property with ID '{$propertyId}' is not of type {$propertyType}.\");\n        }\n\n        /** @psalm-var T $property */\n        return $property;\n    }\n}\n"
  },
  {
    "path": "src/Pages/Properties/PropertyFactory.php",
    "content": "<?php\n\nnamespace Notion\\Pages\\Properties;\n\nclass PropertyFactory\n{\n    /**\n     * @param array{ type: string, ... } $array\n     */\n    public static function fromArray(array $array): PropertyInterface\n    {\n        $type = $array[\"type\"];\n\n        return match ($type) {\n            PropertyType::Checkbox->value       => Checkbox::fromArray($array),\n            PropertyType::CreatedBy->value      => CreatedBy::fromArray($array),\n            PropertyType::CreatedTime->value    => CreatedTime::fromArray($array),\n            PropertyType::Date->value           => Date::fromArray($array),\n            PropertyType::Email->value          => Email::fromArray($array),\n            PropertyType::Files->value          => Files::fromArray($array),\n            PropertyType::Formula->value        => Formula::fromArray($array),\n            PropertyType::LastEditedBy->value   => LastEditedBy::fromArray($array),\n            PropertyType::LastEditedTime->value => LastEditedTime::fromArray($array),\n            PropertyType::MultiSelect->value    => MultiSelect::fromArray($array),\n            PropertyType::Number->value         => Number::fromArray($array),\n            PropertyType::People->value         => People::fromArray($array),\n            PropertyType::PhoneNumber->value    => PhoneNumber::fromArray($array),\n            PropertyType::Relation->value       => Relation::fromArray($array),\n            PropertyType::RichText->value       => RichTextProperty::fromArray($array),\n            PropertyType::Select->value         => Select::fromArray($array),\n            PropertyType::Status->value         => Status::fromArray($array),\n            PropertyType::Title->value          => Title::fromArray($array),\n            PropertyType::UniqueId->value       => UniqueId::fromArray($array),\n            PropertyType::Url->value            => Url::fromArray($array),\n            default                             => Unknown::fromArray($array),\n        };\n    }\n}\n"
  },
  {
    "path": "src/Pages/Properties/PropertyInterface.php",
    "content": "<?php\n\nnamespace Notion\\Pages\\Properties;\n\n/** @psalm-immutable */\ninterface PropertyInterface\n{\n    /** @internal */\n    public static function fromArray(array $array): self;\n    /** @internal */\n    public function toArray(): array;\n\n    public function metadata(): PropertyMetadata;\n}\n"
  },
  {
    "path": "src/Pages/Properties/PropertyMetadata.php",
    "content": "<?php\n\nnamespace Notion\\Pages\\Properties;\n\n/**\n * @psalm-type PropertyMetadataJson = array{ id: string, type: string, ... }\n *\n * @psalm-immutable\n */\nclass PropertyMetadata\n{\n    private function __construct(\n        public readonly string $id,\n        public readonly PropertyType $type,\n        private readonly string|null $unknownType = null,\n    ) {\n    }\n\n    /** @psalm-mutation-free */\n    public static function create(string $id, PropertyType $type): self\n    {\n        return new self($id, $type);\n    }\n\n    /**\n     * @param PropertyMetadataJson $array\n     *\n     * @internal\n     */\n    public static function fromArray(array $array): self\n    {\n        $type = PropertyType::tryFrom($array[\"type\"]) ?? PropertyType::Unknown;\n\n        return new self(\n            $array[\"id\"],\n            $type,\n            $type === PropertyType::Unknown ? $array[\"type\"] : null,\n        );\n    }\n\n    public function toArray(): array\n    {\n        $type = $this->type !== PropertyType::Unknown ? $this->type->value : $this->unknownType;\n\n        return [\n            \"id\"   => $this->id,\n            \"type\" => $type,\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Pages/Properties/PropertyType.php",
    "content": "<?php\n\nnamespace Notion\\Pages\\Properties;\n\nenum PropertyType: string\n{\n    case Checkbox = \"checkbox\";\n    case CreatedBy = \"created_by\";\n    case CreatedTime = \"created_time\";\n    case Date = \"date\";\n    case Email = \"email\";\n    case Files = \"files\";\n    case Formula = \"formula\";\n    case LastEditedBy = \"last_edited_by\";\n    case LastEditedTime = \"last_edited_time\";\n    case MultiSelect = \"multi_select\";\n    case Number = \"number\";\n    case People = \"people\";\n    case PhoneNumber = \"phone_number\";\n    case Relation = \"relation\";\n    case RichText = \"rich_text\";\n    case Rollup = \"rollup\";\n    case Select = \"select\";\n    case Status = \"status\";\n    case Title = \"title\";\n    case UniqueId = \"unique_id\";\n    case Url = \"url\";\n    case Unknown = \"unknown\";\n}\n"
  },
  {
    "path": "src/Pages/Properties/Relation.php",
    "content": "<?php\n\nnamespace Notion\\Pages\\Properties;\n\n/**\n * @psalm-type RelationJson = array{\n *      id: string,\n *      type: \"relation\",\n *      relation: array{ id: non-empty-string }[],\n * }\n *\n * @psalm-immutable\n */\nclass Relation implements PropertyInterface\n{\n    /** @param string[] $pageIds */\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n        public readonly array $pageIds\n    ) {\n    }\n\n    public static function create(string ...$pageIds): self\n    {\n        $property = PropertyMetadata::create(\"\", PropertyType::Relation);\n\n        return new self($property, $pageIds);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var RelationJson $array */\n\n        $property = PropertyMetadata::fromArray($array);\n\n        $pageIds = array_map(\n            function (array $pageReference): string {\n                return $pageReference[\"id\"];\n            },\n            $array[\"relation\"],\n        );\n\n        return new self($property, $pageIds);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"relation\"] = array_map(\n            function (string $pageId): array {\n                return [ \"id\" => $pageId ];\n            },\n            $this->pageIds,\n        );\n\n        return $array;\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    /** @param string[] $pageIds */\n    public function changeRelations(string ...$pageIds): self\n    {\n        return new self($this->metadata, $pageIds);\n    }\n\n    public function addRelation(string $pageId): self\n    {\n        $pageIds = $this->pageIds;\n        $pageIds[] = $pageId;\n\n        return new self($this->metadata, $pageIds);\n    }\n\n    public function removeRelation(string $pageId): self\n    {\n        return new self(\n            $this->metadata,\n            array_filter($this->pageIds, fn (string $p) => $p !== $pageId),\n        );\n    }\n}\n"
  },
  {
    "path": "src/Pages/Properties/RichTextProperty.php",
    "content": "<?php\n\nnamespace Notion\\Pages\\Properties;\n\nuse Notion\\Common\\RichText;\n\n/**\n * @psalm-import-type RichTextJson from \\Notion\\Common\\RichText\n *\n * @psalm-type RichTextPropertyMetadataJson = array{\n *      id: string,\n *      type: \"rich_text\",\n *      rich_text: list<RichTextJson>,\n * }\n *\n * @psalm-immutable\n */\nclass RichTextProperty implements PropertyInterface\n{\n    /** @param RichText[] $text */\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n        public readonly array $text\n    ) {\n    }\n\n    public static function fromText(RichText ...$texts): self\n    {\n        $metadata = PropertyMetadata::create(\"\", PropertyType::RichText);\n\n        return new self($metadata, $texts);\n    }\n\n    public static function fromString(string $text): self\n    {\n        $metadata = PropertyMetadata::create(\"\", PropertyType::RichText);\n        $texts = [ RichText::fromString($text) ];\n\n        return new self($metadata, $texts);\n    }\n\n    public static function createEmpty(): self\n    {\n        $metadata = PropertyMetadata::create(\"\", PropertyType::RichText);\n\n        return new self($metadata, []);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var RichTextPropertyMetadataJson $array */\n\n        $metadata = PropertyMetadata::fromArray($array);\n\n        $text = array_map(\n            function (array $richTextArray): RichText {\n                return RichText::fromArray($richTextArray);\n            },\n            $array[\"rich_text\"],\n        );\n\n        return new self($metadata, $text);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"rich_text\"] = array_map(fn(RichText $richText) => $richText->toArray(), $this->text);\n\n        return $array;\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function changeText(RichText ...$texts): self\n    {\n        return new self($this->metadata, $texts);\n    }\n\n    public function clear(): self\n    {\n        return new self($this->metadata, []);\n    }\n\n    public function isEmpty(): bool\n    {\n        return count($this->text) === 0;\n    }\n\n    public function toString(): string\n    {\n        $string = \"\";\n        foreach ($this->text as $textPart) {\n            $string = $string . $textPart->plainText;\n        }\n\n        return $string;\n    }\n}\n"
  },
  {
    "path": "src/Pages/Properties/Select.php",
    "content": "<?php\n\nnamespace Notion\\Pages\\Properties;\n\nuse Notion\\Databases\\Properties\\SelectOption;\n\n/**\n * @psalm-type SelectJson = array{\n *      id: string,\n *      type: \"select\",\n *      select: array{ id: string, name: string, color: string }|null\n * }\n *\n * @psalm-immutable\n */\nclass Select implements PropertyInterface\n{\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n        public readonly SelectOption|null $option\n    ) {\n    }\n\n    public static function fromId(string $id): self\n    {\n        $metadata = PropertyMetadata::create(\"\", PropertyType::Select);\n        $option = SelectOption::fromId($id);\n\n        return new self($metadata, $option);\n    }\n\n    public static function fromName(string $name): self\n    {\n        $metadata = PropertyMetadata::create(\"\", PropertyType::Select);\n        $option = SelectOption::fromName($name);\n\n        return new self($metadata, $option);\n    }\n\n    public static function fromOption(SelectOption $option): self\n    {\n        $metadata = PropertyMetadata::create(\"\", PropertyType::Select);\n\n        return new self($metadata, $option);\n    }\n\n    public static function createEmpty(): self\n    {\n        $metadata = PropertyMetadata::create(\"\", PropertyType::Select);\n\n        return new self($metadata, null);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var SelectJson $array */\n        $metadata = PropertyMetadata::fromArray($array);\n\n        $option = $array[\"select\"] ? SelectOption::fromArray($array[\"select\"]) : null;\n\n        return new self($metadata, $option);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n        $array[\"select\"] = $this->option?->toArray();\n\n        return $array;\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function changeOption(SelectOption $option): self\n    {\n        return new self($this->metadata, $option);\n    }\n\n    public function clear(): self\n    {\n        return new self($this->metadata, null);\n    }\n\n    public function isEmpty(): bool\n    {\n        return $this->option === null;\n    }\n}\n"
  },
  {
    "path": "src/Pages/Properties/Status.php",
    "content": "<?php\n\nnamespace Notion\\Pages\\Properties;\n\nuse Notion\\Common\\Color;\nuse Notion\\Databases\\Properties\\StatusOption;\n\n/**\n * @psalm-type StatusJson = array{\n *      id: string,\n *      type: \"status\",\n *      status: array{ id: string, name: string, color: string }\n * }\n *\n * @psalm-immutable\n */\nclass Status implements PropertyInterface\n{\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n        public readonly StatusOption $option,\n    ) {\n    }\n\n    public static function fromId(string $id): self\n    {\n        $metadata = PropertyMetadata::create(\"\", PropertyType::Status);\n        $option = StatusOption::fromId($id);\n\n        return new self($metadata, $option);\n    }\n\n    public static function fromName(string $name): self\n    {\n        $metadata = PropertyMetadata::create(\"\", PropertyType::Status);\n        $option = StatusOption::fromName($name);\n\n        return new self($metadata, $option);\n    }\n\n    public function changeColor(Color $color): self\n    {\n        return new self($this->metadata, $this->option->changeColor($color));\n    }\n\n\n    public static function fromOption(StatusOption $option): self\n    {\n        $metadata = PropertyMetadata::create(\"\", PropertyType::Status);\n\n        return new self($metadata, $option);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var StatusJson $array */\n        $metadata = PropertyMetadata::fromArray($array);\n\n        $option = StatusOption::fromArray($array[\"status\"]);\n\n        return new self($metadata, $option);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"status\"] = $this->option->toArray();\n\n        return $array;\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function changeOption(StatusOption $option): self\n    {\n        return new self($this->metadata, $option);\n    }\n}\n"
  },
  {
    "path": "src/Pages/Properties/Title.php",
    "content": "<?php\n\nnamespace Notion\\Pages\\Properties;\n\nuse Notion\\Common\\RichText;\n\nuse function PHPUnit\\Framework\\isEmpty;\n\n/**\n * @psalm-import-type RichTextJson from \\Notion\\Common\\RichText\n *\n * @psalm-type TitleJson = array{\n *      id: \"title\",\n *      type: \"title\",\n *      title: list<RichTextJson>,\n * }\n *\n * @psalm-immutable\n */\nclass Title implements PropertyInterface\n{\n    /** @param RichText[] $title */\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n        public readonly array $title\n    ) {\n    }\n\n    /** @psalm-mutation-free */\n    public static function fromText(RichText ...$title): self\n    {\n        $property = PropertyMetadata::create(\"title\", PropertyType::Title);\n\n        return new self($property, $title);\n    }\n\n    /** @psalm-mutation-free */\n    public static function fromString(string $title): self\n    {\n        $title = RichText::fromString($title);\n\n        return self::fromText($title);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var TitleJson $array */\n\n        $property = PropertyMetadata::fromArray($array);\n\n        $title = array_map(\n            function (array $richTextArray): RichText {\n                return RichText::fromArray($richTextArray);\n            },\n            $array[\"title\"],\n        );\n\n        return new self($property, $title);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"title\"] = array_map(fn(RichText $richText) => $richText->toArray(), $this->title);\n\n        return $array;\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function change(RichText ...$title): self\n    {\n        return new self($this->metadata, $title);\n    }\n\n    public function isEmpty(): bool\n    {\n        $title = RichText::multipleToString(...$this->title);\n\n        return strlen(trim($title)) === 0;\n    }\n\n    public function toString(): string\n    {\n        return RichText::multipleToString(...$this->title);\n    }\n}\n"
  },
  {
    "path": "src/Pages/Properties/UniqueId.php",
    "content": "<?php\n\nnamespace Notion\\Pages\\Properties;\n\n/**\n * @psalm-type UniqueIdJson = array{\n *      id: string,\n *      type: \"unique_id\",\n *      unique_id: array{\n *          number: int,\n *          prefix: string|null,\n *      },\n * }\n *\n * @psalm-immutable\n */\nclass UniqueId implements PropertyInterface\n{\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n        public readonly int $number,\n        public readonly string|null $prefix,\n    ) {\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var UniqueIdJson $array */\n        $metadata = PropertyMetadata::fromArray($array);\n\n        $number = $array[\"unique_id\"][\"number\"];\n        $prefix = $array[\"unique_id\"][\"prefix\"];\n\n        return new self($metadata, $number, $prefix);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"unique_id\"] = [\n            \"number\" => $this->number,\n            \"prefix\" => $this->prefix,\n        ];\n\n        return $array;\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n}\n"
  },
  {
    "path": "src/Pages/Properties/Unknown.php",
    "content": "<?php\n\nnamespace Notion\\Pages\\Properties;\n\n/**\n * @psalm-type UnknownJson = array{\n *      id: string,\n *      type: string\n * }\n *\n * @psalm-immutable\n */\nclass Unknown implements PropertyInterface\n{\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n        private readonly array $data,\n    ) {\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var UnknownJson $array */\n        $metadata = PropertyMetadata::fromArray($array);\n\n        return new self($metadata, $array);\n    }\n\n    public function toArray(): array\n    {\n        return $this->data;\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n}\n"
  },
  {
    "path": "src/Pages/Properties/Url.php",
    "content": "<?php\n\nnamespace Notion\\Pages\\Properties;\n\n/**\n * @psalm-type UrlJson = array{\n *      id: string,\n *      type: \"url\",\n *      url: string|null,\n * }\n *\n * @psalm-immutable\n */\nclass Url implements PropertyInterface\n{\n    private function __construct(\n        private readonly PropertyMetadata $metadata,\n        public readonly string|null $url\n    ) {\n    }\n\n    public static function create(string $url): self\n    {\n        $metadata = PropertyMetadata::create(\"\", PropertyType::Url);\n\n        return new self($metadata, $url);\n    }\n\n    public static function createEmpty(): self\n    {\n        $metadata = PropertyMetadata::create(\"\", PropertyType::Url);\n\n        return new self($metadata, null);\n    }\n\n    public static function fromArray(array $array): self\n    {\n        /** @psalm-var UrlJson $array */\n\n        $metadata = PropertyMetadata::fromArray($array);\n\n        $url = $array[\"url\"];\n\n        return new self($metadata, $url);\n    }\n\n    public function toArray(): array\n    {\n        $array = $this->metadata->toArray();\n\n        $array[\"url\"] = $this->url;\n\n        return $array;\n    }\n\n    public function metadata(): PropertyMetadata\n    {\n        return $this->metadata;\n    }\n\n    public function changeUrl(string $url): self\n    {\n        return new self($this->metadata, $url);\n    }\n\n    public function clear(): self\n    {\n        return new self($this->metadata, null);\n    }\n\n    public function isEmpty(): bool\n    {\n        return $this->url === null;\n    }\n}\n"
  },
  {
    "path": "src/Search/Client.php",
    "content": "<?php\n\nnamespace Notion\\Search;\n\nuse Notion\\Configuration;\nuse Notion\\Infrastructure\\Http;\nuse stdClass;\n\n/** @psalm-import-type ResultJson from \\Notion\\Search\\Result */\nclass Client\n{\n    /**\n     * @internal Use `\\Notion\\Notion::search()` instead\n     */\n    public function __construct(\n        private readonly Configuration $config,\n    ) {\n    }\n\n    public function search(Query $query): Result\n    {\n        $data = $query->toArray();\n        if (empty($data)) {\n            $data = new stdClass();\n        }\n\n        $json = json_encode($data);\n        $url = \"https://api.notion.com/v1/search\";\n        $request = Http::createRequest($url, $this->config)\n            ->withMethod(\"POST\")\n            ->withHeader(\"Content-Type\", \"application/json\");\n        $request->getBody()->write($json);\n\n        /** @psalm-var ResultJson $body */\n        $body = Http::sendRequest($request, $this->config);\n\n        return Result::fromArray($body);\n    }\n}\n"
  },
  {
    "path": "src/Search/Filter.php",
    "content": "<?php\n\nnamespace Notion\\Search;\n\n/** @psalm-immutable */\nclass Filter\n{\n    private function __construct(\n        public readonly FilterValue $value,\n        public readonly FilterProperty $property,\n    ) {\n    }\n\n    /** @psalm-mutation-free */\n    public static function byPages(): self\n    {\n        return new self(FilterValue::Page, FilterProperty::Object);\n    }\n\n    /** @psalm-mutation-free */\n    public static function byDatabases(): self\n    {\n        return new self(FilterValue::Database, FilterProperty::Object);\n    }\n\n    /**\n     * @internal\n     *\n     * @return array{ value: \"page\"|\"database\", property: \"object\" }\n     */\n    public function toArray(): array\n    {\n        return [\n            \"value\" => $this->value->value,\n            \"property\" => $this->property->value,\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Search/FilterProperty.php",
    "content": "<?php\n\nnamespace Notion\\Search;\n\nenum FilterProperty: string\n{\n    case Object = \"object\";\n}\n"
  },
  {
    "path": "src/Search/FilterValue.php",
    "content": "<?php\n\nnamespace Notion\\Search;\n\nenum FilterValue: string\n{\n    case Page = \"page\";\n    case Database = \"database\";\n}\n"
  },
  {
    "path": "src/Search/Query.php",
    "content": "<?php\n\nnamespace Notion\\Search;\n\n/** @psalm-immutable */\nclass Query\n{\n    private function __construct(\n        public readonly string|null $query = null,\n        public readonly Filter|null $filter = null,\n        public readonly Sort|null $sort = null,\n        public readonly string|null $startCursor = null,\n        public readonly int|null $pageSize = null,\n    ) {\n    }\n\n    public static function all(): self\n    {\n        return new self();\n    }\n\n    public static function title(string $query): self\n    {\n        return new self($query);\n    }\n\n    public function filterByPages(): self\n    {\n        return new self(\n            $this->query,\n            Filter::byPages(),\n            $this->sort,\n            $this->startCursor,\n            $this->pageSize,\n        );\n    }\n\n    public function filterByDatabases(): self\n    {\n        return new self(\n            $this->query,\n            Filter::byDatabases(),\n            $this->sort,\n            $this->startCursor,\n            $this->pageSize,\n        );\n    }\n\n    public function sortByLastEditedTime(SortDirection $direction): self\n    {\n        $sort = Sort::create()->byLastEditedTime();\n\n        return new self(\n            $this->query,\n            $this->filter,\n            $direction === SortDirection::Ascending ? $sort->ascending() : $sort->descending(),\n            $this->startCursor,\n            $this->pageSize,\n        );\n    }\n\n    public function changeStartCursor(string $startCursor): self\n    {\n        return new self(\n            $this->query,\n            $this->filter,\n            $this->sort,\n            $startCursor,\n            $this->pageSize,\n        );\n    }\n\n    public function changePageSize(int $pageSize): self\n    {\n        return new self(\n            $this->query,\n            $this->filter,\n            $this->sort,\n            $this->startCursor,\n            $pageSize,\n        );\n    }\n\n    /**\n     * @internal\n     *\n     * @return array{\n     *      query?: string,\n     *      filter?: array{ value: string, property: string },\n     *      sort?: array{ direction: string, timestamp: string },\n     *      start_cursor?: string,\n     *      page_size?: int\n     * }\n     */\n    public function toArray(): array\n    {\n        $array = [];\n\n        if ($this->query !== null) {\n            $array[\"query\"] = $this->query;\n        }\n\n        if ($this->filter !== null) {\n            $array[\"filter\"] = $this->filter->toArray();\n        }\n\n        if ($this->sort !== null) {\n            $array[\"sort\"] = $this->sort->toArray();\n        }\n\n        if ($this->startCursor !== null) {\n            $array[\"start_cursor\"] = $this->startCursor;\n        }\n\n        if ($this->pageSize !== null) {\n            $array[\"page_size\"] = $this->pageSize;\n        }\n\n        return $array;\n    }\n}\n"
  },
  {
    "path": "src/Search/Result.php",
    "content": "<?php\n\nnamespace Notion\\Search;\n\nuse Notion\\Databases\\Database;\nuse Notion\\Pages\\Page;\n\n/**\n * @psalm-immutable\n *\n * @psalm-import-type PageJson from \\Notion\\Pages\\Page\n * @psalm-import-type DatabaseJson from \\Notion\\Databases\\Database\n *\n * @psalm-type ResultJson = array{\n *      object: string,\n *      results: array<int, PageJson|DatabaseJson>,\n *      next_cursor: string|null,\n *      has_more: bool\n * }\n */\nclass Result\n{\n    /** @psalm-param array<int, Page|Database> $results */\n    private function __construct(\n        public readonly array $results,\n        public readonly string|null $nextCursor,\n        public readonly bool $hasMore,\n    ) {\n    }\n\n    /**\n     * @psalm-param ResultJson $array\n     */\n    public static function fromArray(array $array): self\n    {\n        $results = [];\n        foreach ($array[\"results\"] as $result) {\n            if ($result[\"object\"] === \"page\") {\n                /** @psalm-var PageJson $result */\n                $results[] = Page::fromArray($result);\n            }\n\n            if ($result[\"object\"] === \"database\") {\n                /** @psalm-var DatabaseJson $result */\n                $results[] = Database::fromArray($result);\n            }\n        }\n\n        /** @psalm-var array<int, Page|Database> $results */\n        return new self(\n            $results,\n            $array[\"next_cursor\"],\n            $array[\"has_more\"],\n        );\n    }\n}\n"
  },
  {
    "path": "src/Search/Sort.php",
    "content": "<?php\n\nnamespace Notion\\Search;\n\n/** @psalm-immutable */\nclass Sort\n{\n    private function __construct(\n        public readonly SortDirection $direction,\n        public readonly SortTimestamp $timestamp,\n    ) {\n    }\n\n    /** @psalm-mutation-free */\n    public static function create(): self\n    {\n        return new self(SortDirection::Descending, SortTimestamp::LastEditedTime);\n    }\n\n    public function byLastEditedTime(): self\n    {\n        return new self($this->direction, SortTimestamp::LastEditedTime);\n    }\n\n    public function ascending(): self\n    {\n        return new self(SortDirection::Ascending, $this->timestamp);\n    }\n\n    public function descending(): self\n    {\n        return new self(SortDirection::Descending, $this->timestamp);\n    }\n\n    /**\n     * @internal\n     *\n     * @return array{ direction: \"ascending\"|\"descending\", timestamp: \"last_edited_time\" }\n     */\n    public function toArray(): array\n    {\n        return [\n            \"direction\" => $this->direction->value,\n            \"timestamp\" => $this->timestamp->value,\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Search/SortDirection.php",
    "content": "<?php\n\nnamespace Notion\\Search;\n\nenum SortDirection: string\n{\n    case Ascending = \"ascending\";\n    case Descending = \"descending\";\n}\n"
  },
  {
    "path": "src/Search/SortTimestamp.php",
    "content": "<?php\n\nnamespace Notion\\Search;\n\nenum SortTimestamp: string\n{\n    case LastEditedTime = \"last_edited_time\";\n}\n"
  },
  {
    "path": "src/Users/Bot.php",
    "content": "<?php\n\nnamespace Notion\\Users;\n\n/**\n * @psalm-import-type WorkspaceLimitsJson from WorkspaceLimits\n *\n * @psalm-type BotJson = array{\n *    object: \"bot\",\n *    workspace_limits: WorkspaceLimitsJson,\n * }\n *\n * @psalm-immutable\n */\nclass Bot\n{\n    private function __construct(\n        public readonly WorkspaceLimits $workspaceLimits\n    ) {\n    }\n\n    /**\n     * @param BotJson $array\n     *\n     * @psalm-suppress PossiblyUnusedParam\n     */\n    public static function fromArray(array $array): self\n    {\n        $workspaceLimits = WorkspaceLimits::fromArray($array[\"workspace_limits\"] ?? []);\n\n        return new self($workspaceLimits);\n    }\n\n    /** @return BotJson */\n    public function toArray(): array\n    {\n        return [\n            \"object\" => \"bot\",\n            \"workspace_limits\" => $this->workspaceLimits->toArray(),\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Users/Client.php",
    "content": "<?php\n\nnamespace Notion\\Users;\n\nuse Notion\\Configuration;\nuse Notion\\Infrastructure\\Http;\n\n/** @psalm-import-type UserJson from User */\nclass Client\n{\n    /**\n     * @internal Use `\\Notion\\Notion::pages()` instead\n     */\n    public function __construct(\n        private readonly Configuration $config,\n    ) {\n    }\n\n    public function find(string $userId): User\n    {\n        $url = \"https://api.notion.com/v1/users/{$userId}\";\n        $request = Http::createRequest($url, $this->config);\n\n        /** @psalm-var UserJson $body */\n        $body = Http::sendRequest($request, $this->config);\n\n        return User::fromArray($body);\n    }\n\n    /**\n     * @return User[]\n     */\n    public function findAll(): array\n    {\n        $url = \"https://api.notion.com/v1/users\";\n        $request = Http::createRequest($url, $this->config);\n\n        /** @var array{ results: UserJson[] } $body */\n        $body = Http::sendRequest($request, $this->config);\n\n        return array_map(\n            function (array $userData): User {\n                return User::fromArray($userData);\n            },\n            $body[\"results\"],\n        );\n    }\n\n    public function me(): User\n    {\n        $url = \"https://api.notion.com/v1/users/me\";\n        $request = Http::createRequest($url, $this->config);\n\n        /** @psalm-var UserJson $body */\n        $body = Http::sendRequest($request, $this->config);\n\n        return User::fromArray($body);\n    }\n}\n"
  },
  {
    "path": "src/Users/Person.php",
    "content": "<?php\n\nnamespace Notion\\Users;\n\n/**\n * @psalm-type PersonJson = array{email: string}\n *\n * @psalm-immutable\n */\nclass Person\n{\n    private function __construct(\n        public readonly string $email,\n    ) {\n    }\n\n    /** @param PersonJson $array */\n    public static function fromArray(array $array): self\n    {\n        return new self($array[\"email\"]);\n    }\n\n    /** @return PersonJson */\n    public function toArray(): array\n    {\n        return [\n            \"email\" => $this->email,\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Users/User.php",
    "content": "<?php\n\nnamespace Notion\\Users;\n\n/**\n * @psalm-import-type PersonJson from Person\n * @psalm-import-type BotJson from Bot\n *\n * @psalm-type UserJson = array{\n *     object: \"user\",\n *     id: string,\n *     name?: string,\n *     avatar_url?: string,\n *     type?: \"person\"|\"bot\",\n *     person?: PersonJson,\n *     bot?: BotJson,\n * }\n *\n * @psalm-immutable\n */\nclass User\n{\n    private function __construct(\n        public readonly string $id,\n        public readonly string|null $name,\n        public readonly string|null $avatarUrl,\n        public readonly UserType|null $type,\n        public readonly Person|null $person,\n        public readonly Bot|null $bot,\n    ) {\n    }\n\n    public static function create(string $id): self\n    {\n        return new self(\n            id: $id,\n            name: null,\n            avatarUrl: null,\n            type: null,\n            person: null,\n            bot: null,\n        );\n    }\n\n    /** @psalm-param UserJson $array */\n    public static function fromArray(array $array): self\n    {\n        $person = array_key_exists(\"person\", $array) ? Person::fromArray($array[\"person\"]) : null;\n        $bot = array_key_exists(\"bot\", $array) ? Bot::fromArray($array[\"bot\"]) : null;\n\n        $name = array_key_exists(\"name\", $array) ? $array[\"name\"] : null;\n        $avatarUrl = array_key_exists(\"avatar_url\", $array) ? $array[\"avatar_url\"] : null;\n        $userType = array_key_exists(\"type\", $array) ? UserType::from($array[\"type\"]) : null;\n\n        return new self(\n            $array[\"id\"],\n            $name,\n            $avatarUrl,\n            $userType,\n            $person,\n            $bot,\n        );\n    }\n\n    /** @psalm-return UserJson */\n    public function toArray(): array\n    {\n        $array = [\n            \"object\" => \"user\",\n            \"id\"     => $this->id,\n        ];\n\n        if ($this->type !== null) {\n            $array[\"type\"] = $this->type->value;\n        }\n\n        if ($this->name !== null) {\n            $array[\"name\"] = $this->name;\n        }\n\n        if ($this->avatarUrl !== null) {\n            $array[\"avatar_url\"] = $this->avatarUrl;\n        }\n\n        if ($this->isPerson()) {\n            $array[\"person\"] = $this->person->toArray();\n        }\n        if ($this->isBot()) {\n            $array[\"bot\"] = $this->bot->toArray();\n        }\n\n        return $array;\n    }\n\n    /**\n     * @psalm-assert-if-true Person $this->person\n     */\n    public function isPerson(): bool\n    {\n        return $this->type === UserType::Person;\n    }\n\n    /**\n     * @psalm-assert-if-true Bot $this->bot\n     */\n    public function isBot(): bool\n    {\n        return $this->type === UserType::Bot;\n    }\n}\n"
  },
  {
    "path": "src/Users/UserType.php",
    "content": "<?php\n\nnamespace Notion\\Users;\n\nenum UserType: string\n{\n    case Person = \"person\";\n    case Bot = \"bot\";\n}\n"
  },
  {
    "path": "src/Users/WorkspaceLimits.php",
    "content": "<?php\n\nnamespace Notion\\Users;\n\n/**\n * @psalm-type WorkspaceLimitsJson = array{\n *     max_file_upload_size_in_bytes?: int,\n * }\n *\n * @psalm-immutable\n */\nclass WorkspaceLimits\n{\n    private function __construct(\n        public readonly int $maxFileUploadSizeInBytes\n    ) {\n    }\n\n    /**\n     * @param WorkspaceLimitsJson $array\n     */\n    public static function fromArray(array $array): self\n    {\n        return new self(\n            $array[\"max_file_upload_size_in_bytes\"] ?? 0,\n        );\n    }\n\n    /** @return WorkspaceLimitsJson */\n    public function toArray(): array\n    {\n        return [\n            \"max_file_upload_size_in_bytes\" => $this->maxFileUploadSizeInBytes\n        ];\n    }\n}\n"
  },
  {
    "path": "tests/Integration/BlocksTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Integration;\n\nuse Notion\\Blocks\\BlockType;\nuse Notion\\Blocks\\Bookmark;\nuse Notion\\Blocks\\Breadcrumb;\nuse Notion\\Blocks\\BulletedListItem;\nuse Notion\\Blocks\\Callout;\nuse Notion\\Blocks\\Code;\nuse Notion\\Blocks\\CodeLanguage;\nuse Notion\\Blocks\\Column;\nuse Notion\\Blocks\\ColumnList;\nuse Notion\\Blocks\\Divider;\nuse Notion\\Blocks\\EquationBlock;\nuse Notion\\Blocks\\Heading1;\nuse Notion\\Blocks\\Heading2;\nuse Notion\\Blocks\\Heading3;\nuse Notion\\Blocks\\NumberedListItem;\nuse Notion\\Blocks\\Paragraph;\nuse Notion\\Blocks\\TableOfContents;\nuse Notion\\Blocks\\ToDo;\nuse Notion\\Blocks\\Toggle;\nuse Notion\\Common\\RichText;\nuse Notion\\Exceptions\\ApiException;\nuse PHPUnit\\Framework\\TestCase;\n\nclass BlocksTest extends TestCase\n{\n    public function test_create_page_change_all_blocks(): void\n    {\n        $client = Helper::client();\n        $page = Helper::newPage()->changeTitle(\"Blocks test\");\n\n        $content = [\n            Bookmark::fromUrl(\"https://notion.so\"),\n            Breadcrumb::create(),\n            BulletedListItem::create()->changeText(RichText::fromString(\"List item \")),\n            Callout::create()->changeText(RichText::fromString(\"Callout\")),\n            // TODO: Child database\n            // TODO: Child page\n            Code::fromText([\n                RichText::fromString(\"<?php echo 'Hello world!';\"),\n            ], CodeLanguage::Php),\n            Divider::create(),\n            // TODO: Embed\n            EquationBlock::fromString(\"a^2 + b^2 = c^2\"),\n            // TODO: File\n            Heading1::fromText()->changeText(RichText::fromString(\"Heading 1\")),\n            Heading2::fromText()->changeText(RichText::fromString(\"Heading 2\")),\n            Heading3::fromText()->changeText(RichText::fromString(\"Heading 3\")),\n            // TODO: Image\n            NumberedListItem::create()->changeText(RichText::fromString(\"List item \")),\n            Paragraph::fromString(\"Paragraph\"),\n            // TODO: PDF\n            TableOfContents::create(),\n            ToDo::fromString(\"To do item\"),\n            Toggle::fromString(\"Toggle\"),\n            // TODO: Video\n            ColumnList::create(\n                Column::create(Paragraph::fromString(\"Paragraph\")),\n                Column::create(Paragraph::fromString(\"Paragraph\")),\n            ),\n        ];\n\n        $newPage = $client->pages()->create($page, $content);\n\n        $newPageContent = $client->blocks()->findChildrenRecursive($newPage->id);\n\n        foreach ($content as $index => $block) {\n            $this->assertInstanceOf($block::class, $newPageContent[$index]);\n        }\n\n        $client->pages()->delete($newPage);\n    }\n\n    public function test_find_block(): void\n    {\n        $client = Helper::client();\n        $page = Helper::newPage()->changeTitle(\"Blocks test\");\n\n        $content = [\n            Heading1::fromText()->changeText(RichText::fromString(\"Heading 1\")),\n        ];\n\n        $newPage = $client->pages()->create($page, $content);\n\n        $children = $client->blocks()->findChildren($newPage->id);\n\n        $block = $client->blocks()->find($children[0]->metadata()->id);\n\n        $client->pages()->delete($newPage);\n\n        $this->assertSame(BlockType::Heading1, $block->metadata()->type);\n    }\n\n    public function test_find_inexistent_block(): void\n    {\n        $client = Helper::client();\n\n        $this->expectException(ApiException::class);\n        $client->blocks()->find(\"inexistentId\");\n    }\n\n    public function test_find_children_of_inexistent_block(): void\n    {\n        $client = Helper::client();\n\n        $this->expectException(ApiException::class);\n        $client->blocks()->findChildren(\"inexistentId\");\n    }\n\n    public function test_delete_block(): void\n    {\n        $client = Helper::client();\n        $page = Helper::newPage()->changeTitle(\"Blocks test\");\n\n        $content = [\n            Heading1::fromText()->changeText(RichText::fromString(\"Heading 1\")),\n        ];\n\n        $newPage = $client->pages()->create($page, $content);\n\n        $childrenBeforeDelete = $client->blocks()->findChildren($newPage->id);\n\n        $block = $childrenBeforeDelete[0];\n\n        $deletedBlock = $client->blocks()->delete($block->metadata()->id);\n\n        $childrenAfterDelete = $client->blocks()->findChildren($newPage->id);\n\n        $this->assertTrue($deletedBlock->metadata()->inTrash);\n        $this->assertEmpty($childrenAfterDelete);\n\n        $client->pages()->delete($newPage);\n    }\n\n    public function test_delete_inexistent(): void\n    {\n        $client = Helper::client();\n\n        $this->expectException(ApiException::class);\n        $client->blocks()->delete(\"inexistentId\");\n    }\n\n    public function test_add_block(): void\n    {\n        $client = Helper::client();\n\n        $blocks = $client->blocks()->append(Helper::testPageId(), [\n            Paragraph::fromString(\"This is a simple paragraph\"),\n        ]);\n\n        foreach ($blocks as $block) {\n            $client->blocks()->delete($block->metadata()->id);\n        }\n\n        $this->assertSame(BlockType::Paragraph, $blocks[0]->metadata()->type);\n    }\n\n    public function test_add_to_inexistent_block(): void\n    {\n        $client = Helper::client();\n\n        $this->expectException(ApiException::class);\n        $client->blocks()->append(\"inexistentId\", [\n            Paragraph::fromString(\"This is a simple paragraph\"),\n        ]);\n    }\n\n    public function test_update_block(): void\n    {\n        $client = Helper::client();\n\n        $blocks = $client->blocks()->append(\n            Helper::testPageId(),\n            [\n                Bookmark::fromUrl(\"https://notion.so\"),\n                Breadcrumb::create(),\n                BulletedListItem::create()->changeText(RichText::fromString(\"List item \")),\n                Callout::create()->changeText(RichText::fromString(\"Callout\")),\n                // TODO: Child database\n                // TODO: Child page\n                Code::fromText([\n                    RichText::fromString(\"<?php echo 'Hello world!';\"),\n                ], CodeLanguage::Php),\n                Divider::create(),\n                // TODO: Embed\n                EquationBlock::fromString(\"a^2 + b^2 = c^2\"),\n                // TODO: File\n                Heading1::fromText()->changeText(RichText::fromString(\"Heading 1\")),\n                Heading2::fromText()->changeText(RichText::fromString(\"Heading 2\")),\n                Heading3::fromText()->changeText(RichText::fromString(\"Heading 3\")),\n                // TODO: Image\n                NumberedListItem::create()->changeText(RichText::fromString(\"List item \")),\n                Paragraph::fromString(\"Paragraph\"),\n                // TODO: PDF\n                TableOfContents::create(),\n                ToDo::fromString(\"To do item\"),\n                Toggle::fromString(\"Toggle\"),\n                // TODO: Video\n                // TODO: ColumnList\n            ]\n        );\n\n        foreach ($blocks as $block) {\n            $block = $client->blocks()->update($block->delete());\n            $archivedBlock = $client->blocks()->find($block->metadata()->id);\n            $this->assertTrue($archivedBlock->metadata()->inTrash);\n        }\n    }\n\n    public function test_update_newly_created_block(): void\n    {\n        $client = Helper::client();\n\n        $paragraph = Paragraph::fromString(\"This is a simple paragraph\");\n\n        $this->expectException(ApiException::class);\n        $client->blocks()->update($paragraph);\n    }\n}\n"
  },
  {
    "path": "tests/Integration/CommentsTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Integration;\n\nuse Notion\\Comments\\Comment;\nuse Notion\\Common\\RichText;\nuse Notion\\Notion;\nuse Notion\\Pages\\Page;\nuse Notion\\Pages\\PageParent;\nuse PHPUnit\\Framework\\TestCase;\n\nclass CommentsTest extends TestCase\n{\n    public function test_create_and_read_page_comment(): void\n    {\n        $notion = Helper::client();\n        $page = Helper::newPage()->changeTitle(\"Comments test\");\n\n        $page = $notion->pages()->create($page);\n\n        $comment = Comment::create($page->id, RichText::fromString(\"Sample comment\"));\n        $comment = $notion->comments()->create($comment);\n\n        $comments = $notion->comments()->list($page->id);\n\n        $this->assertSame($comment->id, $comments[0]->id);\n        $this->assertSame(\"Sample comment\", $comments[0]->text[0]->toString());\n\n        $notion->pages()->delete($page);\n    }\n}\n"
  },
  {
    "path": "tests/Integration/DatabasesTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Integration;\n\nuse DateTimeImmutable;\nuse Notion\\Common\\Color;\nuse Notion\\Common\\Emoji;\nuse Notion\\Common\\RichText;\nuse Notion\\Databases\\Database;\nuse Notion\\Databases\\DatabaseParent;\nuse Notion\\Databases\\Properties\\Date;\nuse Notion\\Databases\\Properties\\People;\nuse Notion\\Databases\\Properties\\RichTextProperty;\nuse Notion\\Databases\\Properties\\Select;\nuse Notion\\Databases\\Properties\\SelectOption;\nuse Notion\\Databases\\Properties\\Title;\nuse Notion\\Databases\\Query;\nuse Notion\\Databases\\Query\\CompoundFilter;\nuse Notion\\Databases\\Query\\DateFilter;\nuse Notion\\Databases\\Query\\SelectFilter;\nuse Notion\\Exceptions\\ApiException;\nuse Notion\\Pages\\Page;\nuse Notion\\Pages\\PageParent;\nuse Notion\\Pages\\Properties\\Date as DateProp;\nuse Notion\\Pages\\Properties\\People as PeopleProp;\nuse Notion\\Pages\\Properties\\Select as SelectProp;\nuse Notion\\Search\\Query as SearchQuery;\nuse Notion\\Users\\User;\nuse PHPUnit\\Framework\\TestCase;\n\nclass DatabasesTest extends TestCase\n{\n    private static int $bigDatabaseSize = 110;\n\n    public function test_create_empty_database(): void\n    {\n        $client = Helper::client();\n\n        $database = Database::create(DatabaseParent::page(Helper::testPageId()))\n            ->changeTitle(\"Empty database\")\n            ->changeIcon(Emoji::fromString(\"🌻\"));\n\n        $database = $client->databases()->create($database);\n\n        $databaseFound = $client->databases()->find($database->id);\n\n        $this->assertEquals(\"Empty database\", $database->title[0]->plainText);\n        if ($databaseFound->icon?->isEmoji()) {\n            $this->assertEquals(\"🌻\", $databaseFound->icon->emoji?->emoji);\n        }\n\n        $client->databases()->delete($database);\n    }\n\n    public function test_create_inline_database(): void\n    {\n        $client = Helper::client();\n\n        $database = Database::create(DatabaseParent::page(Helper::testPageId()))\n            ->changeTitle(\"Inline database\")\n            ->enableInline();\n\n        $database = $client->databases()->create($database);\n\n        $databaseFound = $client->databases()->find($database->id);\n\n        $this->assertEquals(\"Inline database\", $database->title[0]->plainText);\n        $this->assertTrue($databaseFound->isInline);\n\n        $client->databases()->delete($database);\n    }\n\n    public function test_update_database(): void\n    {\n        $client = Helper::client();\n\n        $database = Database::create(DatabaseParent::page(Helper::testPageId()))\n            ->changeTitle(\"Test database\");\n        $database = $client->databases()->create($database);\n\n        $database = $database->addProperty(RichTextProperty::create(\"Test prop\"));\n        $database = $client->databases()->update($database);\n\n        $this->assertEquals(\n            \"Test prop\",\n            $database->properties()->get(\"Test prop\")->metadata()->name\n        );\n\n        $client->databases()->delete($database);\n    }\n\n    public function test_find_inexistent_database(): void\n    {\n        $client = Helper::client();\n\n        $this->expectException(ApiException::class);\n        $this->expectExceptionMessage(\"Could not find database with ID: b30f9991-ffcb-4b72-847a-39a74e0a774b.\");\n        $client->databases()->find(\"b30f9991-ffcb-4b72-847a-39a74e0a774b\");\n    }\n\n    public function test_create_change_inexistent_parent(): void\n    {\n        $client = Helper::client();\n\n        $database = Database::create(DatabaseParent::page(\"b30f9991-ffcb-4b72-847a-39a74e0a774b\"));\n\n        $this->expectException(ApiException::class);\n        $this->expectExceptionMessage(\"Could not find page with ID: b30f9991-ffcb-4b72-847a-39a74e0a774b.\");\n        $client->databases()->create($database);\n    }\n\n    public function test_update_deleted_database(): void\n    {\n        $client = Helper::client();\n\n        $database = Database::create(DatabaseParent::page(Helper::testPageId()))\n            ->changeAdvancedTitle(RichText::fromString(\"Dummy database\"));\n\n        $database = $client->databases()->create($database);\n\n        $client->databases()->delete($database);\n\n        $this->expectException(ApiException::class);\n        $client->databases()->update($database);\n    }\n\n    public function test_delete_inexistent(): void\n    {\n        $client = Helper::client();\n\n        $database = Database::create(DatabaseParent::page(Helper::testPageId()));\n\n        $this->expectException(ApiException::class);\n        $client->databases()->delete($database);\n    }\n\n    public function test_query_all_pages_from_database(): void\n    {\n        $database = self::moviesDatabase();\n\n        $client = Helper::client();\n        $pages = $client->databases()->queryAllPages($database);\n\n        $client->databases()->delete($database);\n\n        $this->assertCount(5, $pages);\n    }\n\n    /** @group bigdb */\n    public function test_query_big_database(): void\n    {\n        $client = Helper::client();\n        $result = $client->search()->search(SearchQuery::title(\"Big dataset\")->filterByDatabases());\n\n        if (\n            count($result->results) > 0 &&\n            $result->results[0]::class === Database::class\n        ) {\n            /** @var Database */\n            $bigDatabase = $result->results[0];\n        } else {\n            $bigDatabase = self::bigDatabase();\n        }\n\n        $pages = $client->databases()->queryAllPages($bigDatabase);\n\n        $this->assertCount(self::$bigDatabaseSize, $pages);\n    }\n\n    public function test_query_database(): void\n    {\n        $client = Helper::client();\n\n        $database = self::moviesDatabase();\n\n        /**\n         * 90s drama movies\n         *\n         * Category == Drama AND\n         * Release >= 1990-01-01 AND\n         * Release <= 1999-12-31\n         *\n         */\n        $query = Query::create()->changeFilter(\n            CompoundFilter::and(\n                SelectFilter::property(\"Category\")->equals(\"Drama\"),\n                DateFilter::property(\"Release date\")->onOrAfter(\"1990-01-01\"),\n                DateFilter::property(\"Release date\")->onOrBefore(\"1999-12-31\"),\n            ),\n        );\n\n        $result = $client->databases()->query($database, $query);\n\n        $client->databases()->delete($database);\n\n        $this->assertCount(1, $result->pages);\n    }\n\n    public function test_query_inexistent_database(): void\n    {\n        $client = Helper::client();\n\n        $database = Database::create(DatabaseParent::page(\"a-page-id\"));\n        $query = Query::create();\n\n        $this->expectException(ApiException::class);\n        $client->databases()->query($database, $query);\n    }\n\n    public function test_rename_database_with_people_property(): void\n    {\n        $client = Helper::client();\n\n        $database = self::moviesDatabase();\n        $database = $database->changeTitle(\"New movies database\");\n        $database = $client->databases()->update($database);\n\n        $client->databases()->delete($database);\n\n        $this->assertSame(\"New movies database\", $database->title[0]->plainText);\n    }\n\n    public function test_rename_database_page_with_people_property(): void\n    {\n        $client = Helper::client();\n\n        $database = self::moviesDatabase();\n\n        $users = Helper::client()->users()->findAll();\n        $userId = $users[0]->id;\n\n        $newPage = self::moviePage($database->id, \"Sample movie\", \"2023-01-01\", \"Action\", $userId);\n        $newPage = $client->pages()->create($newPage);\n\n        $newPage = $newPage->changeTitle(\"Updated sample movie\");\n        $client->pages()->update($newPage);\n\n        $client->databases()->delete($database);\n\n        $this->assertSame(\"Updated sample movie\", $newPage->title()?->toString());\n    }\n\n    private static function moviesDatabase(): Database\n    {\n        $databaseParent = DatabaseParent::page(Helper::testPageId());\n\n        $categories = [\n            SelectOption::fromName(\"Action\")->changeColor(Color::Orange),\n            SelectOption::fromName(\"Comedy\")->changeColor(Color::Yellow),\n            SelectOption::fromName(\"Drama\")->changeColor(Color::Red),\n        ];\n\n        $database = Database::create($databaseParent)\n            ->changeTitle(\"Movies\")\n            ->changeProperties([\n                \"Movies\" => Title::create(\"Movie\"),\n                \"Release date\" => Date::create(\"Release date\"),\n                \"Category\" => Select::create(\"Category\", $categories),\n                \"People\" => People::create(\"People\"),\n            ]);\n\n        $database = Helper::client()->databases()->create($database);\n\n        $users = Helper::client()->users()->findAll();\n        $userId = $users[0]->id;\n\n        $pages = [\n            self::moviePage($database->id, \"A Clockwork Orange\", \"1972-12-19\", \"Drama\", $userId),\n            self::moviePage($database->id, \"Dead Poets Society\", \"1989-06-02\", \"Drama\", $userId),\n            self::moviePage($database->id, \"Batman\", \"1989-10-26\", \"Action\", $userId),\n            self::moviePage($database->id, \"The Mask\", \"1994-12-23\", \"Comedy\", $userId),\n            self::moviePage($database->id, \"American Beauty\", \"1999-09-08\", \"Drama\", $userId),\n        ];\n\n        $client = Helper::client();\n        foreach ($pages as $page) {\n            $client->pages()->create($page);\n        }\n\n        return $database;\n    }\n\n    private static function moviePage(\n        string $databaseId,\n        string $title,\n        string $releaseDate,\n        string $category,\n        string $userId\n    ): Page {\n        $date = new DateTimeImmutable($releaseDate);\n        return Page::create(PageParent::database($databaseId))\n            ->changeTitle($title)\n            ->addProperty(\"Release date\", DateProp::create($date))\n            ->addProperty(\"Category\", SelectProp::fromName($category))\n            ->addProperty(\"People\", PeopleProp::create(User::create($userId)));\n    }\n\n    private static function bigDatabase(): Database\n    {\n        $client = Helper::client();\n\n        $database = Database::create(DatabaseParent::page(Helper::testPageId()))\n            ->changeTitle(\"Big dataset\");\n\n        $database = $client->databases()->create($database);\n\n        $parent = PageParent::database($database->id);\n        for ($i = 0; $i < self::$bigDatabaseSize; $i++) {\n            $page = Page::create($parent)->changeTitle(\"Page #{$i}\");\n            $client->pages()->create($page);\n        }\n\n        return $database;\n    }\n}\n"
  },
  {
    "path": "tests/Integration/Helper.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Integration;\n\nuse Exception;\nuse Notion\\Configuration;\nuse Notion\\Notion;\nuse Notion\\Pages\\Page;\nuse Notion\\Pages\\PageParent;\n\nfinal class Helper\n{\n    public static function client(): Notion\n    {\n        $token = getenv(\"NOTION_TOKEN\");\n        if (!$token) {\n            throw new Exception(\"Notion token is required to run integration tests.\");\n        }\n\n        $config = Configuration::create($token)\n            ->enableRetryOnConflict(10);\n\n        return Notion::createFromConfig($config);\n    }\n\n    public static function testPageId(): string\n    {\n        $pageId = getenv(\"TEST_PAGE_ID\");\n        if (!$pageId) {\n            throw new Exception(\"Test page ID required to run integration tests.\");\n        }\n\n        return $pageId;\n    }\n\n    public static function newPage(): Page\n    {\n        return Page::create(PageParent::page(self::testPageId()));\n    }\n}\n"
  },
  {
    "path": "tests/Integration/PagesTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Integration;\n\nuse Notion\\Common\\Emoji;\nuse Notion\\Exceptions\\ApiException;\nuse Notion\\Pages\\Page;\nuse Notion\\Pages\\PageParent;\nuse PHPUnit\\Framework\\TestCase;\n\nclass PagesTest extends TestCase\n{\n    public function test_create_empty_page(): void\n    {\n        $client = Helper::client();\n\n        $page = Helper::newPage()\n            ->changeTitle(\"Empty page\")\n            ->changeIcon(Emoji::fromString(\"⭐\"));\n\n        $page = $client->pages()->create($page);\n\n        $pageFound = $client->pages()->find($page->id);\n\n        $this->assertEquals(\"Empty page\", $page->title()?->toString());\n\n        if ($pageFound->icon?->isEmoji()) {\n            $this->assertEquals(\"⭐\", $pageFound->icon->emoji?->emoji);\n        }\n\n        $client->pages()->delete($page);\n    }\n\n    public function test_find_page(): void\n    {\n        $client = Helper::client();\n\n        $page = $client->pages()->find(Helper::testPageId());\n\n        $this->assertNotNull($page->title()?->toString());\n    }\n\n    public function test_find_inexistent_page(): void\n    {\n        $client = Helper::client();\n\n        $this->expectException(ApiException::class);\n        $this->expectExceptionMessage(\"Could not find page with ID: 60e79d42-4742-41ca-8d70-cc51660cbd3c.\");\n        $client->pages()->find(\"60e79d42-4742-41ca-8d70-cc51660cbd3c\");\n    }\n\n    public function test_create_change_inexistent_parent(): void\n    {\n        $client = Helper::client();\n\n        $page = Page::create(PageParent::page(\"60e79d42-4742-41ca-8d70-cc51660cbd3c\"));\n\n        $this->expectException(ApiException::class);\n        $this->expectExceptionMessage(\"Could not find page with ID: 60e79d42-4742-41ca-8d70-cc51660cbd3c.\");\n        $client->pages()->create($page);\n    }\n\n    public function test_update_deleted_page(): void\n    {\n        $client = Helper::client();\n\n        $page = Helper::newPage()\n            ->changeTitle(\"Page to be deleted\");\n\n        $page = $client->pages()->create($page);\n        $page = $client->pages()->delete($page);\n\n        $page = $page->changeTitle(\"Title after deleted\");\n\n        $this->expectException(ApiException::class);\n        $client->pages()->update($page);\n    }\n}\n"
  },
  {
    "path": "tests/Integration/SearchTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Integration;\n\nuse Notion\\Search\\Query;\nuse PHPUnit\\Framework\\TestCase;\n\nclass SearchTest extends TestCase\n{\n    public function test_search(): void\n    {\n        $client = Helper::client();\n\n        $testPage = $client->pages()->find(Helper::testPageId());\n        $title = $testPage->title()?->toString() ?? \"\";\n\n        $query = Query::title($title);\n        $result = $client->search()->search($query);\n\n        $this->assertGreaterThan(0, count($result->results));\n    }\n\n    public function test_search_all(): void\n    {\n        $client = Helper::client();\n\n        $query = Query::all();\n        $result = $client->search()->search($query);\n\n        $this->assertGreaterThan(0, count($result->results));\n    }\n}\n"
  },
  {
    "path": "tests/Integration/UsersTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Integration;\n\nuse Notion\\Exceptions\\ApiException;\nuse PHPUnit\\Framework\\TestCase;\n\nclass UsersTest extends TestCase\n{\n    public function test_find_current_user(): void\n    {\n        $client = Helper::client();\n\n        $user = $client->users()->me();\n        $sameUser = $client->users()->find($user->id);\n\n        $this->assertTrue($user->isBot());\n        $this->assertEquals($user, $sameUser);\n    }\n\n    public function test_find_all_users(): void\n    {\n        $client = Helper::client();\n\n        $users = $client->users()->findAll();\n\n        $this->assertTrue(count($users) > 1);\n    }\n\n    public function test_find_inexistent_user(): void\n    {\n        $client = Helper::client();\n\n        $this->expectException(ApiException::class);\n        $this->expectExceptionMessage(\n            \"Could not find user with ID: 7c3bd31e-63fa-4c60-956d-2264ceb2c522.\"\n        );\n        $client->users()->find(\"7c3bd31e-63fa-4c60-956d-2264ceb2c522\");\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/BlockMetadataTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks;\n\nuse Notion\\Blocks\\BlockMetadata;\nuse Notion\\Blocks\\BlockType;\nuse Notion\\Exceptions\\BlockException;\nuse PHPUnit\\Framework\\TestCase;\n\nclass BlockMetadataTest extends TestCase\n{\n    public function test_restore(): void\n    {\n        $metadata = BlockMetadata::create(BlockType::Paragraph);\n\n        $metadata = $metadata->delete();\n        $metadata = $metadata->restore();\n\n        $this->assertFalse($metadata->inTrash);\n    }\n\n    public function test_check_type(): void\n    {\n        $metadata = BlockMetadata::create(BlockType::Paragraph);\n\n        $this->expectException(BlockException::class);\n        $metadata->checkType(BlockType::BulletedListItem);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/BookmarkTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks;\n\nuse Notion\\Blocks\\BlockFactory;\nuse Notion\\Blocks\\Bookmark;\nuse Notion\\Blocks\\Paragraph;\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Common\\Date;\nuse Notion\\Common\\RichText;\nuse PHPUnit\\Framework\\TestCase;\n\nclass BookmarkTest extends TestCase\n{\n    public function test_create_bookmark(): void\n    {\n        $bookmark = Bookmark::fromUrl(\"https://my-site.com\");\n\n        $this->assertEquals(\"https://my-site.com\", $bookmark->url);\n    }\n\n    public function test_create_from_array(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"bookmark\",\n            \"bookmark\"         => [ \"url\" => \"https://my-site.com\", \"caption\" => [] ],\n        ];\n\n        $bookmark = Bookmark::fromArray($array);\n\n        $this->assertEquals(\"https://my-site.com\", $bookmark->url);\n\n        $this->assertEquals($bookmark, BlockFactory::fromArray($array));\n    }\n\n    public function test_error_on_wrong_type(): void\n    {\n        $this->expectException(BlockException::class);\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"wrong-type\",\n            \"bookmark\"         => [ \"url\" => \"https://my-site.com\", \"caption\" => [] ],\n        ];\n\n        Bookmark::fromArray($array);\n    }\n\n    public function test_transform_in_array(): void\n    {\n        $bookmark = Bookmark::fromUrl(\"https://my-site.com\");\n\n        $expected = [\n            \"object\"           => \"block\",\n            \"created_time\"     => $bookmark->metadata()->createdTime->format(Date::FORMAT),\n            \"last_edited_time\" => $bookmark->metadata()->createdTime->format(Date::FORMAT),\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"bookmark\",\n            \"bookmark\"         => [ \"url\" => \"https://my-site.com\", \"caption\" => [] ],\n        ];\n\n        $this->assertEquals($expected, $bookmark->toArray());\n    }\n\n    public function test_replace_url(): void\n    {\n        $old = Bookmark::fromUrl(\"https://my-site.com\");\n        $new = $old->changeUrl(\"https://another-site.com\");\n\n        $this->assertEquals(\"https://my-site.com\", $old->url);\n        $this->assertEquals(\"https://another-site.com\", $new->url);\n    }\n\n    public function test_replace_caption(): void\n    {\n        $caption = [ RichText::fromString(\"Bookmark caption\") ];\n        $bookmark = Bookmark::fromUrl(\"https://my-site.com\")->changeCaption(...$caption);\n\n        $this->assertEquals($caption, $bookmark->caption);\n    }\n\n    public function test_no_children_support(): void\n    {\n        $bookmark = Bookmark::fromUrl(\"https://my-site.com\");\n\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $bookmark->changeChildren();\n    }\n\n    public function test_no_children_support_2(): void\n    {\n        $bookmark = Bookmark::fromUrl(\"https://my-site.com\");\n\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $bookmark->addChild(Paragraph::create());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/BreadcrumbTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks;\n\nuse Notion\\Blocks\\BlockFactory;\nuse Notion\\Blocks\\Breadcrumb;\nuse Notion\\Blocks\\Paragraph;\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Common\\Date;\nuse PHPUnit\\Framework\\TestCase;\n\nclass BreadcrumbTest extends TestCase\n{\n    public function test_create_breadcrumb(): void\n    {\n        $breadcrumb = Breadcrumb::create();\n\n        $this->assertEquals(\"breadcrumb\", $breadcrumb->metadata()->type->value);\n    }\n\n    public function test_create_from_array(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"breadcrumb\",\n            \"breadcrumb\"       => new \\stdClass(),\n        ];\n\n        $breadcrumb = Breadcrumb::fromArray($array);\n\n        $this->assertEquals($breadcrumb, BlockFactory::fromArray($array));\n    }\n\n    public function test_error_on_wrong_type(): void\n    {\n        $this->expectException(BlockException::class);\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"wrong-type\",\n            \"breadcrumb\"       => new \\stdClass(),\n        ];\n\n        Breadcrumb::fromArray($array);\n    }\n\n    public function test_transform_in_array(): void\n    {\n        $breadcrumb = Breadcrumb::create();\n\n        $expected = [\n            \"object\"           => \"block\",\n            \"created_time\"     => $breadcrumb->metadata()->createdTime->format(Date::FORMAT),\n            \"last_edited_time\" => $breadcrumb->metadata()->createdTime->format(Date::FORMAT),\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"breadcrumb\",\n            \"breadcrumb\"       => new \\stdClass(),\n        ];\n\n        $this->assertEquals($expected, $breadcrumb->toArray());\n    }\n\n    public function test_no_children_support(): void\n    {\n        $block = Breadcrumb::create();\n\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $block->changeChildren();\n    }\n\n    public function test_no_children_support_2(): void\n    {\n        $block = Breadcrumb::create();\n\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $block->addChild(Paragraph::create());\n    }\n\n    public function test_delete(): void\n    {\n        $block = Breadcrumb::create();\n\n        $block = $block->delete();\n\n        $this->assertTrue($block->metadata()->inTrash);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/BulletedListItemTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks;\n\nuse Notion\\Blocks\\BlockFactory;\nuse Notion\\Blocks\\BulletedListItem;\nuse Notion\\Common\\Color;\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Common\\Date;\nuse Notion\\Common\\RichText;\nuse PHPUnit\\Framework\\TestCase;\n\nclass BulletedListItemTest extends TestCase\n{\n    public function test_create_empty_item(): void\n    {\n        $item = BulletedListItem::create();\n\n        $this->assertEmpty($item->text);\n        $this->assertEmpty($item->children);\n    }\n\n    public function test_create_from_string(): void\n    {\n        $item = BulletedListItem::fromString(\"Dummy item.\");\n\n        $this->assertEquals(\"Dummy item.\", $item->toString());\n    }\n\n    public function test_create_from_array(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"bulleted_list_item\",\n            \"bulleted_list_item\"        => [\n                \"rich_text\" => [\n                    [\n                        \"plain_text\"  => \"Notion items \",\n                        \"href\"        => null,\n                        \"type\"        => \"text\",\n                        \"text\"        => [\n                            \"content\" => \"Notion items \",\n                        ],\n                        \"annotations\" => [\n                            \"bold\"          => false,\n                            \"italic\"        => false,\n                            \"strikethrough\" => false,\n                            \"underline\"     => false,\n                            \"code\"          => false,\n                            \"color\"         => \"default\",\n                        ],\n                    ],\n                    [\n                        \"plain_text\"  => \"rock!\",\n                        \"href\"        => null,\n                        \"type\"        => \"text\",\n                        \"text\"        => [\n                            \"content\" => \"rock!\",\n                        ],\n                        \"annotations\" => [\n                            \"bold\"          => true,\n                            \"italic\"        => false,\n                            \"strikethrough\" => false,\n                            \"underline\"     => false,\n                            \"code\"          => false,\n                            \"color\"         => \"red\",\n                        ],\n                    ],\n                ],\n                \"children\" => [],\n            ],\n        ];\n\n        $item = BulletedListItem::fromArray($array);\n\n        $this->assertCount(2, $item->text);\n        $this->assertEmpty($item->children);\n        $this->assertEquals(\"Notion items rock!\", $item->toString());\n        $this->assertFalse($item->metadata()->inTrash);\n\n        $this->assertEquals($item, BlockFactory::fromArray($array));\n    }\n\n    public function test_error_on_wrong_type(): void\n    {\n        $this->expectException(BlockException::class);\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"wrong-type\",\n            \"bulleted_list_item\"        => [\n                \"rich_text\"     => [],\n                \"children\" => [],\n            ],\n        ];\n\n        BulletedListItem::fromArray($array);\n    }\n\n    public function test_transform_in_array(): void\n    {\n        $i = BulletedListItem::fromString(\"Simple item\");\n\n        $expected = [\n            \"object\"           => \"block\",\n            \"created_time\"     => $i->metadata()->createdTime->format(Date::FORMAT),\n            \"last_edited_time\" => $i->metadata()->lastEditedTime->format(Date::FORMAT),\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"bulleted_list_item\",\n            \"bulleted_list_item\" => [\n                \"rich_text\" => [[\n                    \"plain_text\"  => \"Simple item\",\n                    \"href\"        => null,\n                    \"type\"        => \"text\",\n                    \"text\"        => [\n                        \"content\" => \"Simple item\",\n                    ],\n                    \"annotations\" => [\n                        \"bold\"          => false,\n                        \"italic\"        => false,\n                        \"strikethrough\" => false,\n                        \"underline\"     => false,\n                        \"code\"          => false,\n                        \"color\"         => \"default\",\n                    ],\n                ]],\n                \"color\" => \"default\",\n                \"children\" => [],\n            ],\n        ];\n\n        $this->assertEquals($expected, $i->toArray());\n    }\n\n    public function test_replace_text(): void\n    {\n        $oldItem = BulletedListItem::fromString(\"This is an old item\");\n\n        $newItem = $oldItem->changeText(\n            RichText::fromString(\"This is a \"),\n            RichText::fromString(\"new item\"),\n        );\n\n        $this->assertEquals(\"This is an old item\", $oldItem->toString());\n        $this->assertEquals(\"This is a new item\", $newItem->toString());\n    }\n\n    public function test_add_text(): void\n    {\n        $oldItem = BulletedListItem::fromString(\"A item\");\n\n        $newItem = $oldItem->addText(\n            RichText::fromString(\" can be extended.\")\n        );\n\n        $this->assertEquals(\"A item\", $oldItem->toString());\n        $this->assertEquals(\"A item can be extended.\", $newItem->toString());\n    }\n\n    public function test_replace_children(): void\n    {\n        $nested1 = BulletedListItem::fromString(\"Nested item 1\");\n        $nested2 = BulletedListItem::fromString(\"Nested item 2\");\n        $item = BulletedListItem::fromString(\"Simple item.\")->changeChildren($nested1, $nested2);\n\n        $this->assertCount(2, $item->children);\n        $this->assertEquals($nested1, $item->children[0]);\n        $this->assertEquals($nested2, $item->children[1]);\n    }\n\n    public function test_add_child(): void\n    {\n        $item = BulletedListItem::fromString(\"Simple item.\");\n        $nested = BulletedListItem::fromString(\"Nested item\");\n        $item = $item->addChild($nested);\n\n        $this->assertCount(1, $item->children);\n        $this->assertEquals($nested, $item->children[0]);\n    }\n\n    public function test_change_color(): void\n    {\n        $block = BulletedListItem::fromString(\"Hello World!\")->changeColor(Color::Red);\n\n        $this->assertSame(Color::Red, $block->color);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/CalloutTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks;\n\nuse Notion\\Blocks\\BlockFactory;\nuse Notion\\Blocks\\Callout;\nuse Notion\\Common\\Color;\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Common\\Date;\nuse Notion\\Common\\Emoji;\nuse Notion\\Common\\File;\nuse Notion\\Common\\RichText;\nuse PHPUnit\\Framework\\TestCase;\n\nclass CalloutTest extends TestCase\n{\n    public function test_create_empty_callout(): void\n    {\n        $callout = Callout::create();\n\n        $this->assertEmpty($callout->text);\n        $this->assertEmpty($callout->children);\n    }\n\n    public function test_create_from_string(): void\n    {\n        $callout = Callout::fromString(\"☀️\", \"Dummy callout.\");\n\n        $this->assertEquals(\"Dummy callout.\", $callout->toString());\n    }\n\n    public function test_create_from_array_change_emoji_icon(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"callout\",\n            \"callout\"        => [\n                \"rich_text\" => [\n                    [\n                        \"plain_text\"  => \"Notion callouts \",\n                        \"href\"        => null,\n                        \"type\"        => \"text\",\n                        \"text\"        => [\n                            \"content\" => \"Notion callouts \",\n                        ],\n                        \"annotations\" => [\n                            \"bold\"          => false,\n                            \"italic\"        => false,\n                            \"strikethrough\" => false,\n                            \"underline\"     => false,\n                            \"code\"          => false,\n                            \"color\"         => \"default\",\n                        ],\n                    ],\n                    [\n                        \"plain_text\"  => \"rock!\",\n                        \"href\"        => null,\n                        \"type\"        => \"text\",\n                        \"text\"        => [\n                            \"content\" => \"rock!\",\n                        ],\n                        \"annotations\" => [\n                            \"bold\"          => true,\n                            \"italic\"        => false,\n                            \"strikethrough\" => false,\n                            \"underline\"     => false,\n                            \"code\"          => false,\n                            \"color\"         => \"red\",\n                        ],\n                    ],\n                ],\n                \"icon\" => [\n                    \"type\"  => \"emoji\",\n                    \"emoji\" => \"☀️\",\n                ],\n                \"children\" => [],\n            ],\n        ];\n\n        $callout = Callout::fromArray($array);\n\n        $this->assertCount(2, $callout->text);\n        $this->assertEmpty($callout->children);\n        $this->assertEquals(\"Notion callouts rock!\", $callout->toString());\n        if ($callout->icon->isEmoji()) {\n            $this->assertEquals(\"☀️\", $callout->icon->emoji->toString());\n        }\n        $this->assertFalse($callout->metadata()->inTrash);\n\n        $this->assertEquals($callout, BlockFactory::fromArray($array));\n    }\n\n    public function test_create_from_array_change_icon_file(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"callout\",\n            \"callout\"        => [\n                \"rich_text\" => [\n                    [\n                        \"plain_text\"  => \"Notion callouts \",\n                        \"href\"        => null,\n                        \"type\"        => \"text\",\n                        \"text\"        => [\n                            \"content\" => \"Notion callouts \",\n                        ],\n                        \"annotations\" => [\n                            \"bold\"          => false,\n                            \"italic\"        => false,\n                            \"strikethrough\" => false,\n                            \"underline\"     => false,\n                            \"code\"          => false,\n                            \"color\"         => \"default\",\n                        ],\n                    ],\n                    [\n                        \"plain_text\"  => \"rock!\",\n                        \"href\"        => null,\n                        \"type\"        => \"text\",\n                        \"text\"        => [\n                            \"content\" => \"rock!\",\n                        ],\n                        \"annotations\" => [\n                            \"bold\"          => true,\n                            \"italic\"        => false,\n                            \"strikethrough\" => false,\n                            \"underline\"     => false,\n                            \"code\"          => false,\n                            \"color\"         => \"red\",\n                        ],\n                    ],\n                ],\n                \"icon\" => [\n                    \"type\"  => \"external\",\n                    \"external\"  => [\n                        \"type\" => \"external\",\n                        \"url\"  => \"https://imgur.com/gallery/Iy8yE5h\",\n                    ],\n                ],\n                \"children\" => [],\n            ],\n        ];\n\n        $callout = Callout::fromArray($array);\n\n        $this->assertCount(2, $callout->text);\n        $this->assertEmpty($callout->children);\n        $this->assertEquals(\"Notion callouts rock!\", $callout->toString());\n        $this->assertFalse($callout->metadata()->inTrash);\n    }\n\n    public function test_error_on_wrong_type(): void\n    {\n        $this->expectException(BlockException::class);\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"wrong-type\",\n            \"callout\"        => [\n                \"rich_text\"     => [],\n                \"children\" => [],\n                \"icon\"     => [\n                    \"type\"  => \"emoji\",\n                    \"emoji\" => \"☀️\",\n                ],\n            ],\n        ];\n\n        Callout::fromArray($array);\n    }\n\n    public function test_error_on_wrong_icon_type(): void\n    {\n        $this->expectException(\\ValueError::class);\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"callout\",\n            \"callout\"        => [\n                \"rich_text\"     => [],\n                \"children\" => [],\n                \"icon\"     => [ \"type\"  => \"wrong-type\"],\n            ],\n        ];\n\n        Callout::fromArray($array);\n    }\n\n    public function test_transform_in_array(): void\n    {\n        $c = Callout::fromString(\"☀️\", \"Simple callout\");\n\n        $expected = [\n            \"object\"           => \"block\",\n            \"created_time\"     => $c->metadata()->createdTime->format(Date::FORMAT),\n            \"last_edited_time\" => $c->metadata()->lastEditedTime->format(Date::FORMAT),\n            \"in_trash\"         => false,\n            \"has_children\"      => false,\n            \"type\"             => \"callout\",\n            \"callout\"        => [\n                \"rich_text\" => [[\n                    \"plain_text\"  => \"Simple callout\",\n                    \"href\"        => null,\n                    \"type\"        => \"text\",\n                    \"text\"        => [\n                        \"content\" => \"Simple callout\",\n                    ],\n                    \"annotations\" => [\n                        \"bold\"          => false,\n                        \"italic\"        => false,\n                        \"strikethrough\" => false,\n                        \"underline\"     => false,\n                        \"code\"          => false,\n                        \"color\"         => \"default\",\n                    ],\n                ]],\n                \"icon\" => [\n                    \"type\"  => \"emoji\",\n                    \"emoji\" => \"☀️\",\n                ],\n                \"color\" => \"default\",\n                \"children\" => [],\n            ],\n        ];\n\n        $this->assertEquals($expected, $c->toArray());\n    }\n\n    public function test_replace_text(): void\n    {\n        $oldCallout = Callout::fromString(\"☀️\", \"This is an old callout\");\n\n        $newCallout = $oldCallout->changeText(\n            RichText::fromString(\"This is a \"),\n            RichText::fromString(\"new callout\"),\n        );\n\n        $this->assertEquals(\"This is an old callout\", $oldCallout->toString());\n        $this->assertEquals(\"This is a new callout\", $newCallout->toString());\n    }\n\n    public function test_add_text(): void\n    {\n        $oldCallout = Callout::fromString(\"☀️\", \"A callout\");\n\n        $newCallout = $oldCallout->addText(\n            RichText::fromString(\" can be extended.\")\n        );\n\n        $this->assertEquals(\"A callout\", $oldCallout->toString());\n        $this->assertEquals(\"A callout can be extended.\", $newCallout->toString());\n    }\n\n    public function test_replace_children(): void\n    {\n        $nested1 = Callout::fromString(\"☀️\", \"Nested callout 1\");\n        $nested2 = Callout::fromString(\"☀️\", \"Nested callout 2\");\n        $callout = Callout::fromString(\"☀️\", \"Simple callout.\")->changeChildren($nested1, $nested2);\n\n        $this->assertCount(2, $callout->children);\n        $this->assertEquals($nested1, $callout->children[0]);\n        $this->assertEquals($nested2, $callout->children[1]);\n    }\n\n    public function test_add_child(): void\n    {\n        $callout = Callout::fromString(\"☀️\", \"Simple callout.\");\n        $nested = Callout::fromString(\"☀️\", \"Nested callout\");\n        $callout = $callout->addChild($nested);\n\n        $this->assertCount(1, $callout->children);\n        $this->assertEquals($nested, $callout->children[0]);\n    }\n\n    public function test_replace_icon(): void\n    {\n        $callout = Callout::fromString(\"☀️\", \"Simple callout.\")\n            ->changeIcon(Emoji::fromString(\"🌙\"));\n\n        if ($callout->icon->isEmoji()) {\n            $this->assertEquals(\"🌙\", $callout->icon->emoji->toString());\n        }\n    }\n\n    public function test_replace_icon_file(): void\n    {\n        $file = File::createExternal(\"https://example.com/icon.png\");\n\n        $callout = Callout::fromString(\"☀️\", \"Simple callout.\")\n            ->changeIcon($file);\n\n        $this->assertTrue($callout->icon->isFile());\n    }\n\n    public function test_change_color(): void\n    {\n        $block = Callout::fromString(\"☀️\", \"Hello World!\")->changeColor(Color::Red);\n\n        $this->assertSame(Color::Red, $block->color);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/ChildDatabaseTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks;\n\nuse Notion\\Blocks\\BlockFactory;\nuse Notion\\Blocks\\ChildDatabase;\nuse Notion\\Blocks\\Paragraph;\nuse Notion\\Exceptions\\BlockException;\nuse PHPUnit\\Framework\\TestCase;\n\nclass ChildDatabaseTest extends TestCase\n{\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"child_database\",\n            \"child_database\"   => [ \"title\" => \"Database title\" ],\n        ];\n\n        $childDatabase = ChildDatabase::fromArray($array);\n\n        $this->assertEquals(\"Database title\", $childDatabase->title);\n        $this->assertFalse($childDatabase->metadata()->inTrash);\n\n        $this->assertEquals($childDatabase, BlockFactory::fromArray($array));\n        $this->assertEquals($array, $childDatabase->toArray());\n    }\n\n    public function test_error_on_wrong_type(): void\n    {\n        $this->expectException(BlockException::class);\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"wrong-type\",\n            \"child_database\"   => [ \"title\" => \"Wrong array\" ],\n        ];\n\n        ChildDatabase::fromArray($array);\n    }\n\n    public function test_not_allow_to_change_children(): void\n    {\n        $block = ChildDatabase::fromArray($this->mockArray());\n\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $block->changeChildren(\n            Paragraph::fromString(\"Sample paragraph.\")\n        );\n    }\n\n    public function test_not_allow_to_add_child(): void\n    {\n        $block = ChildDatabase::fromArray($this->mockArray());\n\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $block->addChild(\n            Paragraph::fromString(\"Sample paragraph.\")\n        );\n    }\n\n    public function test_delete(): void\n    {\n        $block = ChildDatabase::fromArray($this->mockArray());\n\n        $block = $block->delete();\n\n        $this->assertTrue($block->metadata()->inTrash);\n    }\n\n    private function mockArray(): array\n    {\n        return [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"child_database\",\n            \"child_database\"   => [ \"title\" => \"Database title\" ],\n        ];\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/ChildPageTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks;\n\nuse Notion\\Blocks\\BlockFactory;\nuse Notion\\Blocks\\ChildPage;\nuse Notion\\Blocks\\Paragraph;\nuse Notion\\Exceptions\\BlockException;\nuse PHPUnit\\Framework\\TestCase;\n\nclass ChildPageTest extends TestCase\n{\n    public function test_create_from_array(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"child_page\",\n            \"child_page\"       => [ \"title\" => \"Page title\" ],\n        ];\n\n        $childPage = ChildPage::fromArray($array);\n\n        $this->assertEquals(\"Page title\", $childPage->title);\n        $this->assertFalse($childPage->metadata()->inTrash);\n\n        $this->assertEquals($childPage, BlockFactory::fromArray($array));\n        $this->assertEquals($array, $childPage->toArray());\n    }\n\n    public function test_error_on_wrong_type(): void\n    {\n        $this->expectException(BlockException::class);\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"wrong-type\",\n            \"child_page\"       => [ \"title\" => \"Wrong array\" ],\n        ];\n\n        ChildPage::fromArray($array);\n    }\n\n    public function test_not_allow_to_change_children(): void\n    {\n        $block = ChildPage::fromArray($this->mockArray());\n\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $block->changeChildren(\n            Paragraph::fromString(\"Sample paragraph.\")\n        );\n    }\n\n    public function test_not_allow_to_add_child(): void\n    {\n        $block = ChildPage::fromArray($this->mockArray());\n\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $block->addChild(\n            Paragraph::fromString(\"Sample paragraph.\")\n        );\n    }\n\n    public function test_delete(): void\n    {\n        $block = ChildPage::fromArray($this->mockArray());\n\n        $block = $block->delete();\n\n        $this->assertTrue($block->metadata()->inTrash);\n    }\n\n    private function mockArray(): array\n    {\n        return [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"child_page\",\n            \"child_page\"       => [ \"title\" => \"Page title\" ],\n        ];\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/CodeTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks;\n\nuse Notion\\Blocks\\BlockFactory;\nuse Notion\\Blocks\\Code;\nuse Notion\\Blocks\\CodeLanguage;\nuse Notion\\Blocks\\Divider;\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Common\\Date;\nuse Notion\\Common\\RichText;\nuse PHPUnit\\Framework\\TestCase;\n\nclass CodeTest extends TestCase\n{\n    public function test_create_empty_code_block(): void\n    {\n        $code = Code::create();\n\n        $this->assertEmpty($code->text);\n        $this->assertEquals(CodeLanguage::PlainText, $code->language);\n    }\n\n    public function test_create_from_array(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"code\",\n            \"code\"        => [\n                \"rich_text\" => [\n                    [\n                        \"plain_text\"  => \"<?php\\necho 'Hello World!';\",\n                        \"href\"        => null,\n                        \"type\"        => \"text\",\n                        \"text\"        => [\n                            \"content\" => \"<?php\\necho 'Hello World!';\",\n                        ],\n                        \"annotations\" => [\n                            \"bold\"          => false,\n                            \"italic\"        => false,\n                            \"strikethrough\" => false,\n                            \"underline\"     => false,\n                            \"code\"          => false,\n                            \"color\"         => \"default\",\n                        ],\n                    ],\n                ],\n                \"caption\" => [\n                    [\n                        \"plain_text\"  => \"Code caption example\",\n                        \"href\"        => null,\n                        \"type\"        => \"text\",\n                        \"text\"        => [\n                            \"content\" => \"Code caption example\",\n                        ],\n                        \"annotations\" => [\n                            \"bold\"          => false,\n                            \"italic\"        => false,\n                            \"strikethrough\" => false,\n                            \"underline\"     => false,\n                            \"code\"          => false,\n                            \"color\"         => \"default\",\n                        ],\n                    ],\n                ],\n                \"language\" => \"php\",\n            ],\n        ];\n\n        $code = Code::fromArray($array);\n\n        $this->assertCount(1, $code->text);\n        $this->assertEquals(\"<?php\\necho 'Hello World!';\", $code->toString());\n        $this->assertEquals($code, BlockFactory::fromArray($array));\n    }\n\n    public function test_error_on_wrong_type(): void\n    {\n        $this->expectException(BlockException::class);\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"wrong-type\",\n            \"code\"             => [\n                \"language\"  => \"php\",\n                \"rich_text\" => [],\n                \"caption\"   => [],\n            ],\n        ];\n\n        $this->expectException(\\Exception::class);\n        Code::fromArray($array);\n    }\n\n    public function test_transform_in_array(): void\n    {\n        $h = Code::fromText(\n            [ RichText::fromString(\"<?php\\necho 'Hello World!';\") ],\n            CodeLanguage::Php,\n        );\n\n        $expected = [\n            \"object\"           => \"block\",\n            \"created_time\"     => $h->metadata()->createdTime->format(Date::FORMAT),\n            \"last_edited_time\" => $h->metadata()->lastEditedTime->format(Date::FORMAT),\n            \"in_trash\"         => false,\n            \"has_children\"      => false,\n            \"type\"             => \"code\",\n            \"code\"        => [\n                \"language\" => \"php\",\n                \"rich_text\" => [[\n                    \"plain_text\"  => \"<?php\\necho 'Hello World!';\",\n                    \"href\"        => null,\n                    \"type\"        => \"text\",\n                    \"text\"        => [\n                        \"content\" => \"<?php\\necho 'Hello World!';\",\n                    ],\n                    \"annotations\" => [\n                        \"bold\"          => false,\n                        \"italic\"        => false,\n                        \"strikethrough\" => false,\n                        \"underline\"     => false,\n                        \"code\"          => false,\n                        \"color\"         => \"default\",\n                    ],\n                ]],\n                \"caption\" => [],\n            ],\n        ];\n\n        $this->assertEquals($expected, $h->toArray());\n    }\n\n    public function test_replace_text(): void\n    {\n        $oldHeading = Code::fromString(\"This is an old code\");\n\n        $newHeading = $oldHeading->changeText(\n            RichText::fromString(\"This is a \"),\n            RichText::fromString(\"new code\"),\n        );\n\n        $this->assertEquals(\"This is an old code\", $oldHeading->toString());\n        $this->assertEquals(\"This is a new code\", $newHeading->toString());\n    }\n\n    public function test_add_text(): void\n    {\n        $oldHeading = Code::fromString(\"A code\");\n\n        $newHeading = $oldHeading->addText(\n            RichText::fromString(\" can be extended.\")\n        );\n\n        $this->assertEquals(\"A code\", $oldHeading->toString());\n        $this->assertEquals(\"A code can be extended.\", $newHeading->toString());\n    }\n\n    public function test_change_language(): void\n    {\n        $language = CodeLanguage::Php;\n        $code = Code::fromString(\"Simple code\")->changeLanguage($language);\n\n        $this->assertEquals($language, $code->language);\n    }\n\n    public function test_no_children_support(): void\n    {\n        $block = Code::create();\n\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $block->changeChildren();\n    }\n\n    public function test_no_children_support_add(): void\n    {\n        $block = Code::create();\n\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $block->addChild(Divider::create());\n    }\n\n    public function test_change_caption(): void\n    {\n        $block = Code::create()\n            ->addText(RichText::fromString(\"<?php echo 'Hi!'\"))\n            ->changeLanguage(CodeLanguage::Php)\n            ->changeCaption(RichText::fromString(\"Code caption\"));\n\n        $this->assertSame(\"Code caption\", RichText::multipleToString(...$block->caption));\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/ColumnListTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks;\n\nuse Notion\\Blocks\\BlockFactory;\nuse Notion\\Blocks\\Column;\nuse Notion\\Blocks\\ColumnList;\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Blocks\\Paragraph;\nuse Notion\\Exceptions\\ColumnListException;\nuse PHPUnit\\Framework\\TestCase;\n\nclass ColumnListTest extends TestCase\n{\n    public function test_create(): void\n    {\n        $column1 = Column::create(Paragraph::fromString(\"Paragraph 1\"));\n        $column2 = Column::create(Paragraph::fromString(\"Paragraph 2\"));\n\n        $list = ColumnList::create($column1, $column2);\n\n        $this->assertEquals([ $column1, $column2 ], $list->columns);\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => true,\n            \"type\"             => \"column_list\",\n            \"column_list\"      => [\n                \"children\"     => [\n                    [\n                        \"object\"           => \"block\",\n                        \"id\"               => \"880f4b72-28b9-497a-b9c3-dd67d61b87ef\",\n                        \"created_time\"     => \"2021-10-18T17:09:00.000000Z\",\n                        \"last_edited_time\" => \"2021-10-18T17:09:00.000000Z\",\n                        \"in_trash\"         => false,\n                        \"has_children\"     => true,\n                        \"type\"             => \"column\",\n                        \"column\"           => [\n                            \"children\"         => [[\n                                \"object\"           => \"block\",\n                                \"id\"               => \"64caffeb-c947-4acd-b6ee-b1856bb91844\",\n                                \"created_time\"     => \"2021-10-18T17:09:00.000000Z\",\n                                \"last_edited_time\" => \"2021-10-18T17:09:00.000000Z\",\n                                \"in_trash\"         => false,\n                                \"has_children\"     => false,\n                                \"type\"             => \"divider\",\n                                \"divider\"          => new \\stdClass(),\n                            ]],\n                        ],\n                    ],\n                    [\n                        \"object\"           => \"block\",\n                        \"id\"               => \"c45d7fef-08ff-4638-843d-d984c7d3ef72\",\n                        \"created_time\"     => \"2021-10-18T17:09:00.000000Z\",\n                        \"last_edited_time\" => \"2021-10-18T17:09:00.000000Z\",\n                        \"in_trash\"         => false,\n                        \"has_children\"     => true,\n                        \"type\"             => \"column\",\n                        \"column\"           => [\n                            \"children\"         => [[\n                                \"object\"           => \"block\",\n                                \"id\"               => \"e99edc10-621a-43cf-9a99-eca8d10ded44\",\n                                \"created_time\"     => \"2021-10-18T17:09:00.000000Z\",\n                                \"last_edited_time\" => \"2021-10-18T17:09:00.000000Z\",\n                                \"in_trash\"         => false,\n                                \"has_children\"     => false,\n                                \"type\"             => \"divider\",\n                                \"divider\"          => new \\stdClass(),\n                            ]],\n                        ],\n                    ],\n                ],\n            ],\n        ];\n\n        $list = ColumnList::fromArray($array);\n        $listFromFactory = BlockFactory::fromArray($array);\n\n        $this->assertEquals($list, $listFromFactory);\n        $this->assertEquals($array, $list->toArray());\n        $this->assertEquals(\"04a13895-f072-4814-8af7-cd11af127040\", $list->metadata()->id);\n        $this->assertTrue($list->metadata()->hasChildren);\n    }\n\n    public function test_change_children(): void\n    {\n        $column1 = Column::create(Paragraph::fromString(\"Paragraph 1\"));\n        $column2 = Column::create(Paragraph::fromString(\"Paragraph 2\"));\n\n        $list = ColumnList::create($column1)->changeChildren($column2);\n        $this->assertEquals([ $column2 ], $list->columns);\n    }\n\n    public function test_change_children_to_not_columns(): void\n    {\n        $column = Column::create(Paragraph::fromString(\"Paragraph 1\"));\n\n        $list = ColumnList::create($column);\n\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $list->changeChildren(Paragraph::fromString(\"This should be a column.\"));\n    }\n\n    public function test_add_child(): void\n    {\n        $column = Column::create(Paragraph::fromString(\"Paragraph 1\"));\n\n        $list = ColumnList::create()->addChild($column);\n        $this->assertEquals([ $column ], $list->columns);\n    }\n\n    public function test_add_child_that_is_not_column(): void\n    {\n        $child = Paragraph::fromString(\"Not a column\");\n\n        $this->expectException(ColumnListException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        ColumnList::create()->addChild($child);\n    }\n\n    public function test_delete(): void\n    {\n        $block = ColumnList::create();\n\n        $block = $block->delete();\n\n        $this->assertTrue($block->metadata()->inTrash);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/ColumnTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks;\n\nuse Notion\\Blocks\\Column;\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Blocks\\Paragraph;\nuse PHPUnit\\Framework\\TestCase;\n\nclass ColumnTest extends TestCase\n{\n    public function test_create_column(): void\n    {\n        $children = [ Paragraph::fromString(\"A paragraph.\") ];\n        $column = Column::create(...$children);\n\n        $this->assertEquals($children, $column->children);\n    }\n\n    public function test_create_change_child_column(): void\n    {\n        $childColumn = Column::create(Paragraph::fromString(\"A paragraph\"));\n\n        $this->expectException(BlockException::class);\n        Column::create($childColumn);\n    }\n\n    public function test_add_child(): void\n    {\n        $paragraph1 = Paragraph::fromString(\"Paragraph 1.\");\n        $paragraph2 = Paragraph::fromString(\"Paragraph 2.\");\n\n        $column = Column::create($paragraph1)->addChild($paragraph2);\n\n        $this->assertEquals([ $paragraph1, $paragraph2 ], $column->children);\n    }\n\n    public function test_change_child(): void\n    {\n        $children1 = [ Paragraph::fromString(\"Paragraph 1.\") ];\n        $children2 = [ Paragraph::fromString(\"Paragraph 2.\") ];\n\n        $column = Column::create(...$children1)->changeChildren(...$children2);\n\n        $this->assertEquals($children2, $column->children);\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => true,\n            \"type\"             => \"column\",\n            \"column\"           => [\n                \"children\"         => [[\n                    \"object\"           => \"block\",\n                    \"id\"               => \"64caffeb-c947-4acd-b6ee-b1856bb91844\",\n                    \"created_time\"     => \"2021-10-18T17:09:00.000000Z\",\n                    \"last_edited_time\" => \"2021-10-18T17:09:00.000000Z\",\n                    \"in_trash\"         => false,\n                    \"has_children\"     => false,\n                    \"type\"             => \"divider\",\n                    \"divider\"          => new \\stdClass(),\n                ]],\n            ],\n        ];\n\n        $column = Column::fromArray($array);\n\n        $this->assertEquals($array, $column->toArray());\n    }\n\n    public function test_delete(): void\n    {\n        $block = Column::create();\n\n        $block = $block->delete();\n\n        $this->assertTrue($block->metadata()->inTrash);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/DividerTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks;\n\nuse Notion\\Blocks\\BlockFactory;\nuse Notion\\Blocks\\Divider;\nuse Notion\\Blocks\\Paragraph;\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Common\\Date;\nuse PHPUnit\\Framework\\TestCase;\n\nclass DividerTest extends TestCase\n{\n    public function test_create_divider(): void\n    {\n        $divider = Divider::create();\n\n        $this->assertEquals(\"divider\", $divider->metadata()->type->value);\n    }\n\n    public function test_create_from_array(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"divider\",\n            \"divider\"          => new \\stdClass(),\n        ];\n\n        $divider = Divider::fromArray($array);\n\n        $this->assertEquals($divider, BlockFactory::fromArray($array));\n    }\n\n    public function test_error_on_wrong_type(): void\n    {\n        $this->expectException(BlockException::class);\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"wrong-type\",\n            \"divider\"          => new \\stdClass(),\n        ];\n\n        Divider::fromArray($array);\n    }\n\n    public function test_transform_in_array(): void\n    {\n        $divider = Divider::create();\n\n        $expected = [\n            \"object\"           => \"block\",\n            \"created_time\"     => $divider->metadata()->createdTime->format(Date::FORMAT),\n            \"last_edited_time\" => $divider->metadata()->createdTime->format(Date::FORMAT),\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"divider\",\n            \"divider\"          => new \\stdClass(),\n        ];\n\n        $this->assertEquals($expected, $divider->toArray());\n    }\n\n    public function test_no_children_support(): void\n    {\n        $block = Divider::create();\n\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $block->changeChildren();\n    }\n\n    public function test_no_children_support_2(): void\n    {\n        $block = Divider::create();\n\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $block->addChild(Paragraph::create());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/EmbedTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks;\n\nuse Notion\\Blocks\\BlockFactory;\nuse Notion\\Blocks\\Embed;\nuse Notion\\Blocks\\Paragraph;\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Common\\Date;\nuse PHPUnit\\Framework\\TestCase;\n\nclass EmbedTest extends TestCase\n{\n    public function test_create_embed(): void\n    {\n        $embed = Embed::fromUrl(\"https://my-site.com\");\n\n        $this->assertEquals(\"https://my-site.com\", $embed->url);\n    }\n\n    public function test_create_from_array(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"embed\",\n            \"embed\"            => [ \"url\" => \"https://my-site.com\" ],\n        ];\n\n        $embed = Embed::fromArray($array);\n\n        $this->assertEquals(\"https://my-site.com\", $embed->url);\n\n        $this->assertEquals($embed, BlockFactory::fromArray($array));\n    }\n\n    public function test_error_on_wrong_type(): void\n    {\n        $this->expectException(BlockException::class);\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"wrong-type\",\n            \"embed\"            => [ \"url\" => \"https://my-site.com\" ],\n        ];\n\n        Embed::fromArray($array);\n    }\n\n    public function test_transform_in_array(): void\n    {\n        $embed = Embed::fromUrl(\"https://my-site.com\");\n\n        $expected = [\n            \"object\"           => \"block\",\n            \"created_time\"     => $embed->metadata()->createdTime->format(Date::FORMAT),\n            \"last_edited_time\" => $embed->metadata()->createdTime->format(Date::FORMAT),\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"embed\",\n            \"embed\"            => [ \"url\" => \"https://my-site.com\" ],\n        ];\n\n        $this->assertEquals($expected, $embed->toArray());\n    }\n\n    public function test_replace_url(): void\n    {\n        $old = Embed::fromUrl(\"https://my-site.com\");\n        $new = $old->changeUrl(\"https://another-site.com\");\n\n        $this->assertEquals(\"https://my-site.com\", $old->url);\n        $this->assertEquals(\"https://another-site.com\", $new->url);\n    }\n\n    public function test_no_children_support(): void\n    {\n        $block = Embed::fromUrl();\n\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $block->changeChildren();\n    }\n\n    public function test_no_children_support_2(): void\n    {\n        $block = Embed::fromUrl();\n\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $block->addChild(Paragraph::create());\n    }\n\n    public function test_delete(): void\n    {\n        $block = Embed::fromUrl();\n\n        $block = $block->delete();\n\n        $this->assertTrue($block->metadata()->inTrash);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/EquationBlockTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks;\n\nuse Notion\\Blocks\\BlockFactory;\nuse Notion\\Blocks\\EquationBlock;\nuse Notion\\Blocks\\Paragraph;\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Common\\Date;\nuse Notion\\Common\\Equation;\nuse PHPUnit\\Framework\\TestCase;\n\nclass EquationBlockTest extends TestCase\n{\n    public function test_create_equation(): void\n    {\n        $equation = EquationBlock::fromString(\"a^2 + b^2 = c^2\");\n\n        $this->assertEquals(\"a^2 + b^2 = c^2\", $equation->equation->expression);\n    }\n\n    public function test_create_from_array(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"equation\",\n            \"equation\"         => [ \"expression\" => \"a^2 + b^2 = c^2\" ],\n        ];\n\n        $equation = EquationBlock::fromArray($array);\n\n        $this->assertEquals(\"a^2 + b^2 = c^2\", $equation->equation->expression);\n\n        $this->assertEquals($equation, BlockFactory::fromArray($array));\n    }\n\n    public function test_error_on_wrong_type(): void\n    {\n        $this->expectException(BlockException::class);\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"wrong-type\",\n            \"equation\"         => [ \"expression\" => \"a^2 + b^2 = c^2\" ],\n        ];\n\n        EquationBlock::fromArray($array);\n    }\n\n    public function test_transform_in_array(): void\n    {\n        $equation = EquationBlock::fromString(\"a^2 + b^2 = c^2\");\n\n        $expected = [\n            \"object\"           => \"block\",\n            \"created_time\"     => $equation->metadata()->createdTime->format(Date::FORMAT),\n            \"last_edited_time\" => $equation->metadata()->createdTime->format(Date::FORMAT),\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"equation\",\n            \"equation\"         => [ \"expression\" => \"a^2 + b^2 = c^2\" ],\n        ];\n\n        $this->assertEquals($expected, $equation->toArray());\n    }\n\n    public function test_replace_equation(): void\n    {\n        $equation = Equation::fromString(\"a^2 + b^2 = c^2\");\n        $equationBlock = EquationBlock::fromString()->changeEquation($equation);\n\n        $this->assertEquals($equation, $equationBlock->equation);\n    }\n\n    public function test_no_children_support(): void\n    {\n        $block = EquationBlock::fromString();\n\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $block->changeChildren();\n    }\n\n    public function test_no_children_support_2(): void\n    {\n        $block = EquationBlock::fromString();\n\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $block->addChild(Paragraph::create());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/FileBlockTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks;\n\nuse Notion\\Blocks\\BlockFactory;\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Blocks\\FileBlock;\nuse Notion\\Blocks\\Paragraph;\nuse Notion\\Common\\Date;\nuse Notion\\Common\\File;\nuse PHPUnit\\Framework\\TestCase;\n\nclass FileBlockTest extends TestCase\n{\n    public function test_create_file(): void\n    {\n        $file = File::createExternal(\"https://my-site.com/file.doc\");\n        $fileBlock = FileBlock::fromFile($file);\n\n        $this->assertEquals($file, $fileBlock->file());\n    }\n\n    public function test_create_from_array(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"file\",\n            \"file\"            => [\n                \"type\"     => \"external\",\n                \"external\" => [\n                    \"url\" => \"https://my-site.com/file.doc\"\n                ],\n            ],\n        ];\n\n        $fileBlock = FileBlock::fromArray($array);\n\n        $this->assertEquals(\"https://my-site.com/file.doc\", $fileBlock->file()->url);\n\n        $this->assertEquals($fileBlock, BlockFactory::fromArray($array));\n    }\n\n    public function test_error_on_wrong_type(): void\n    {\n        $this->expectException(BlockException::class);\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"wrong-type\",\n            \"file\"            => [\n                \"type\"     => \"external\",\n                \"external\" => [\n                    \"url\" => \"https://my-site.com/file.doc\"\n                ],\n            ],\n        ];\n\n        FileBlock::fromArray($array);\n    }\n\n    public function test_transform_in_array(): void\n    {\n        $file = File::createExternal(\"https://my-site.com/file.doc\");\n        $fileBlock = FileBlock::fromFile($file);\n\n        $expected = [\n            \"object\"           => \"block\",\n            \"created_time\"     => $fileBlock->metadata()->createdTime->format(Date::FORMAT),\n            \"last_edited_time\" => $fileBlock->metadata()->createdTime->format(Date::FORMAT),\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"file\",\n            \"file\"            => [\n                \"type\"     => \"external\",\n                \"external\" => [\n                    \"url\" => \"https://my-site.com/file.doc\"\n                ],\n            ],\n        ];\n\n        $this->assertEquals($expected, $fileBlock->toArray());\n    }\n\n    public function test_replace_file(): void\n    {\n        $file1 = File::createExternal(\"https://my-site.com/file1.doc\");\n        $file2 = File::createExternal(\"https://my-site.com/file2.doc\");\n\n        $old = FileBlock::fromFile($file1);\n        $new = $old->changeFile($file2);\n\n        $this->assertEquals($file1, $old->file());\n        $this->assertEquals($file2, $new->file());\n    }\n\n    public function test_no_children_support(): void\n    {\n        $file = File::createExternal(\"https://my-site.com/file.doc\");\n        $block = FileBlock::fromFile($file);\n\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $block->changeChildren();\n    }\n\n    public function test_no_children_support_2(): void\n    {\n        $file = File::createExternal(\"https://my-site.com/file.doc\");\n        $block = FileBlock::fromFile($file);\n\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $block->addChild(Paragraph::create());\n    }\n\n    public function test_delete(): void\n    {\n        $file = File::createExternal(\"https://example.com/file.doc\");\n        $block = FileBlock::fromFile($file);\n\n        $block = $block->delete();\n\n        $this->assertTrue($block->metadata()->inTrash);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/Heading1Test.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks;\n\nuse Notion\\Blocks\\BlockFactory;\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Blocks\\Heading1;\nuse Notion\\Blocks\\Paragraph;\nuse Notion\\Common\\Color;\nuse Notion\\Common\\Date;\nuse Notion\\Common\\RichText;\nuse Notion\\Exceptions\\HeadingException;\nuse PHPUnit\\Framework\\TestCase;\n\nclass Heading1Test extends TestCase\n{\n    public function test_create_empty_heading(): void\n    {\n        $heading = Heading1::fromText();\n\n        $this->assertEmpty($heading->text);\n    }\n\n    public function test_create_from_string(): void\n    {\n        $heading = Heading1::fromString(\"Dummy heading.\");\n\n        $this->assertEquals(\"Dummy heading.\", $heading->toString());\n    }\n\n    public function test_create_from_array(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"heading_1\",\n            \"heading_1\"        => [\n                \"rich_text\" => [\n                    [\n                        \"plain_text\"  => \"Notion headings \",\n                        \"href\"        => null,\n                        \"type\"        => \"text\",\n                        \"text\"        => [\n                            \"content\" => \"Notion headings \",\n                        ],\n                        \"annotations\" => [\n                            \"bold\"          => false,\n                            \"italic\"        => false,\n                            \"strikethrough\" => false,\n                            \"underline\"     => false,\n                            \"code\"          => false,\n                            \"color\"         => \"default\",\n                        ],\n                    ],\n                    [\n                        \"plain_text\"  => \"rock!\",\n                        \"href\"        => null,\n                        \"type\"        => \"text\",\n                        \"text\"        => [\n                            \"content\" => \"rock!\",\n                        ],\n                        \"annotations\" => [\n                            \"bold\"          => true,\n                            \"italic\"        => false,\n                            \"strikethrough\" => false,\n                            \"underline\"     => false,\n                            \"code\"          => false,\n                            \"color\"         => \"red\",\n                        ],\n                    ],\n                ],\n                \"is_toggleable\" => false,\n                \"children\" => [],\n                \"color\" => \"default\",\n            ],\n        ];\n\n        $heading = Heading1::fromArray($array);\n\n        $this->assertCount(2, $heading->text);\n        $this->assertEquals(\"Notion headings rock!\", $heading->toString());\n        $this->assertFalse($heading->isToggleable);\n        $this->assertFalse($heading->metadata()->inTrash);\n\n        $this->assertEquals($heading, BlockFactory::fromArray($array));\n    }\n\n    public function test_error_on_wrong_type(): void\n    {\n        $this->expectException(BlockException::class);\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"wrong-type\",\n            \"heading_1\"        => [\n                \"rich_text\"     => [],\n                \"is_toggleable\"  => false,\n            ],\n        ];\n\n        Heading1::fromArray($array);\n    }\n\n    public function test_transform_in_array(): void\n    {\n        $h = Heading1::fromString(\"Simple heading\");\n\n        $expected = [\n            \"object\"           => \"block\",\n            \"created_time\"     => $h->metadata()->createdTime->format(Date::FORMAT),\n            \"last_edited_time\" => $h->metadata()->lastEditedTime->format(Date::FORMAT),\n            \"in_trash\"         => false,\n            \"has_children\"      => false,\n            \"type\"             => \"heading_1\",\n            \"heading_1\"        => [\n                \"rich_text\" => [[\n                    \"plain_text\"  => \"Simple heading\",\n                    \"href\"        => null,\n                    \"type\"        => \"text\",\n                    \"text\"        => [\n                        \"content\" => \"Simple heading\",\n                    ],\n                    \"annotations\" => [\n                        \"bold\"          => false,\n                        \"italic\"        => false,\n                        \"strikethrough\" => false,\n                        \"underline\"     => false,\n                        \"code\"          => false,\n                        \"color\"         => \"default\",\n                    ],\n                ]],\n                \"is_toggleable\" => false,\n                \"children\" => [],\n                \"color\" => \"default\",\n            ],\n        ];\n\n        $this->assertEquals($expected, $h->toArray());\n    }\n\n    public function test_replace_text(): void\n    {\n        $oldHeading = Heading1::fromString(\"This is an old heading\");\n\n        $newHeading = $oldHeading->changeText(\n            RichText::fromString(\"This is a \"),\n            RichText::fromString(\"new heading\"),\n        );\n\n        $this->assertEquals(\"This is an old heading\", $oldHeading->toString());\n        $this->assertEquals(\"This is a new heading\", $newHeading->toString());\n    }\n\n    public function test_add_text(): void\n    {\n        $oldHeading = Heading1::fromString(\"A heading\");\n\n        $newHeading = $oldHeading->addText(\n            RichText::fromString(\" can be extended.\")\n        );\n\n        $this->assertEquals(\"A heading\", $oldHeading->toString());\n        $this->assertEquals(\"A heading can be extended.\", $newHeading->toString());\n    }\n\n    public function test_no_children_support(): void\n    {\n        $block = Heading1::fromText();\n\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $block->changeChildren();\n    }\n\n    public function test_togglify(): void\n    {\n        $block = Heading1::fromText()\n            ->toggllify();\n\n        $this->assertTrue($block->isToggleable);\n    }\n\n    public function test_untogglify(): void\n    {\n        $block = Heading1::fromText()->toggllify()->untogglify();\n\n        $this->assertFalse($block->isToggleable);\n    }\n\n    public function test_untogglify_with_children(): void\n    {\n        $this->expectException(HeadingException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        Heading1::fromText()\n            ->toggllify()\n            ->addChild(Paragraph::fromString(\"Inside paragraph.\"))\n            ->untogglify();\n    }\n\n    public function test_change_children_from_toggleable_heading(): void\n    {\n        $block = Heading1::fromText()\n            ->toggllify()\n            ->changeChildren(\n                Paragraph::fromString(\"Paragraph 1\"),\n                Paragraph::fromString(\"Paragraph 2\"),\n            );\n\n        $this->assertCount(2, $block->children ?? []);\n    }\n\n    public function test_add_child_to_untoggleable_heading(): void\n    {\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        Heading1::fromText()->addChild(Paragraph::fromString(\"Paragraph 1\"));\n    }\n\n    public function test_toggleable_array_conversion(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"heading_1\",\n            \"heading_1\"        => [\n                \"rich_text\" => [\n                    [\n                        \"plain_text\"  => \"Notion headings \",\n                        \"href\"        => null,\n                        \"type\"        => \"text\",\n                        \"text\"        => [\n                            \"content\" => \"Notion headings \",\n                        ],\n                        \"annotations\" => [\n                            \"bold\"          => false,\n                            \"italic\"        => false,\n                            \"strikethrough\" => false,\n                            \"underline\"     => false,\n                            \"code\"          => false,\n                            \"color\"         => \"default\",\n                        ],\n                    ],\n                    [\n                        \"plain_text\"  => \"rock!\",\n                        \"href\"        => null,\n                        \"type\"        => \"text\",\n                        \"text\"        => [\n                            \"content\" => \"rock!\",\n                        ],\n                        \"annotations\" => [\n                            \"bold\"          => true,\n                            \"italic\"        => false,\n                            \"strikethrough\" => false,\n                            \"underline\"     => false,\n                            \"code\"          => false,\n                            \"color\"         => \"red\",\n                        ],\n                    ],\n                ],\n                \"is_toggleable\" => true,\n                \"children\" => [\n                    [\n                        \"object\"           => \"block\",\n                        \"id\"               => \"a5dc4448-6c0a-48ab-9b43-0ea838bb6070\",\n                        \"created_time\"     => \"2021-10-18T17:09:00.000000Z\",\n                        \"last_edited_time\" => \"2021-10-18T17:09:00.000000Z\",\n                        \"in_trash\"         => false,\n                        \"has_children\"     => false,\n                        \"type\"             => \"divider\",\n                        \"divider\"          => new \\stdClass(),\n                    ]\n                ],\n                \"color\" => \"default\",\n            ],\n        ];\n\n        $heading = Heading1::fromArray($array);\n\n        $this->assertEquals($array, $heading->toArray());\n    }\n\n    public function test_change_color(): void\n    {\n        $h1 = Heading1::fromString(\"Hello!\")->changeColor(Color::Green);\n\n        $this->assertSame(Color::Green, $h1->color);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/Heading2Test.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks;\n\nuse Notion\\Blocks\\BlockFactory;\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Blocks\\Heading2;\nuse Notion\\Blocks\\Paragraph;\nuse Notion\\Common\\Color;\nuse Notion\\Common\\Date;\nuse Notion\\Common\\RichText;\nuse Notion\\Exceptions\\HeadingException;\nuse PHPUnit\\Framework\\TestCase;\n\nclass Heading2Test extends TestCase\n{\n    public function test_create_empty_heading(): void\n    {\n        $heading = Heading2::fromText();\n\n        $this->assertEmpty($heading->text);\n    }\n\n    public function test_create_from_string(): void\n    {\n        $heading = Heading2::fromString(\"Dummy heading.\");\n\n        $this->assertEquals(\"Dummy heading.\", $heading->toString());\n    }\n\n    public function test_create_from_array(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"heading_2\",\n            \"heading_2\"        => [\n                \"rich_text\" => [\n                    [\n                        \"plain_text\"  => \"Notion headings \",\n                        \"href\"        => null,\n                        \"type\"        => \"text\",\n                        \"text\"        => [\n                            \"content\" => \"Notion headings \",\n                        ],\n                        \"annotations\" => [\n                            \"bold\"          => false,\n                            \"italic\"        => false,\n                            \"strikethrough\" => false,\n                            \"underline\"     => false,\n                            \"code\"          => false,\n                            \"color\"         => \"default\",\n                        ],\n                    ],\n                    [\n                        \"plain_text\"  => \"rock!\",\n                        \"href\"        => null,\n                        \"type\"        => \"text\",\n                        \"text\"        => [\n                            \"content\" => \"rock!\",\n                        ],\n                        \"annotations\" => [\n                            \"bold\"          => true,\n                            \"italic\"        => false,\n                            \"strikethrough\" => false,\n                            \"underline\"     => false,\n                            \"code\"          => false,\n                            \"color\"         => \"red\",\n                        ],\n                    ],\n                ],\n                \"is_toggleable\" => false,\n                \"children\" => [],\n                \"color\" => \"default\",\n            ],\n        ];\n\n        $heading = Heading2::fromArray($array);\n\n        $this->assertCount(2, $heading->text);\n        $this->assertEquals(\"Notion headings rock!\", $heading->toString());\n        $this->assertFalse($heading->isToggleable);\n        $this->assertFalse($heading->metadata()->inTrash);\n\n        $this->assertEquals($heading, BlockFactory::fromArray($array));\n    }\n\n    public function test_error_on_wrong_type(): void\n    {\n        $this->expectException(BlockException::class);\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"wrong-type\",\n            \"heading_2\"        => [\n                \"rich_text\"     => [],\n                \"is_toggleable\"  => false,\n            ],\n        ];\n\n        Heading2::fromArray($array);\n    }\n\n    public function test_transform_in_array(): void\n    {\n        $h = Heading2::fromString(\"Simple heading\");\n\n        $expected = [\n            \"object\"           => \"block\",\n            \"created_time\"     => $h->metadata()->createdTime->format(Date::FORMAT),\n            \"last_edited_time\" => $h->metadata()->lastEditedTime->format(Date::FORMAT),\n            \"in_trash\"         => false,\n            \"has_children\"      => false,\n            \"type\"             => \"heading_2\",\n            \"heading_2\"        => [\n                \"rich_text\" => [[\n                    \"plain_text\"  => \"Simple heading\",\n                    \"href\"        => null,\n                    \"type\"        => \"text\",\n                    \"text\"        => [\n                        \"content\" => \"Simple heading\",\n                    ],\n                    \"annotations\" => [\n                        \"bold\"          => false,\n                        \"italic\"        => false,\n                        \"strikethrough\" => false,\n                        \"underline\"     => false,\n                        \"code\"          => false,\n                        \"color\"         => \"default\",\n                    ],\n                ]],\n                \"is_toggleable\" => false,\n                \"children\" => [],\n                \"color\" => \"default\",\n            ],\n        ];\n\n        $this->assertEquals($expected, $h->toArray());\n    }\n\n    public function test_replace_text(): void\n    {\n        $oldHeading = Heading2::fromString(\"This is an old heading\");\n\n        $newHeading = $oldHeading->changeText(\n            RichText::fromString(\"This is a \"),\n            RichText::fromString(\"new heading\"),\n        );\n\n        $this->assertEquals(\"This is an old heading\", $oldHeading->toString());\n        $this->assertEquals(\"This is a new heading\", $newHeading->toString());\n    }\n\n    public function test_add_text(): void\n    {\n        $oldHeading = Heading2::fromString(\"A heading\");\n\n        $newHeading = $oldHeading->addText(\n            RichText::fromString(\" can be extended.\")\n        );\n\n        $this->assertEquals(\"A heading\", $oldHeading->toString());\n        $this->assertEquals(\"A heading can be extended.\", $newHeading->toString());\n    }\n\n    public function test_no_children_support(): void\n    {\n        $block = Heading2::fromText();\n\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $block->changeChildren();\n    }\n\n    public function test_togglify(): void\n    {\n        $block = Heading2::fromText()\n            ->toggllify();\n\n        $this->assertTrue($block->isToggleable);\n    }\n\n    public function test_untogglify(): void\n    {\n        $block = Heading2::fromText()->toggllify()->untogglify();\n\n        $this->assertFalse($block->isToggleable);\n    }\n\n    public function test_untogglify_with_children(): void\n    {\n        $this->expectException(HeadingException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        Heading2::fromText()\n            ->toggllify()\n            ->addChild(Paragraph::fromString(\"Inside paragraph.\"))\n            ->untogglify();\n    }\n\n    public function test_change_children_from_toggleable_heading(): void\n    {\n        $block = Heading2::fromText()\n            ->toggllify()\n            ->changeChildren(\n                Paragraph::fromString(\"Paragraph 1\"),\n                Paragraph::fromString(\"Paragraph 2\"),\n            );\n\n        $this->assertCount(2, $block->children ?? []);\n    }\n\n    public function test_add_child_to_untoggleable_heading(): void\n    {\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        Heading2::fromText()->addChild(Paragraph::fromString(\"Paragraph 1\"));\n    }\n\n    public function test_toggleable_array_conversion(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"heading_2\",\n            \"heading_2\"        => [\n                \"rich_text\" => [\n                    [\n                        \"plain_text\"  => \"Notion headings \",\n                        \"href\"        => null,\n                        \"type\"        => \"text\",\n                        \"text\"        => [\n                            \"content\" => \"Notion headings \",\n                        ],\n                        \"annotations\" => [\n                            \"bold\"          => false,\n                            \"italic\"        => false,\n                            \"strikethrough\" => false,\n                            \"underline\"     => false,\n                            \"code\"          => false,\n                            \"color\"         => \"default\",\n                        ],\n                    ],\n                    [\n                        \"plain_text\"  => \"rock!\",\n                        \"href\"        => null,\n                        \"type\"        => \"text\",\n                        \"text\"        => [\n                            \"content\" => \"rock!\",\n                        ],\n                        \"annotations\" => [\n                            \"bold\"          => true,\n                            \"italic\"        => false,\n                            \"strikethrough\" => false,\n                            \"underline\"     => false,\n                            \"code\"          => false,\n                            \"color\"         => \"red\",\n                        ],\n                    ],\n                ],\n                \"is_toggleable\" => true,\n                \"children\" => [\n                    [\n                        \"object\"           => \"block\",\n                        \"id\"               => \"a5dc4448-6c0a-48ab-9b43-0ea838bb6070\",\n                        \"created_time\"     => \"2021-10-18T17:09:00.000000Z\",\n                        \"last_edited_time\" => \"2021-10-18T17:09:00.000000Z\",\n                        \"in_trash\"         => false,\n                        \"has_children\"     => false,\n                        \"type\"             => \"divider\",\n                        \"divider\"          => new \\stdClass(),\n                    ]\n                ],\n                \"color\" => \"default\",\n            ],\n        ];\n\n        $heading = Heading2::fromArray($array);\n\n        $this->assertEquals($array, $heading->toArray());\n    }\n\n    public function test_change_color(): void\n    {\n        $h1 = Heading2::fromString(\"Hello!\")->changeColor(Color::Green);\n\n        $this->assertSame(Color::Green, $h1->color);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/Heading3Test.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks;\n\nuse Notion\\Blocks\\BlockFactory;\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Blocks\\Heading3;\nuse Notion\\Blocks\\Paragraph;\nuse Notion\\Common\\Color;\nuse Notion\\Common\\Date;\nuse Notion\\Common\\RichText;\nuse Notion\\Exceptions\\HeadingException;\nuse PHPUnit\\Framework\\TestCase;\n\nclass Heading3Test extends TestCase\n{\n    public function test_create_empty_heading(): void\n    {\n        $heading = Heading3::fromText();\n\n        $this->assertEmpty($heading->text);\n    }\n\n    public function test_create_from_string(): void\n    {\n        $heading = Heading3::fromString(\"Dummy heading.\");\n\n        $this->assertEquals(\"Dummy heading.\", $heading->toString());\n    }\n\n    public function test_create_from_array(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"heading_3\",\n            \"heading_3\"        => [\n                \"rich_text\" => [\n                    [\n                        \"plain_text\"  => \"Notion headings \",\n                        \"href\"        => null,\n                        \"type\"        => \"text\",\n                        \"text\"        => [\n                            \"content\" => \"Notion headings \",\n                        ],\n                        \"annotations\" => [\n                            \"bold\"          => false,\n                            \"italic\"        => false,\n                            \"strikethrough\" => false,\n                            \"underline\"     => false,\n                            \"code\"          => false,\n                            \"color\"         => \"default\",\n                        ],\n                    ],\n                    [\n                        \"plain_text\"  => \"rock!\",\n                        \"href\"        => null,\n                        \"type\"        => \"text\",\n                        \"text\"        => [\n                            \"content\" => \"rock!\",\n                        ],\n                        \"annotations\" => [\n                            \"bold\"          => true,\n                            \"italic\"        => false,\n                            \"strikethrough\" => false,\n                            \"underline\"     => false,\n                            \"code\"          => false,\n                            \"color\"         => \"red\",\n                        ],\n                    ],\n                ],\n                \"is_toggleable\" => false,\n                \"children\" => [],\n                \"color\" => \"default\",\n            ],\n        ];\n\n        $heading = Heading3::fromArray($array);\n\n        $this->assertCount(2, $heading->text);\n        $this->assertEquals(\"Notion headings rock!\", $heading->toString());\n        $this->assertFalse($heading->isToggleable);\n        $this->assertFalse($heading->metadata()->inTrash);\n\n        $this->assertEquals($heading, BlockFactory::fromArray($array));\n    }\n\n    public function test_error_on_wrong_type(): void\n    {\n        $this->expectException(BlockException::class);\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"wrong-type\",\n            \"heading_3\"        => [\n                \"rich_text\"     => [],\n                \"is_toggleable\"  => false,\n            ],\n        ];\n\n        Heading3::fromArray($array);\n    }\n\n    public function test_transform_in_array(): void\n    {\n        $h = Heading3::fromString(\"Simple heading\");\n\n        $expected = [\n            \"object\"           => \"block\",\n            \"created_time\"     => $h->metadata()->createdTime->format(Date::FORMAT),\n            \"last_edited_time\" => $h->metadata()->lastEditedTime->format(Date::FORMAT),\n            \"in_trash\"         => false,\n            \"has_children\"      => false,\n            \"type\"             => \"heading_3\",\n            \"heading_3\"        => [\n                \"rich_text\" => [[\n                    \"plain_text\"  => \"Simple heading\",\n                    \"href\"        => null,\n                    \"type\"        => \"text\",\n                    \"text\"        => [\n                        \"content\" => \"Simple heading\",\n                    ],\n                    \"annotations\" => [\n                        \"bold\"          => false,\n                        \"italic\"        => false,\n                        \"strikethrough\" => false,\n                        \"underline\"     => false,\n                        \"code\"          => false,\n                        \"color\"         => \"default\",\n                    ],\n                ]],\n                \"is_toggleable\" => false,\n                \"children\" => [],\n                \"color\" => \"default\",\n            ],\n        ];\n\n        $this->assertEquals($expected, $h->toArray());\n    }\n\n    public function test_replace_text(): void\n    {\n        $oldHeading = Heading3::fromString(\"This is an old heading\");\n\n        $newHeading = $oldHeading->changeText(\n            RichText::fromString(\"This is a \"),\n            RichText::fromString(\"new heading\"),\n        );\n\n        $this->assertEquals(\"This is an old heading\", $oldHeading->toString());\n        $this->assertEquals(\"This is a new heading\", $newHeading->toString());\n    }\n\n    public function test_add_text(): void\n    {\n        $oldHeading = Heading3::fromString(\"A heading\");\n\n        $newHeading = $oldHeading->addText(\n            RichText::fromString(\" can be extended.\")\n        );\n\n        $this->assertEquals(\"A heading\", $oldHeading->toString());\n        $this->assertEquals(\"A heading can be extended.\", $newHeading->toString());\n    }\n\n    public function test_no_children_support(): void\n    {\n        $block = Heading3::fromText();\n\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $block->changeChildren();\n    }\n\n    public function test_togglify(): void\n    {\n        $block = Heading3::fromText()\n            ->toggllify();\n\n        $this->assertTrue($block->isToggleable);\n    }\n\n    public function test_untogglify(): void\n    {\n        $block = Heading3::fromText()->toggllify()->untogglify();\n\n        $this->assertFalse($block->isToggleable);\n    }\n\n    public function test_untogglify_with_children(): void\n    {\n        $this->expectException(HeadingException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        Heading3::fromText()\n            ->toggllify()\n            ->addChild(Paragraph::fromString(\"Inside paragraph.\"))\n            ->untogglify();\n    }\n\n    public function test_change_children_from_toggleable_heading(): void\n    {\n        $block = Heading3::fromText()\n            ->toggllify()\n            ->changeChildren(\n                Paragraph::fromString(\"Paragraph 1\"),\n                Paragraph::fromString(\"Paragraph 2\"),\n            );\n\n        $this->assertCount(2, $block->children ?? []);\n    }\n\n    public function test_add_child_to_untoggleable_heading(): void\n    {\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        Heading3::fromText()->addChild(Paragraph::fromString(\"Paragraph 1\"));\n    }\n\n    public function test_toggleable_array_conversion(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"heading_3\",\n            \"heading_3\"        => [\n                \"rich_text\" => [\n                    [\n                        \"plain_text\"  => \"Notion headings \",\n                        \"href\"        => null,\n                        \"type\"        => \"text\",\n                        \"text\"        => [\n                            \"content\" => \"Notion headings \",\n                        ],\n                        \"annotations\" => [\n                            \"bold\"          => false,\n                            \"italic\"        => false,\n                            \"strikethrough\" => false,\n                            \"underline\"     => false,\n                            \"code\"          => false,\n                            \"color\"         => \"default\",\n                        ],\n                    ],\n                    [\n                        \"plain_text\"  => \"rock!\",\n                        \"href\"        => null,\n                        \"type\"        => \"text\",\n                        \"text\"        => [\n                            \"content\" => \"rock!\",\n                        ],\n                        \"annotations\" => [\n                            \"bold\"          => true,\n                            \"italic\"        => false,\n                            \"strikethrough\" => false,\n                            \"underline\"     => false,\n                            \"code\"          => false,\n                            \"color\"         => \"red\",\n                        ],\n                    ],\n                ],\n                \"is_toggleable\" => true,\n                \"children\" => [\n                    [\n                        \"object\"           => \"block\",\n                        \"id\"               => \"a5dc4448-6c0a-48ab-9b43-0ea838bb6070\",\n                        \"created_time\"     => \"2021-10-18T17:09:00.000000Z\",\n                        \"last_edited_time\" => \"2021-10-18T17:09:00.000000Z\",\n                        \"in_trash\"         => false,\n                        \"has_children\"     => false,\n                        \"type\"             => \"divider\",\n                        \"divider\"          => new \\stdClass(),\n                    ]\n                ],\n                \"color\" => \"default\",\n            ],\n        ];\n\n        $heading = Heading3::fromArray($array);\n\n        $this->assertEquals($array, $heading->toArray());\n    }\n\n    public function test_change_color(): void\n    {\n        $h1 = Heading3::fromString(\"Hello!\")->changeColor(Color::Green);\n\n        $this->assertSame(Color::Green, $h1->color);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/ImageTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks;\n\nuse Notion\\Blocks\\BlockFactory;\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Blocks\\Image;\nuse Notion\\Blocks\\Paragraph;\nuse Notion\\Common\\Date;\nuse Notion\\Common\\File;\nuse Notion\\Common\\RichText;\nuse PHPUnit\\Framework\\TestCase;\n\nclass ImageTest extends TestCase\n{\n    public function test_create_image(): void\n    {\n        $file = File::createExternal(\"https://my-site.com/image.png\");\n        $image = Image::fromFile($file);\n\n        $this->assertEquals($file, $image->file);\n    }\n\n    public function test_create_from_array(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"image\",\n            \"image\"            => [\n                \"type\"     => \"external\",\n                \"external\" => [\n                    \"url\" => \"https://my-site.com/image.png\"\n                ],\n            ],\n        ];\n\n        $image = Image::fromArray($array);\n\n        $this->assertEquals(\"https://my-site.com/image.png\", $image->file->url);\n\n        $this->assertEquals($image, BlockFactory::fromArray($array));\n    }\n\n    public function test_error_on_wrong_type(): void\n    {\n        $this->expectException(BlockException::class);\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"wrong-type\",\n            \"image\"            => [\n                \"type\"     => \"external\",\n                \"external\" => [\n                    \"url\" => \"https://my-site.com/image.png\"\n                ],\n            ],\n        ];\n\n        Image::fromArray($array);\n    }\n\n    public function test_transform_in_array(): void\n    {\n        $file = File::createExternal(\"https://my-site.com/image.png\");\n        $image = Image::fromFile($file);\n\n        $expected = [\n            \"object\"           => \"block\",\n            \"created_time\"     => $image->metadata()->createdTime->format(Date::FORMAT),\n            \"last_edited_time\" => $image->metadata()->createdTime->format(Date::FORMAT),\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"image\",\n            \"image\"            => [\n                \"type\"     => \"external\",\n                \"external\" => [\n                    \"url\" => \"https://my-site.com/image.png\"\n                ],\n            ],\n        ];\n\n        $this->assertEquals($expected, $image->toArray());\n    }\n\n    public function test_replace_file(): void\n    {\n        $file1 = File::createExternal(\"https://my-site.com/image1.png\");\n        $file2 = File::createExternal(\"https://my-site.com/image2.png\");\n\n        $old = Image::fromFile($file1);\n        $new = $old->changeFile($file2);\n\n        $this->assertEquals($file1, $old->file);\n        $this->assertEquals($file2, $new->file);\n    }\n\n    public function test_no_children_support(): void\n    {\n        $file = File::createExternal(\"https://my-site.com/image.png\");\n        $block = Image::fromFile($file);\n\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $block->changeChildren();\n    }\n\n    public function test_no_children_support_2(): void\n    {\n        $file = File::createExternal(\"https://my-site.com/image.png\");\n        $block = Image::fromFile($file);\n\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $block->addChild(Paragraph::create());\n    }\n\n    public function test_delete(): void\n    {\n        $file = File::createExternal(\"https://my-site.com/image.png\");\n        $block = Image::fromFile($file);\n\n        $block = $block->delete();\n\n        $this->assertTrue($block->metadata()->inTrash);\n    }\n\n    public function test_change_caption(): void\n    {\n        $file = File::createExternal(\"https://my-site.com/image.png\");\n\n        $caption = RichText::fromString(\"Sample caption\");\n        $block = Image::fromFile($file)->changeCaption($caption);\n\n        $this->assertEquals([$caption], $block->file->caption);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/LinkPreviewTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks;\n\nuse Notion\\Blocks\\BlockFactory;\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Blocks\\LinkPreview;\nuse Notion\\Blocks\\Paragraph;\nuse PHPUnit\\Framework\\TestCase;\n\nclass LinkPreviewTest extends TestCase\n{\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"link_preview\",\n            \"link_preview\"     => [\n                \"url\" => \"https://github.com/mariosimao/notion-sdk-php/issues/1\",\n            ],\n        ];\n\n        $linkPreview = LinkPreview::fromArray($array);\n\n        $this->assertEquals($array, $linkPreview->toArray());\n        $this->assertEquals(\n            \"https://github.com/mariosimao/notion-sdk-php/issues/1\",\n            $linkPreview->url,\n        );\n    }\n\n    public function test_from_invalid_type(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"heading1\",\n            \"link_preview\"     => [\n                \"url\" => \"https://github.com/mariosimao/notion-sdk-php/issues/1\",\n            ],\n        ];\n\n        $this->expectException(\\Exception::class);\n        LinkPreview::fromArray($array);\n    }\n\n    public function test_create_change_factory(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"link_preview\",\n            \"link_preview\"     => [\n                \"url\" => \"https://github.com/mariosimao/notion-sdk-php/issues/1\",\n            ],\n        ];\n\n        $block = BlockFactory::fromArray($array);\n\n        $this->assertInstanceOf(LinkPreview::class, $block);\n    }\n\n    public function test_no_children_support(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"link_preview\",\n            \"link_preview\"     => [\n                \"url\" => \"https://github.com/mariosimao/notion-sdk-php/issues/1\",\n            ],\n        ];\n\n        $block = LinkPreview::fromArray($array);\n\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $block->changeChildren();\n    }\n\n    public function test_no_children_support_2(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"link_preview\",\n            \"link_preview\"     => [\n                \"url\" => \"https://github.com/mariosimao/notion-sdk-php/issues/1\",\n            ],\n        ];\n\n        $block = LinkPreview::fromArray($array);\n\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $block->addChild(Paragraph::create());\n    }\n\n    public function test_delete(): void\n    {\n        $block = LinkPreview::fromArray([\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"link_preview\",\n            \"link_preview\"     => [\n                \"url\" => \"https://github.com/mariosimao/notion-sdk-php/issues/1\",\n            ],\n        ]);\n\n        $block = $block->delete();\n\n        $this->assertTrue($block->metadata()->inTrash);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/NumberedListItemTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks;\n\nuse Notion\\Blocks\\BlockFactory;\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Blocks\\NumberedListItem;\nuse Notion\\Common\\Color;\nuse Notion\\Common\\Date;\nuse Notion\\Common\\RichText;\nuse PHPUnit\\Framework\\TestCase;\n\nclass NumberedListItemTest extends TestCase\n{\n    public function test_create_empty_item(): void\n    {\n        $item = NumberedListItem::create();\n\n        $this->assertEmpty($item->text);\n        $this->assertEmpty($item->children);\n    }\n\n    public function test_create_from_string(): void\n    {\n        $item = NumberedListItem::fromString(\"Dummy item.\");\n\n        $this->assertEquals(\"Dummy item.\", $item->toString());\n    }\n\n    public function test_create_from_array(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"numbered_list_item\",\n            \"numbered_list_item\"        => [\n                \"rich_text\" => [\n                    [\n                        \"plain_text\"  => \"Notion items \",\n                        \"href\"        => null,\n                        \"type\"        => \"text\",\n                        \"text\"        => [\n                            \"content\" => \"Notion items \",\n                        ],\n                        \"annotations\" => [\n                            \"bold\"          => false,\n                            \"italic\"        => false,\n                            \"strikethrough\" => false,\n                            \"underline\"     => false,\n                            \"code\"          => false,\n                            \"color\"         => \"default\",\n                        ],\n                    ],\n                    [\n                        \"plain_text\"  => \"rock!\",\n                        \"href\"        => null,\n                        \"type\"        => \"text\",\n                        \"text\"        => [\n                            \"content\" => \"rock!\",\n                        ],\n                        \"annotations\" => [\n                            \"bold\"          => true,\n                            \"italic\"        => false,\n                            \"strikethrough\" => false,\n                            \"underline\"     => false,\n                            \"code\"          => false,\n                            \"color\"         => \"red\",\n                        ],\n                    ],\n                ],\n                \"children\" => [],\n            ],\n        ];\n\n        $item = NumberedListItem::fromArray($array);\n\n        $this->assertCount(2, $item->text);\n        $this->assertEmpty($item->children);\n        $this->assertEquals(\"Notion items rock!\", $item->toString());\n        $this->assertFalse($item->metadata()->inTrash);\n\n        $this->assertEquals($item, BlockFactory::fromArray($array));\n    }\n\n    public function test_error_on_wrong_type(): void\n    {\n        $this->expectException(BlockException::class);\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"wrong-type\",\n            \"numbered_list_item\"        => [\n                \"rich_text\"     => [],\n                \"children\" => [],\n            ],\n        ];\n\n        NumberedListItem::fromArray($array);\n    }\n\n    public function test_transform_in_array(): void\n    {\n        $i = NumberedListItem::fromString(\"Simple item\");\n\n        $expected = [\n            \"object\"           => \"block\",\n            \"created_time\"     => $i->metadata()->createdTime->format(Date::FORMAT),\n            \"last_edited_time\" => $i->metadata()->lastEditedTime->format(Date::FORMAT),\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"numbered_list_item\",\n            \"numbered_list_item\" => [\n                \"rich_text\" => [[\n                    \"plain_text\"  => \"Simple item\",\n                    \"href\"        => null,\n                    \"type\"        => \"text\",\n                    \"text\"        => [\n                        \"content\" => \"Simple item\",\n                    ],\n                    \"annotations\" => [\n                        \"bold\"          => false,\n                        \"italic\"        => false,\n                        \"strikethrough\" => false,\n                        \"underline\"     => false,\n                        \"code\"          => false,\n                        \"color\"         => \"default\",\n                    ],\n                ]],\n                \"color\" => \"default\",\n                \"children\" => [],\n            ],\n        ];\n\n        $this->assertEquals($expected, $i->toArray());\n    }\n\n    public function test_replace_text(): void\n    {\n        $oldItem = NumberedListItem::fromString(\"This is an old item\");\n\n        $newItem = $oldItem->changeText(\n            RichText::fromString(\"This is a \"),\n            RichText::fromString(\"new item\"),\n        );\n\n        $this->assertEquals(\"This is an old item\", $oldItem->toString());\n        $this->assertEquals(\"This is a new item\", $newItem->toString());\n    }\n\n    public function test_add_text(): void\n    {\n        $oldItem = NumberedListItem::fromString(\"A item\");\n\n        $newItem = $oldItem->addText(\n            RichText::fromString(\" can be extended.\")\n        );\n\n        $this->assertEquals(\"A item\", $oldItem->toString());\n        $this->assertEquals(\"A item can be extended.\", $newItem->toString());\n    }\n\n    public function test_replace_children(): void\n    {\n        $nested1 = NumberedListItem::fromString(\"Nested item 1\");\n        $nested2 = NumberedListItem::fromString(\"Nested item 2\");\n        $item = NumberedListItem::fromString(\"Simple item.\")->changeChildren($nested1, $nested2);\n\n        $this->assertCount(2, $item->children);\n        $this->assertEquals($nested1, $item->children[0]);\n        $this->assertEquals($nested2, $item->children[1]);\n    }\n\n    public function test_add_child(): void\n    {\n        $item = NumberedListItem::fromString(\"Simple item.\");\n        $nested = NumberedListItem::fromString(\"Nested item\");\n        $item = $item->addChild($nested);\n\n        $this->assertCount(1, $item->children);\n        $this->assertEquals($nested, $item->children[0]);\n    }\n\n    public function test_change_color(): void\n    {\n        $block = NumberedListItem::fromString(\"Hello World!\")->changeColor(Color::Red);\n\n        $this->assertSame(Color::Red, $block->color);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/ParagraphTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks;\n\nuse Notion\\Blocks\\BlockFactory;\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Blocks\\Paragraph;\nuse Notion\\Common\\Color;\nuse Notion\\Common\\Date;\nuse Notion\\Common\\RichText;\nuse PHPUnit\\Framework\\TestCase;\n\nclass ParagraphTest extends TestCase\n{\n    public function test_create_empty_paragraph(): void\n    {\n        $paragraph = Paragraph::create();\n\n        $this->assertEmpty($paragraph->text);\n        $this->assertEmpty($paragraph->children);\n    }\n\n    public function test_create_from_string(): void\n    {\n        $paragraph = Paragraph::fromString(\"Dummy paragraph.\");\n\n        $this->assertEquals(\"Dummy paragraph.\", $paragraph->toString());\n    }\n\n    public function test_create_from_array(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"paragraph\",\n            \"paragraph\"        => [\n                \"rich_text\" => [\n                    [\n                        \"plain_text\"  => \"Notion paragraphs \",\n                        \"href\"        => null,\n                        \"type\"        => \"text\",\n                        \"text\"        => [\n                            \"content\" => \"Notion paragraphs \",\n                        ],\n                        \"annotations\" => [\n                            \"bold\"          => false,\n                            \"italic\"        => false,\n                            \"strikethrough\" => false,\n                            \"underline\"     => false,\n                            \"code\"          => false,\n                            \"color\"         => \"default\",\n                        ],\n                    ],\n                    [\n                        \"plain_text\"  => \"rock!\",\n                        \"href\"        => null,\n                        \"type\"        => \"text\",\n                        \"text\"        => [\n                            \"content\" => \"rock!\",\n                        ],\n                        \"annotations\" => [\n                            \"bold\"          => true,\n                            \"italic\"        => false,\n                            \"strikethrough\" => false,\n                            \"underline\"     => false,\n                            \"code\"          => false,\n                            \"color\"         => \"red\",\n                        ],\n                    ],\n                ],\n                \"children\" => [],\n                \"color\" => \"green\",\n            ],\n        ];\n\n        $paragraph = Paragraph::fromArray($array);\n\n        $this->assertCount(2, $paragraph->text);\n        $this->assertEmpty($paragraph->children);\n        $this->assertEquals(\"Notion paragraphs rock!\", $paragraph->toString());\n        $this->assertFalse($paragraph->metadata()->inTrash);\n\n        $this->assertEquals($paragraph, BlockFactory::fromArray($array));\n    }\n\n    public function test_error_on_wrong_type(): void\n    {\n        $this->expectException(BlockException::class);\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"wrong-type\",\n            \"paragraph\"        => [\n                \"rich_text\"     => [],\n                \"children\" => [],\n            ],\n        ];\n        Paragraph::fromArray($array);\n    }\n\n    public function test_transform_in_array(): void\n    {\n        $p = Paragraph::fromString(\"Simple paragraph\");\n\n        $expected = [\n            \"object\"           => \"block\",\n            \"created_time\"     => $p->metadata()->createdTime->format(Date::FORMAT),\n            \"last_edited_time\" => $p->metadata()->lastEditedTime->format(Date::FORMAT),\n            \"in_trash\"         => false,\n            \"has_children\"      => false,\n            \"type\"             => \"paragraph\",\n            \"paragraph\"        => [\n                \"rich_text\" => [[\n                    \"plain_text\"  => \"Simple paragraph\",\n                    \"href\"        => null,\n                    \"type\"        => \"text\",\n                    \"text\"        => [\n                        \"content\" => \"Simple paragraph\",\n                    ],\n                    \"annotations\" => [\n                        \"bold\"          => false,\n                        \"italic\"        => false,\n                        \"strikethrough\" => false,\n                        \"underline\"     => false,\n                        \"code\"          => false,\n                        \"color\"         => \"default\",\n                    ],\n                ]],\n                \"children\" => [],\n                \"color\" => \"default\",\n            ],\n        ];\n\n        $this->assertEquals($expected, $p->toArray());\n    }\n\n    public function test_replace_text(): void\n    {\n        $oldParagraph = Paragraph::fromString(\"This is an old paragraph\");\n\n        $newParagraph = $oldParagraph->changeText([\n            RichText::fromString(\"This is a \"),\n            RichText::fromString(\"new paragraph\"),\n        ]);\n\n        $this->assertEquals(\"This is an old paragraph\", $oldParagraph->toString());\n        $this->assertEquals(\"This is a new paragraph\", $newParagraph->toString());\n    }\n\n    public function test_add_text(): void\n    {\n        $oldParagraph = Paragraph::fromString(\"A paragraph\");\n\n        $newParagraph = $oldParagraph->addText(\n            RichText::fromString(\" can be extended.\")\n        );\n\n        $this->assertEquals(\"A paragraph\", $oldParagraph->toString());\n        $this->assertEquals(\"A paragraph can be extended.\", $newParagraph->toString());\n    }\n\n    public function test_replace_children(): void\n    {\n        $nested1 = Paragraph::fromString(\"Nested paragraph 1\");\n        $nested2 = Paragraph::fromString(\"Nested paragraph 2\");\n        $paragraph = Paragraph::fromString(\"Simple paragraph.\")->changeChildren($nested1, $nested2);\n\n        $this->assertCount(2, $paragraph->children);\n        $this->assertEquals($nested1, $paragraph->children[0]);\n        $this->assertEquals($nested2, $paragraph->children[1]);\n    }\n\n    public function test_add_child(): void\n    {\n        $paragraph = Paragraph::fromString(\"Simple paragraph.\");\n        $nested = Paragraph::fromString(\"Nested paragraph\");\n        $paragraph = $paragraph->addChild($nested);\n\n        $this->assertCount(1, $paragraph->children);\n        $this->assertEquals($nested, $paragraph->children[0]);\n    }\n\n    public function test_change_color(): void\n    {\n        $paragraph = Paragraph::fromString(\"Hello World!\")->changeColor(Color::Red);\n\n        $this->assertSame(Color::Red, $paragraph->color);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/PdfTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks;\n\nuse Notion\\Blocks\\BlockFactory;\nuse Notion\\Blocks\\Paragraph;\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Blocks\\Pdf;\nuse Notion\\Common\\Date;\nuse Notion\\Common\\File;\nuse PHPUnit\\Framework\\TestCase;\n\nclass PdfTest extends TestCase\n{\n    public function test_create_pdf(): void\n    {\n        $file = File::createExternal(\"https://my-site.com/document.pdf\");\n        $pdf = Pdf::fromFile($file);\n\n        $this->assertEquals($file, $pdf->file);\n    }\n\n    public function test_create_from_array(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"pdf\",\n            \"pdf\"            => [\n                \"type\"     => \"external\",\n                \"external\" => [\n                    \"url\" => \"https://my-site.com/document.pdf\"\n                ],\n            ],\n        ];\n\n        $pdf = Pdf::fromArray($array);\n\n        $this->assertEquals(\"https://my-site.com/document.pdf\", $pdf->file->url);\n\n        $this->assertEquals($pdf, BlockFactory::fromArray($array));\n    }\n\n    public function test_error_on_wrong_type(): void\n    {\n        $this->expectException(BlockException::class);\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"wrong-type\",\n            \"pdf\"            => [\n                \"type\"     => \"external\",\n                \"external\" => [\n                    \"url\" => \"https://my-site.com/document.pdf\"\n                ],\n            ],\n        ];\n\n        Pdf::fromArray($array);\n    }\n\n    public function test_transform_in_array(): void\n    {\n        $file = File::createExternal(\"https://my-site.com/document.pdf\");\n        $pdf = Pdf::fromFile($file);\n\n        $expected = [\n            \"object\"           => \"block\",\n            \"created_time\"     => $pdf->metadata()->createdTime->format(Date::FORMAT),\n            \"last_edited_time\" => $pdf->metadata()->createdTime->format(Date::FORMAT),\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"pdf\",\n            \"pdf\"            => [\n                \"type\"     => \"external\",\n                \"external\" => [\n                    \"url\" => \"https://my-site.com/document.pdf\"\n                ],\n            ],\n        ];\n\n        $this->assertEquals($expected, $pdf->toArray());\n    }\n\n    public function test_replace_file(): void\n    {\n        $file1 = File::createExternal(\"https://my-site.com/pdf1.png\");\n        $file2 = File::createExternal(\"https://my-site.com/pdf2.png\");\n\n        $old = Pdf::fromFile($file1);\n        $new = $old->changeFile($file2);\n\n        $this->assertEquals($file1, $old->file);\n        $this->assertEquals($file2, $new->file);\n    }\n\n    public function test_no_children_support(): void\n    {\n        $file = File::createExternal(\"https://my-site.com/document.pdf\");\n        $block = Pdf::fromFile($file);\n\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $block->changeChildren();\n    }\n\n    public function test_no_children_support_2(): void\n    {\n        $file = File::createExternal(\"https://my-site.com/document.pdf\");\n        $block = Pdf::fromFile($file);\n\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $block->addChild(Paragraph::create());\n    }\n\n    public function test_delete(): void\n    {\n        $file = File::createExternal(\"https://example.com/document.pdf\");\n        $block = Pdf::fromFile($file);\n\n        $block = $block->delete();\n\n        $this->assertTrue($block->metadata()->inTrash);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/QuoteTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks;\n\nuse Notion\\Blocks\\BlockFactory;\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Blocks\\Quote;\nuse Notion\\Common\\Color;\nuse Notion\\Common\\Date;\nuse Notion\\Common\\RichText;\nuse PHPUnit\\Framework\\TestCase;\n\nclass QuoteTest extends TestCase\n{\n    public function test_create_empty_quote(): void\n    {\n        $quote = Quote::create();\n\n        $this->assertEmpty($quote->text);\n        $this->assertEmpty($quote->children);\n    }\n\n    public function test_create_from_string(): void\n    {\n        $quote = Quote::fromString(\"Dummy quote.\");\n\n        $this->assertEquals(\"Dummy quote.\", $quote->toString());\n    }\n\n    public function test_create_from_array(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"quote\",\n            \"quote\"        => [\n                \"rich_text\" => [\n                    [\n                        \"plain_text\"  => \"Notion quotes \",\n                        \"href\"        => null,\n                        \"type\"        => \"text\",\n                        \"text\"        => [\n                            \"content\" => \"Notion quotes \",\n                        ],\n                        \"annotations\" => [\n                            \"bold\"          => false,\n                            \"italic\"        => false,\n                            \"strikethrough\" => false,\n                            \"underline\"     => false,\n                            \"code\"          => false,\n                            \"color\"         => \"default\",\n                        ],\n                    ],\n                    [\n                        \"plain_text\"  => \"rock!\",\n                        \"href\"        => null,\n                        \"type\"        => \"text\",\n                        \"text\"        => [\n                            \"content\" => \"rock!\",\n                        ],\n                        \"annotations\" => [\n                            \"bold\"          => true,\n                            \"italic\"        => false,\n                            \"strikethrough\" => false,\n                            \"underline\"     => false,\n                            \"code\"          => false,\n                            \"color\"         => \"red\",\n                        ],\n                    ],\n                ],\n                \"children\" => [],\n            ],\n        ];\n\n        $quote = Quote::fromArray($array);\n\n        $this->assertCount(2, $quote->text);\n        $this->assertEmpty($quote->children);\n        $this->assertEquals(\"Notion quotes rock!\", $quote->toString());\n        $this->assertFalse($quote->metadata()->inTrash);\n\n        $this->assertEquals($quote, BlockFactory::fromArray($array));\n    }\n\n    public function test_error_on_wrong_type(): void\n    {\n        $this->expectException(BlockException::class);\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"wrong-type\",\n            \"quote\"        => [\n                \"rich_text\"     => [],\n                \"children\" => [],\n            ],\n        ];\n\n        Quote::fromArray($array);\n    }\n\n    public function test_transform_in_array(): void\n    {\n        $q = Quote::fromString(\"Simple quote\");\n\n        $expected = [\n            \"object\"           => \"block\",\n            \"created_time\"     => $q->metadata()->createdTime->format(Date::FORMAT),\n            \"last_edited_time\" => $q->metadata()->lastEditedTime->format(Date::FORMAT),\n            \"in_trash\"         => false,\n            \"has_children\"      => false,\n            \"type\"             => \"quote\",\n            \"quote\"        => [\n                \"rich_text\" => [[\n                    \"plain_text\"  => \"Simple quote\",\n                    \"href\"        => null,\n                    \"type\"        => \"text\",\n                    \"text\"        => [\n                        \"content\" => \"Simple quote\",\n                    ],\n                    \"annotations\" => [\n                        \"bold\"          => false,\n                        \"italic\"        => false,\n                        \"strikethrough\" => false,\n                        \"underline\"     => false,\n                        \"code\"          => false,\n                        \"color\"         => \"default\",\n                    ],\n                ]],\n                \"color\" => \"default\",\n                \"children\" => [],\n            ],\n        ];\n\n        $this->assertEquals($expected, $q->toArray());\n    }\n\n    public function test_replace_text(): void\n    {\n        $oldQuote = Quote::fromString(\"This is an old quote\");\n\n        $newQuote = $oldQuote->changeText([\n            RichText::fromString(\"This is a \"),\n            RichText::fromString(\"new quote\"),\n        ]);\n\n        $this->assertEquals(\"This is an old quote\", $oldQuote->toString());\n        $this->assertEquals(\"This is a new quote\", $newQuote->toString());\n    }\n\n    public function test_add_text(): void\n    {\n        $oldQuote = Quote::fromString(\"A quote\");\n\n        $newQuote = $oldQuote->addText(\n            RichText::fromString(\" can be extended.\")\n        );\n\n        $this->assertEquals(\"A quote\", $oldQuote->toString());\n        $this->assertEquals(\"A quote can be extended.\", $newQuote->toString());\n    }\n\n    public function test_replace_children(): void\n    {\n        $nested1 = Quote::fromString(\"Nested quote 1\");\n        $nested2 = Quote::fromString(\"Nested quote 2\");\n        $quote = Quote::fromString(\"Simple quote.\")->changeChildren($nested1, $nested2);\n\n        $this->assertCount(2, $quote->children);\n        $this->assertEquals($nested1, $quote->children[0]);\n        $this->assertEquals($nested2, $quote->children[1]);\n    }\n\n    public function test_add_child(): void\n    {\n        $quote = Quote::fromString(\"Simple quote.\");\n        $nested = Quote::fromString(\"Nested quote\");\n        $quote = $quote->addChild($nested);\n\n        $this->assertCount(1, $quote->children);\n        $this->assertEquals($nested, $quote->children[0]);\n    }\n\n    public function test_delete(): void\n    {\n        $quote = Quote::fromString(\"Simple quote.\");\n        $quote = $quote->delete();\n\n        $this->assertTrue($quote->metadata()->inTrash);\n    }\n\n    public function test_change_color(): void\n    {\n        $block = Quote::fromString(\"Hello World!\")->changeColor(Color::Red);\n\n        $this->assertSame(Color::Red, $block->color);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/Renderer/Markdown/BookmarkRendererTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\Bookmark;\nuse Notion\\Blocks\\Divider;\nuse Notion\\Blocks\\Renderer\\Markdown\\BookmarkRenderer;\nuse Notion\\Blocks\\Unknown;\nuse PHPUnit\\Framework\\TestCase;\n\nclass BookmarkRendererTest extends TestCase\n{\n    public function test_render(): void\n    {\n        $bookmark = Bookmark::fromUrl(\"https://example.com\");\n\n        $markdown = BookmarkRenderer::render($bookmark);\n\n        $expected = \"<https://example.com>\";\n\n        $this->assertSame($expected, $markdown);\n    }\n\n    public function test_invalid_block(): void\n    {\n        $markdown = BookmarkRenderer::render(Divider::create());\n\n        $this->assertSame(\"\", $markdown);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/Renderer/Markdown/BreadcrumbRendererTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\Breadcrumb;\nuse Notion\\Blocks\\Divider;\nuse Notion\\Blocks\\Renderer\\Markdown\\BreadcrumbRenderer;\nuse PHPUnit\\Framework\\TestCase;\n\nclass BreadcrumbRendererTest extends TestCase\n{\n    public function test_render(): void\n    {\n        $block = Breadcrumb::create();\n\n        $markdown = BreadcrumbRenderer::render($block);\n\n        $expected = \"[Breadcrumb]\";\n\n        $this->assertSame($expected, $markdown);\n    }\n\n\n    public function test_invalid_block(): void\n    {\n        $markdown = BreadcrumbRenderer::render(Divider::create());\n\n        $this->assertSame(\"\", $markdown);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/Renderer/Markdown/BulletedListItemRendererTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\BulletedListItem;\nuse Notion\\Blocks\\Divider;\nuse Notion\\Blocks\\Renderer\\Markdown\\BulletedListItemRenderer;\nuse PHPUnit\\Framework\\TestCase;\n\nclass BulletedListItemRendererTest extends TestCase\n{\n    public function test_render(): void\n    {\n        $block = BulletedListItem::fromString(\"Item 1\")\n            ->addChild(BulletedListItem::fromString(\"Item 2\"))\n            ->addChild(BulletedListItem::fromString(\"Item 3\")\n                ->addChild(BulletedListItem::fromString(\"Item 4\")));\n\n        $markdown = BulletedListItemRenderer::render($block);\n\n        $expected = <<<MARKDOWN\n- Item 1\n  - Item 2\n  - Item 3\n    - Item 4\nMARKDOWN;\n\n        $this->assertSame($expected, $markdown);\n    }\n\n\n    public function test_invalid_block(): void\n    {\n        $markdown = BulletedListItemRenderer::render(Divider::create());\n\n        $this->assertSame(\"\", $markdown);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/Renderer/Markdown/CalloutRendererTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\Callout;\nuse Notion\\Blocks\\Divider;\nuse Notion\\Blocks\\Paragraph;\nuse Notion\\Blocks\\Renderer\\Markdown\\CalloutRenderer;\nuse PHPUnit\\Framework\\TestCase;\n\nclass CalloutRendererTest extends TestCase\n{\n    public function test_render(): void\n    {\n        $block = Callout::fromString(\"💡\", \"Tip\")\n            ->addChild(Paragraph::fromString(\"A simple tip.\"));\n\n        $markdown = CalloutRenderer::render($block);\n\n        $expected = <<<MARKDOWN\n> 💡 Tip\n>\n> A simple tip.\n\nMARKDOWN;\n\n        $this->assertSame($expected, $markdown);\n    }\n\n    public function test_invalid_block(): void\n    {\n        $markdown = CalloutRenderer::render(Divider::create());\n\n        $this->assertSame(\"\", $markdown);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/Renderer/Markdown/ChildDatabaseRendererTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\ChildDatabase;\nuse Notion\\Blocks\\Divider;\nuse Notion\\Blocks\\Renderer\\Markdown\\ChildDatabaseRenderer;\nuse PHPUnit\\Framework\\TestCase;\n\nclass ChildDatabaseRendererTest extends TestCase\n{\n    public function test_render(): void\n    {\n        $block = ChildDatabase::fromArray([\n            \"id\"                => \"abc123\",\n            \"created_time\"      => \"2023-01-01 00:00:00\",\n            \"last_edited_time\"  => \"2023-01-01 00:00:00\",\n            \"in_trash\"          => false,\n            \"has_children\"      => false,\n            \"type\"              => \"child_database\",\n            \"child_database\"    => [\n                \"title\" => \"Database title\"\n            ],\n        ]);\n\n        $markdown = ChildDatabaseRenderer::render($block);\n\n        $expected = \"Database title\";\n\n        $this->assertSame($expected, $markdown);\n    }\n\n\n    public function test_invalid_block(): void\n    {\n        $markdown = ChildDatabaseRenderer::render(Divider::create());\n\n        $this->assertSame(\"\", $markdown);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/Renderer/Markdown/ChildPageRendererTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\ChildPage;\nuse Notion\\Blocks\\Divider;\nuse Notion\\Blocks\\Renderer\\Markdown\\ChildPageRenderer;\nuse PHPUnit\\Framework\\TestCase;\n\nclass ChildPageRendererTest extends TestCase\n{\n    public function test_render(): void\n    {\n        $block = ChildPage::fromArray([\n            \"id\"                => \"abc123\",\n            \"created_time\"      => \"2023-01-01 00:00:00\",\n            \"last_edited_time\"  => \"2023-01-01 00:00:00\",\n            \"in_trash\"          => false,\n            \"has_children\"      => false,\n            \"type\"              => \"child_page\",\n            \"child_page\"    => [\n                \"title\" => \"Page title\"\n            ],\n        ]);\n\n        $markdown = ChildPageRenderer::render($block);\n\n        $expected = \"Page title\";\n\n        $this->assertSame($expected, $markdown);\n    }\n\n\n    public function test_invalid_block(): void\n    {\n        $markdown = ChildPageRenderer::render(Divider::create());\n\n        $this->assertSame(\"\", $markdown);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/Renderer/Markdown/CodeRendererTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\Code;\nuse Notion\\Blocks\\CodeLanguage;\nuse Notion\\Blocks\\Divider;\nuse Notion\\Blocks\\Renderer\\Markdown\\CodeRenderer;\nuse PHPUnit\\Framework\\TestCase;\n\nclass CodeRendererTest extends TestCase\n{\n    public function test_render(): void\n    {\n        $block = Code::fromString(\"<?php\\n\\necho 'Hello world!';\", CodeLanguage::Php);\n\n        $markdown = CodeRenderer::render($block);\n\n        $expected = <<<MARKDOWN\n```php\n<?php\n\necho 'Hello world!';\n```\nMARKDOWN;\n\n        $this->assertSame($expected, $markdown);\n    }\n\n\n    public function test_invalid_block(): void\n    {\n        $markdown = CodeRenderer::render(Divider::create());\n\n        $this->assertSame(\"\", $markdown);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/Renderer/Markdown/ColumnRendererTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\Column;\nuse Notion\\Blocks\\ColumnList;\nuse Notion\\Blocks\\Divider;\nuse Notion\\Blocks\\Paragraph;\nuse Notion\\Blocks\\Renderer\\Markdown\\ColumnListRenderer;\nuse Notion\\Blocks\\Renderer\\Markdown\\ColumnRenderer;\nuse PHPUnit\\Framework\\TestCase;\n\nclass ColumnRendererTest extends TestCase\n{\n    public function test_render(): void\n    {\n        $col1 = Column::create(\n            Paragraph::fromString(\"Text 1\"),\n            Paragraph::fromString(\"Text 2\"),\n        );\n        $col2 = Column::create(\n            Paragraph::fromString(\"Text 3\"),\n        );\n\n        $columns = ColumnList::create($col1, $col2);\n\n        $markdown = ColumnListRenderer::render($columns);\n\n        $expected = <<<MARKDOWN\nText 1\n\n\nText 2\n\n\nText 3\n\nMARKDOWN;\n\n        $this->assertSame($expected, $markdown);\n    }\n\n\n    public function test_invalid_block(): void\n    {\n        $markdown = ColumnRenderer::render(Divider::create());\n\n        $this->assertSame(\"\", $markdown);\n    }\n\n    public function test_invalid_block_column_list(): void\n    {\n        $markdown = ColumnListRenderer::render(Divider::create());\n\n        $this->assertSame(\"\", $markdown);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/Renderer/Markdown/DividerRendererTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\Breadcrumb;\nuse Notion\\Blocks\\Divider;\nuse Notion\\Blocks\\Renderer\\Markdown\\DividerRenderer;\nuse PHPUnit\\Framework\\TestCase;\n\nclass DividerRendererTest extends TestCase\n{\n    public function test_render(): void\n    {\n        $block = Divider::create();\n\n        $markdown = DividerRenderer::render($block);\n\n        $expected = \"---\";\n\n        $this->assertSame($expected, $markdown);\n    }\n\n    public function test_invalid_block(): void\n    {\n        $markdown = DividerRenderer::render(Breadcrumb::create());\n\n        $this->assertSame(\"\", $markdown);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/Renderer/Markdown/EmbedRendererTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\Divider;\nuse Notion\\Blocks\\Embed;\nuse Notion\\Blocks\\Renderer\\Markdown\\EmbedRenderer;\nuse PHPUnit\\Framework\\TestCase;\n\nclass EmbedRendererTest extends TestCase\n{\n    public function test_render(): void\n    {\n        $block = Embed::fromUrl(\"https://example.com\");\n\n        $markdown = EmbedRenderer::render($block);\n\n        $expected = \"https://example.com\";\n\n        $this->assertSame($expected, $markdown);\n    }\n\n    public function test_invalid_block(): void\n    {\n        $markdown = EmbedRenderer::render(Divider::create());\n\n        $this->assertSame(\"\", $markdown);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/Renderer/Markdown/EquationRendererTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\Divider;\nuse Notion\\Blocks\\EquationBlock;\nuse Notion\\Blocks\\Renderer\\Markdown\\EquationRenderer;\nuse PHPUnit\\Framework\\TestCase;\n\nclass EquationRendererTest extends TestCase\n{\n    public function test_render(): void\n    {\n        $block = EquationBlock::fromString(\"e = mc^2\");\n\n        $markdown = EquationRenderer::render($block);\n\n        $expected = \"$$ e = mc^2 $$\";\n\n        $this->assertSame($expected, $markdown);\n    }\n\n    public function test_invalid_block(): void\n    {\n        $markdown = EquationRenderer::render(Divider::create());\n\n        $this->assertSame(\"\", $markdown);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/Renderer/Markdown/FileRendererTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\Divider;\nuse Notion\\Blocks\\FileBlock;\nuse Notion\\Blocks\\Renderer\\Markdown\\FileRenderer;\nuse Notion\\Common\\File;\nuse PHPUnit\\Framework\\TestCase;\n\nclass FileRendererTest extends TestCase\n{\n    public function test_render(): void\n    {\n        $block = FileBlock::fromFile(File::createExternal(\"https://example.com/my-file.doc\"));\n\n        $markdown = FileRenderer::render($block);\n\n        $expected = \"https://example.com/my-file.doc\";\n\n        $this->assertSame($expected, $markdown);\n    }\n\n    public function test_invalid_block(): void\n    {\n        $markdown = FileRenderer::render(Divider::create());\n\n        $this->assertSame(\"\", $markdown);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/Renderer/Markdown/Heading1RendererTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\Divider;\nuse Notion\\Blocks\\Heading1;\nuse Notion\\Blocks\\Renderer\\Markdown\\Heading1Renderer;\nuse PHPUnit\\Framework\\TestCase;\n\nclass Heading1RendererTest extends TestCase\n{\n    public function test_render(): void\n    {\n        $block = Heading1::fromString(\"Section title\");\n\n        $markdown = Heading1Renderer::render($block);\n\n        $expected = <<<MARKDOWN\n# Section title\nMARKDOWN;\n\n        $this->assertSame($expected, $markdown);\n    }\n\n    public function test_invalid_block(): void\n    {\n        $markdown = Heading1Renderer::render(Divider::create());\n\n        $this->assertSame(\"\", $markdown);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/Renderer/Markdown/Heading2RendererTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\Divider;\nuse Notion\\Blocks\\Heading2;\nuse Notion\\Blocks\\Renderer\\Markdown\\Heading2Renderer;\nuse PHPUnit\\Framework\\TestCase;\n\nclass Heading2RendererTest extends TestCase\n{\n    public function test_render(): void\n    {\n        $block = Heading2::fromString(\"Section title\");\n\n        $markdown = Heading2Renderer::render($block);\n\n        $expected = <<<MARKDOWN\n## Section title\nMARKDOWN;\n\n        $this->assertSame($expected, $markdown);\n    }\n\n    public function test_invalid_block(): void\n    {\n        $markdown = Heading2Renderer::render(Divider::create());\n\n        $this->assertSame(\"\", $markdown);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/Renderer/Markdown/Heading3RendererTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\Divider;\nuse Notion\\Blocks\\Heading3;\nuse Notion\\Blocks\\Renderer\\Markdown\\Heading3Renderer;\nuse PHPUnit\\Framework\\TestCase;\n\nclass Heading3RendererTest extends TestCase\n{\n    public function test_render(): void\n    {\n        $block = Heading3::fromString(\"Section title\");\n\n        $markdown = Heading3Renderer::render($block);\n\n        $expected = <<<MARKDOWN\n### Section title\nMARKDOWN;\n\n        $this->assertSame($expected, $markdown);\n    }\n\n    public function test_invalid_block(): void\n    {\n        $markdown = Heading3Renderer::render(Divider::create());\n\n        $this->assertSame(\"\", $markdown);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/Renderer/Markdown/ImageRendererTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\Divider;\nuse Notion\\Blocks\\Image;\nuse Notion\\Blocks\\Renderer\\Markdown\\ImageRenderer;\nuse Notion\\Common\\File;\nuse PHPUnit\\Framework\\TestCase;\n\nclass ImageRendererTest extends TestCase\n{\n    public function test_render(): void\n    {\n        $block = Image::fromFile(File::createExternal(\"https://example.com/dog.jpg\"));\n\n        $markdown = ImageRenderer::render($block);\n\n        $expected = \"![](https://example.com/dog.jpg)\";\n\n        $this->assertSame($expected, $markdown);\n    }\n\n    public function test_invalid_block(): void\n    {\n        $markdown = ImageRenderer::render(Divider::create());\n\n        $this->assertSame(\"\", $markdown);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/Renderer/Markdown/LinkPreviewRendererTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\Divider;\nuse Notion\\Blocks\\LinkPreview;\nuse Notion\\Blocks\\Renderer\\Markdown\\LinkPreviewRenderer;\nuse PHPUnit\\Framework\\TestCase;\n\nclass LinkPreviewRendererTest extends TestCase\n{\n    public function test_render(): void\n    {\n        $block = LinkPreview::fromArray([\n            \"id\"                => \"abc123\",\n            \"created_time\"      => \"2023-01-01 00:00:00\",\n            \"last_edited_time\"  => \"2023-01-01 00:00:00\",\n            \"in_trash\"          => false,\n            \"has_children\"      => false,\n            \"type\"              => \"link_preview\",\n            \"link_preview\"    => [\n                \"url\" => \"https://example.com\"\n            ],\n        ]);\n\n        $markdown = LinkPreviewRenderer::render($block);\n\n        $expected = \"https://example.com\";\n\n        $this->assertSame($expected, $markdown);\n    }\n\n    public function test_invalid_block(): void\n    {\n        $markdown = LinkPreviewRenderer::render(Divider::create());\n\n        $this->assertSame(\"\", $markdown);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/Renderer/Markdown/NumberedListItemRendererTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\NumberedListItem;\nuse Notion\\Blocks\\Divider;\nuse Notion\\Blocks\\Renderer\\Markdown\\NumberedListItemRenderer;\nuse PHPUnit\\Framework\\TestCase;\n\nclass NumberedListItemRendererTest extends TestCase\n{\n    public function test_render(): void\n    {\n        $block = NumberedListItem::fromString(\"Item 1\")\n            ->addChild(NumberedListItem::fromString(\"Item 2\"))\n            ->addChild(NumberedListItem::fromString(\"Item 3\")\n                ->addChild(NumberedListItem::fromString(\"Item 4\")));\n\n        $markdown = NumberedListItemRenderer::render($block);\n\n        $expected = <<<MARKDOWN\n1. Item 1\n  1. Item 2\n  1. Item 3\n    1. Item 4\nMARKDOWN;\n\n        $this->assertSame($expected, $markdown);\n    }\n\n\n    public function test_invalid_block(): void\n    {\n        $markdown = NumberedListItemRenderer::render(Divider::create());\n\n        $this->assertSame(\"\", $markdown);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/Renderer/Markdown/ParagraphRendererTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\Divider;\nuse Notion\\Blocks\\Paragraph;\nuse Notion\\Blocks\\Renderer\\Markdown\\ParagraphRenderer;\nuse Notion\\Common\\Equation;\nuse Notion\\Common\\RichText;\nuse PHPUnit\\Framework\\TestCase;\n\nclass ParagraphRendererTest extends TestCase\n{\n    public function test_render(): void\n    {\n        $block = Paragraph::create()\n            ->addText(RichText::fromString(\"This \")->bold())\n            ->addText(RichText::fromString(\"is \")->italic())\n            ->addText(RichText::fromString(\"an\")->strikeThrough())\n            ->addText(RichText::fromString(\"a \")->underline())\n            ->addText(RichText::fromString(\"paragraph\")->code())\n            ->addText(RichText::fromEquation(Equation::fromString(\"e = mc^2\")))\n            ->addText(RichText::fromString(\"link\")->changeHref(\"https://example.com\"))\n            ->addChild(Paragraph::fromString(\"Child\"));\n\n        $markdown = ParagraphRenderer::render($block);\n\n        $expected = <<<MARKDOWN\n**This** *is* ~~an~~<u>a </u>`paragraph`\\$e = mc^2\\$[link](https://example.com)\n\n\n  Child\n\nMARKDOWN;\n\n        $this->assertSame($expected, $markdown);\n    }\n\n    public function test_invalid_block(): void\n    {\n        $markdown = ParagraphRenderer::render(Divider::create());\n\n        $this->assertSame(\"\", $markdown);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/Renderer/Markdown/PdfRendererTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\Divider;\nuse Notion\\Blocks\\Pdf;\nuse Notion\\Blocks\\Renderer\\Markdown\\PdfRenderer;\nuse Notion\\Common\\File;\nuse PHPUnit\\Framework\\TestCase;\n\nclass PdfRendererTest extends TestCase\n{\n    public function test_render(): void\n    {\n        $block = Pdf::fromFile(File::createExternal(\"https://example.com/file.pdf\"));\n\n        $markdown = PdfRenderer::render($block);\n\n        $expected = \"https://example.com/file.pdf\";\n\n        $this->assertSame($expected, $markdown);\n    }\n\n    public function test_invalid_block(): void\n    {\n        $markdown = PdfRenderer::render(Divider::create());\n\n        $this->assertSame(\"\", $markdown);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/Renderer/Markdown/QuoteRendererTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\Divider;\nuse Notion\\Blocks\\Quote;\nuse Notion\\Blocks\\Paragraph;\nuse Notion\\Blocks\\Renderer\\Markdown\\QuoteRenderer;\nuse PHPUnit\\Framework\\TestCase;\n\nclass QuoteRendererTest extends TestCase\n{\n    public function test_render(): void\n    {\n        $block = Quote::fromString(\"To be or not to be...\")\n            ->addChild(Paragraph::fromString(\"Wrote Shakespeare\"));\n\n        $markdown = QuoteRenderer::render($block);\n\n        $expected = <<<MARKDOWN\n> To be or not to be...\n>\n> Wrote Shakespeare\n\nMARKDOWN;\n\n        $this->assertSame($expected, $markdown);\n    }\n\n    public function test_invalid_block(): void\n    {\n        $markdown = QuoteRenderer::render(Divider::create());\n\n        $this->assertSame(\"\", $markdown);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/Renderer/Markdown/TableOfContentsRendererTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\Divider;\nuse Notion\\Blocks\\TableOfContents;\nuse Notion\\Blocks\\Renderer\\Markdown\\TableOfContentsRenderer;\nuse PHPUnit\\Framework\\TestCase;\n\nclass TableOfContentsRendererTest extends TestCase\n{\n    public function test_render(): void\n    {\n        $block = TableOfContents::create();\n\n        $markdown = TableOfContentsRenderer::render($block);\n\n        $expected = \"[TableOfContents]\";\n\n        $this->assertSame($expected, $markdown);\n    }\n\n    public function test_invalid_block(): void\n    {\n        $markdown = TableOfContentsRenderer::render(Divider::create());\n\n        $this->assertSame(\"\", $markdown);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/Renderer/Markdown/ToDoRendererTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\Divider;\nuse Notion\\Blocks\\ToDo;\nuse Notion\\Blocks\\Renderer\\Markdown\\ToDoRenderer;\nuse PHPUnit\\Framework\\TestCase;\n\nclass ToDoRendererTest extends TestCase\n{\n    public function test_render(): void\n    {\n        $block = ToDo::fromString(\"Item 1\")\n            ->addChild(ToDo::fromString(\"Item 2\")->check())\n            ->addChild(ToDo::fromString(\"Item 3\")\n                ->addChild(ToDo::fromString(\"Item 4\")));\n\n        $markdown = ToDoRenderer::render($block);\n\n        $expected = <<<MARKDOWN\n- [ ] Item 1\n  - [x] Item 2\n  - [ ] Item 3\n    - [ ] Item 4\nMARKDOWN;\n\n        $this->assertSame($expected, $markdown);\n    }\n\n    public function test_invalid_block(): void\n    {\n        $markdown = ToDoRenderer::render(Divider::create());\n\n        $this->assertSame(\"\", $markdown);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/Renderer/Markdown/ToggleRendererTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\Divider;\nuse Notion\\Blocks\\Paragraph;\nuse Notion\\Blocks\\Toggle;\nuse Notion\\Blocks\\Renderer\\Markdown\\ToggleRenderer;\nuse PHPUnit\\Framework\\TestCase;\n\nclass ToggleRendererTest extends TestCase\n{\n    public function test_render(): void\n    {\n        $block = Toggle::fromString(\"Expand\")\n            ->addChild(Paragraph::fromString(\"Hidden text\"));\n\n        $markdown = ToggleRenderer::render($block);\n\n        $expected = <<<MARKDOWN\n<details>\n<summary>Expand</summary>\n\nHidden text\n</details>\nMARKDOWN;\n\n        $this->assertSame($expected, $markdown);\n    }\n\n    public function test_invalid_block(): void\n    {\n        $markdown = ToggleRenderer::render(Divider::create());\n\n        $this->assertSame(\"\", $markdown);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/Renderer/Markdown/VideoRendererTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks\\Renderer\\Markdown;\n\nuse Notion\\Blocks\\Divider;\nuse Notion\\Blocks\\Video;\nuse Notion\\Blocks\\Renderer\\Markdown\\VideoRenderer;\nuse Notion\\Common\\File;\nuse PHPUnit\\Framework\\TestCase;\n\nclass VideoRendererTest extends TestCase\n{\n    public function test_render(): void\n    {\n        $block = Video::fromFile(File::createExternal(\"https://example.com/movie.mp4\"));\n\n        $markdown = VideoRenderer::render($block);\n\n        $expected = \"![](https://example.com/movie.mp4)\";\n\n        $this->assertSame($expected, $markdown);\n    }\n\n    public function test_invalid_block(): void\n    {\n        $markdown = VideoRenderer::render(Divider::create());\n\n        $this->assertSame(\"\", $markdown);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/Renderer/MarkdownRendererTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks\\Renderer;\n\nuse Notion\\Blocks\\BlockType;\nuse Notion\\Blocks\\BulletedListItem;\nuse Notion\\Blocks\\Heading1;\nuse Notion\\Blocks\\Heading2;\nuse Notion\\Blocks\\Paragraph;\nuse Notion\\Blocks\\Renderer\\Markdown\\Heading1Renderer;\nuse Notion\\Blocks\\Renderer\\MarkdownRenderer;\nuse PHPUnit\\Framework\\TestCase;\n\nclass MarkdownRendererTest extends TestCase\n{\n    public function test_render(): void\n    {\n        $blocks = [\n            Heading1::fromString(\"Shopping list\"),\n            Paragraph::fromString(\"My shopping list\"),\n            Heading2::fromString(\"Supermarket\"),\n            BulletedListItem::fromString(\"Tomato\"),\n            BulletedListItem::fromString(\"Onions\"),\n            BulletedListItem::fromString(\"Potato\"),\n            Heading2::fromString(\"Mall\"),\n            BulletedListItem::fromString(\"Black T-Shirt\"),\n        ];\n\n        $markdown = MarkdownRenderer::render(...$blocks);\n\n        $expected = <<<MARKDOWN\n# Shopping list\nMy shopping list\n\n## Supermarket\n- Tomato\n- Onions\n- Potato\n## Mall\n- Black T-Shirt\n\nMARKDOWN;\n\n        $this->assertSame($expected, $markdown);\n    }\n\n    public function test_render_with_overrides(): void\n    {\n        $blocks = [\n            Heading1::fromString(\"Post title\"),\n            Paragraph::fromString(\"My dummy post content.\"),\n        ];\n\n        $overrides = [\n            BlockType::Heading1->value => new class implements \\Notion\\Blocks\\Renderer\\BlockRendererInterface {\n                public static function render(\\Notion\\Blocks\\BlockInterface $block, int $depth = 0): string\n                {\n                    $original = Heading1Renderer::render($block, $depth);\n                    return \"{$original} (OVERRIDE)\";\n                }\n            },\n        ];\n\n        $markdown = MarkdownRenderer::renderWithOverrides($overrides, ...$blocks);\n\n        $expected = <<<MARKDOWN\n# Post title (OVERRIDE)\nMy dummy post content.\n\n\nMARKDOWN;\n\n        $this->assertSame($expected, $markdown);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/TableOfContentsTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks;\n\nuse Notion\\Blocks\\BlockFactory;\nuse Notion\\Blocks\\Paragraph;\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Blocks\\TableOfContents;\nuse Notion\\Common\\Color;\nuse Notion\\Common\\Date;\nuse PHPUnit\\Framework\\TestCase;\n\nclass TableOfContentsTest extends TestCase\n{\n    public function test_create_table_of_contents(): void\n    {\n        $tableOfContents = TableOfContents::create();\n\n        $this->assertEquals(\"table_of_contents\", $tableOfContents->metadata()->type->value);\n    }\n\n    public function test_create_from_array(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"table_of_contents\",\n            \"table_of_contents\" => [\n                \"color\" => \"green\",\n            ]\n        ];\n\n        $tableOfContents = TableOfContents::fromArray($array);\n\n        $this->assertEquals($tableOfContents, BlockFactory::fromArray($array));\n    }\n\n    public function test_error_on_wrong_type(): void\n    {\n        $this->expectException(BlockException::class);\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"wrong-type\",\n            \"table_of_contents\" => []\n        ];\n\n        TableOfContents::fromArray($array);\n    }\n\n    public function test_transform_in_array(): void\n    {\n        $tableOfContents = TableOfContents::create();\n\n        $expected = [\n            \"object\"           => \"block\",\n            \"created_time\"     => $tableOfContents->metadata()->createdTime->format(Date::FORMAT),\n            \"last_edited_time\" => $tableOfContents->metadata()->createdTime->format(Date::FORMAT),\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"table_of_contents\",\n            \"table_of_contents\" => new \\stdClass(),\n        ];\n\n        $this->assertEquals($expected, $tableOfContents->toArray());\n    }\n\n    public function test_no_children_support(): void\n    {\n        $block = TableOfContents::create();\n\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $block->changeChildren();\n    }\n\n    public function test_no_children_support_2(): void\n    {\n        $block = TableOfContents::create();\n\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $block->addChild(Paragraph::create());\n    }\n\n    public function test_change_color(): void\n    {\n        $block = TableOfContents::create()->changeColor(Color::Red);\n\n        $this->assertSame(Color::Red, $block->color);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/TableTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks;\n\nuse Notion\\Blocks\\BlockType;\nuse Notion\\Blocks\\Table;\nuse Notion\\Blocks\\TableRow;\nuse Notion\\Common\\RichText;\nuse PHPUnit\\Framework\\TestCase;\n\nclass TableTest extends TestCase\n{\n    public function test_crate_empty_table(): void\n    {\n        $table = Table::create();\n\n        $this->assertEmpty($table->rows);\n        $this->assertFalse($table->hasColumnHeader);\n        $this->assertFalse($table->hasRowHeader);\n        $this->assertSame(1, $table->tableWidth);\n        $this->assertSame(BlockType::Table, $table->metadata()->type);\n    }\n\n    public function test_change_width(): void\n    {\n        $table = Table::create()->changeWidth(3);\n\n        $this->assertSame(3, $table->tableWidth);\n    }\n\n    public function test_enable_column_header(): void\n    {\n        $table = Table::create()->enableColumnHeader();\n\n        $this->assertTrue($table->hasColumnHeader);\n    }\n\n    public function test_disable_column_header(): void\n    {\n        $table = Table::create()->enableColumnHeader()->disableColumnHeader();\n\n        $this->assertFalse($table->hasColumnHeader);\n    }\n\n    public function test_enable_row_header(): void\n    {\n        $table = Table::create()->enableRowHeader();\n\n        $this->assertTrue($table->hasRowHeader);\n    }\n\n    public function test_disable_row_header(): void\n    {\n        $table = Table::create()->enableRowHeader()->disableRowHeader();\n\n        $this->assertFalse($table->hasRowHeader);\n    }\n\n    public function test_change_rows(): void\n    {\n        $rows = [\n            $this->createRow(\"A1\", \"B1\"),\n            $this->createRow(\"A2\", \"B2\"),\n            $this->createRow(\"A3\", \"B3\"),\n        ];\n\n        $table = Table::create()\n                      ->changeWidth(2)\n                      ->changeRows(...$rows);\n\n        $this->assertEquals($rows, $table->rows);\n    }\n\n    public function test_add_row(): void\n    {\n        $row = $this->createRow(\"A1\", \"B1\");\n\n        $table = Table::create()\n                      ->changeWidth(2)\n                      ->addRow($row);\n\n        $this->assertEquals($row, $table->rows[0]);\n    }\n\n    public function test_remove_all_rows(): void\n    {\n        $row = $this->createRow(\"A1\", \"B1\");\n\n        $table = Table::create()\n                      ->changeWidth(2)\n                      ->addRow($row)\n                      ->changeRows();\n\n        $this->assertEmpty($table->rows);\n    }\n\n    public function test_delete(): void\n    {\n        $table = Table::create();\n\n        $deleted = $table->delete();\n\n        $this->assertTrue($deleted->metadata()->inTrash);\n    }\n\n    public function test_delete_row(): void\n    {\n        $row = $this->createRow(\"A1\", \"B1\");\n\n        $table = Table::create()\n                      ->changeWidth(2)\n                      ->addRow($row);\n\n        $deletedRow = $table->rows[0]->delete();\n\n        $this->assertTrue($deletedRow->metadata()->inTrash);\n    }\n\n\n    private function createRow(string $col1, string $col2): TableRow\n    {\n        return TableRow::create()\n                       ->addCell(RichText::fromString($col1))\n                       ->addCell(RichText::fromString($col2));\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/ToDoTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks;\n\nuse Notion\\Blocks\\BlockFactory;\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Blocks\\ToDo;\nuse Notion\\Common\\Color;\nuse Notion\\Common\\Date;\nuse Notion\\Common\\RichText;\nuse PHPUnit\\Framework\\TestCase;\n\nclass ToDoTest extends TestCase\n{\n    public function test_create_empty_to_do(): void\n    {\n        $toDo = ToDo::create();\n\n        $this->assertEmpty($toDo->text);\n        $this->assertEmpty($toDo->children);\n        $this->assertFalse($toDo->checked);\n    }\n\n    public function test_create_from_string(): void\n    {\n        $toDo = ToDo::fromString(\"Dummy to do.\");\n\n        $this->assertEquals(\"Dummy to do.\", $toDo->toString());\n    }\n\n    public function test_create_from_array(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"to_do\",\n            \"to_do\"        => [\n                \"checked\" => true,\n                \"rich_text\" => [\n                    [\n                        \"plain_text\"  => \"Notion to dos \",\n                        \"href\"        => null,\n                        \"type\"        => \"text\",\n                        \"text\"        => [\n                            \"content\" => \"Notion to dos \",\n                        ],\n                        \"annotations\" => [\n                            \"bold\"          => false,\n                            \"italic\"        => false,\n                            \"strikethrough\" => false,\n                            \"underline\"     => false,\n                            \"code\"          => false,\n                            \"color\"         => \"default\",\n                        ],\n                    ],\n                    [\n                        \"plain_text\"  => \"rock!\",\n                        \"href\"        => null,\n                        \"type\"        => \"text\",\n                        \"text\"        => [\n                            \"content\" => \"rock!\",\n                        ],\n                        \"annotations\" => [\n                            \"bold\"          => true,\n                            \"italic\"        => false,\n                            \"strikethrough\" => false,\n                            \"underline\"     => false,\n                            \"code\"          => false,\n                            \"color\"         => \"red\",\n                        ],\n                    ],\n                ],\n                \"children\" => [],\n            ],\n        ];\n\n        $toDo = ToDo::fromArray($array);\n\n        $this->assertCount(2, $toDo->text);\n        $this->assertEmpty($toDo->children);\n        $this->assertEquals(\"Notion to dos rock!\", $toDo->toString());\n        $this->assertTrue($toDo->checked);\n        $this->assertFalse($toDo->metadata()->inTrash);\n\n        $this->assertEquals($toDo, BlockFactory::fromArray($array));\n    }\n\n    public function test_error_on_wrong_type(): void\n    {\n        $this->expectException(BlockException::class);\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"wrong-type\",\n            \"to_do\"        => [\n                \"checked\"  => false,\n                \"rich_text\"     => [],\n                \"children\" => [],\n            ],\n        ];\n\n        ToDo::fromArray($array);\n    }\n\n    public function test_transform_in_array(): void\n    {\n        $p = ToDo::fromString(\"Simple to do\");\n\n        $expected = [\n            \"object\"           => \"block\",\n            \"created_time\"     => $p->metadata()->createdTime->format(Date::FORMAT),\n            \"last_edited_time\" => $p->metadata()->lastEditedTime->format(Date::FORMAT),\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"to_do\",\n            \"to_do\"            => [\n                \"checked\" => false,\n                \"rich_text\" => [[\n                    \"plain_text\"  => \"Simple to do\",\n                    \"href\"        => null,\n                    \"type\"        => \"text\",\n                    \"text\"        => [\n                        \"content\" => \"Simple to do\",\n                    ],\n                    \"annotations\" => [\n                        \"bold\"          => false,\n                        \"italic\"        => false,\n                        \"strikethrough\" => false,\n                        \"underline\"     => false,\n                        \"code\"          => false,\n                        \"color\"         => \"default\",\n                    ],\n                ]],\n                \"color\" => \"default\",\n                \"children\" => [],\n            ],\n        ];\n\n        $this->assertEquals($expected, $p->toArray());\n    }\n\n    public function test_replace_text(): void\n    {\n        $oldToDo = ToDo::fromString(\"This is an old to do\");\n\n        $newToDo = $oldToDo->changeText(\n            RichText::fromString(\"This is a \"),\n            RichText::fromString(\"new to do\"),\n        );\n\n        $this->assertEquals(\"This is an old to do\", $oldToDo->toString());\n        $this->assertEquals(\"This is a new to do\", $newToDo->toString());\n    }\n\n    public function test_add_text(): void\n    {\n        $oldToDo = ToDo::fromString(\"A to do\");\n\n        $newToDo = $oldToDo->addText(\n            RichText::fromString(\" can be extended.\")\n        );\n\n        $this->assertEquals(\"A to do\", $oldToDo->toString());\n        $this->assertEquals(\"A to do can be extended.\", $newToDo->toString());\n    }\n\n    public function test_replace_children(): void\n    {\n        $nested1 = ToDo::fromString(\"Nested to do 1\");\n        $nested2 = ToDo::fromString(\"Nested to do 2\");\n        $toDo = ToDo::fromString(\"Simple to do.\")->changeChildren($nested1, $nested2);\n\n        $this->assertCount(2, $toDo->children);\n        $this->assertEquals($nested1, $toDo->children[0]);\n        $this->assertEquals($nested2, $toDo->children[1]);\n    }\n\n    public function test_add_child(): void\n    {\n        $toDo = ToDo::fromString(\"Simple to do.\");\n        $nested = ToDo::fromString(\"Nested to do\");\n        $toDo = $toDo->addChild($nested);\n\n        $this->assertCount(1, $toDo->children);\n        $this->assertEquals($nested, $toDo->children[0]);\n    }\n\n    public function test_check_item(): void\n    {\n        $toDo = ToDo::fromString(\"Simple to do.\");\n        $toDo = $toDo->check();\n\n        $this->assertTrue($toDo->checked);\n    }\n\n    public function test_uncheck_item(): void\n    {\n        $toDo = ToDo::fromString(\"Simple to do.\")->check()->uncheck();\n\n        $this->assertFalse($toDo->checked);\n    }\n\n    public function test_change_color(): void\n    {\n        $block = ToDo::fromString(\"Hello World!\")->changeColor(Color::Red);\n\n        $this->assertSame(Color::Red, $block->color);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/ToggleTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks;\n\nuse Notion\\Blocks\\BlockFactory;\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Blocks\\Toggle;\nuse Notion\\Common\\Color;\nuse Notion\\Common\\Date;\nuse Notion\\Common\\RichText;\nuse PHPUnit\\Framework\\TestCase;\n\nclass ToggleTest extends TestCase\n{\n    public function test_create_empty_toggle(): void\n    {\n        $toggle = Toggle::createEmpty();\n\n        $this->assertEmpty($toggle->text);\n        $this->assertEmpty($toggle->children);\n    }\n\n    public function test_create_from_string(): void\n    {\n        $toggle = Toggle::fromString(\"Dummy toggle.\");\n\n        $this->assertEquals(\"Dummy toggle.\", $toggle->toString());\n    }\n\n    public function test_create_from_array(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"toggle\",\n            \"toggle\"        => [\n                \"rich_text\" => [\n                    [\n                        \"plain_text\"  => \"Notion toggles \",\n                        \"href\"        => null,\n                        \"type\"        => \"text\",\n                        \"text\"        => [\n                            \"content\" => \"Notion toggles \",\n                        ],\n                        \"annotations\" => [\n                            \"bold\"          => false,\n                            \"italic\"        => false,\n                            \"strikethrough\" => false,\n                            \"underline\"     => false,\n                            \"code\"          => false,\n                            \"color\"         => \"default\",\n                        ],\n                    ],\n                    [\n                        \"plain_text\"  => \"rock!\",\n                        \"href\"        => null,\n                        \"type\"        => \"text\",\n                        \"text\"        => [\n                            \"content\" => \"rock!\",\n                        ],\n                        \"annotations\" => [\n                            \"bold\"          => true,\n                            \"italic\"        => false,\n                            \"strikethrough\" => false,\n                            \"underline\"     => false,\n                            \"code\"          => false,\n                            \"color\"         => \"red\",\n                        ],\n                    ],\n                ],\n                \"children\" => [],\n            ],\n        ];\n\n        $toggle = Toggle::fromArray($array);\n\n        $this->assertCount(2, $toggle->text);\n        $this->assertEmpty($toggle->children);\n        $this->assertEquals(\"Notion toggles rock!\", $toggle->toString());\n        $this->assertFalse($toggle->metadata()->inTrash);\n\n        $this->assertEquals($toggle, BlockFactory::fromArray($array));\n    }\n\n    public function test_error_on_wrong_type(): void\n    {\n        $this->expectException(BlockException::class);\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"wrong-type\",\n            \"toggle\"        => [\n                \"rich_text\"     => [],\n                \"children\" => [],\n            ],\n        ];\n        Toggle::fromArray($array);\n    }\n\n    public function test_transform_in_array(): void\n    {\n        $p = Toggle::fromString(\"Simple toggle\");\n\n        $expected = [\n            \"object\"           => \"block\",\n            \"created_time\"     => $p->metadata()->createdTime->format(Date::FORMAT),\n            \"last_edited_time\" => $p->metadata()->lastEditedTime->format(Date::FORMAT),\n            \"in_trash\"         => false,\n            \"has_children\"      => false,\n            \"type\"             => \"toggle\",\n            \"toggle\"        => [\n                \"rich_text\" => [[\n                    \"plain_text\"  => \"Simple toggle\",\n                    \"href\"        => null,\n                    \"type\"        => \"text\",\n                    \"text\"        => [\n                        \"content\" => \"Simple toggle\",\n                    ],\n                    \"annotations\" => [\n                        \"bold\"          => false,\n                        \"italic\"        => false,\n                        \"strikethrough\" => false,\n                        \"underline\"     => false,\n                        \"code\"          => false,\n                        \"color\"         => \"default\",\n                    ],\n                ]],\n                \"color\" => \"default\",\n                \"children\" => [],\n            ],\n        ];\n\n        $this->assertEquals($expected, $p->toArray());\n    }\n\n    public function test_replace_text(): void\n    {\n        $oldToggle = Toggle::fromString(\"This is an old toggle\");\n\n        $newToggle = $oldToggle->changeText(\n            RichText::fromString(\"This is a \"),\n            RichText::fromString(\"new toggle\"),\n        );\n\n        $this->assertEquals(\"This is an old toggle\", $oldToggle->toString());\n        $this->assertEquals(\"This is a new toggle\", $newToggle->toString());\n    }\n\n    public function test_add_text(): void\n    {\n        $oldToggle = Toggle::fromString(\"A toggle\");\n\n        $newToggle = $oldToggle->addText(\n            RichText::fromString(\" can be extended.\")\n        );\n\n        $this->assertEquals(\"A toggle\", $oldToggle->toString());\n        $this->assertEquals(\"A toggle can be extended.\", $newToggle->toString());\n    }\n\n    public function test_replace_children(): void\n    {\n        $nested1 = Toggle::fromString(\"Nested toggle 1\");\n        $nested2 = Toggle::fromString(\"Nested toggle 2\");\n        $toggle = Toggle::fromString(\"Simple toggle.\")->changeChildren($nested1, $nested2);\n\n        $this->assertCount(2, $toggle->children);\n        $this->assertEquals($nested1, $toggle->children[0]);\n        $this->assertEquals($nested2, $toggle->children[1]);\n    }\n\n    public function test_add_child(): void\n    {\n        $toggle = Toggle::fromString(\"Simple toggle.\");\n        $nestedToggle = Toggle::fromString(\"Nested toggle\");\n        $toggle = $toggle->addChild($nestedToggle);\n\n        $this->assertCount(1, $toggle->children);\n        $this->assertEquals($nestedToggle, $toggle->children[0]);\n    }\n\n    public function test_change_color(): void\n    {\n        $block = Toggle::fromString(\"Hello World!\")->changeColor(Color::Red);\n\n        $this->assertSame(Color::Red, $block->color);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/UnknownTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks;\n\nuse Notion\\Blocks\\BlockFactory;\nuse Notion\\Blocks\\BlockType;\nuse Notion\\Blocks\\Paragraph;\nuse Notion\\Blocks\\Unknown;\nuse PHPUnit\\Framework\\TestCase;\n\nclass UnknownTest extends TestCase\n{\n    /** @var array{ type: string, ... } */\n    private array $rawBlock = [\n        \"object\"           => \"block\",\n        \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n        \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n        \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n        \"in_trash\"         => false,\n        \"has_children\"     => false,\n        \"type\"             => \"blabla\",\n        \"blabla\"           => [],\n    ];\n\n    public function test_deserilaize(): void\n    {\n        $block = BlockFactory::fromArray($this->rawBlock);\n\n        $this->assertInstanceOf(Unknown::class, $block);\n        $this->assertSame(BlockType::Unknown, $block->metadata()->type);\n        $this->assertEquals($this->rawBlock, $block->toArray());\n    }\n\n    public function test_delete(): void\n    {\n        $block = Unknown::fromArray($this->rawBlock)->delete();\n\n        $this->assertTrue($block->metadata()->inTrash);\n    }\n\n    public function test_add_child(): void\n    {\n        $block = Unknown::fromArray($this->rawBlock);\n        $block = $block->addChild(Paragraph::fromString(\"aaa\"));\n\n        $this->assertTrue($block->metadata()->hasChildren);\n\n        /** @psalm-suppress MixedArgument */\n        $this->assertCount(1, $block->toArray()[\"children\"]);\n    }\n\n    public function test_change_children(): void\n    {\n        $block = Unknown::fromArray($this->rawBlock);\n        $block = $block->changeChildren(Paragraph::fromString(\"aaa\"));\n\n        $this->assertTrue($block->metadata()->hasChildren);\n\n        /** @psalm-suppress MixedArgument */\n        $this->assertCount(1, $block->toArray()[\"children\"]);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Blocks/VideoTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Blocks;\n\nuse Notion\\Blocks\\BlockFactory;\nuse Notion\\Blocks\\Paragraph;\nuse Notion\\Exceptions\\BlockException;\nuse Notion\\Blocks\\Video;\nuse Notion\\Common\\Date;\nuse Notion\\Common\\File;\nuse PHPUnit\\Framework\\TestCase;\n\nclass VideoTest extends TestCase\n{\n    public function test_create_video(): void\n    {\n        $file = File::createExternal(\"https://my-site.com/video.mp4\");\n        $video = Video::fromFile($file);\n\n        $this->assertEquals($file, $video->file);\n    }\n\n    public function test_create_from_array(): void\n    {\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"video\",\n            \"video\"            => [\n                \"type\"     => \"external\",\n                \"external\" => [\n                    \"url\" => \"https://my-site.com/video.mp4\"\n                ],\n            ],\n        ];\n\n        $video = Video::fromArray($array);\n\n        $this->assertEquals(\"https://my-site.com/video.mp4\", $video->file->url);\n\n        $this->assertEquals($video, BlockFactory::fromArray($array));\n    }\n\n    public function test_error_on_wrong_type(): void\n    {\n        $this->expectException(BlockException::class);\n        $array = [\n            \"object\"           => \"block\",\n            \"id\"               => \"04a13895-f072-4814-8af7-cd11af127040\",\n            \"created_time\"     => \"2021-10-18T17:09:00.000Z\",\n            \"last_edited_time\" => \"2021-10-18T17:09:00.000Z\",\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"wrong-type\",\n            \"video\"            => [\n                \"type\"     => \"external\",\n                \"external\" => [\n                    \"url\" => \"https://my-site.com/video.mp4\"\n                ],\n            ],\n        ];\n\n        Video::fromArray($array);\n    }\n\n    public function test_transform_in_array(): void\n    {\n        $file = File::createExternal(\"https://my-site.com/video.mp4\");\n        $video = Video::fromFile($file);\n\n        $expected = [\n            \"object\"           => \"block\",\n            \"created_time\"     => $video->metadata()->createdTime->format(Date::FORMAT),\n            \"last_edited_time\" => $video->metadata()->createdTime->format(Date::FORMAT),\n            \"in_trash\"         => false,\n            \"has_children\"     => false,\n            \"type\"             => \"video\",\n            \"video\"            => [\n                \"type\"     => \"external\",\n                \"external\" => [\n                    \"url\" => \"https://my-site.com/video.mp4\"\n                ],\n            ],\n        ];\n\n        $this->assertEquals($expected, $video->toArray());\n    }\n\n    public function test_replace_file(): void\n    {\n        $file1 = File::createExternal(\"https://my-site.com/video1.mp4\");\n        $file2 = File::createExternal(\"https://my-site.com/video2.mp4\");\n\n        $old = Video::fromFile($file1);\n        $new = $old->changeFile($file2);\n\n        $this->assertEquals($file1, $old->file);\n        $this->assertEquals($file2, $new->file);\n    }\n\n    public function test_no_children_support(): void\n    {\n        $file = File::createExternal(\"https://my-site.com/video.mp4\");\n        $block = Video::fromFile($file);\n\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $block->changeChildren();\n    }\n\n    public function test_no_children_support_2(): void\n    {\n        $file = File::createExternal(\"https://my-site.com/video.mp4\");\n        $block = Video::fromFile($file);\n\n        $this->expectException(BlockException::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $block->addChild(Paragraph::create());\n    }\n\n    public function test_delete(): void\n    {\n        $file = File::createExternal(\"https://my-site.com/video.mp4\");\n        $block = Video::fromFile($file);\n\n        $block = $block->delete();\n\n        $this->assertTrue($block->metadata()->inTrash);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Comments/CommentTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Comments;\n\nuse Notion\\Comments\\Comment;\nuse Notion\\Common\\ParentType;\nuse Notion\\Common\\RichText;\nuse PHPUnit\\Framework\\TestCase;\n\nclass CommentTest extends TestCase\n{\n    public function test_create_page_comment(): void\n    {\n        $pageId = \"8e2b2ad4-63f6-4e9c-8036-9a19c8d3c896\";\n        $comment = Comment::create($pageId, RichText::fromString(\"Sample comment\"));\n\n        $this->assertNull($comment->discussionId);\n        $this->assertSame($pageId, $comment->parent?->id);\n        $this->assertSame(ParentType::Page, $comment->parent?->type);\n        $this->assertSame(\"Sample comment\", RichText::multipleToString(...$comment->text));\n    }\n\n    public function test_create_disscussion_comment(): void\n    {\n        $discussionId = \"27948296-9e5f-4c26-8fc5-46bea548cc33\";\n        $comment = Comment::createReply($discussionId, RichText::fromString(\"Sample comment\"));\n\n        $this->assertNull($comment->parent);\n        $this->assertSame($discussionId, $comment->discussionId);\n        $this->assertSame(\"Sample comment\", RichText::multipleToString(...$comment->text));\n    }\n\n    public function test_array_conversion_parent(): void\n    {\n        $array = [\n            \"id\" => \"9d85f9a4-bd50-4fab-8c27-a3b7f5167acf\",\n            \"parent\" => [\n                \"type\" => \"page_id\",\n                \"page_id\" => \"d30f32b9-0bfd-419a-bc8a-3e27397a8efe\",\n            ],\n            \"created_time\" => \"2022-07-15T21:17:00.000000Z\",\n            \"last_edited_time\" => \"2022-07-15T21:17:00.000000Z\",\n            \"created_by\" => [\n                \"object\" => \"user\",\n                \"id\" => \"9ea7e392-1ac8-4bf6-86c9-7379e16ae94c\",\n            ],\n            \"rich_text\" => [\n                [\n                    \"plain_text\"  => \"Sample comment\",\n                    \"href\"        => null,\n                    \"type\"        => \"text\",\n                    \"text\"        => [\n                        \"content\" => \"Sample comment\",\n                    ],\n                    \"annotations\" => [\n                        \"bold\"          => false,\n                        \"italic\"        => false,\n                        \"strikethrough\" => false,\n                        \"underline\"     => false,\n                        \"code\"          => false,\n                        \"color\"         => \"default\",\n                    ],\n                ],\n            ],\n        ];\n\n        $comment = Comment::fromArray($array);\n        $this->assertEquals($array, $comment->toArray());\n    }\n\n    public function test_array_conversion_discussion(): void\n    {\n        $array = [\n            \"id\" => \"9d85f9a4-bd50-4fab-8c27-a3b7f5167acf\",\n            \"discussion_id\" => \"b341338f-b014-4f49-8ea6-459e9780ed64\",\n            \"created_time\" => \"2022-07-15T21:17:00.000000Z\",\n            \"last_edited_time\" => \"2022-07-15T21:17:00.000000Z\",\n            \"created_by\" => [\n                \"object\" => \"user\",\n                \"id\" => \"9ea7e392-1ac8-4bf6-86c9-7379e16ae94c\",\n            ],\n            \"rich_text\" => [\n                [\n                    \"plain_text\"  => \"Sample comment\",\n                    \"href\"        => null,\n                    \"type\"        => \"text\",\n                    \"text\"        => [\n                        \"content\" => \"Sample comment\",\n                    ],\n                    \"annotations\" => [\n                        \"bold\"          => false,\n                        \"italic\"        => false,\n                        \"strikethrough\" => false,\n                        \"underline\"     => false,\n                        \"code\"          => false,\n                        \"color\"         => \"default\",\n                    ],\n                ],\n            ],\n        ];\n\n        $comment = Comment::fromArray($array);\n        $this->assertEquals($array, $comment->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Common/AnnotationsTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Common;\n\nuse Notion\\Common\\Annotations;\nuse Notion\\Common\\Color;\nuse PHPUnit\\Framework\\TestCase;\n\nclass AnnotationsTest extends TestCase\n{\n    public function test_without_annotations(): void\n    {\n        $annotations = Annotations::create();\n\n        $this->assertFalse($annotations->isBold);\n        $this->assertFalse($annotations->isItalic);\n        $this->assertFalse($annotations->isStrikeThrough);\n        $this->assertFalse($annotations->isUnderline);\n        $this->assertFalse($annotations->isCode);\n        $this->assertEquals(Color::Default, $annotations->color);\n    }\n\n    public function test_bold(): void\n    {\n        $annotations = Annotations::create()->bold();\n\n        $this->assertTrue($annotations->isBold);\n    }\n\n    public function test_italic(): void\n    {\n        $annotations = Annotations::create()->italic();\n\n        $this->assertTrue($annotations->isItalic);\n    }\n\n    public function test_strike_through(): void\n    {\n        $annotations = Annotations::create()->strikeThrough();\n\n        $this->assertTrue($annotations->isStrikeThrough);\n    }\n\n    public function test_underline(): void\n    {\n        $annotations = Annotations::create()->underline();\n\n        $this->assertTrue($annotations->isUnderline);\n    }\n\n    public function test_code(): void\n    {\n        $annotations = Annotations::create()->code();\n\n        $this->assertTrue($annotations->isCode);\n    }\n\n    public function test_change_color(): void\n    {\n        $annotations = Annotations::create()->changeColor(Color::Red);\n\n        $this->assertEquals(Color::Red, $annotations->color);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Common/DateTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Common;\n\nuse DateTimeImmutable;\nuse Notion\\Common\\Date;\nuse PHPUnit\\Framework\\TestCase;\n\nclass DateTest extends TestCase\n{\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"start\" => \"2021-01-01T00:00:00.000000Z\",\n            \"end\"   => \"2021-12-31T00:00:00.000000Z\",\n        ];\n\n        $date = Date::fromArray($array);\n        $this->assertEquals($array, $date->toArray());\n    }\n\n    public function test_create_date(): void\n    {\n        $start = new DateTimeImmutable(\"2021-01-01\");\n        $date = Date::create($start);\n\n        $this->assertSame($start, $date->start);\n        $this->assertNull($date->end);\n        $this->assertFalse($date->isRange());\n    }\n\n    public function test_create_range(): void\n    {\n        $start = new DateTimeImmutable(\"2021-01-01\");\n        $end = new DateTimeImmutable(\"2021-12-31\");\n        $date = Date::createRange($start, $end);\n\n        $this->assertSame($start, $date->start);\n        $this->assertSame($end, $date->end);\n        $this->assertTrue($date->isRange());\n    }\n\n    public function test_change_start(): void\n    {\n        $oldStart = new DateTimeImmutable(\"2021-01-01\");\n        $newStart = new DateTimeImmutable(\"2022-01-01\");\n\n        $date = Date::create($oldStart)->changeStart($newStart);\n\n        $this->assertSame($newStart, $date->start);\n    }\n\n    public function test_change_end(): void\n    {\n        $start = new DateTimeImmutable(\"2021-01-01\");\n        $end = new DateTimeImmutable(\"2022-01-01\");\n\n        $date = Date::create($start)->changeEnd($end);\n\n        $this->assertSame($end, $date->end);\n    }\n\n    public function test_remove_end(): void\n    {\n        $start = new DateTimeImmutable(\"2021-01-01\");\n        $end = new DateTimeImmutable(\"2021-12-31\");\n        $date = Date::createRange($start, $end)->removeEnd();\n\n        $this->assertNull($date->end);\n        $this->assertFalse($date->isRange());\n    }\n\n    public function test_now(): void\n    {\n        $now = new DateTimeImmutable(\"now\");\n        $date = Date::now();\n\n        $this->assertNull($date->end);\n        $this->assertSame($now->format(\"Y-m-d\"), $date->start->format(\"Y-m-d\"));\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Common/EquationTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Common;\n\nuse Notion\\Common\\Equation;\nuse PHPUnit\\Framework\\TestCase;\n\nclass EquationTest extends TestCase\n{\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"expression\" => \"a^2 + b^2 = c^2\",\n        ];\n\n        $equation = Equation::fromArray($array);\n\n        $this->assertEquals($array, $equation->toArray());\n    }\n\n    public function test_create_from_expression(): void\n    {\n        $equation = Equation::fromString(\"a^2 + b^2 = c^2\");\n\n        $this->assertEquals(\"a^2 + b^2 = c^2\", $equation->expression);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Common/FileTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Common;\n\nuse DateTimeImmutable;\nuse Notion\\Common\\File;\nuse Notion\\Common\\FileType;\nuse Notion\\Common\\RichText;\nuse PHPUnit\\Framework\\TestCase;\n\nclass FileTest extends TestCase\n{\n    public function test_create_internal(): void\n    {\n        $expiryTime = new DateTimeImmutable(\"2021-01-01\");\n        $file = File::createInternal(\"https://notion.so/image.png\", $expiryTime);\n\n        $this->assertTrue($file->isInternal());\n        $this->assertEquals(FileType::Internal, $file->type);\n        $this->assertEquals(\"https://notion.so/image.png\", $file->url);\n        $this->assertEquals($expiryTime, $file->expiryTime);\n    }\n\n    public function test_create_external(): void\n    {\n        $file = File::createExternal(\"https://my-site.com/image.png\");\n\n        $this->assertTrue($file->isExternal());\n        $this->assertEquals(FileType::External, $file->type);\n        $this->assertEquals(\"https://my-site.com/image.png\", $file->url);\n        $this->assertNull($file->expiryTime);\n    }\n\n    public function test_intenral_array_conversion(): void\n    {\n        $array = [\n            \"type\" => \"file\",\n            \"name\" => \"Test file\",\n            \"file\" => [\n                \"url\" => \"https://notion.so/image.png\",\n                \"expiry_time\" => \"2020-12-08T12:00:00.000000Z\",\n            ],\n        ];\n        $file = File::fromArray($array);\n\n        $this->assertEquals($array, $file->toArray());\n    }\n\n    public function test_external_array_conversion(): void\n    {\n        $array = [\n            \"type\" => \"external\",\n            \"name\" => \"Test file\",\n            \"external\" => [ \"url\" => \"https://my-site.com/image.png\" ],\n            \"caption\" => [[\n                \"plain_text\" => \"Sample caption\",\n                \"href\" => null,\n                \"annotations\" => [\n                    \"bold\"          => false,\n                    \"italic\"        => false,\n                    \"strikethrough\" => false,\n                    \"underline\"     => false,\n                    \"code\"          => false,\n                    \"color\"         => \"default\",\n                ],\n                \"type\" => \"text\",\n                \"text\" => [ \"content\" => \"Sample caption\" ],\n            ]],\n        ];\n        $file = File::fromArray($array);\n\n        $this->assertEquals($array, $file->toArray());\n    }\n\n    public function test_change_url(): void\n    {\n        $file = File::createExternal(\"\")->changeUrl(\"https://my-site.com/image.png\");\n\n        $this->assertEquals(\"https://my-site.com/image.png\", $file->url);\n    }\n\n    public function test_change_name(): void\n    {\n        $file = File::createExternal(\"\")->changeName(\"My file name\");\n\n        $this->assertSame(\"My file name\", $file->name);\n    }\n\n    public function test_change_caption(): void\n    {\n        $caption = [ RichText::fromString(\"Sample caption.\") ];\n\n        $file = File::createExternal(\"\")->changeCaption(...$caption);\n        $this->assertEquals($caption, $file->caption);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Common/IconTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Common;\n\nuse Notion\\Common\\File;\nuse Notion\\Common\\Icon;\nuse PHPUnit\\Framework\\TestCase;\n\nclass IconTest extends TestCase\n{\n    public function test_icon_from_file_array_conversion(): void\n    {\n        $file = File::createExternal(\"http://example.com/icon.png\");\n        $icon = Icon::fromFile($file);\n\n        $expected = [\n            \"type\" => \"external\",\n            \"external\" => [\"url\" => \"http://example.com/icon.png\"],\n        ];\n\n        $this->assertEquals($expected, $icon->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Common/MentionTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Common;\n\nuse DateTimeImmutable;\nuse Notion\\Common\\Date;\nuse Notion\\Common\\Mention;\nuse Notion\\Common\\MentionType;\nuse Notion\\Users\\User;\nuse PHPUnit\\Framework\\TestCase;\n\nclass MentionTest extends TestCase\n{\n    public function test_mention_page(): void\n    {\n        $mention = Mention::page(\"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\");\n\n        $this->assertTrue($mention->isPage());\n        $this->assertEquals(MentionType::Page, $mention->type);\n        $this->assertEquals(\"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\", $mention->pageId);\n    }\n\n    public function test_mention_database(): void\n    {\n        $mention = Mention::database(\"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\");\n\n        $this->assertTrue($mention->isDatabase());\n        $this->assertEquals(\"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\", $mention->databaseId);\n    }\n\n    public function test_mention_user(): void\n    {\n        $user = User::fromArray([\n            \"object\"     => \"user\",\n            \"id\"         => \"b0688871-85db-4637-8fc9-043a240fcaec\",\n            \"name\"       => \"Mario Simao\",\n            \"avatar_url\" => \"http://example.com\",\n            \"type\"       => \"person\",\n            \"person\"     => [ \"email\" => \"mariosimao@email.com\" ],\n        ]);\n\n        $mention = Mention::user($user);\n\n        $this->assertTrue($mention->isUser());\n        $this->assertEquals($user, $mention->user);\n    }\n\n    public function test_mention_date(): void\n    {\n        $date = Date::create(new DateTimeImmutable(\"2021-01-01\"));\n        $mention = Mention::date($date);\n\n        $this->assertTrue($mention->isDate());\n        $this->assertEquals($date, $mention->date);\n    }\n\n    public function test_page_array_conversion(): void\n    {\n        $array = [\n            \"type\" => \"page\",\n            \"page\" => [ \"id\" => \"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\" ],\n        ];\n        $mention = Mention::fromArray($array);\n\n        $this->assertEquals($array, $mention->toArray());\n    }\n\n    public function test_database_array_conversion(): void\n    {\n        $array = [\n            \"type\" => \"database\",\n            \"database\" => [ \"id\" => \"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\" ],\n        ];\n        $mention = Mention::fromArray($array);\n\n        $this->assertEquals($array, $mention->toArray());\n    }\n\n    public function test_user_array_conversion(): void\n    {\n        $array = [\n            \"type\" => \"user\",\n            \"user\" => [\n                \"object\"     => \"user\",\n                \"id\"         => \"b0688871-85db-4637-8fc9-043a240fcaec\",\n                \"name\"       => \"Mario Simao\",\n                \"avatar_url\" => \"http://example.com\",\n                \"type\"       => \"person\",\n                \"person\"     => [ \"email\" => \"mariosimao@email.com\" ],\n            ],\n        ];\n        $mention = Mention::fromArray($array);\n\n        $this->assertEquals($array, $mention->toArray());\n    }\n\n    public function test_date_array_conversion(): void\n    {\n        $array = [\n            \"type\" => \"date\",\n            \"date\" => [ \"start\" => \"2021-01-01T00:00:00.000000Z\", \"end\" => null ],\n        ];\n        $mention = Mention::fromArray($array);\n\n        $this->assertEquals($array, $mention->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Common/ParentBlockTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Common;\n\nuse Notion\\Common\\ParentBlock;\nuse Notion\\Common\\ParentType;\nuse PHPUnit\\Framework\\TestCase;\n\nclass ParentBlockTest extends TestCase\n{\n    public function test_page(): void\n    {\n        $id = \"5e406817-16ab-4a71-a7d4-e1f0ea5629e4\";\n        $parent = ParentBlock::page($id);\n\n        $this->assertSame($id, $parent->id);\n        $this->assertSame(ParentType::Page, $parent->type);\n    }\n\n    public function test_database(): void\n    {\n        $id = \"5e406817-16ab-4a71-a7d4-e1f0ea5629e4\";\n        $parent = ParentBlock::database($id);\n\n        $this->assertSame($id, $parent->id);\n        $this->assertSame(ParentType::Database, $parent->type);\n    }\n\n    public function test_block(): void\n    {\n        $id = \"5e406817-16ab-4a71-a7d4-e1f0ea5629e4\";\n        $parent = ParentBlock::block($id);\n\n        $this->assertSame($id, $parent->id);\n        $this->assertSame(ParentType::Block, $parent->type);\n    }\n\n    public function test_workspace(): void\n    {\n        $parent = ParentBlock::workspace();\n\n        $this->assertNull($parent->id);\n        $this->assertSame(ParentType::Workspace, $parent->type);\n    }\n\n    public function test_array_conversion_page(): void\n    {\n        $array = [\n            \"type\" => \"page_id\",\n            \"page_id\" => \"c2c2b3c3-edc6-4c2b-950d-de4e0ccdb052\",\n        ];\n        $parent = ParentBlock::fromArray($array);\n\n        $this->assertSame($array, $parent->toArray());\n    }\n\n    public function test_array_conversion_database(): void\n    {\n        $array = [\n            \"type\" => \"database_id\",\n            \"database_id\" => \"c2c2b3c3-edc6-4c2b-950d-de4e0ccdb052\",\n        ];\n        $parent = ParentBlock::fromArray($array);\n\n        $this->assertSame($array, $parent->toArray());\n    }\n\n    public function test_array_conversion_block(): void\n    {\n        $array = [\n            \"type\" => \"block_id\",\n            \"block_id\" => \"c2c2b3c3-edc6-4c2b-950d-de4e0ccdb052\",\n        ];\n        $parent = ParentBlock::fromArray($array);\n\n        $this->assertSame($array, $parent->toArray());\n    }\n\n    public function test_array_conversion_workspace(): void\n    {\n        $array = [\n            \"type\" => \"workspace\",\n            \"workspace\" => true,\n        ];\n        $parent = ParentBlock::fromArray($array);\n\n        $this->assertSame($array, $parent->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Common/RichTextTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Common;\n\nuse DateTimeImmutable;\nuse Notion\\Common\\Color;\nuse Notion\\Common\\Date;\nuse Notion\\Common\\Equation;\nuse Notion\\Common\\Mention;\nuse Notion\\Common\\RichText;\nuse Notion\\Common\\Text;\nuse PHPUnit\\Framework\\TestCase;\n\nclass RichTextTest extends TestCase\n{\n    public function test_create_text(): void\n    {\n        $richText = RichText::fromString(\"Simple text\");\n\n        $this->assertTrue($richText->isText());\n        $this->assertEquals(\"Simple text\", $richText->text?->content);\n    }\n\n    public function test_create_link(): void\n    {\n        $richText = RichText::createLink(\"Click here\", \"https://notion.so\");\n\n        $this->assertTrue($richText->isText());\n        $this->assertEquals(\"Click here\", $richText->plainText);\n        $this->assertEquals(\"Click here\", $richText->text?->content);\n        $this->assertEquals(\"https://notion.so\", $richText->href);\n        $this->assertEquals(\"https://notion.so\", $richText->text?->url);\n    }\n\n    public function test_create_from_text(): void\n    {\n        $text = Text::fromString(\"My text\");\n\n        $richText = RichText::fromText($text);\n\n        $this->assertTrue($richText->isText());\n        $this->assertEquals(\"My text\", $richText->plainText);\n        $this->assertEquals(\"My text\", $richText->text?->content);\n    }\n\n    public function test_create_from_equation(): void\n    {\n        $equation = Equation::fromString(\"a^2 + b^2 = c^2\");\n        $richText = RichText::fromEquation($equation);\n\n        $this->assertTrue($richText->isEquation());\n        $this->assertEquals(\"a^2 + b^2 = c^2\", $richText->equation?->expression);\n    }\n\n    public function test_create_from_mention(): void\n    {\n        $date = Date::create(new DateTimeImmutable(\"2022-10-31\"));\n        $mention = Mention::date($date);\n        $richText = RichText::fromMention($mention);\n\n        $this->assertTrue($richText->mention?->isDate());\n    }\n\n    public function test_change_to_bold(): void\n    {\n        $richText = RichText::fromString(\"Simple text\")->bold();\n\n        $this->assertTrue($richText->annotations->isBold);\n    }\n\n    public function test_change_to_italic(): void\n    {\n        $richText = RichText::fromString(\"Simple text\")->italic();\n\n        $this->assertTrue($richText->annotations->isItalic);\n    }\n\n    public function test_change_to_strike_through(): void\n    {\n        $richText = RichText::fromString(\"Simple text\")->strikeThrough();\n\n        $this->assertTrue($richText->annotations->isStrikeThrough);\n    }\n\n    public function test_change_to_underline(): void\n    {\n        $richText = RichText::fromString(\"Simple text\")->underline();\n\n        $this->assertTrue($richText->annotations->isUnderline);\n    }\n\n    public function test_change_to_code(): void\n    {\n        $richText = RichText::fromString(\"Simple text\")->code();\n\n        $this->assertTrue($richText->annotations->isCode);\n    }\n\n    public function test_change_color(): void\n    {\n        $richText = RichText::fromString(\"Simple text\")->color(Color::Red);\n\n        $this->assertEquals(Color::Red, $richText->annotations->color);\n    }\n\n    public function test_change_href(): void\n    {\n        $richText = RichText::fromString(\"Simple text\")->changeHref(\"https://notion.so\");\n\n        $this->assertEquals(\"https://notion.so\", $richText->href);\n    }\n\n    public function test_mention_array_conversion(): void\n    {\n        $array = [\n            \"plain_text\" => \"Page title\",\n            \"href\" => null,\n            \"annotations\" => [\n                \"bold\"          => false,\n                \"italic\"        => false,\n                \"strikethrough\" => false,\n                \"underline\"     => false,\n                \"code\"          => false,\n                \"color\"         => \"default\",\n            ],\n            \"type\" => \"mention\",\n            \"mention\" => [\n                \"type\" => \"page\",\n                \"page\" => [ \"id\" => \"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\" ],\n            ],\n        ];\n        $richText = RichText::fromArray($array);\n\n        $this->assertEquals($array, $richText->toArray());\n        $this->assertNotNull($richText->mention);\n    }\n\n    public function test_equation_array_conversion(): void\n    {\n        $array = [\n            \"plain_text\" => \"Page title\",\n            \"href\" => null,\n            \"annotations\" => [\n                \"bold\"          => false,\n                \"italic\"        => false,\n                \"strikethrough\" => false,\n                \"underline\"     => false,\n                \"code\"          => false,\n                \"color\"         => \"default\",\n            ],\n            \"type\" => \"equation\",\n            \"equation\" => [ \"expression\" => \"a^2 + b^2 = c^2\" ],\n        ];\n        $richText = RichText::fromArray($array);\n\n        $this->assertEquals($array, $richText->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Common/TextTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Common;\n\nuse Notion\\Common\\Text;\nuse PHPUnit\\Framework\\TestCase;\n\nclass TextTest extends TestCase\n{\n    public function test_create_text(): void\n    {\n        $text = Text::fromString(\"Simple text\");\n\n        $this->assertEquals(\"Simple text\", $text->content);\n    }\n\n    public function test_change_text(): void\n    {\n        $text = Text::fromString(\"\")->changeContent(\"Simple text\");\n\n        $this->assertEquals(\"Simple text\", $text->content);\n    }\n\n    public function test_change_url(): void\n    {\n        $text = Text::fromString(\"Simple text\")->changeUrl(\"https://notion.so\");\n\n        $this->assertEquals(\"https://notion.so\", $text->url);\n    }\n\n    public function test_remove_url(): void\n    {\n        $text = Text::fromString(\"Simple text\")\n            ->changeUrl(\"https://notion.so\")\n            ->removeUrl();\n\n        $this->assertNull($text->url);\n    }\n\n    public function test_convert_test_with_url_to_array(): void\n    {\n        $text = Text::fromString(\"Simple text\")\n            ->changeUrl(\"https://notion.so\");\n\n        $expected = [\n            \"content\" => \"Simple text\",\n            \"link\"    => [ \"url\" => \"https://notion.so\" ],\n        ];\n\n        $this->assertSame($expected, $text->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/ConfigurationTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit;\n\nuse GuzzleHttp\\Client;\nuse GuzzleHttp\\Psr7\\HttpFactory;\nuse Notion\\Configuration;\nuse Notion\\Notion;\nuse PHPUnit\\Framework\\TestCase;\n\nfinal class ConfigurationTest extends TestCase\n{\n    public function test_create_default_configuration(): void\n    {\n        $token = \"secret_123abc\";\n        $config = Configuration::create($token);\n\n        $this->assertSame($token, $config->token);\n        $this->assertSame(Notion::API_VERSION, $config->version);\n        $this->assertSame(true, $config->retryOnConflict);\n        $this->assertSame(3, $config->retryOnConflictAttempts);\n    }\n\n    public function test_create_from_psr_implementations(): void\n    {\n        $token = \"secret_123abc\";\n        $client = new Client();\n        $factory = new HttpFactory();\n\n        $config = Configuration::createFromPsrImplementations($token, $client, $factory);\n\n        $this->assertSame($client, $config->httpClient);\n        $this->assertSame($factory, $config->requestFactory);\n    }\n\n    public function test_enable_retry_on_conflict(): void\n    {\n        $config = Configuration::create(\"secret_123abc\")->enableRetryOnConflict(3);\n\n        $this->assertTrue($config->retryOnConflict);\n        $this->assertSame(3, $config->retryOnConflictAttempts);\n    }\n\n    public function test_disable_retry_on_conflict(): void\n    {\n        $config = Configuration::create(\"secret_123abc\")->disableRetryOnConflict();\n\n        $this->assertFalse($config->retryOnConflict);\n        $this->assertSame(0, $config->retryOnConflictAttempts);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/DatabaseParentTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases;\n\nuse Notion\\Databases\\DatabaseParent;\nuse PHPUnit\\Framework\\TestCase;\n\nclass DatabaseParentTest extends TestCase\n{\n    public function test_create_parent_page(): void\n    {\n        $parent = DatabaseParent::page(\"058d158b-09de-4d69-be07-901c20a7ca5c\");\n\n        $this->assertTrue($parent->isPage());\n        $this->assertEquals(\"058d158b-09de-4d69-be07-901c20a7ca5c\", $parent->id);\n    }\n\n    public function test_create_parent_workspace(): void\n    {\n        $parent = DatabaseParent::workspace();\n\n        $this->assertTrue($parent->isWorkspace());\n        $this->assertEquals(\"workspace\", $parent->type->value);\n    }\n\n    public function test_create_parent_block(): void\n    {\n        $parent = DatabaseParent::block(\"0181c3aa-1112-489f-b34a-515b4e3583ed\");\n\n        $this->assertTrue($parent->isBlock());\n        $this->assertSame(\"0181c3aa-1112-489f-b34a-515b4e3583ed\", $parent->id);\n    }\n\n    public function test_page_array_conversion(): void\n    {\n        $array = [\n            \"type\" => \"page_id\",\n            \"page_id\" => \"7a774b5d-ca74-4679-9f18-689b5a98f138\",\n        ];\n        $parent = DatabaseParent::fromArray($array);\n\n        $this->assertEquals($array[\"page_id\"], $parent->toArray()[\"page_id\"]);\n    }\n\n    public function test_workspace_array_conversion(): void\n    {\n        $array = [\n            \"type\" => \"workspace\",\n            \"workspace\" => true,\n        ];\n        $parent = DatabaseParent::fromArray($array);\n\n        $this->assertEquals($array[\"workspace\"], $parent->toArray()[\"workspace\"]);\n    }\n\n    public function test_block_array_conversion(): void\n    {\n        $array = [\n            \"type\" => \"block_id\",\n            \"block_id\" => \"7a774b5d-ca74-4679-9f18-689b5a98f138\",\n        ];\n        $parent = DatabaseParent::fromArray($array);\n\n        $this->assertEquals($array[\"block_id\"], $parent->toArray()[\"block_id\"]);\n    }\n\n    public function test_invalid_type_array(): void\n    {\n        $this->expectException(\\ValueError::class);\n        /** @psalm-suppress InvalidArgument */\n        DatabaseParent::fromArray([ \"type\" => \"invalid-type\" ]);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/DatabaseTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases;\n\nuse Exception;\nuse Notion\\Common\\Date;\nuse Notion\\Common\\Emoji;\nuse Notion\\Common\\File;\nuse Notion\\Common\\Icon;\nuse Notion\\Common\\RichText;\nuse Notion\\Databases\\Database;\nuse Notion\\Databases\\DatabaseParent;\nuse Notion\\Databases\\Properties\\Number;\nuse Notion\\Databases\\Properties\\NumberFormat;\nuse Notion\\Databases\\Properties\\Title;\nuse PHPUnit\\Framework\\TestCase;\n\nclass DatabaseTest extends TestCase\n{\n    public function test_create_database(): void\n    {\n        $parent = DatabaseParent::page(\"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\");\n        $database = Database::create($parent);\n\n        $this->assertEquals(\"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\", $database->parent->id);\n        $this->assertCount(1, $database->properties); // Title property\n    }\n\n    public function test_add_title(): void\n    {\n        $parent = DatabaseParent::page(\"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\");\n        $database = Database::create($parent)->changeTitle(\"Database title\");\n\n        $this->assertEquals(\"Database title\", RichText::multipleToString(...$database->title));\n    }\n\n    public function test_add_advanced_title(): void\n    {\n        $parent = DatabaseParent::page(\"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\");\n        $database = Database::create($parent)->changeAdvancedTitle(\n            RichText::fromString(\"Database title\")\n        );\n\n        $this->assertEquals(\"Database title\", $database->title[0]->plainText);\n    }\n\n    public function test_add_icon(): void\n    {\n        $parent = DatabaseParent::page(\"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\");\n        $database = Database::create($parent)->changeIcon(\n            Icon::fromEmoji(Emoji::fromString(\"⭐\"))\n        );\n\n        if ($database->icon?->isEmoji()) {\n            $this->assertEquals(\"⭐\", $database->icon->emoji?->emoji);\n        }\n        $this->assertTrue($database->hasIcon());\n    }\n\n    public function test_add_file_icon(): void\n    {\n        $parent = DatabaseParent::page(\"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\");\n        $database = Database::create($parent)->changeIcon(\n            File::createExternal(\"http://example.com/icon.png\")\n        );\n\n        $this->assertTrue($database->icon?->isFile());\n    }\n\n    public function test_remove_icon(): void\n    {\n        $parent = DatabaseParent::page(\"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\");\n        $database = Database::create($parent)\n            ->changeIcon(Icon::fromEmoji(Emoji::fromString(\"⭐\")))\n            ->removeIcon();\n\n        $this->assertNull($database->icon);\n        $this->assertFalse($database->hasIcon());\n    }\n\n    public function test_add_cover(): void\n    {\n        $parent = DatabaseParent::page(\"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\");\n        $coverImage = File::createExternal(\"https://my-site.com/image.png\");\n        $database = Database::create($parent)->changeCover($coverImage);\n\n        $this->assertEquals($coverImage, $database->cover);\n    }\n\n    public function test_remove_cover(): void\n    {\n        $parent = DatabaseParent::page(\"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\");\n        $coverImage = File::createExternal(\"https://my-site.com/image.png\");\n        $database = Database::create($parent)->changeCover($coverImage)->removeCover();\n\n        $this->assertNull($database->cover);\n    }\n\n    public function test_move_page(): void\n    {\n        $oldParent = DatabaseParent::page(\"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\");\n        $newParent = DatabaseParent::page(\"08da99e5-f11d-4d26-827d-112a3a9bd07d\");\n        $database = Database::create($oldParent)->changeParent($newParent);\n\n        $this->assertSame($newParent, $database->parent);\n    }\n\n    public function test_error_change_internal_cover_image(): void\n    {\n        $parent = DatabaseParent::page(\"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\");\n        $cover = FIle::createInternal(\"https://notion.so/image.png\");\n\n        $this->expectException(\\Exception::class);\n        /** @psalm-suppress UnusedMethodCall */\n        Database::create($parent)->changeCover($cover);\n    }\n\n    public function test_replace_properties(): void\n    {\n        $parent = DatabaseParent::page(\"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\");\n        $properties = [\n            \"Dummy prop name\" => Title::create(\"Dummy prop name\")\n        ];\n        $database = Database::create($parent)->changeProperties($properties);\n\n        $this->assertCount(1, $database->properties);\n    }\n\n    public function test_add_property(): void\n    {\n        $prop = Number::create(\"Price\");\n\n        $parent = DatabaseParent::page(\"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\");\n        $database = Database::create($parent)\n            ->addProperty($prop);\n\n        $this->assertSame($prop, $database->properties()->get(\"Price\"));\n    }\n\n    public function test_change_property(): void\n    {\n        $parent = DatabaseParent::page(\"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\");\n        $database = Database::create($parent)->addProperty(Number::create(\"Price\"));\n\n        $prop = $database->properties()->getNumber(\"Price\")->changeFormat(NumberFormat::Dollar);\n\n        $database = $database->changeProperty($prop);\n\n        $this->assertSame(\n            NumberFormat::Dollar,\n            $database->properties()->getNumber(\"Price\")->format\n        );\n    }\n\n    public function test_remove_property(): void\n    {\n        $parent = DatabaseParent::page(\"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\");\n        $database = Database::create($parent)->addProperty(Number::create(\"Price\"));\n\n        $database = $database->removePropertyByName(\"Price\");\n\n        $this->expectException(Exception::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $database->properties()->get(\"Price\");\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"object\" => \"database\",\n            \"id\" => \"a7e80c0b-a766-43c3-a9e9-21ce94595e0e\",\n            \"created_time\" => \"2020-12-08T12:00:00.000000Z\",\n            \"last_edited_time\" => \"2020-12-08T12:00:00.000000Z\",\n            \"title\" => [[\n                \"plain_text\" => \"Database title\",\n                \"href\" => null,\n                \"annotations\" => [\n                    \"bold\"          => false,\n                    \"italic\"        => false,\n                    \"strikethrough\" => false,\n                    \"underline\"     => false,\n                    \"code\"          => false,\n                    \"color\"         => \"default\",\n                ],\n                \"type\" => \"text\",\n                \"text\" => [ \"content\" => \"Database title\" ],\n            ]],\n            \"description\" => [[\n                \"plain_text\" => \"Database description\",\n                \"href\" => null,\n                \"annotations\" => [\n                    \"bold\"          => false,\n                    \"italic\"        => false,\n                    \"strikethrough\" => false,\n                    \"underline\"     => false,\n                    \"code\"          => false,\n                    \"color\"         => \"default\",\n                ],\n                \"type\" => \"text\",\n                \"text\" => [ \"content\" => \"Database title\" ],\n            ]],\n            \"icon\" => null,\n            \"cover\" => null,\n            \"properties\" => [\n                \"title\" => [\n                    \"id\"    => \"title\",\n                    \"name\"  => \"Dummy prop name\",\n                    \"type\"  => \"title\",\n                    \"title\" => new \\stdClass(),\n                ],\n            ],\n            \"parent\" => [\n                \"type\" => \"page_id\",\n                \"page_id\" => \"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\",\n            ],\n            \"url\" => \"https://notion.so/a7e80c0ba76643c3a9e921ce94595e0e\",\n            \"is_inline\" => true,\n        ];\n        $database = Database::fromArray($array);\n\n        $outArray = $array;\n        unset($outArray[\"parent\"][\"type\"]);\n\n        $this->assertEquals($outArray, $database->toArray());\n        $this->assertSame(\"a7e80c0b-a766-43c3-a9e9-21ce94595e0e\", $database->id);\n        $this->assertSame(\"https://notion.so/a7e80c0ba76643c3a9e921ce94595e0e\", $database->url);\n        $this->assertEquals(\n            \"2020-12-08T12:00:00.000000Z\",\n            $database->createdTime->format(Date::FORMAT),\n        );\n        $this->assertEquals(\n            \"2020-12-08T12:00:00.000000Z\",\n            $database->lastEditedTime->format(Date::FORMAT),\n        );\n        $this->assertTrue($database->isInline);\n    }\n\n    public function test_from_array_change_emoji_icon(): void\n    {\n        $array = [\n            \"object\" => \"database\",\n            \"id\" => \"a7e80c0b-a766-43c3-a9e9-21ce94595e0e\",\n            \"created_time\" => \"2020-12-08T12:00:00.000000Z\",\n            \"last_edited_time\" => \"2020-12-08T12:00:00.000000Z\",\n            \"title\" => [[\n                \"plain_text\" => \"Page title\",\n                \"href\" => null,\n                \"annotations\" => [\n                    \"bold\"          => false,\n                    \"italic\"        => false,\n                    \"strikethrough\" => false,\n                    \"underline\"     => false,\n                    \"code\"          => false,\n                    \"color\"         => \"default\",\n                ],\n                \"type\" => \"text\",\n                \"text\" => [ \"content\" => \"Database title\" ],\n            ]],\n            \"description\" => [],\n            \"icon\" => [\n                \"type\" => \"emoji\",\n                \"emoji\" => \"⭐\",\n            ],\n            \"cover\" => null,\n            \"properties\" => [\n                \"Title\" => [\n                    \"id\"    => \"title\",\n                    \"name\"  => \"Title\",\n                    \"type\"  => \"title\",\n                    \"title\" => new \\stdClass(),\n                ],\n            ],\n            \"parent\" => [\n                \"type\" => \"page_id\",\n                \"page_id\" => \"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\",\n            ],\n            \"url\" => \"https://notion.so/a7e80c0ba76643c3a9e921ce94595e0e\",\n            \"is_inline\" => false,\n        ];\n        $database = Database::fromArray($array);\n\n        if ($database->icon?->isEmoji()) {\n            $this->assertEquals(\"⭐\", $database->icon->emoji?->emoji);\n        }\n    }\n\n    public function test_from_array_change_file_icon(): void\n    {\n        $array = [\n            \"object\" => \"database\",\n            \"id\" => \"a7e80c0b-a766-43c3-a9e9-21ce94595e0e\",\n            \"created_time\" => \"2020-12-08T12:00:00.000000Z\",\n            \"last_edited_time\" => \"2020-12-08T12:00:00.000000Z\",\n            \"title\" => [[\n                \"plain_text\" => \"Page title\",\n                \"href\" => null,\n                \"annotations\" => [\n                    \"bold\"          => false,\n                    \"italic\"        => false,\n                    \"strikethrough\" => false,\n                    \"underline\"     => false,\n                    \"code\"          => false,\n                    \"color\"         => \"default\",\n                ],\n                \"type\" => \"text\",\n                \"text\" => [ \"content\" => \"Database title\" ],\n            ]],\n            \"description\" => [],\n            \"icon\" => [\n                \"type\" => \"external\",\n                \"external\" => [ \"url\" => \"https://my-site.com/image.png\" ],\n            ],\n            \"cover\" => null,\n            \"properties\" => [\n                \"Title\" => [\n                    \"id\"    => \"title\",\n                    \"name\"  => \"Title\",\n                    \"type\"  => \"title\",\n                    \"title\" => new \\stdClass(),\n                ],\n            ],\n            \"parent\" => [\n                \"type\" => \"page_id\",\n                \"page_id\" => \"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\",\n            ],\n            \"url\" => \"https://notion.so/a7e80c0ba76643c3a9e921ce94595e0e\",\n            \"is_inline\" => false,\n        ];\n        $database = Database::fromArray($array);\n\n        if ($database->icon?->isFile()) {\n            $this->assertEquals(\"https://my-site.com/image.png\", $database->icon->file?->url);\n        }\n    }\n\n    public function test_inline(): void\n    {\n        $parent = DatabaseParent::page(\"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\");\n        $database = Database::create($parent);\n        $this->assertFalse($database->isInline);\n\n        $database = $database->enableInline();\n        $this->assertTrue($database->isInline);\n\n        $database = $database->disableInline();\n        $this->assertFalse($database->isInline);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Properties/CheckboxTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Properties;\n\nuse Notion\\Databases\\Properties\\PropertyFactory;\nuse Notion\\Databases\\Properties\\Checkbox;\nuse Notion\\Databases\\Properties\\PropertyType;\nuse PHPUnit\\Framework\\TestCase;\n\nclass CheckboxTest extends TestCase\n{\n    public function test_create(): void\n    {\n        $checkbox = Checkbox::create(\"Dummy prop name\");\n\n        $this->assertEquals(\"Dummy prop name\", $checkbox->metadata()->name);\n        $this->assertEquals(PropertyType::Checkbox, $checkbox->metadata()->type);\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"checkbox\",\n            \"checkbox\" => new \\stdClass(),\n        ];\n        $checkbox = Checkbox::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $checkbox->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Properties/CreatedByTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Properties;\n\nuse Notion\\Databases\\Properties\\PropertyFactory;\nuse Notion\\Databases\\Properties\\CreatedBy;\nuse Notion\\Databases\\Properties\\PropertyType;\nuse PHPUnit\\Framework\\TestCase;\n\nclass CreatedByTest extends TestCase\n{\n    public function test_create(): void\n    {\n        $createdBy = CreatedBy::create(\"Dummy prop name\");\n\n        $this->assertEquals(\"Dummy prop name\", $createdBy->metadata()->name);\n        $this->assertEquals(PropertyType::CreatedBy, $createdBy->metadata()->type);\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"created_by\",\n            \"created_by\" => new \\stdClass(),\n            \"description\" => \"foo bar\",\n        ];\n        $createdBy = CreatedBy::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $createdBy->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Properties/CreatedTimeTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Properties;\n\nuse Notion\\Databases\\Properties\\PropertyFactory;\nuse Notion\\Databases\\Properties\\CreatedTime;\nuse Notion\\Databases\\Properties\\PropertyType;\nuse PHPUnit\\Framework\\TestCase;\n\nclass CreatedTimeTest extends TestCase\n{\n    public function test_create(): void\n    {\n        $createdTime = CreatedTime::create(\"Dummy prop name\");\n\n        $this->assertEquals(\"Dummy prop name\", $createdTime->metadata()->name);\n        $this->assertEquals(PropertyType::CreatedTime, $createdTime->metadata()->type);\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"created_time\",\n            \"created_time\" => new \\stdClass(),\n        ];\n        $createdTime = CreatedTime::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $createdTime->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Properties/DateTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Properties;\n\nuse Notion\\Databases\\Properties\\PropertyFactory;\nuse Notion\\Databases\\Properties\\Date;\nuse Notion\\Databases\\Properties\\PropertyType;\nuse PHPUnit\\Framework\\TestCase;\n\nclass DateTest extends TestCase\n{\n    public function test_create(): void\n    {\n        $date = Date::create(\"Dummy prop name\");\n\n        $this->assertEquals(\"Dummy prop name\", $date->metadata()->name);\n        $this->assertEquals(PropertyType::Date, $date->metadata()->type);\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"date\",\n            \"date\" => new \\stdClass(),\n        ];\n        $date = Date::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $date->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Properties/EmailTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Properties;\n\nuse Notion\\Databases\\Properties\\PropertyFactory;\nuse Notion\\Databases\\Properties\\Email;\nuse Notion\\Databases\\Properties\\PropertyType;\nuse PHPUnit\\Framework\\TestCase;\n\nclass EmailTest extends TestCase\n{\n    public function test_create(): void\n    {\n        $email = Email::create(\"Dummy prop name\");\n\n        $this->assertEquals(\"Dummy prop name\", $email->metadata()->name);\n        $this->assertEquals(PropertyType::Email, $email->metadata()->type);\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"email\",\n            \"email\" => new \\stdClass(),\n        ];\n        $email = Email::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $email->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Properties/FilesTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Properties;\n\nuse Notion\\Databases\\Properties\\PropertyFactory;\nuse Notion\\Databases\\Properties\\Files;\nuse Notion\\Databases\\Properties\\PropertyType;\nuse PHPUnit\\Framework\\TestCase;\n\nclass FilesTest extends TestCase\n{\n    public function test_create(): void\n    {\n        $files = Files::create(\"Dummy prop name\");\n\n        $this->assertEquals(\"Dummy prop name\", $files->metadata()->name);\n        $this->assertEquals(PropertyType::Files, $files->metadata()->type);\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"files\",\n            \"files\" => new \\stdClass(),\n        ];\n        $files = Files::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $files->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Properties/FormulaTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Properties;\n\nuse Notion\\Databases\\Properties\\PropertyFactory;\nuse Notion\\Databases\\Properties\\Formula;\nuse Notion\\Databases\\Properties\\PropertyType;\nuse PHPUnit\\Framework\\TestCase;\n\nclass FormulaTest extends TestCase\n{\n    public function test_create(): void\n    {\n        $expression = \"if(prop(\\\"In stock\\\"), 0, prop(\\\"Price\\\"))\";\n        $formula = Formula::create(\"Dummy prop name\", $expression);\n\n        $this->assertEquals(\"Dummy prop name\", $formula->metadata()->name);\n        $this->assertEquals(PropertyType::Formula, $formula->metadata()->type);\n        $this->assertEquals($expression, $formula->expression);\n    }\n\n    public function test_change_expression(): void\n    {\n        $expression = \"if(prop(\\\"In stock\\\"), 0, prop(\\\"Price\\\"))\";\n        $formula = Formula::create()->changeExpression($expression);\n\n        $this->assertEquals($expression, $formula->expression);\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"formula\",\n            \"formula\" => [\n                \"expression\" => \"if(prop(\\\"In stock\\\"), 0, prop(\\\"Price\\\"))\",\n            ],\n        ];\n        $formula = Formula::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $formula->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Properties/LastEditedByTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Properties;\n\nuse Notion\\Databases\\Properties\\PropertyFactory;\nuse Notion\\Databases\\Properties\\LastEditedBy;\nuse Notion\\Databases\\Properties\\PropertyType;\nuse PHPUnit\\Framework\\TestCase;\n\nclass LastEditedByTest extends TestCase\n{\n    public function test_create(): void\n    {\n        $lastEditedBy = LastEditedBy::create(\"Dummy prop name\");\n\n        $this->assertEquals(\"Dummy prop name\", $lastEditedBy->metadata()->name);\n        $this->assertEquals(PropertyType::LastEditedBy, $lastEditedBy->metadata()->type);\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"last_edited_by\",\n            \"last_edited_by\" => new \\stdClass(),\n        ];\n        $lastEditedBy = LastEditedBy::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $lastEditedBy->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Properties/LastEditedTimeTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Properties;\n\nuse Notion\\Databases\\Properties\\PropertyFactory;\nuse Notion\\Databases\\Properties\\LastEditedTime;\nuse Notion\\Databases\\Properties\\PropertyType;\nuse PHPUnit\\Framework\\TestCase;\n\nclass LastEditedTimeTest extends TestCase\n{\n    public function test_create(): void\n    {\n        $lastEditedTime = LastEditedTime::create(\"Dummy prop name\");\n\n        $this->assertEquals(\"Dummy prop name\", $lastEditedTime->metadata()->name);\n        $this->assertEquals(PropertyType::LastEditedTime, $lastEditedTime->metadata()->type);\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"last_edited_time\",\n            \"last_edited_time\" => new \\stdClass(),\n        ];\n        $lastEditedTime = LastEditedTime::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $lastEditedTime->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Properties/MultiSelectTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Properties;\n\nuse Notion\\Databases\\Properties\\PropertyFactory;\nuse Notion\\Databases\\Properties\\MultiSelect;\nuse Notion\\Databases\\Properties\\PropertyType;\nuse Notion\\Databases\\Properties\\SelectOption;\nuse PHPUnit\\Framework\\TestCase;\n\nclass MultiSelectTest extends TestCase\n{\n    public function test_create(): void\n    {\n        $select = MultiSelect::create(\"Dummy prop name\");\n\n        $this->assertEquals(\"Dummy prop name\", $select->metadata()->name);\n        $this->assertEquals(PropertyType::MultiSelect, $select->metadata()->type);\n        $this->assertEmpty($select->options);\n    }\n\n    public function test_replace_options(): void\n    {\n        $select = MultiSelect::create()->changeOptions(\n            SelectOption::fromName(\"Option A\"),\n            SelectOption::fromName(\"Option B\"),\n        );\n\n        $this->assertCount(2, $select->options);\n    }\n\n    public function test_add_option(): void\n    {\n        $option = SelectOption::fromName(\"Option A\");\n        $select = MultiSelect::create()->addOption($option);\n\n        $this->assertEquals([ $option ], $select->options);\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\"    => \"abc\",\n            \"name\"  => \"MultiSelect\",\n            \"type\"  => \"multi_select\",\n            \"multi_select\" => [\n                \"options\" => [\n                    [ \"id\" => \"aaa\", \"name\" => \"Option A\", \"color\" => \"default\" ],\n                    [ \"id\" => \"bbb\", \"name\" => \"Option B\", \"color\" => \"default\" ],\n                ],\n            ],\n        ];\n        $select = MultiSelect::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $select->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Properties/NumberTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Properties;\n\nuse Notion\\Databases\\Properties\\PropertyFactory;\nuse Notion\\Databases\\Properties\\Number;\nuse Notion\\Databases\\Properties\\NumberFormat;\nuse Notion\\Databases\\Properties\\PropertyType;\nuse PHPUnit\\Framework\\TestCase;\n\nclass NumberTest extends TestCase\n{\n    public function test_create(): void\n    {\n        $price = Number::create(\"Price\", NumberFormat::Dollar);\n\n        $this->assertEquals(\"Price\", $price->metadata()->name);\n        $this->assertEquals(NumberFormat::Dollar, $price->format);\n        $this->assertEquals(PropertyType::Number, $price->metadata()->type);\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\"    => \"abc\",\n            \"name\"  => \"Price\",\n            \"type\"  => \"number\",\n            \"number\" => [\n                \"format\" => \"dollar\",\n            ],\n        ];\n        $number = Number::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $number->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n    }\n\n    public function test_change_format(): void\n    {\n        $price = Number::create(\"Price\", NumberFormat::Dollar)->changeFormat(NumberFormat::Euro);\n\n        $this->assertEquals(NumberFormat::Euro, $price->format);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Properties/PeopleTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Properties;\n\nuse Notion\\Databases\\Properties\\PropertyFactory;\nuse Notion\\Databases\\Properties\\People;\nuse Notion\\Databases\\Properties\\PropertyType;\nuse PHPUnit\\Framework\\TestCase;\n\nclass PeopleTest extends TestCase\n{\n    public function test_create(): void\n    {\n        $people = People::create(\"Dummy prop name\");\n\n        $this->assertEquals(\"Dummy prop name\", $people->metadata()->name);\n        $this->assertEquals(PropertyType::People, $people->metadata()->type);\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"people\",\n            \"people\" => new \\stdClass(),\n        ];\n        $people = People::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $people->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Properties/PhoneNumberTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Properties;\n\nuse Notion\\Databases\\Properties\\PropertyFactory;\nuse Notion\\Databases\\Properties\\PhoneNumber;\nuse Notion\\Databases\\Properties\\PropertyType;\nuse PHPUnit\\Framework\\TestCase;\n\nclass PhoneNumberTest extends TestCase\n{\n    public function test_create(): void\n    {\n        $phoneNumber = PhoneNumber::create(\"Dummy prop name\");\n\n        $this->assertEquals(\"Dummy prop name\", $phoneNumber->metadata()->name);\n        $this->assertEquals(PropertyType::PhoneNumber, $phoneNumber->metadata()->type);\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"phone_number\",\n            \"phone_number\" => new \\stdClass(),\n        ];\n        $phoneNumber = PhoneNumber::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $phoneNumber->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Properties/PropertyCollectionTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Properties;\n\nuse DateTimeImmutable;\nuse Exception;\nuse Notion\\Databases\\Properties\\PropertyCollection;\nuse Notion\\Databases\\Properties;\nuse Notion\\Databases\\Properties\\PropertyType;\nuse PHPUnit\\Framework\\TestCase;\n\nclass PropertyCollectionTest extends TestCase\n{\n    public function test_create(): void\n    {\n        $collection = PropertyCollection::create(\n            Properties\\Title::create(\"Product\"),\n            Properties\\Number::create(\"Price\"),\n        );\n\n        $this->assertCount(2, $collection->getAll());\n    }\n\n    public function test_add(): void\n    {\n        $c = PropertyCollection::create(\n            Properties\\Title::create(\"Product\"),\n            Properties\\Number::create(\"Price\"),\n        )->add(Properties\\Number::create(\"Quantity\"));\n\n        $this->assertCount(3, $c->getAll());\n    }\n\n    public function test_change(): void\n    {\n        $c = PropertyCollection::create(\n            Properties\\Title::create(\"Movie\"),\n            Properties\\Number::create(\"Release Date\"),\n        )->change(Properties\\Date::create(\"Release Date\"));\n\n        $this->assertTrue($c->get(\"Release Date\")->metadata()->type === PropertyType::Date);\n    }\n\n    public function test_remove(): void\n    {\n        $c = PropertyCollection::create(\n            Properties\\Title::create(\"Product\"),\n            Properties\\Number::create(\"Price\"),\n        )->remove(\"Product\");\n\n        $this->assertCount(1, $c->getAll());\n    }\n\n    public function test_get(): void\n    {\n        $product = Properties\\Title::create(\"Product\");\n        $price = Properties\\Number::create(\"Price\");\n        $c = PropertyCollection::create($product, $price);\n\n        $this->assertSame($product, $c->get(\"Product\"));\n    }\n\n    public function test_get_not_found(): void\n    {\n        $c = PropertyCollection::create();\n\n        $this->expectException(Exception::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $c->get(\"Product\");\n    }\n\n    public function test_get_by_id(): void\n    {\n        $prop = Properties\\Number::fromArray([\n            \"id\"    => \"abc\",\n            \"name\"  => \"Price\",\n            \"type\"  => \"number\",\n            \"number\" => [\n                \"format\" => \"dollar\",\n            ],\n        ]);\n\n        $c = PropertyCollection::create($prop);\n\n        $this->assertSame($prop, $c->getById(\"abc\"));\n    }\n\n    public function test_get_by_id_not_found(): void\n    {\n        $c = PropertyCollection::create();\n\n        $this->expectException(Exception::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $c->getById(\"abc\");\n    }\n\n    public function test_get_checkbox(): void\n    {\n        $p = Properties\\Checkbox::create(\"Name\");\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getCheckbox(\"Name\"));\n    }\n\n    public function test_get_created_by(): void\n    {\n        $p = Properties\\CreatedBy::create(\"Name\");\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getCreatedBy(\"Name\"));\n    }\n\n    public function test_get_created_time(): void\n    {\n        $p = Properties\\CreatedTime::create(\"Name\");\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getCreatedTime(\"Name\"));\n    }\n\n    public function test_get_date(): void\n    {\n        $p = Properties\\Date::create(\"Name\");\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getDate(\"Name\"));\n    }\n\n    public function test_get_email(): void\n    {\n        $p = Properties\\Email::create(\"Name\");\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getEmail(\"Name\"));\n    }\n\n    public function test_get_files(): void\n    {\n        $p = Properties\\Files::create(\"Name\");\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getFiles(\"Name\"));\n    }\n\n    public function test_get_formula(): void\n    {\n        $p = Properties\\Formula::create(\"Name\");\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getFormula(\"Name\"));\n    }\n\n    public function test_get_last_edited_by(): void\n    {\n        $p = Properties\\LastEditedBy::create(\"Name\");\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getLastEditedBy(\"Name\"));\n    }\n\n    public function test_get_last_edited_time(): void\n    {\n        $p = Properties\\LastEditedTime::create(\"Name\");\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getLastEditedTime(\"Name\"));\n    }\n\n    public function test_get_multi_select(): void\n    {\n        $p = Properties\\MultiSelect::create(\"Name\");\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getMultiSelect(\"Name\"));\n    }\n\n    public function test_get_number(): void\n    {\n        $p = Properties\\Number::create(\"Name\");\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getNumber(\"Name\"));\n    }\n\n    public function test_get_people(): void\n    {\n        $p = Properties\\People::create(\"Name\");\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getPeople(\"Name\"));\n    }\n\n    public function test_get_phone_number(): void\n    {\n        $p = Properties\\PhoneNumber::create(\"Name\");\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getPhoneNumber(\"Name\"));\n    }\n\n    public function test_get_relation(): void\n    {\n        $p = Properties\\Relation::createUnidirectional(\"Name\", \"abc123\");\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getRelation(\"Name\"));\n    }\n\n    public function test_get_rich_text(): void\n    {\n        $p = Properties\\RichTextProperty::create(\"Name\");\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getRichText(\"Name\"));\n    }\n\n    public function test_get_select(): void\n    {\n        $p = Properties\\Select::create(\"Name\");\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getSelect(\"Name\"));\n    }\n\n    public function test_get_status(): void\n    {\n        $p = Properties\\Status::fromArray([\n            \"id\"    => \"abc\",\n            \"name\"  => \"Name\",\n            \"type\"  => \"status\",\n            \"status\" => [\n                \"groups\" => [\n                    [ \"id\" => \"111\", \"option_ids\" => [\"aaa\"], \"color\" => \"green\", \"name\" => \"To-do\" ],\n                    [ \"id\" => \"222\", \"option_ids\" => [\"bbb\"], \"color\" => \"yellow\", \"name\" => \"In Progress\" ],\n                    [ \"id\" => \"333\", \"option_ids\" => [\"ccc\"], \"color\" => \"red\", \"name\" => \"Complete\" ],\n                ],\n                \"options\" => [\n                    [ \"id\" => \"aaa\", \"name\" => \"Option A\", \"color\" => \"default\" ],\n                    [ \"id\" => \"bbb\", \"name\" => \"Option B\", \"color\" => \"default\" ],\n                    [ \"id\" => \"ccc\", \"name\" => \"Option C\", \"color\" => \"default\" ],\n                ],\n            ],\n        ]);\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getStatus(\"Name\"));\n    }\n\n    public function test_get_title(): void\n    {\n        $p = Properties\\Title::create(\"Name\");\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getTitle(\"Name\"));\n    }\n\n    public function test_get_url(): void\n    {\n        $p = Properties\\Url::create(\"Name\");\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getUrl(\"Name\"));\n    }\n\n    /////// GET BY ID\n\n    public function test_get_checkbox_by_id(): void\n    {\n        $p = Properties\\Checkbox::fromArray([\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"checkbox\",\n            \"checkbox\" => new \\stdClass(),\n        ]);\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getCheckboxById(\"abc\"));\n    }\n\n    public function test_get_created_by_by_id(): void\n    {\n        $p = Properties\\CreatedBy::fromArray([\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"created_by\",\n            \"created_by\" => new \\stdClass(),\n        ]);\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getCreatedByById(\"abc\"));\n    }\n\n    public function test_get_created_time_by_id(): void\n    {\n        $p = Properties\\CreatedTime::fromArray([\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"created_time\",\n            \"created_time\" => new \\stdClass(),\n        ]);\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getCreatedTimeById(\"abc\"));\n    }\n\n    public function test_get_date_by_id(): void\n    {\n        $p = Properties\\Date::fromArray([\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"date\",\n            \"date\" => new \\stdClass(),\n        ]);\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getDateById(\"abc\"));\n    }\n\n    public function test_get_email_by_id(): void\n    {\n        $p = Properties\\Email::fromArray([\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"email\",\n            \"email\" => new \\stdClass(),\n        ]);\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getEmailById(\"abc\"));\n    }\n\n    public function test_get_files_by_id(): void\n    {\n        $p = Properties\\Files::fromArray([\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"files\",\n            \"files\" => new \\stdClass(),\n        ]);\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getFilesById(\"abc\"));\n    }\n\n    public function test_get_formula_by_id(): void\n    {\n        $p = Properties\\Formula::fromArray([\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"formula\",\n            \"formula\" => [\n                \"expression\" => \"if(prop(\\\"In stock\\\"), 0, prop(\\\"Price\\\"))\",\n            ],\n        ]);\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getFormulaById(\"abc\"));\n    }\n\n    public function test_get_last_edited_by_by_id(): void\n    {\n        $p = Properties\\LastEditedBy::fromArray([\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"last_edited_by\",\n            \"last_edited_by\" => new \\stdClass(),\n        ]);\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getLastEditedByById(\"abc\"));\n    }\n\n    public function test_get_last_edited_time_by_id(): void\n    {\n        $p = Properties\\LastEditedTime::fromArray([\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"last_edited_time\",\n            \"last_edited_time\" => new \\stdClass(),\n        ]);\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getLastEditedTimeById(\"abc\"));\n    }\n\n    public function test_get_multi_select_by_id(): void\n    {\n        $p = Properties\\MultiSelect::fromArray([\n            \"id\"    => \"abc\",\n            \"name\"  => \"MultiSelect\",\n            \"type\"  => \"multi_select\",\n            \"multi_select\" => [\n                \"options\" => [\n                    [ \"id\" => \"aaa\", \"name\" => \"Option A\", \"color\" => \"default\" ],\n                    [ \"id\" => \"bbb\", \"name\" => \"Option B\", \"color\" => \"default\" ],\n                ],\n            ],\n        ]);\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getMultiSelectById(\"abc\"));\n    }\n\n    public function test_get_number_by_id(): void\n    {\n        $p = Properties\\Number::fromArray([\n            \"id\"    => \"abc\",\n            \"name\"  => \"Price\",\n            \"type\"  => \"number\",\n            \"number\" => [\n                \"format\" => \"dollar\",\n            ],\n        ]);\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getNumberById(\"abc\"));\n    }\n\n    public function test_get_people_by_id(): void\n    {\n        $p = Properties\\People::fromArray([\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"people\",\n            \"people\" => new \\stdClass(),\n        ]);\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getPeopleById(\"abc\"));\n    }\n\n    public function test_get_phone_number_by_id(): void\n    {\n        $p = Properties\\PhoneNumber::fromArray([\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"phone_number\",\n            \"phone_number\" => new \\stdClass(),\n        ]);\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getPhoneNumberById(\"abc\"));\n    }\n\n    public function test_get_relation_by_id(): void\n    {\n        $p = Properties\\Relation::fromArray([\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"relation\",\n            \"relation\" => [\n                \"database_id\" => \"84660ad0-9cb9-45d0-aae0-91e2c2526e12\",\n                \"type\" => \"single_property\",\n                \"single_property\" => new \\stdClass(),\n            ],\n        ]);\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getRelationById(\"abc\"));\n    }\n\n    public function test_get_rich_text_by_id(): void\n    {\n        $p = Properties\\RichTextProperty::fromArray([\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"rich_text\",\n            \"rich_text\" => new \\stdClass(),\n        ]);\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getRichTextById(\"abc\"));\n    }\n\n    public function test_get_select_by_id(): void\n    {\n        $p = Properties\\Select::fromArray([\n            \"id\"    => \"abc\",\n            \"name\"  => \"Select\",\n            \"type\"  => \"select\",\n            \"select\" => [\n                \"options\" => [\n                    [ \"id\" => \"aaa\", \"name\" => \"Option A\", \"color\" => \"default\" ],\n                    [ \"id\" => \"bbb\", \"name\" => \"Option B\", \"color\" => \"default\" ],\n                ],\n            ],\n        ]);\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getSelectById(\"abc\"));\n    }\n\n    public function test_get_status_by_id(): void\n    {\n        $p = Properties\\Status::fromArray([\n            \"id\"    => \"abc\",\n            \"name\"  => \"Status\",\n            \"type\"  => \"status\",\n            \"status\" => [\n                \"groups\" => [\n                    [ \"id\" => \"111\", \"option_ids\" => [\"aaa\"], \"color\" => \"green\", \"name\" => \"To-do\" ],\n                    [ \"id\" => \"222\", \"option_ids\" => [\"bbb\"], \"color\" => \"yellow\", \"name\" => \"In Progress\" ],\n                    [ \"id\" => \"333\", \"option_ids\" => [\"ccc\"], \"color\" => \"red\", \"name\" => \"Complete\" ],\n                ],\n                \"options\" => [\n                    [ \"id\" => \"aaa\", \"name\" => \"Option A\", \"color\" => \"default\" ],\n                    [ \"id\" => \"bbb\", \"name\" => \"Option B\", \"color\" => \"default\" ],\n                    [ \"id\" => \"ccc\", \"name\" => \"Option C\", \"color\" => \"default\" ],\n                ],\n            ],\n        ]);\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getStatusById(\"abc\"));\n    }\n\n    public function test_get_title_by_id(): void\n    {\n        $p = Properties\\Title::fromArray([\n            \"id\"    => \"title\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"title\",\n            \"title\" => new \\stdClass(),\n        ]);\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getTitleById(\"title\"));\n    }\n\n    public function test_get_url_by_id(): void\n    {\n        $p = Properties\\Url::fromArray([\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"url\",\n            \"url\" => new \\stdClass(),\n        ]);\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getUrlById(\"abc\"));\n    }\n\n    public function test_get_unique_id(): void\n    {\n        $p = Properties\\UniqueId::fromArray([\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"unique_id\",\n            \"unique_id\" => new \\stdClass(),\n        ]);\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getUniqueId(\"dummy\"));\n    }\n\n    public function test_get_unique_id_by_id(): void\n    {\n        $p = Properties\\UniqueId::fromArray([\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"unique_id\",\n            \"unique_id\" => new \\stdClass(),\n        ]);\n\n        $c = PropertyCollection::create($p);\n\n        $this->assertSame($p, $c->getUniqueIdById(\"abc\"));\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Properties/PropertyMetadataTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Properties;\n\nuse Notion\\Databases\\Properties\\PropertyMetadata;\nuse Notion\\Databases\\Properties\\PropertyType;\nuse PHPUnit\\Framework\\TestCase;\n\nclass PropertyMetadataTest extends TestCase\n{\n    public function test_create(): void\n    {\n        $metadata = PropertyMetadata::create(\"abc\", \"Dummy prop name\", PropertyType::CreatedBy, \"foo bar\");\n\n        $this->assertEquals(\"abc\", $metadata->id);\n        $this->assertEquals(\"Dummy prop name\", $metadata->name);\n        $this->assertEquals(PropertyType::CreatedBy, $metadata->type);\n        $this->assertEquals(\"foo bar\", $metadata->description);\n    }\n\n    public function test_from_array(): void\n    {\n        $array = [\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"created_by\",\n            \"created_by\" => new \\stdClass(),\n            \"description\" => \"foo bar\",\n        ];\n        $metadata = PropertyMetadata::fromArray($array);\n\n        $this->assertEquals($array[\"id\"], $metadata->id);\n        $this->assertEquals($array[\"name\"], $metadata->name);\n        $this->assertEquals(PropertyType::CreatedBy, $metadata->type);\n        $this->assertEquals($array[\"description\"], $metadata->description);\n\n        $toArray = $metadata->toArray();\n\n        $this->assertEqualsCanonicalizing(['id', 'name', 'type', 'description'], array_keys($toArray));\n    }\n\n    public function test_from_array_without_description(): void\n    {\n        $array = [\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"created_by\",\n            \"created_by\" => new \\stdClass(),\n        ];\n\n        $metadata = PropertyMetadata::fromArray($array);\n\n        $this->assertEquals($array[\"id\"], $metadata->id);\n        $this->assertEquals($array[\"name\"], $metadata->name);\n        $this->assertEquals(PropertyType::CreatedBy, $metadata->type);\n        $this->assertNull($metadata->description);\n\n        $toArray = $metadata->toArray();\n\n        // When there is no description, its key won't show up\n        $this->assertEqualsCanonicalizing(['id', 'name', 'type'], array_keys($toArray));\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Properties/RelationTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Properties;\n\nuse Notion\\Databases\\Properties\\PropertyFactory;\nuse Notion\\Databases\\Properties\\Relation;\nuse PHPUnit\\Framework\\TestCase;\n\nclass RelationTest extends TestCase\n{\n    public function test_create_unidirectional(): void\n    {\n        $databaseId = \"04eecdf8-f2d9-43a0-abbc-476182192c8f\";\n        $relation = Relation::createUnidirectional(\"My relation\", $databaseId);\n\n        $this->assertSame(\"My relation\", $relation->metadata()->name);\n        $this->assertSame($databaseId, $relation->databaseId);\n        $this->assertTrue($relation->isUniderectional());\n    }\n\n    public function test_create_bidirectional(): void\n    {\n        $databaseId = \"04eecdf8-f2d9-43a0-abbc-476182192c8f\";\n        $syncedPropertyName = \"Prop name\";\n        $syncedPropertyId = \"12ac\";\n\n        $relation = Relation::createBidirectional(\n            \"My relation\",\n            $databaseId,\n            $syncedPropertyName,\n            $syncedPropertyId,\n        );\n\n        $this->assertSame(\"My relation\", $relation->metadata()->name);\n        $this->assertSame($databaseId, $relation->databaseId);\n        $this->assertSame($syncedPropertyName, $relation->syncedPropertyName);\n        $this->assertSame($syncedPropertyId, $relation->syncedPropertyId);\n        $this->assertTrue($relation->isBiderectional());\n    }\n\n    public function test_change_to_unidirectional(): void\n    {\n        $databaseId = \"04eecdf8-f2d9-43a0-abbc-476182192c8f\";\n        $syncedPropertyName = \"Prop name\";\n        $syncedPropertyId = \"12ac\";\n\n        $relation = Relation::createBidirectional(\n            \"My relation\",\n            $databaseId,\n            $syncedPropertyName,\n            $syncedPropertyId,\n        );\n\n        $relation = $relation->changeToUnidirectional();\n\n        $this->assertTrue($relation->isUniderectional());\n        $this->assertNull($relation->syncedPropertyId);\n        $this->assertNull($relation->syncedPropertyName);\n    }\n\n    public function test_change_to_bidirectional(): void\n    {\n        $databaseId = \"04eecdf8-f2d9-43a0-abbc-476182192c8f\";\n        $relation = Relation::createUnidirectional(\"My relation\", $databaseId);\n\n        $syncedPropertyName = \"Prop name\";\n        $syncedPropertyId = \"12ac\";\n\n\n        $relation = $relation->changeToBidirectional($syncedPropertyName, $syncedPropertyId);\n\n        $this->assertTrue($relation->isBiderectional());\n        $this->assertSame($syncedPropertyName, $relation->syncedPropertyName);\n        $this->assertSame($syncedPropertyId, $relation->syncedPropertyId);\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"relation\",\n            \"relation\" => [\n                \"database_id\" => \"84660ad0-9cb9-45d0-aae0-91e2c2526e12\",\n                \"type\" => \"single_property\",\n                \"single_property\" => new \\stdClass(),\n            ],\n        ];\n        $relation = Relation::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $relation->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Properties/RichTextTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Properties;\n\nuse Notion\\Common\\RichText;\nuse Notion\\Databases\\Properties\\PropertyFactory;\nuse Notion\\Databases\\Properties\\PropertyType;\nuse Notion\\Databases\\Properties\\RichTextProperty;\nuse PHPUnit\\Framework\\TestCase;\n\nclass RichTextTest extends TestCase\n{\n    public function test_create(): void\n    {\n        $text = RichTextProperty::create(\"Dummy prop name\");\n\n        $this->assertEquals(\"Dummy prop name\", $text->metadata()->name);\n        $this->assertEquals(PropertyType::RichText, $text->metadata()->type);\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"rich_text\",\n            \"rich_text\" => new \\stdClass(),\n        ];\n        $text = RichTextProperty::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $text->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n    }\n\n    public function test_new_line(): void\n    {\n        $text = RichText::newLine();\n\n        $this->assertSame(\"\\n\", $text->toString());\n    }\n\n    public function test_mutiple_to_string(): void\n    {\n        $text = [\n            RichText::fromString(\"Multiple \")->bold(),\n            RichText::fromString(\"text \")->italic(),\n            RichText::fromString(\"example\")->underline(),\n        ];\n\n        $this->assertSame(\"Multiple text example\", RichText::multipleToString(...$text));\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Properties/SelectOptionTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Properties;\n\nuse Notion\\Common\\Color;\nuse Notion\\Databases\\Properties\\SelectOption;\nuse PHPUnit\\Framework\\TestCase;\n\nclass SelectOptionTest extends TestCase\n{\n    public function test_change_color(): void\n    {\n        $option = SelectOption::fromName(\"Comedy\")\n            ->changeColor(Color::Red);\n\n        $this->assertSame(Color::Red, $option->color);\n    }\n\n    public function test_change_name(): void\n    {\n        $option = SelectOption::fromId(\"abc123\")\n            ->changeName(\"Comedy\");\n\n        $this->assertSame(\"Comedy\", $option->name);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Properties/SelectTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Properties;\n\nuse Notion\\Databases\\Properties\\PropertyFactory;\nuse Notion\\Databases\\Properties\\PropertyType;\nuse Notion\\Databases\\Properties\\Select;\nuse Notion\\Databases\\Properties\\SelectOption;\nuse PHPUnit\\Framework\\TestCase;\n\nclass SelectTest extends TestCase\n{\n    public function test_create(): void\n    {\n        $select = Select::create(\"Dummy prop name\");\n\n        $this->assertEquals(\"Dummy prop name\", $select->metadata()->name);\n        $this->assertEquals(PropertyType::Select, $select->metadata()->type);\n        $this->assertEmpty($select->options);\n    }\n\n    public function test_replace_options(): void\n    {\n        $select = Select::create()->changeOptions(\n            SelectOption::fromName(\"Option A\"),\n            SelectOption::fromName(\"Option B\"),\n        );\n\n        $this->assertCount(2, $select->options);\n    }\n\n    public function test_add_option(): void\n    {\n        $option = SelectOption::fromName(\"Option A\");\n        $select = Select::create()->addOption($option);\n\n        $this->assertEquals([ $option ], $select->options);\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\"    => \"abc\",\n            \"name\"  => \"Select\",\n            \"type\"  => \"select\",\n            \"select\" => [\n                \"options\" => [\n                    [ \"id\" => \"aaa\", \"name\" => \"Option A\", \"color\" => \"default\" ],\n                    [ \"id\" => \"bbb\", \"name\" => \"Option B\", \"color\" => \"default\" ],\n                ],\n            ],\n        ];\n        $select = Select::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $select->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Properties/StatusTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Properties;\n\nuse Notion\\Databases\\Properties\\PropertyFactory;\nuse Notion\\Databases\\Properties\\PropertyType;\nuse Notion\\Databases\\Properties\\Status;\nuse PHPUnit\\Framework\\TestCase;\n\nclass StatusTest extends TestCase\n{\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\"    => \"abc\",\n            \"name\"  => \"Status\",\n            \"type\"  => \"status\",\n            \"status\" => [\n                \"groups\" => [\n                    [ \"id\" => \"111\", \"option_ids\" => [\"aaa\"], \"color\" => \"green\", \"name\" => \"To-do\" ],\n                    [ \"id\" => \"222\", \"option_ids\" => [\"bbb\"], \"color\" => \"yellow\", \"name\" => \"In Progress\" ],\n                    [ \"id\" => \"333\", \"option_ids\" => [\"ccc\"], \"color\" => \"red\", \"name\" => \"Complete\" ],\n                ],\n                \"options\" => [\n                    [ \"id\" => \"aaa\", \"name\" => \"Option A\", \"color\" => \"default\" ],\n                    [ \"id\" => \"bbb\", \"name\" => \"Option B\", \"color\" => \"default\" ],\n                    [ \"id\" => \"ccc\", \"name\" => \"Option C\", \"color\" => \"default\" ],\n                ],\n            ],\n        ];\n        $status = Status::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $status->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n        $this->assertSame(PropertyType::Status, $status->metadata()->type);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Properties/TitleTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Properties;\n\nuse Notion\\Databases\\Properties\\PropertyFactory;\nuse Notion\\Databases\\Properties\\PropertyType;\nuse Notion\\Databases\\Properties\\Title;\nuse PHPUnit\\Framework\\TestCase;\n\nclass TitleTest extends TestCase\n{\n    public function test_create(): void\n    {\n        $title = Title::create(\"Dummy prop name\");\n\n        $this->assertEquals(\"Dummy prop name\", $title->metadata()->name);\n        $this->assertEmpty($title->metadata()->id);\n        $this->assertEquals(PropertyType::Title, $title->metadata()->type);\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\"    => \"title\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"title\",\n            \"title\" => new \\stdClass(),\n        ];\n        $title = Title::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $title->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Properties/UniqueIdTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Properties;\n\nuse Notion\\Databases\\Properties\\PropertyFactory;\nuse Notion\\Databases\\Properties\\PropertyType;\nuse Notion\\Databases\\Properties\\UniqueId;\nuse PHPUnit\\Framework\\TestCase;\n\nclass UniqueIdTest extends TestCase\n{\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"unique_id\",\n            \"unique_id\" => new \\stdClass(),\n        ];\n        $prop = UniqueId::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $prop->toArray());\n        $this->assertSame(PropertyType::UniqueId, $prop->metadata()->type);\n        $this->assertEquals($array, $fromFactory->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Properties/UnknownTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Properties;\n\nuse Notion\\Databases\\Properties\\PropertyFactory;\nuse Notion\\Databases\\Properties\\PropertyType;\nuse Notion\\Databases\\Properties\\Unknown;\nuse PHPUnit\\Framework\\TestCase;\n\nclass UnknownTest extends TestCase\n{\n    public function test_serialization(): void\n    {\n        $array = [\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"blabla\",\n            \"blabla\" => new \\stdClass(),\n        ];\n        $property = Unknown::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $property->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n        $this->assertSame(PropertyType::Unknown, $property->metadata()->type);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Properties/UrlTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Properties;\n\nuse Notion\\Databases\\Properties\\PropertyFactory;\nuse Notion\\Databases\\Properties\\PropertyType;\nuse Notion\\Databases\\Properties\\Url;\nuse PHPUnit\\Framework\\TestCase;\n\nclass UrlTest extends TestCase\n{\n    public function test_create(): void\n    {\n        $url = Url::create(\"Dummy prop name\");\n\n        $this->assertEquals(\"Dummy prop name\", $url->metadata()->name);\n        $this->assertEquals(PropertyType::Url, $url->metadata()->type);\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\"    => \"abc\",\n            \"name\"  => \"dummy\",\n            \"type\"  => \"url\",\n            \"url\" => new \\stdClass(),\n        ];\n        $url = Url::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $url->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Query/CheckboxFilterTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Query;\n\nuse Notion\\Databases\\Query\\CheckboxFilter;\nuse Notion\\Databases\\Query\\Operator;\nuse PHPUnit\\Framework\\TestCase;\n\nclass CheckboxFilterTest extends TestCase\n{\n    public function test_empty_filter(): void\n    {\n        $filter = CheckboxFilter::property(\"Done\");\n\n        $this->assertSame(\"property\", $filter->propertyType());\n        $this->assertSame(\"Done\", $filter->propertyName());\n        $this->assertSame(Operator::Equals, $filter->operator());\n        $this->assertTrue($filter->value());\n    }\n\n    public function test_equals(): void\n    {\n        $filter = CheckboxFilter::property(\"Done\")->equals(true);\n\n        $expected = [\n            \"property\" => \"Done\",\n            \"checkbox\" => [ \"equals\" => true ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_does_not_equal(): void\n    {\n        $filter = CheckboxFilter::property(\"Done\")->doesNotEqual(true);\n\n        $expected = [\n            \"property\" => \"Done\",\n            \"checkbox\" => [ \"does_not_equal\" => true ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Query/CompoundFilterTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Query;\n\nuse Notion\\Databases\\Query\\CompoundFilter;\nuse Notion\\Databases\\Query\\DateFilter;\nuse Notion\\Databases\\Query\\SelectFilter;\nuse Notion\\Databases\\Query\\TextFilter;\nuse PHPUnit\\Framework\\TestCase;\nuse stdClass;\n\nclass CompoundFilterTest extends TestCase\n{\n    public function test_and(): void\n    {\n        $filter = CompoundFilter::and(\n            TextFilter::property(\"Title\")->isNotEmpty(),\n            DateFilter::createdTime()->pastWeek()\n        );\n\n        $expected = [\n            \"and\" => [\n                [\n                    \"property\" => \"Title\",\n                    \"rich_text\" => [ \"is_not_empty\" => true ],\n                ],\n                [\n                    \"timestamp\" => \"created_time\",\n                    \"created_time\" => [ \"past_week\" => new stdClass() ],\n                ],\n            ],\n        ];\n        $this->assertEquals($expected, $filter->toArray());\n    }\n\n    public function test_or(): void\n    {\n        $filter = CompoundFilter::or(\n            TextFilter::property(\"Title\")->isNotEmpty(),\n            DateFilter::createdTime()->pastWeek()\n        );\n\n        $expected = [\n            \"or\" => [\n                [\n                    \"property\" => \"Title\",\n                    \"rich_text\" => [ \"is_not_empty\" => true ],\n                ],\n                [\n                    \"timestamp\" => \"created_time\",\n                    \"created_time\" => [ \"past_week\" => new stdClass() ],\n                ],\n            ],\n        ];\n        $this->assertEquals($expected, $filter->toArray());\n    }\n\n    public function test_nested(): void\n    {\n        // Drama movies from the 70s or 90s\n        $filter = CompoundFilter::or(\n            CompoundFilter::and(\n                DateFilter::property(\"Release date\")->onOrAfter(\"1990-01-01\"),\n                DateFilter::property(\"Release date\")->onOrBefore(\"1999-12-31\"),\n            ),\n            CompoundFilter::and(\n                DateFilter::property(\"Release date\")->onOrAfter(\"1970-01-01\"),\n                DateFilter::property(\"Release date\")->onOrBefore(\"1979-12-31\"),\n            ),\n        );\n\n        $expected = [\n            \"or\" => [\n                [\n                    \"and\" => [\n                        [\n                            \"property\" => \"Release date\",\n                            \"date\" => [ \"on_or_after\" => \"1990-01-01\" ],\n                        ],\n                        [\n                            \"property\" => \"Release date\",\n                            \"date\" => [ \"on_or_before\" => \"1999-12-31\" ],\n                        ],\n                    ],\n                ],\n                [\n                    \"and\" => [\n                        [\n                            \"property\" => \"Release date\",\n                            \"date\" => [ \"on_or_after\" => \"1970-01-01\" ],\n                        ],\n                        [\n                            \"property\" => \"Release date\",\n                            \"date\" => [ \"on_or_before\" => \"1979-12-31\" ],\n                        ],\n                    ],\n                ],\n            ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Query/DateFilterTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Query;\n\nuse Notion\\Databases\\Query\\DateFilter;\nuse Notion\\Databases\\Query\\Operator;\nuse PHPUnit\\Framework\\TestCase;\nuse stdClass;\n\nclass DateFilterTest extends TestCase\n{\n    public function test_property(): void\n    {\n        $filter = DateFilter::property(\"Release date\");\n\n        $this->assertSame(\"property\", $filter->propertyType());\n        $this->assertSame(\"Release date\", $filter->propertyName());\n        $this->assertSame(Operator::IsNotEmpty, $filter->operator());\n        $this->assertTrue($filter->value());\n    }\n\n    public function test_created_time(): void\n    {\n        $filter = DateFilter::createdTime();\n\n        $this->assertSame(\"timestamp\", $filter->propertyType());\n        $this->assertSame(\"created_time\", $filter->propertyName());\n        $this->assertSame(Operator::IsNotEmpty, $filter->operator());\n        $this->assertTrue($filter->value());\n    }\n\n    public function test_last_edited_Time(): void\n    {\n        $filter = DateFilter::lastEditedTime();\n\n        $this->assertSame(\"timestamp\", $filter->propertyType());\n        $this->assertSame(\"last_edited_time\", $filter->propertyName());\n        $this->assertSame(Operator::IsNotEmpty, $filter->operator());\n        $this->assertTrue($filter->value());\n    }\n\n    public function test_equals(): void\n    {\n        $filter = DateFilter::createdTime()\n            ->equals(\"2022-02-13\");\n\n        $expected = [\n            \"timestamp\" => \"created_time\",\n            \"created_time\" => [ \"equals\" => \"2022-02-13\" ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_before(): void\n    {\n        $filter = DateFilter::createdTime()\n            ->before(\"2021-05-10T12:00:00\");\n\n        $expected = [\n            \"timestamp\" => \"created_time\",\n            \"created_time\" => [ \"before\" => \"2021-05-10T12:00:00\" ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_after(): void\n    {\n        $filter = DateFilter::createdTime()\n            ->after(\"2021-05-10T12:00:00\");\n\n        $expected = [\n            \"timestamp\" => \"created_time\",\n            \"created_time\" => [ \"after\" => \"2021-05-10T12:00:00\" ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_on_or_before(): void\n    {\n        $filter = DateFilter::property(\"Release date\")\n            ->onOrBefore(\"1997-12-27\");\n\n        $expected = [\n            \"property\" => \"Release date\",\n            \"date\" => [ \"on_or_before\" => \"1997-12-27\" ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_is_empty(): void\n    {\n        $filter = DateFilter::property(\"Release date\")\n            ->isEmpty();\n\n        $expected = [\n            \"property\" => \"Release date\",\n            \"date\" => [ \"is_empty\" => true ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_is_not_empty(): void\n    {\n        $filter = DateFilter::property(\"Release date\")\n            ->isNotEmpty();\n\n        $expected = [\n            \"property\" => \"Release date\",\n            \"date\" => [ \"is_not_empty\" => true ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_on_or_after(): void\n    {\n        $filter = DateFilter::property(\"Release date\")\n            ->onOrAfter(\"1997-12-27\");\n\n        $expected = [\n            \"property\" => \"Release date\",\n            \"date\" => [ \"on_or_after\" => \"1997-12-27\" ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_past_week(): void\n    {\n        $filter = DateFilter::property(\"Release date\")\n            ->pastWeek();\n\n        $expected = [\n            \"property\" => \"Release date\",\n            \"date\" => [ \"past_week\" => new stdClass() ],\n        ];\n        $this->assertEquals($expected, $filter->toArray());\n    }\n\n    public function test_past_month(): void\n    {\n        $filter = DateFilter::property(\"Release date\")\n            ->pastMonth();\n\n        $expected = [\n            \"property\" => \"Release date\",\n            \"date\" => [ \"past_month\" => new stdClass() ],\n        ];\n        $this->assertEquals($expected, $filter->toArray());\n    }\n\n    public function test_past_year(): void\n    {\n        $filter = DateFilter::property(\"Release date\")\n            ->pastYear();\n\n        $expected = [\n            \"property\" => \"Release date\",\n            \"date\" => [ \"past_year\" => new stdClass() ],\n        ];\n        $this->assertEquals($expected, $filter->toArray());\n    }\n\n    public function test_next_week(): void\n    {\n        $filter = DateFilter::property(\"Release date\")\n            ->nextWeek();\n\n        $expected = [\n            \"property\" => \"Release date\",\n            \"date\" => [ \"next_week\" => new stdClass() ],\n        ];\n        $this->assertEquals($expected, $filter->toArray());\n    }\n\n    public function test_next_month(): void\n    {\n        $filter = DateFilter::property(\"Release date\")\n            ->nextMonth();\n\n        $expected = [\n            \"property\" => \"Release date\",\n            \"date\" => [ \"next_month\" => new stdClass() ],\n        ];\n        $this->assertEquals($expected, $filter->toArray());\n    }\n\n    public function test_next_year(): void\n    {\n        $filter = DateFilter::property(\"Release date\")\n            ->nextYear();\n\n        $expected = [\n            \"property\" => \"Release date\",\n            \"date\" => [ \"next_year\" => new stdClass() ],\n        ];\n        $this->assertEquals($expected, $filter->toArray());\n    }\n\n    public function test_this_week(): void\n    {\n        $filter = DateFilter::property(\"Release date\")\n            ->thisWeek();\n\n        $expected = [\n            \"property\" => \"Release date\",\n            \"date\" => [ \"this_week\" => new stdClass() ],\n        ];\n        $this->assertEquals($expected, $filter->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Query/MultiSelectFilterTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Query;\n\nuse Notion\\Databases\\Query\\MultiSelectFilter;\nuse Notion\\Databases\\Query\\Operator;\nuse PHPUnit\\Framework\\TestCase;\n\nclass MultiSelectFilterTest extends TestCase\n{\n    public function test_empty_filter(): void\n    {\n        $filter = MultiSelectFilter::property(\"Categories\");\n\n        $this->assertSame(\"Categories\", $filter->propertyName());\n        $this->assertSame(Operator::IsNotEmpty, $filter->operator());\n        $this->assertTrue($filter->value());\n    }\n\n    public function test_contains(): void\n    {\n        $filter = MultiSelectFilter::property(\"Categories\")\n            ->contains(\"Comedy\");\n\n        $this->assertSame(\"Categories\", $filter->propertyName());\n        $this->assertSame(Operator::Contains, $filter->operator());\n        $this->assertSame(\"Comedy\", $filter->value());\n\n        $expected = [\n            \"property\" => \"Categories\",\n            \"multi_select\" => [ \"contains\" => \"Comedy\" ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_does_not_contain(): void\n    {\n        $filter = MultiSelectFilter::property(\"Categories\")\n            ->doesNotContain(\"Comedy\");\n\n        $expected = [\n            \"property\" => \"Categories\",\n            \"multi_select\" => [ \"does_not_contain\" => \"Comedy\" ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_is_empty(): void\n    {\n        $filter = MultiSelectFilter::property(\"Categories\")->isEmpty();\n\n        $expected = [\n            \"property\" => \"Categories\",\n            \"multi_select\" => [ \"is_empty\" => true ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_is_not_empty(): void\n    {\n        $filter = MultiSelectFilter::property(\"Categories\")->isNotEmpty();\n\n        $expected = [\n            \"property\" => \"Categories\",\n            \"multi_select\" => [ \"is_not_empty\" => true ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Query/NumberFilterTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Query;\n\nuse Notion\\Databases\\Query\\NumberFilter;\nuse Notion\\Databases\\Query\\Operator;\nuse PHPUnit\\Framework\\TestCase;\n\nclass NumberFilterTest extends TestCase\n{\n    public function test_empty_filter(): void\n    {\n        $filter = NumberFilter::property(\"Downloads\");\n\n        $this->assertSame(\"Downloads\", $filter->propertyName());\n        $this->assertSame(Operator::IsNotEmpty, $filter->operator());\n        $this->assertTrue($filter->value());\n    }\n\n    public function test_equals(): void\n    {\n        $filter = NumberFilter::property(\"Downloads\")->equals(1000);\n\n        $expected = [\n            \"property\" => \"Downloads\",\n            \"number\" => [ \"equals\" => 1000 ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_does_not_equal(): void\n    {\n        $filter = NumberFilter::property(\"Downloads\")->doesNotEqual(1000);\n\n        $expected = [\n            \"property\" => \"Downloads\",\n            \"number\" => [ \"does_not_equal\" => 1000 ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_greater_than(): void\n    {\n        $filter = NumberFilter::property(\"Downloads\")->greaterThan(1000);\n\n        $expected = [\n            \"property\" => \"Downloads\",\n            \"number\" => [ \"greater_than\" => 1000 ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_less_than(): void\n    {\n        $filter = NumberFilter::property(\"Downloads\")->lessThan(1000);\n\n        $expected = [\n            \"property\" => \"Downloads\",\n            \"number\" => [ \"less_than\" => 1000 ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_greater_than_or_equal_to(): void\n    {\n        $filter = NumberFilter::property(\"Downloads\")->greaterThanOrEqualTo(1000);\n\n        $expected = [\n            \"property\" => \"Downloads\",\n            \"number\" => [ \"greater_than_or_equal_to\" => 1000 ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_less_than_or_equal_to(): void\n    {\n        $filter = NumberFilter::property(\"Downloads\")->lessThanOrEqualTo(1000);\n\n        $expected = [\n            \"property\" => \"Downloads\",\n            \"number\" => [ \"less_than_or_equal_to\" => 1000 ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_is_empty(): void\n    {\n        $filter = NumberFilter::property(\"Downloads\")->isEmpty();\n\n        $expected = [\n            \"property\" => \"Downloads\",\n            \"number\" => [ \"is_empty\" => true ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_is_not_empty(): void\n    {\n        $filter = NumberFilter::property(\"Downloads\")->isNotEmpty();\n\n        $expected = [\n            \"property\" => \"Downloads\",\n            \"number\" => [ \"is_not_empty\" => true ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Query/PeopleFilterTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Query;\n\nuse Notion\\Databases\\Query\\Operator;\nuse Notion\\Databases\\Query\\PeopleFilter;\nuse PHPUnit\\Framework\\TestCase;\n\nclass PeopleFilterTest extends TestCase\n{\n    public function test_empty_filter(): void\n    {\n        $filter = PeopleFilter::property(\"Friends\");\n\n        $this->assertSame(\"Friends\", $filter->propertyName());\n        $this->assertSame(Operator::IsNotEmpty, $filter->operator());\n        $this->assertTrue($filter->value());\n    }\n\n    public function test_created_by(): void\n    {\n        $filter = PeopleFilter::createdBy();\n\n        $this->assertSame(\"created_by\", $filter->propertyName());\n    }\n\n    public function test_last_edited_by(): void\n    {\n        $filter = PeopleFilter::lastEditedBy();\n\n        $this->assertSame(\"last_edited_by\", $filter->propertyName());\n    }\n\n    public function test_contains(): void\n    {\n        $filter = PeopleFilter::property(\"Friends\")\n            ->contains(\"7b23ad4e145c41aea5604374406c2bc0\");\n\n        $this->assertSame(\"Friends\", $filter->propertyName());\n        $this->assertSame(Operator::Contains, $filter->operator());\n        $this->assertSame(\"7b23ad4e145c41aea5604374406c2bc0\", $filter->value());\n\n        $expected = [\n            \"property\" => \"Friends\",\n            \"people\" => [ \"contains\" => \"7b23ad4e145c41aea5604374406c2bc0\" ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_does_not_contain(): void\n    {\n        $filter = PeopleFilter::property(\"Friends\")\n            ->doesNotContain(\"7b23ad4e145c41aea5604374406c2bc0\");\n\n        $expected = [\n            \"property\" => \"Friends\",\n            \"people\" => [ \"does_not_contain\" => \"7b23ad4e145c41aea5604374406c2bc0\" ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_is_empty(): void\n    {\n        $filter = PeopleFilter::property(\"Friends\")->isEmpty();\n\n        $expected = [\n            \"property\" => \"Friends\",\n            \"people\" => [ \"is_empty\" => true ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_is_not_empty(): void\n    {\n        $filter = PeopleFilter::property(\"Friends\")->isNotEmpty();\n\n        $expected = [\n            \"property\" => \"Friends\",\n            \"people\" => [ \"is_not_empty\" => true ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Query/RelationFilterTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Query;\n\nuse Notion\\Databases\\Query\\Operator;\nuse Notion\\Databases\\Query\\RelationFilter;\nuse PHPUnit\\Framework\\TestCase;\n\nclass RelationFilterTest extends TestCase\n{\n    public function test_empty_filter(): void\n    {\n        $filter = RelationFilter::property(\"Category\");\n\n        $this->assertSame(\"Category\", $filter->propertyName());\n        $this->assertSame(Operator::IsNotEmpty, $filter->operator());\n        $this->assertTrue($filter->value());\n    }\n\n    public function test_contains(): void\n    {\n        $filter = RelationFilter::property(\"Category\")->contains(\"Blog\");\n\n        $expected = [\n            \"property\" => \"Category\",\n            \"relation\" => [ \"contains\" => \"Blog\" ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_does_not_contain(): void\n    {\n        $filter = RelationFilter::property(\"Category\")->doesNotContain(\"Blog\");\n\n        $expected = [\n            \"property\" => \"Category\",\n            \"relation\" => [ \"does_not_contain\" => \"Blog\" ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_is_empty(): void\n    {\n        $filter = RelationFilter::property(\"Category\")->isEmpty();\n\n        $expected = [\n            \"property\" => \"Category\",\n            \"relation\" => [ \"is_empty\" => true ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_is_not_empty(): void\n    {\n        $filter = RelationFilter::property(\"Category\")->isNotEmpty();\n\n        $expected = [\n            \"property\" => \"Category\",\n            \"relation\" => [ \"is_not_empty\" => true ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Query/ResultTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Query;\n\nuse Notion\\Databases\\Query\\Result;\nuse PHPUnit\\Framework\\TestCase;\n\nclass ResultTest extends TestCase\n{\n    public function test_from_array(): void\n    {\n        $apiResponse = [\n            \"results\" => [\n                [\n                    \"object\" => \"page\",\n                    \"id\" => \"a7e80c0b-a766-43c3-a9e9-21ce94595e0e\",\n                    \"created_time\" => \"2020-12-08T12:00:00.000000Z\",\n                    \"last_edited_time\" => \"2020-12-08T12:00:00.000000Z\",\n                    \"in_trash\" => false,\n                    \"icon\" => null,\n                    \"cover\" => null,\n                    \"properties\" => [],\n                    \"parent\" => [\n                        \"type\" => \"page_id\",\n                        \"page_id\" => \"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\",\n                    ],\n                    \"url\" => \"https://notion.so/a7e80c0ba76643c3a9e921ce94595e0e\",\n                ],\n            ],\n            \"has_more\" => true,\n            \"next_cursor\" => \"889431ed-4f50-460b-a926-36f6cf0f9669\",\n        ];\n\n        $result = Result::fromArray($apiResponse);\n\n        $this->assertCount(1, $result->pages);\n        $this->assertTrue($result->hasMore);\n        $this->assertSame(\"889431ed-4f50-460b-a926-36f6cf0f9669\", $result->nextCursor);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Query/SelectFilterTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Query;\n\nuse Notion\\Databases\\Query\\Operator;\nuse Notion\\Databases\\Query\\SelectFilter;\nuse PHPUnit\\Framework\\TestCase;\n\nclass SelectFilterTest extends TestCase\n{\n    public function test_empty_filter(): void\n    {\n        $filter = SelectFilter::property(\"Category\");\n\n        $this->assertSame(\"Category\", $filter->propertyName());\n        $this->assertSame(Operator::IsNotEmpty, $filter->operator());\n        $this->assertTrue($filter->value());\n    }\n\n    public function test_equals(): void\n    {\n        $filter = SelectFilter::property(\"Category\")->equals(\"Comedy\");\n\n        $expected = [\n            \"property\" => \"Category\",\n            \"select\" => [ \"equals\" => \"Comedy\" ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_does_not_equal(): void\n    {\n        $filter = SelectFilter::property(\"Category\")->doesNotEqual(\"Comedy\");\n\n        $expected = [\n            \"property\" => \"Category\",\n            \"select\" => [ \"does_not_equal\" => \"Comedy\" ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_is_empty(): void\n    {\n        $filter = SelectFilter::property(\"Category\")->isEmpty();\n\n        $expected = [\n            \"property\" => \"Category\",\n            \"select\" => [ \"is_empty\" => true ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_is_not_empty(): void\n    {\n        $filter = SelectFilter::property(\"Category\")->isNotEmpty();\n\n        $expected = [\n            \"property\" => \"Category\",\n            \"select\" => [ \"is_not_empty\" => true ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Query/SortTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Query;\n\nuse Notion\\Databases\\Query\\Sort;\nuse PHPUnit\\Framework\\TestCase;\n\nclass SortTest extends TestCase\n{\n    public function test_sort_by_property(): void\n    {\n        $sort = Sort::property(\"Title\");\n\n        $expected = [\n            \"property\" => \"Title\",\n            \"direction\" => \"ascending\",\n        ];\n\n        $this->assertSame($expected, $sort->toArray());\n    }\n\n    public function test_sort_by_created_time(): void\n    {\n        $sort = Sort::createdTime();\n\n        $expected = [\n            \"timestamp\" => \"created_time\",\n            \"direction\" => \"ascending\",\n        ];\n\n        $this->assertSame($expected, $sort->toArray());\n    }\n\n    public function test_sort_by_last_edited_time(): void\n    {\n        $sort = Sort::lastEditedTime();\n\n        $expected = [\n            \"timestamp\" => \"last_edited_time\",\n            \"direction\" => \"ascending\",\n        ];\n\n        $this->assertSame($expected, $sort->toArray());\n    }\n\n    public function test_sort_ascending(): void\n    {\n        $sort = Sort::property(\"Title\")->ascending();\n\n        $this->assertSame(\"ascending\", $sort->toArray()[\"direction\"]);\n    }\n\n    public function test_sort_descending(): void\n    {\n        $sort = Sort::property(\"Title\")->descending();\n\n        $this->assertSame(\"descending\", $sort->toArray()[\"direction\"]);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Query/StatusFilterTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Query;\n\nuse Notion\\Databases\\Query\\Operator;\nuse Notion\\Databases\\Query\\StatusFilter;\nuse PHPUnit\\Framework\\TestCase;\n\nclass StatusFilterTest extends TestCase\n{\n    public function test_empty_filter(): void\n    {\n        $filter = StatusFilter::property(\"Status\");\n\n        $this->assertSame(\"Status\", $filter->propertyName());\n        $this->assertSame(Operator::IsNotEmpty, $filter->operator());\n        $this->assertTrue($filter->value());\n    }\n\n    public function test_equals(): void\n    {\n        $filter = StatusFilter::property(\"Status\")->equals(\"Completed\");\n\n        $expected = [\n            \"property\" => \"Status\",\n            \"status\" => [ \"equals\" => \"Completed\" ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_does_not_equal(): void\n    {\n        $filter = StatusFilter::property(\"Status\")->doesNotEqual(\"In Progress\");\n\n        $expected = [\n            \"property\" => \"Status\",\n            \"status\" => [ \"does_not_equal\" => \"In Progress\" ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_is_empty(): void\n    {\n        $filter = StatusFilter::property(\"Status\")->isEmpty();\n\n        $expected = [\n            \"property\" => \"Status\",\n            \"status\" => [ \"is_empty\" => true ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_is_not_empty(): void\n    {\n        $filter = StatusFilter::property(\"Status\")->isNotEmpty();\n\n        $expected = [\n            \"property\" => \"Status\",\n            \"status\" => [ \"is_not_empty\" => true ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/Query/TextFilterTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases\\Query;\n\nuse Notion\\Databases\\Query\\Operator;\nuse Notion\\Databases\\Query\\TextFilter;\nuse PHPUnit\\Framework\\TestCase;\n\nclass TextFilterTest extends TestCase\n{\n    public function test_empty_filter(): void\n    {\n        $filter = TextFilter::property(\"Title\");\n\n        $this->assertSame(\"Title\", $filter->propertyName());\n        $this->assertSame(Operator::Contains, $filter->operator());\n        $this->assertSame(\"\", $filter->value());\n    }\n\n    public function test_equals(): void\n    {\n        $filter = TextFilter::property(\"Title\")->equals(\"Harry Potter\");\n\n        $this->assertSame(\"Title\", $filter->propertyName());\n        $this->assertSame(Operator::Equals, $filter->operator());\n        $this->assertSame(\"Harry Potter\", $filter->value());\n\n        $expected = [\n            \"property\"  => \"Title\",\n            \"rich_text\" => [ \"equals\" => \"Harry Potter\" ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_does_not_equal(): void\n    {\n        $filter = TextFilter::property(\"Title\")->doesNotEqual(\"Harry Potter\");\n\n        $expected = [\n            \"property\"  => \"Title\",\n            \"rich_text\" => [ \"does_not_equal\" => \"Harry Potter\" ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_contains(): void\n    {\n        $filter = TextFilter::property(\"Title\")->contains(\"Harry Potter\");\n\n        $expected = [\n            \"property\"  => \"Title\",\n            \"rich_text\" => [ \"contains\" => \"Harry Potter\" ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_does_not_contain(): void\n    {\n        $filter = TextFilter::property(\"Title\")->doesNotContain(\"Harry Potter\");\n\n        $expected = [\n            \"property\"  => \"Title\",\n            \"rich_text\" => [ \"does_not_contain\" => \"Harry Potter\" ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_starts_with(): void\n    {\n        $filter = TextFilter::property(\"Title\")->startsWith(\"Harry\");\n\n        $expected = [\n            \"property\"  => \"Title\",\n            \"rich_text\" => [ \"starts_with\" => \"Harry\" ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_ends_with(): void\n    {\n        $filter = TextFilter::property(\"Title\")->endsWith(\"Potter\");\n\n        $expected = [\n            \"property\"  => \"Title\",\n            \"rich_text\" => [ \"ends_with\" => \"Potter\" ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_is_empty(): void\n    {\n        $filter = TextFilter::property(\"Title\")->isEmpty();\n\n        $expected = [\n            \"property\"  => \"Title\",\n            \"rich_text\" => [ \"is_empty\" => true ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n\n    public function test_is_not_empty(): void\n    {\n        $filter = TextFilter::property(\"Title\")->isNotEmpty();\n\n        $expected = [\n            \"property\"  => \"Title\",\n            \"rich_text\" => [ \"is_not_empty\" => true ],\n        ];\n        $this->assertSame($expected, $filter->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Databases/QueryTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Databases;\n\nuse Exception;\nuse Notion\\Databases\\Query;\nuse Notion\\Databases\\Query\\Sort;\nuse Notion\\Databases\\Query\\TextFilter;\nuse PHPUnit\\Framework\\TestCase;\n\nclass QueryTest extends TestCase\n{\n    public function test_empty_query(): void\n    {\n        $query = Query::create();\n\n        $this->assertNull($query->filter);\n        $this->assertEmpty($query->sorts);\n        $this->assertNull($query->startCursor);\n        $this->assertSame(Query::MAX_PAGE_SIZE, $query->pageSize);\n    }\n\n    public function test_query_change_filter(): void\n    {\n        $query = Query::create()\n            ->changeFilter(TextFilter::property(\"Title\")->isNotEmpty());\n\n        $this->assertNotNull($query->filter);\n    }\n\n    public function test_add_sort(): void\n    {\n        $query = Query::create()\n            ->addSort(Sort::createdTime()->descending())\n            ->addSort(Sort::property(\"Title\")->ascending());\n\n        $this->assertCount(2, $query->sorts);\n    }\n\n    public function test_replace_sorts(): void\n    {\n        $query = Query::create()\n            ->addSort(Sort::createdTime()->descending())\n            ->addSort(Sort::property(\"Title\")->ascending())\n            ->changeSorts(Sort::lastEditedTime()->descending());\n\n        $this->assertCount(1, $query->sorts);\n    }\n\n    /** @psalm-suppress DeprecatedMethod */\n    public function test_deprecated_change_added_sort(): void\n    {\n        $query = Query::create()\n            ->changeAddedSort(Sort::createdTime()->descending())\n            ->changeAddedSort(Sort::property(\"Title\")->ascending());\n\n        $this->assertCount(2, $query->sorts);\n    }\n\n    public function test_query_change_start_cursor(): void\n    {\n        $query = Query::create()\n            ->changeStartCursor(\"889431ed-4f50-460b-a926-36f6cf0f9669\");\n\n        $this->assertSame(\"889431ed-4f50-460b-a926-36f6cf0f9669\", $query->startCursor);\n    }\n\n    public function test_query_change_custom_page_size(): void\n    {\n        $query = Query::create()\n            ->changePageSize(20);\n\n        $this->assertSame(20, $query->pageSize);\n    }\n\n    public function test_page_size_more_than_limit(): void\n    {\n        $this->expectException(Exception::class);\n\n        /** @psalm-suppress UnusedMethodCall */\n        Query::create()->changePageSize(100000000);\n    }\n\n    public function test_empty_query_to_array(): void\n    {\n        $query = Query::create();\n\n        $expected = [\n            \"sorts\" => [],\n            \"page_size\" => Query::MAX_PAGE_SIZE,\n        ];\n\n        $this->assertSame($expected, $query->toArray());\n    }\n\n    public function test_complete_query_to_array(): void\n    {\n        $query = Query::create()\n            ->changeFilter(TextFilter::property(\"Title\")->contains(\"abc\"))\n            ->addSort(Sort::property(\"Title\")->ascending())\n            ->changeStartCursor(\"889431ed-4f50-460b-a926-36f6cf0f9669\")\n            ->changePageSize(20);\n\n        $expected = [\n            \"filter\" => [\n                \"property\" => \"Title\",\n                \"rich_text\" => [ \"contains\" => \"abc\" ],\n            ],\n            \"sorts\" => [\n                [\n                    \"property\" => \"Title\",\n                    \"direction\" => \"ascending\",\n                ],\n            ],\n            \"start_cursor\" => \"889431ed-4f50-460b-a926-36f6cf0f9669\",\n            \"page_size\" => 20,\n        ];\n\n        $this->assertEquals($expected, $query->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Exceptions/ApiExceptionTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Exceptions;\n\nuse GuzzleHttp\\Psr7\\Response;\nuse Notion\\Exceptions\\ApiException;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Http\\Message\\ResponseInterface;\n\nclass ApiExceptionTest extends TestCase\n{\n    public function test_from_response_body(): void\n    {\n        $body = '{\n            \"object\": \"error\",\n            \"status\": 404,\n            \"code\": \"object_not_found\",\n            \"message\": \"Could not find page with ID: f077308f-dd9e-4cfe-87e6-7420a8488514.\"\n        }';\n        /** @var array{ message: string, code: string } $bodyArray */\n        $bodyArray = json_decode($body, true);\n        $response = new Response(404, [], $body);\n\n        $e = ApiException::fromResponse($response);\n\n        $this->assertSame($bodyArray[\"message\"], $e->getMessage());\n        /** @psalm-suppress DeprecatedMethod */\n        $this->assertSame($bodyArray[\"code\"], $e->getNotionCode());\n        $this->assertSame($bodyArray[\"code\"], $e->notionCode);\n        $this->assertInstanceOf(ResponseInterface::class, $e->response);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Infrastructure/HttpTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Infrastructure;\n\nuse GuzzleHttp\\Client;\nuse GuzzleHttp\\Handler\\MockHandler;\nuse GuzzleHttp\\HandlerStack;\nuse GuzzleHttp\\Psr7\\HttpFactory;\nuse GuzzleHttp\\Psr7\\Response;\nuse Notion\\Configuration;\nuse Notion\\Exceptions\\ConflictException;\nuse Notion\\Notion;\nuse Notion\\Pages\\Page;\nuse Notion\\Pages\\PageParent;\nuse PHPUnit\\Framework\\TestCase;\n\nfinal class HttpTest extends TestCase\n{\n    public function test_retry_sending_request_after_conflict_errors(): void\n    {\n        $mock = new MockHandler([\n            new Response(500, [], $this->conflictErrorJson()),\n            new Response(500, [], $this->conflictErrorJson()),\n            new Response(201, [], $this->createdPageJson()),\n        ]);\n        $client = new Client([ \"handler\" => HandlerStack::create($mock)]);\n\n        $factory = new HttpFactory();\n        $config = Configuration::createFromPsrImplementations(\"secret_123\", $client, $factory)\n                    ->enableRetryOnConflict(2);\n\n        $notion = Notion::createFromConfig($config);\n        $page = Page::create(PageParent::workspace());\n\n        $notion->pages()->create($page);\n\n        $this->assertCount(0, $mock);\n    }\n\n    public function test_retry_sending_request_after_many_conflict_errors(): void\n    {\n        $mock = new MockHandler([\n            new Response(500, [], $this->conflictErrorJson()),\n            new Response(500, [], $this->conflictErrorJson()),\n            new Response(500, [], $this->conflictErrorJson()),\n        ]);\n        $client = new Client([ \"handler\" => HandlerStack::create($mock)]);\n\n        $factory = new HttpFactory();\n        $config = Configuration::createFromPsrImplementations(\"secret_123\", $client, $factory)\n                    ->enableRetryOnConflict(2);\n\n        $notion = Notion::createFromConfig($config);\n        $page = Page::create(PageParent::workspace());\n\n        $this->expectException(ConflictException::class);\n        $notion->pages()->create($page);\n    }\n\n    private function conflictErrorJson(): string\n    {\n        return '{\n            \"object\": \"error\",\n            \"status\": 500,\n            \"code\": \"conflict_error\",\n            \"message\": \"Conflict occurred while saving. Please try again.\"\n        }';\n    }\n\n    private function createdPageJson(): string\n    {\n        return '{\n            \"object\": \"page\",\n            \"id\": \"ff747ce6-bb89-4c54-80c3-a248a2c78bd9\",\n            \"created_time\": \"2023-01-11T21:04:00.000Z\",\n            \"last_edited_time\": \"2023-01-11T21:04:00.000Z\",\n            \"created_by\": {\n                \"object\": \"user\",\n                \"id\": \"e8f2d77a-8756-43f6-bc87-0dc2bf9115fa\"\n            },\n            \"last_edited_by\": {\n                \"object\": \"user\",\n                \"id\": \"e8f2d77a-8756-43f6-bc87-0dc2bf9115fa\"\n            },\n            \"cover\": null,\n            \"icon\": null,\n            \"parent\": {\n                \"type\": \"page_id\",\n                \"page_id\": \"cf735738-35e3-44aa-b3d4-aca944c8f421\"\n            },\n            \"in_trash\": false,\n            \"properties\": {\n                \"title\": {\n                    \"id\": \"title\",\n                    \"type\": \"title\",\n                    \"title\": []\n                }\n            },\n            \"url\": \"https://www.notion.so/ff747ce6bb894c5480c3a248a2c78bd9\"\n        }';\n    }\n}\n"
  },
  {
    "path": "tests/Unit/NotionTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit;\n\nuse GuzzleHttp\\Client;\nuse GuzzleHttp\\Psr7\\HttpFactory;\nuse Notion\\Notion;\nuse PHPUnit\\Framework\\TestCase;\n\nclass NotionTest extends TestCase\n{\n    public function test_custom_psr_implementation(): void\n    {\n        $psrClient = new Client();\n        $requestFactory = new HttpFactory();\n        $token = \"secret_token\";\n\n        $notion = Notion::createWithPsrImplementations(\n            $psrClient,\n            $requestFactory,\n            $token,\n        );\n\n        $this->assertInstanceOf(Notion::class, $notion);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Pages/PageParentTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Pages;\n\nuse Notion\\Pages\\PageParent;\nuse Notion\\Pages\\PageParentType;\nuse PHPUnit\\Framework\\TestCase;\n\nclass PageParentTest extends TestCase\n{\n    public function test_create_parent_database(): void\n    {\n        $parent = PageParent::database(\"058d158b-09de-4d69-be07-901c20a7ca5c\");\n\n        $this->assertTrue($parent->isDatabase());\n        $this->assertEquals(\"058d158b-09de-4d69-be07-901c20a7ca5c\", $parent->id);\n    }\n\n    public function test_create_parent_page(): void\n    {\n        $parent = PageParent::page(\"058d158b-09de-4d69-be07-901c20a7ca5c\");\n\n        $this->assertTrue($parent->isPage());\n        $this->assertEquals(\"058d158b-09de-4d69-be07-901c20a7ca5c\", $parent->id);\n    }\n\n    public function test_create_parent_block(): void\n    {\n        $parent = PageParent::block(\"0181c3aa-1112-489f-b34a-515b4e3583ed\");\n\n        $this->assertTrue($parent->isBlock());\n        $this->assertSame(\"0181c3aa-1112-489f-b34a-515b4e3583ed\", $parent->id);\n    }\n\n    public function test_create_parent_workspace(): void\n    {\n        $parent = PageParent::workspace();\n\n        $this->assertTrue($parent->isWorkspace());\n        $this->assertEquals(PageParentType::Workspace, $parent->type);\n    }\n\n    public function test_page_array_conversion(): void\n    {\n        $array = [\n            \"type\" => \"page_id\",\n            \"page_id\" => \"7a774b5d-ca74-4679-9f18-689b5a98f138\",\n        ];\n        $parent = PageParent::fromArray($array);\n\n        $this->assertEquals($array[\"page_id\"], $parent->toArray()[\"page_id\"]);\n    }\n\n    public function test_database_array_conversion(): void\n    {\n        $array = [\n            \"type\" => \"database_id\",\n            \"database_id\" => \"7a774b5d-ca74-4679-9f18-689b5a98f138\",\n        ];\n        $parent = PageParent::fromArray($array);\n\n        $this->assertEquals($array[\"database_id\"], $parent->toArray()[\"database_id\"]);\n    }\n\n    public function test_workspace_array_conversion(): void\n    {\n        $array = [\n            \"type\" => \"workspace\",\n            \"workspace\" => true,\n        ];\n        $parent = PageParent::fromArray($array);\n\n        $this->assertEquals($array[\"workspace\"], $parent->toArray()[\"workspace\"]);\n    }\n\n    public function test_block_array_conversion(): void\n    {\n        $array = [\n            \"type\" => \"block_id\",\n            \"block_id\" => \"7a774b5d-ca74-4679-9f18-689b5a98f138\",\n        ];\n        $parent = PageParent::fromArray($array);\n\n        $this->assertEquals($array[\"block_id\"], $parent->toArray()[\"block_id\"]);\n    }\n\n    public function test_invalid_type_array(): void\n    {\n        $this->expectException(\\ValueError::class);\n        /** @psalm-suppress InvalidArgument */\n        PageParent::fromArray([ \"type\" => \"invalid-type\" ]);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Pages/PageTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Pages;\n\nuse Notion\\Common\\Date;\nuse Notion\\Common\\Emoji;\nuse Notion\\Common\\File;\nuse Notion\\Common\\Icon;\nuse Notion\\Pages\\Page;\nuse Notion\\Pages\\PageParent;\nuse Notion\\Pages\\Properties\\PropertyType;\nuse Notion\\Pages\\Properties\\RichTextProperty;\nuse Notion\\Pages\\Properties\\Title;\nuse PHPUnit\\Framework\\TestCase;\n\nclass PageTest extends TestCase\n{\n    public function test_create_page(): void\n    {\n        $parent = PageParent::page(\"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\");\n        $page = Page::create($parent);\n\n        $this->assertEquals(\"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\", $page->parent->id);\n        $this->assertEmpty($page->properties);\n    }\n\n    public function test_add_title(): void\n    {\n        $parent = PageParent::page(\"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\");\n        $page = Page::create($parent)->changeTitle(\"Page title\");\n\n        $this->assertEquals(\"Page title\", $page->title()?->toString());\n    }\n\n    public function test_add_icon(): void\n    {\n        $parent = PageParent::page(\"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\");\n        $page = Page::create($parent)->changeIcon(Icon::fromEmoji(Emoji::fromString(\"⭐\")));\n\n        if ($page->icon?->isEmoji()) {\n            $this->assertEquals(\"⭐\", $page->icon->emoji?->emoji);\n        }\n        $this->assertTrue($page->hasIcon());\n        $this->assertTrue($page->icon?->isEmoji());\n    }\n\n    public function test_add_file_icon(): void\n    {\n        $parent = PageParent::page(\"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\");\n        $page = Page::create($parent)->changeIcon(\n            File::createExternal(\"http://example.com/icon.png\")\n        );\n\n        $this->assertTrue($page->hasIcon());\n        $this->assertTrue($page->icon?->isFile());\n    }\n\n    public function test_remove_icon(): void\n    {\n        $parent = PageParent::page(\"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\");\n        $page = Page::create($parent)\n            ->changeIcon(Icon::fromEmoji(Emoji::fromString(\"⭐\")))\n            ->removeIcon();\n\n        $this->assertFalse($page->hasIcon());\n        $this->assertNull($page->icon);\n    }\n\n    public function test_add_cover(): void\n    {\n        $parent = PageParent::page(\"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\");\n        $coverImage = File::createExternal(\"https://my-site.com/image.png\");\n        $page = Page::create($parent)->changeCover($coverImage);\n\n        $this->assertEquals($coverImage, $page->cover);\n    }\n\n    public function test_remove_cover(): void\n    {\n        $parent = PageParent::page(\"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\");\n        $coverImage = File::createExternal(\"https://my-site.com/image.png\");\n        $page = Page::create($parent)->changeCover($coverImage)->removeCover();\n\n        $this->assertNull($page->cover);\n    }\n\n    public function test_delete(): void\n    {\n        $parent = PageParent::page(\"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\");\n        $page = Page::create($parent)->delete();\n\n        $this->assertTrue($page->inTrash);\n    }\n\n    public function test_restore(): void\n    {\n        $parent = PageParent::page(\"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\");\n        $page = Page::create($parent)->delete()->restore();\n\n        $this->assertFalse($page->inTrash);\n    }\n\n    public function test_move_page(): void\n    {\n        $oldParent = PageParent::page(\"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\");\n        $newParent = PageParent::database(\"08da99e5-f11d-4d26-827d-112a3a9bd07d\");\n        $page = Page::create($oldParent)->changeParent($newParent);\n\n        $this->assertSame($newParent, $page->parent);\n    }\n\n    public function test_add_property(): void\n    {\n        $page = Page::create(PageParent::workspace());\n\n        $page = $page->addProperty(\"Rating\", RichTextProperty::fromString(\"⭐⭐⭐\"));\n\n        $this->assertEquals(PropertyType::RichText, $page->getProperty(\"Rating\")->metadata()->type);\n    }\n\n    public function test_get_property_deprecated(): void\n    {\n        $page = Page::create(PageParent::workspace());\n\n        $page = $page->addProperty(\"Rating\", RichTextProperty::fromString(\"⭐⭐⭐\"));\n\n        /** @psalm-suppress DeprecatedMethod */\n        $this->assertEquals(PropertyType::RichText, $page->getProprety(\"Rating\")->metadata()->type);\n    }\n\n    public function test_replace_properties(): void\n    {\n        $parent = PageParent::page(\"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\");\n        $properties = [\n            \"title\" => Title::fromString(\"Page title\")\n        ];\n        $page = Page::create($parent)->changeProperties($properties);\n\n        $this->assertCount(1, $page->properties);\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"object\" => \"page\",\n            \"id\" => \"a7e80c0b-a766-43c3-a9e9-21ce94595e0e\",\n            \"created_time\" => \"2020-12-08T12:00:00.000000Z\",\n            \"last_edited_time\" => \"2020-12-08T12:00:00.000000Z\",\n            \"in_trash\" => false,\n            \"icon\" => null,\n            \"cover\" => null,\n            \"properties\" => [\n                \"title\" => [\n                    \"id\" => \"title\",\n                    \"type\" => \"title\",\n                    \"title\" => [[\n                        \"plain_text\" => \"Page title\",\n                        \"href\" => null,\n                        \"annotations\" => [\n                            \"bold\"          => false,\n                            \"italic\"        => false,\n                            \"strikethrough\" => false,\n                            \"underline\"     => false,\n                            \"code\"          => false,\n                            \"color\"         => \"default\",\n                        ],\n                        \"type\" => \"text\",\n                        \"text\" => [ \"content\" => \"Page title\", ],\n                    ]],\n                ],\n            ],\n            \"parent\" => [\n                \"type\" => \"page_id\",\n                \"page_id\" => \"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\",\n            ],\n            \"url\" => \"https://notion.so/a7e80c0ba76643c3a9e921ce94595e0e\",\n        ];\n        $page = Page::fromArray($array);\n\n        $outArray = $array;\n        unset($outArray[\"parent\"][\"type\"]);\n\n        $this->assertSame($outArray, $page->toArray());\n        $this->assertSame(\"a7e80c0b-a766-43c3-a9e9-21ce94595e0e\", $page->id);\n        $this->assertSame(\"https://notion.so/a7e80c0ba76643c3a9e921ce94595e0e\", $page->url);\n        $this->assertEquals(\n            \"2020-12-08T12:00:00.000000Z\",\n            $page->createdTime->format(Date::FORMAT),\n        );\n        $this->assertEquals(\n            \"2020-12-08T12:00:00.000000Z\",\n            $page->lastEditedTime->format(Date::FORMAT),\n        );\n    }\n\n    public function test_from_array_change_emoji_icon(): void\n    {\n        $array = [\n            \"object\" => \"page\",\n            \"id\" => \"a7e80c0b-a766-43c3-a9e9-21ce94595e0e\",\n            \"created_time\" => \"2020-12-08T12:00:00.000000Z\",\n            \"last_edited_time\" => \"2020-12-08T12:00:00.000000Z\",\n            \"in_trash\" => false,\n            \"icon\" => [\n                \"type\" => \"emoji\",\n                \"emoji\" => \"⭐\",\n            ],\n            \"cover\" => null,\n            \"properties\" => [],\n            \"parent\" => [\n                \"type\" => \"page_id\",\n                \"page_id\" => \"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\",\n            ],\n            \"url\" => \"https://notion.so/a7e80c0ba76643c3a9e921ce94595e0e\",\n        ];\n        $page = Page::fromArray($array);\n\n        if ($page->icon?->isEmoji()) {\n            $this->assertEquals(\"⭐\", $page->icon->emoji?->emoji);\n        }\n        $this->assertTrue($page->icon?->isEmoji());\n    }\n\n    public function test_from_array_change_file_icon(): void\n    {\n        $array = [\n            \"object\" => \"page\",\n            \"id\" => \"a7e80c0b-a766-43c3-a9e9-21ce94595e0e\",\n            \"created_time\" => \"2020-12-08T12:00:00.000000Z\",\n            \"last_edited_time\" => \"2020-12-08T12:00:00.000000Z\",\n            \"in_trash\" => false,\n            \"icon\" => [\n                \"type\" => \"external\",\n                \"external\" => [ \"url\" => \"https://my-site.com/image.png\" ],\n            ],\n            \"cover\" => null,\n            \"properties\" => [],\n            \"parent\" => [\n                \"type\" => \"page_id\",\n                \"page_id\" => \"1ce62b6f-b7f3-4201-afd0-08acb02e61c6\",\n            ],\n            \"url\" => \"https://notion.so/a7e80c0ba76643c3a9e921ce94595e0e\",\n        ];\n        $page = Page::fromArray($array);\n\n        if ($page->icon?->isFile()) {\n            $this->assertEquals(\"https://my-site.com/image.png\", $page->icon->file?->url);\n        }\n        $this->assertTrue($page->icon?->isFile());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Pages/Properties/CheckboxTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Pages\\Properties;\n\nuse Notion\\Pages\\Properties\\Checkbox;\nuse Notion\\Pages\\Properties\\PropertyFactory;\nuse Notion\\Pages\\Properties\\PropertyType;\nuse PHPUnit\\Framework\\TestCase;\n\nclass CheckboxTest extends TestCase\n{\n    public function test_create_checked(): void\n    {\n        $checkbox = Checkbox::createChecked();\n\n        $this->assertEquals(PropertyType::Checkbox, $checkbox->metadata()->type);\n        $this->assertTrue($checkbox->checked);\n    }\n\n    public function test_create_unchecked(): void\n    {\n        $checkbox = Checkbox::createUnchecked();\n\n        $this->assertEquals(PropertyType::Checkbox, $checkbox->metadata()->type);\n        $this->assertFalse($checkbox->checked);\n    }\n\n    public function test_check(): void\n    {\n        $checkbox = Checkbox::createUnchecked()->check();\n\n        $this->assertTrue($checkbox->checked);\n    }\n\n    public function test_uncheck(): void\n    {\n        $checkbox = Checkbox::createChecked()->uncheck();\n\n        $this->assertFalse($checkbox->checked);\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\" => \"abc\",\n            \"type\" => \"checkbox\",\n            \"checkbox\" => true,\n        ];\n\n        $checkbox = Checkbox::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $checkbox->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Pages/Properties/CreatedByTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Pages\\Properties;\n\nuse Notion\\Pages\\Properties\\CreatedBy;\nuse Notion\\Pages\\Properties\\PropertyFactory;\nuse Notion\\Pages\\Properties\\PropertyType;\nuse PHPUnit\\Framework\\TestCase;\n\nclass CreatedByTest extends TestCase\n{\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\" => \"abc\",\n            \"type\" => \"created_by\",\n            \"created_by\" => [\n                \"object\"     => \"user\",\n                \"id\" => \"62e1fd10-8b04-41eb-97c1-d2deddd160d4\",\n                \"name\" => \"Mario\",\n                \"type\" => \"person\",\n                \"person\" => [ \"email\" => \"mario@domain.com\" ],\n            ],\n        ];\n\n        $createdBy = CreatedBy::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $createdBy->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n        $this->assertEquals(\"Mario\", $createdBy->user->name);\n        $this->assertEquals(PropertyType::CreatedBy, $createdBy->metadata()->type);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Pages/Properties/CreatedTimeTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Pages\\Properties;\n\nuse DateTimeImmutable;\nuse Notion\\Pages\\Properties\\CreatedTime;\nuse Notion\\Pages\\Properties\\PropertyFactory;\nuse Notion\\Pages\\Properties\\PropertyType;\nuse PHPUnit\\Framework\\TestCase;\n\nclass CreatedTimeTest extends TestCase\n{\n    public function test_create(): void\n    {\n        $date = new DateTimeImmutable(\"2021-01-01T00:00:00.000000Z\");\n        $time = CreatedTime::create($date);\n\n        $this->assertEquals(PropertyType::CreatedTime, $time->metadata()->type);\n        $this->assertEquals($date, $time->time);\n    }\n\n    public function test_change_time(): void\n    {\n        $date1 = new DateTimeImmutable(\"2021-01-01T00:00:00.000000Z\");\n        $date2 = new DateTimeImmutable(\"2022-01-01T00:00:00.000000Z\");\n\n        $time = CreatedTime::create($date1)->changeTime($date2);\n\n        $this->assertEquals($date2, $time->time);\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\" => \"abc\",\n            \"type\" => \"created_time\",\n            \"created_time\" => \"2021-01-01T00:00:00.000000Z\",\n        ];\n\n        $time = CreatedTime::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $time->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Pages/Properties/DateTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Pages\\Properties;\n\nuse DateTimeImmutable;\nuse Notion\\Pages\\Properties\\Date;\nuse Notion\\Pages\\Properties\\PropertyFactory;\nuse Notion\\Pages\\Properties\\PropertyType;\nuse PHPUnit\\Framework\\TestCase;\n\nclass DateTest extends TestCase\n{\n    public function test_create_date(): void\n    {\n        $someday = new DateTimeImmutable(\"2021-01-01\");\n\n        $date = Date::create($someday);\n\n        $this->assertEquals($someday, $date->start());\n        $this->assertNull($date->end());\n        $this->assertFalse($date->isRange());\n        $this->assertEquals(PropertyType::Date, $date->metadata()->type);\n    }\n\n    public function test_create_range(): void\n    {\n        $start = new DateTimeImmutable(\"2021-01-01\");\n        $end = new DateTimeImmutable(\"2021-12-31\");\n\n        $date = Date::createRange($start, $end);\n\n        $this->assertTrue($date->isRange());\n        $this->assertEquals($start, $date->start());\n        $this->assertEquals($end, $date->end());\n    }\n\n    public function test_create_empty(): void\n    {\n        $date = Date::createEmpty();\n\n        $this->assertTrue($date->isEmpty());\n    }\n\n    public function test_change_start(): void\n    {\n        $newStart = new DateTimeImmutable(\"2021-01-01\");\n\n        $date = Date::create(new DateTimeImmutable(\"2020-01-01\"))\n                    ->changeStart($newStart);\n\n        $this->assertEquals($newStart, $date->start());\n    }\n\n    public function test_change_end(): void\n    {\n        $newEnd = new DateTimeImmutable(\"2021-12-31\");\n\n        $date = Date::create(new DateTimeImmutable(\"2021-01-01\"))\n                    ->changeEnd($newEnd);\n\n        $this->assertEquals($newEnd, $date->end());\n    }\n\n    public function test_remove_end(): void\n    {\n        $date = Date::createRange(\n            new DateTimeImmutable(\"2021-01-01\"),\n            new DateTimeImmutable(\"2021-12-31\"),\n        )->removeEnd();\n\n        $this->assertNull($date->end());\n        $this->assertFalse($date->isRange());\n    }\n\n    public function test_clear(): void\n    {\n        $someday = new DateTimeImmutable(\"2021-01-01\");\n\n        $date = Date::create($someday)->clear();\n\n        $this->assertTrue($date->isEmpty());\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\"   => \"a7ede3b7-c7ae-4eb8-b415-a7f80ac4dfe5\",\n            \"type\" => \"date\",\n            \"date\" => [\n                \"start\" => \"2021-01-01T00:00:00.000000Z\",\n                \"end\"   => \"2021-12-31T00:00:00.000000Z\",\n            ],\n        ];\n        $date = Date::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $date->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n    }\n\n    public function test_is_empty(): void\n    {\n        $array = [\n            \"id\"   => \"a7ede3b7-c7ae-4eb8-b415-a7f80ac4dfe5\",\n            \"type\" => \"date\",\n            \"date\" => null,\n        ];\n        $date = Date::fromArray($array);\n\n        $this->assertTrue($date->isEmpty());\n        $this->assertFalse($date->isRange());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Pages/Properties/EmailTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Pages\\Properties;\n\nuse Notion\\Pages\\Properties\\Email;\nuse Notion\\Pages\\Properties\\PropertyFactory;\nuse Notion\\Pages\\Properties\\PropertyType;\nuse PHPUnit\\Framework\\TestCase;\n\nclass EmailTest extends TestCase\n{\n    public function test_create(): void\n    {\n        $email = Email::create(\"mario@domain.com\");\n\n        $this->assertEquals(PropertyType::Email, $email->metadata()->type);\n        $this->assertEquals(\"mario@domain.com\", $email->email);\n    }\n\n    public function test_create_empty(): void\n    {\n        $email = Email::createEmpty();\n\n        $this->assertTrue($email->isEmpty());\n    }\n\n    public function test_change_email(): void\n    {\n        $email = Email::create(\"mario@domain.com\")->changeEmail(\"luigi@domain.com\");\n\n        $this->assertEquals(\"luigi@domain.com\", $email->email);\n    }\n\n    public function test_clear(): void\n    {\n        $email = Email::create(\"mario@domain.com\")->clear();\n\n        $this->assertTrue($email->isEmpty());\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\" => \"abc\",\n            \"type\" => \"email\",\n            \"email\" => \"mario@domain.com\",\n        ];\n\n        $email = Email::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $email->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n    }\n\n    public function test_is_empty(): void\n    {\n        $array = [\n            \"id\" => \"abc\",\n            \"type\" => \"email\",\n            \"email\" => null,\n        ];\n\n        $email = Email::fromArray($array);\n\n        $this->assertTrue($email->isEmpty());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Pages/Properties/FilesTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Pages\\Properties;\n\nuse Notion\\Common\\File;\nuse Notion\\Pages\\Properties\\PropertyFactory;\nuse Notion\\Pages\\Properties\\Files;\nuse Notion\\Pages\\Properties\\PropertyType;\nuse PHPUnit\\Framework\\TestCase;\n\nclass FilesTest extends TestCase\n{\n    public function test_create(): void\n    {\n        $myFile = File::createExternal(\"https://example.com/image.png\");\n        $files = Files::create($myFile);\n\n        $this->assertEquals(PropertyType::Files, $files->metadata()->type);\n        $this->assertEquals(\"https://example.com/image.png\", $files->files[0]->url);\n    }\n\n    public function test_add_file(): void\n    {\n        $myFile1 = File::createExternal(\"https://example.com/image1.png\");\n        $myFile2 = File::createExternal(\"https://example.com/image2.png\");\n\n        $files = Files::create($myFile1)->addFile($myFile2);\n\n        $this->assertCount(2, $files->files);\n    }\n\n    public function test_change_files(): void\n    {\n        $myFile1 = File::createExternal(\"https://example.com/image1.png\");\n        $myFile2 = File::createExternal(\"https://example.com/image2.png\");\n\n        $files = Files::create($myFile1)->changeFiles($myFile2);\n\n        $this->assertCount(1, $files->files);\n        $this->assertEquals(\"https://example.com/image2.png\", $files->files[0]->url);\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\" => \"abc\",\n            \"type\" => \"files\",\n            \"files\" => [\n                [\n                    \"type\" => \"external\",\n                    \"name\" => \"Test file\",\n                    \"external\" => [\n                        \"url\"  => \"https://example.com/image.png\",\n                    ],\n                ],\n            ],\n        ];\n\n        $files = Files::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $files->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Pages/Properties/FormulaTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Pages\\Properties;\n\nuse DateTimeImmutable;\nuse Notion\\Pages\\Properties\\PropertyFactory;\nuse Notion\\Pages\\Properties\\Formula;\nuse Notion\\Pages\\Properties\\FormulaType;\nuse Notion\\Pages\\Properties\\PropertyType;\nuse PHPUnit\\Framework\\TestCase;\n\nclass FormulaTest extends TestCase\n{\n    public function test_string_from_array(): void\n    {\n        $array = [\n            \"id\" => \"a7ede3b7-c7ae-4eb8-b415-a7f80ac4dfe5\",\n            \"type\" => \"formula\",\n            \"formula\" => [\n                \"type\" => \"string\",\n                \"string\" => \"Formula result\",\n            ],\n        ];\n\n        $formula = Formula::fromArray($array);\n\n        $this->assertEquals(PropertyType::Formula, $formula->metadata()->type);\n        $this->assertEquals($array, $formula->toArray());\n        $this->assertEquals($array, PropertyFactory::fromArray($array)->toArray());\n        $this->assertEquals(FormulaType::String, $formula->type);\n        $this->assertEquals(\"Formula result\", $formula->string);\n    }\n\n    public function test_number_from_array(): void\n    {\n        $array = [\n            \"id\" => \"a7ede3b7-c7ae-4eb8-b415-a7f80ac4dfe5\",\n            \"type\" => \"formula\",\n            \"formula\" => [\n                \"type\" => \"number\",\n                \"number\" => 123,\n            ],\n        ];\n\n        $formula = Formula::fromArray($array);\n\n        $this->assertEquals($array, $formula->toArray());\n        $this->assertEquals($array, PropertyFactory::fromArray($array)->toArray());\n        $this->assertEquals(FormulaType::Number, $formula->type);\n        $this->assertEquals(123, $formula->number);\n    }\n\n    public function test_boolean_from_array(): void\n    {\n        $array = [\n            \"id\" => \"a7ede3b7-c7ae-4eb8-b415-a7f80ac4dfe5\",\n            \"type\" => \"formula\",\n            \"formula\" => [\n                \"type\" => \"boolean\",\n                \"boolean\" => false,\n            ],\n        ];\n\n        $formula = Formula::fromArray($array);\n\n        $this->assertEquals($array, $formula->toArray());\n        $this->assertEquals($array, PropertyFactory::fromArray($array)->toArray());\n        $this->assertEquals(FormulaType::Boolean, $formula->type);\n        $this->assertEquals(false, $formula->boolean);\n    }\n\n    public function test_date_from_array(): void\n    {\n        $array = [\n            \"id\" => \"a7ede3b7-c7ae-4eb8-b415-a7f80ac4dfe5\",\n            \"type\" => \"formula\",\n            \"formula\" => [\n                \"type\" => \"date\",\n                \"date\" => [\n                    \"start\" => \"2021-01-01T00:00:00.000000Z\",\n                    \"end\" => null,\n                ],\n            ],\n        ];\n\n        $formula = Formula::fromArray($array);\n\n        $this->assertEquals($array, $formula->toArray());\n        $this->assertEquals($array, PropertyFactory::fromArray($array)->toArray());\n        $this->assertTrue($formula->type === FormulaType::Date);\n        $this->assertEquals(new DateTimeImmutable(\"2021-01-01T00:00:00.000000Z\"), $formula->date?->start);\n        $this->assertNull($formula->date?->end);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Pages/Properties/LastEditedByTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Pages\\Properties;\n\nuse Notion\\Pages\\Properties\\LastEditedBy;\nuse Notion\\Pages\\Properties\\PropertyFactory;\nuse Notion\\Pages\\Properties\\PropertyType;\nuse PHPUnit\\Framework\\TestCase;\n\nclass LastEditedByTest extends TestCase\n{\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\" => \"abc\",\n            \"type\" => \"last_edited_by\",\n            \"last_edited_by\" => [\n                \"object\"     => \"user\",\n                \"id\" => \"62e1fd10-8b04-41eb-97c1-d2deddd160d4\",\n                \"name\" => \"Mario\",\n                \"type\" => \"person\",\n                \"person\" => [ \"email\" => \"mario@domain.com\" ],\n            ],\n        ];\n\n        $createdBy = LastEditedBy::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $createdBy->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n        $this->assertEquals(\"Mario\", $createdBy->user->name);\n        $this->assertEquals(PropertyType::LastEditedBy, $createdBy->metadata()->type);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Pages/Properties/LastEditedTimeTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Pages\\Properties;\n\nuse Notion\\Pages\\Properties\\LastEditedTime;\nuse Notion\\Pages\\Properties\\PropertyFactory;\nuse Notion\\Pages\\Properties\\PropertyType;\nuse PHPUnit\\Framework\\TestCase;\n\nclass LastEditedTimeTest extends TestCase\n{\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\" => \"abc\",\n            \"type\" => \"last_edited_time\",\n            \"last_edited_time\" => \"2021-01-01T00:00:00.000000Z\",\n        ];\n\n        $time = LastEditedTime::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $time->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n        $this->assertEquals(PropertyType::LastEditedTime, $time->metadata()->type);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Pages/Properties/MultiSelectTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Pages\\Properties;\n\nuse Notion\\Databases\\Properties\\SelectOption;\nuse Notion\\Pages\\Properties\\PropertyFactory;\nuse Notion\\Pages\\Properties\\MultiSelect;\nuse Notion\\Pages\\Properties\\PropertyType;\nuse PHPUnit\\Framework\\TestCase;\n\nclass MultiSelectTest extends TestCase\n{\n    public function test_create_from_ids(): void\n    {\n        $id1 = \"d69f85ae-9425-4851-beeb-4b4831f5786c\";\n        $id2 = \"af65b5c5-8034-4ef9-91af-05b8bc01642e\";\n\n        $multiSelect = MultiSelect::fromIds($id1, $id2);\n\n        $this->assertEquals($id1, $multiSelect->options[0]->id);\n        $this->assertEquals($id2, $multiSelect->options[1]->id);\n\n        $this->assertEquals(PropertyType::MultiSelect, $multiSelect->metadata()->type);\n    }\n\n    public function test_create_from_names(): void\n    {\n        $optionA = \"Option A\";\n        $optionC = \"Option C\";\n\n        $multiSelect = MultiSelect::fromNames($optionA, $optionC);\n\n        $this->assertEquals($optionA, $multiSelect->options[0]->name);\n        $this->assertEquals($optionC, $multiSelect->options[1]->name);\n    }\n\n    public function test_create_from_options(): void\n    {\n        $optionA = SelectOption::fromName(\"Option A\");\n        $optionB = SelectOption::fromName(\"Option B\");\n\n        $multiSelect = MultiSelect::fromOptions($optionA, $optionB);\n\n        $this->assertCount(2, $multiSelect->options);\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\" => \"931db25b-f8af-4fc0-b7bf-eb9c29de6b87\",\n            \"type\" => \"multi_select\",\n            \"multi_select\" => [\n                [ \"name\" => \"Option A\", \"color\" => \"red\" ],\n                [ \"name\" => \"Option C\", \"color\" => \"blue\" ],\n            ],\n        ];\n\n        $multiSelect = MultiSelect::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $multiSelect->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n    }\n\n    public function test_add_option(): void\n    {\n        $multiSelect = MultiSelect::fromNames(\"Option A\", \"Option B\")\n            ->addOption(SelectOption::fromName(\"Option C\"));\n\n        $this->assertCount(3, $multiSelect->options);\n    }\n\n    public function test_remove_options(): void\n    {\n        $optionA = SelectOption::fromId(\"123\");\n        $optionB = SelectOption::fromId(\"456\");\n        $multiSelect = MultiSelect::fromOptions($optionA, $optionB);\n\n        $multiSelect = $multiSelect->removeOption(\"123\");\n\n        $this->assertCount(1, $multiSelect->options);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Pages/Properties/NumberTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Pages\\Properties;\n\nuse Notion\\Common\\RichText;\nuse Notion\\Pages\\Properties\\PropertyFactory;\nuse Notion\\Pages\\Properties\\Number;\nuse Notion\\Pages\\Properties\\PropertyType;\nuse PHPUnit\\Framework\\TestCase;\n\nclass NumberTest extends TestCase\n{\n    public function test_create(): void\n    {\n        $number = Number::create(123);\n\n        $this->assertEquals(123, $number->number);\n        $this->assertEquals(\"\", $number->metadata()->id);\n        $this->assertEquals(PropertyType::Number, $number->metadata()->type);\n    }\n\n    public function test_create_empty(): void\n    {\n        $number = Number::createEmpty();\n\n        $this->assertTrue($number->isEmpty());\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\"    => \"a7ede3b7-c7ae-4eb8-b415-a7f80ac4dfe5\",\n            \"type\"  => \"number\",\n            \"number\" => 123,\n        ];\n\n        $number = Number::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $number->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n    }\n\n    public function test_change_value(): void\n    {\n        $number = Number::create(123)->changeNumber(0.25);\n        $this->assertEquals(0.25, $number->number);\n    }\n\n    public function test_clear(): void\n    {\n        $number = Number::create(123)->clear();\n\n        $this->assertTrue($number->isEmpty());\n    }\n\n    public function test_is_empty(): void\n    {\n        $array = [\n            \"id\"    => \"a7ede3b7-c7ae-4eb8-b415-a7f80ac4dfe5\",\n            \"type\"  => \"number\",\n            \"number\" => null,\n        ];\n\n        $number = Number::fromArray($array);\n\n        $this->assertTrue($number->isEmpty());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Pages/Properties/PeopleTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Pages\\Properties;\n\nuse Notion\\Pages\\Properties\\PropertyFactory;\nuse Notion\\Pages\\Properties\\People;\nuse Notion\\Pages\\Properties\\PropertyType;\nuse Notion\\Users\\User;\nuse PHPUnit\\Framework\\TestCase;\n\nclass PeopleTest extends TestCase\n{\n    public function test_create(): void\n    {\n        $user1 = $this->user1();\n        $user2 = $this->user2();\n\n        $people = People::create($user1, $user2);\n\n        $this->assertEquals([ $user1, $user2 ], $people->users);\n        $this->assertTrue($people->metadata()->type === PropertyType::People);\n    }\n\n    public function test_replace_users(): void\n    {\n        $user1 = $this->user1();\n        $user2 = $this->user2();\n\n        $people = People::create($user1)->changePeople($user2);\n\n        $this->assertEquals([ $user2 ], $people->users);\n    }\n\n    public function test_add_user(): void\n    {\n        $user1 = $this->user1();\n        $user2 = $this->user2();\n\n        $people = People::create($user1)->addPerson($user2);\n\n        $this->assertEquals([ $user1, $user2 ], $people->users);\n    }\n\n    public function test_remove_user(): void\n    {\n        $user = $this->user1();\n\n        $people = People::create($user);\n        $people = $people->removePerson($user->id);\n\n        $this->assertCount(0, $people->users);\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\" => \"abc\",\n            \"type\" => \"people\",\n            \"people\" => [\n                [ \"id\" => $this->user1()->id ],\n                [ \"id\" => $this->user2()->id ],\n            ],\n        ];\n\n        $people = People::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $people->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n    }\n\n    private function user1(): User\n    {\n        return User::fromArray([\n            \"object\"     => \"user\",\n            \"id\" => \"f98bfb6a-08b3-4e65-861b-6f68fb0c7a48\",\n            \"name\" => \"Mario\",\n            \"type\" => \"person\",\n            \"person\" => [ \"email\" => \"mario@website.domain\" ],\n        ]);\n    }\n\n    private function user2(): User\n    {\n        return User::fromArray([\n            \"object\"     => \"user\",\n            \"id\" => \"f98bfb6a-08b3-4e65-861b-6f68fb0c7a48\",\n            \"name\" => \"Luigi\",\n            \"type\" => \"person\",\n            \"person\" => [ \"email\" => \"luigi@website.domain\" ],\n        ]);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Pages/Properties/PhoneNumberTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Pages\\Properties;\n\nuse Notion\\Pages\\Properties\\PhoneNumber;\nuse Notion\\Pages\\Properties\\PropertyFactory;\nuse Notion\\Pages\\Properties\\PropertyType;\nuse PHPUnit\\Framework\\TestCase;\n\nclass PhoneNumberTest extends TestCase\n{\n    public function test_create(): void\n    {\n        $phone = PhoneNumber::create(\"415-000-1111\");\n\n        $this->assertTrue($phone->metadata()->type === PropertyType::PhoneNumber);\n        $this->assertEquals(\"415-000-1111\", $phone->phone);\n    }\n\n    public function test_create_empty(): void\n    {\n        $phone = PhoneNumber::createEmpty();\n\n        $this->assertTrue($phone->isEmpty());\n    }\n\n    public function test_change_phone(): void\n    {\n        $phone = PhoneNumber::create(\"415-000-1111\")->changePhone(\"415-000-2222\");\n\n        $this->assertEquals(\"415-000-2222\", $phone->phone);\n    }\n\n    public function test_clear(): void\n    {\n        $phone = PhoneNumber::create(\"415-000-1111\")->clear();\n\n        $this->assertTrue($phone->isEmpty());\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\" => \"abc\",\n            \"type\" => \"phone_number\",\n            \"phone_number\" => \"415-000-1111\",\n        ];\n\n        $phone = PhoneNumber::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $phone->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n    }\n\n    public function test_is_empty(): void\n    {\n        $array = [\n            \"id\" => \"abc\",\n            \"type\" => \"phone_number\",\n            \"phone_number\" => null,\n        ];\n\n        $phone = PhoneNumber::fromArray($array);\n\n        $this->assertTrue($phone->isEmpty());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Pages/Properties/PropertyCollectionTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Pages\\Properties;\n\nuse DateTimeImmutable;\nuse Exception;\nuse Notion\\Pages\\Properties\\PropertyCollection;\nuse Notion\\Pages\\Properties;\nuse PHPUnit\\Framework\\TestCase;\nuse TypeError;\n\nclass PropertyCollectionTest extends TestCase\n{\n    public function test_create(): void\n    {\n        $properties = [\n            \"Product\" => Properties\\RichTextProperty::fromString(\"Apple\"),\n            \"Price\" => Properties\\Number::create(4.99),\n        ];\n\n        $collection = PropertyCollection::create($properties);\n\n        $this->assertCount(2, $collection->getAll());\n    }\n\n    public function test_add(): void\n    {\n        $properties = [\n            \"Product\" => Properties\\RichTextProperty::fromString(\"Apple\"),\n            \"Price\" => Properties\\Number::create(4.99),\n        ];\n\n        $c = PropertyCollection::create($properties)\n            ->add(\"Quantity\", Properties\\Number::create(3));\n\n        $this->assertSame(3, $c->getNumber(\"Quantity\")->number);\n    }\n\n    public function test_change(): void\n    {\n        $c = PropertyCollection::create([\n            \"Product\" => Properties\\RichTextProperty::fromString(\"Apple\"),\n            \"Price\" => Properties\\Number::create(4.99),\n        ])->change(\"Price\", Properties\\Number::create(3.99));\n\n        $this->assertSame(3.99, $c->getNumber(\"Price\")->number);\n    }\n\n    public function test_get(): void\n    {\n        $c = PropertyCollection::create([\n            \"Product\" => Properties\\RichTextProperty::fromString(\"Apple\"),\n            \"Price\" => Properties\\Number::create(4.99),\n        ]);\n\n        /** @var Properties\\RichTextProperty */\n        $prop = $c->get(\"Product\");\n\n        $this->assertSame(\"Apple\", $prop->toString());\n    }\n\n    public function test_get_not_found(): void\n    {\n        $c = PropertyCollection::create([]);\n\n        $this->expectException(Exception::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $c->get(\"Product\");\n    }\n\n    public function test_get_by_id(): void\n    {\n        $prop = Properties\\Number::fromArray([\n            \"id\" => \"abc123\",\n            \"type\" => \"number\",\n            \"number\" => 42,\n        ]);\n\n        $c = PropertyCollection::create([ \"Answer\" => $prop ]);\n\n        $this->assertSame($prop, $c->getById(\"abc123\"));\n    }\n\n    public function test_get_by_id_not_found(): void\n    {\n        $c = PropertyCollection::create([]);\n\n        $this->expectException(Exception::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $c->getById(\"abc\");\n    }\n\n    public function test_title(): void\n    {\n        $c = PropertyCollection::create([\n            \"Title\" => Properties\\Title::fromString(\"Avatar\")\n        ]);\n\n        $this->assertSame(\"Avatar\", $c->title()?->toString());\n    }\n\n    public function test_null_title(): void\n    {\n        $c = PropertyCollection::create([]);\n\n        $this->assertNull($c->title());\n    }\n\n    public function test_get_typed_wrong_type(): void\n    {\n        $prop = Properties\\Checkbox::createChecked();\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->expectException(TypeError::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $c->getNumber(\"Name\");\n    }\n\n    public function test_get_typed_by_id_wrong_type(): void\n    {\n        $prop = Properties\\Checkbox::fromArray([\n            \"id\" => \"abc\",\n            \"type\" => \"checkbox\",\n            \"checkbox\" => true,\n        ]);\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->expectException(TypeError::class);\n        /** @psalm-suppress UnusedMethodCall */\n        $c->getNumberById(\"abc\");\n    }\n\n    public function test_get_checkbox_by_id(): void\n    {\n        $prop = Properties\\Checkbox::fromArray([\n            \"id\" => \"abc\",\n            \"type\" => \"checkbox\",\n            \"checkbox\" => true,\n        ]);\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getCheckboxById(\"abc\"));\n    }\n\n    public function test_get_checkbox(): void\n    {\n        $prop = Properties\\Checkbox::createChecked();\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getCheckbox(\"Name\"));\n    }\n\n    public function test_get_created_by_by_id(): void\n    {\n        $prop = Properties\\CreatedBy::fromArray([\n            \"id\" => \"abc\",\n            \"type\" => \"created_by\",\n            \"created_by\" => [\n                \"id\" => \"62e1fd10-8b04-41eb-97c1-d2deddd160d4\",\n                \"name\" => \"Mario\",\n                \"avatar_url\" => null,\n                \"type\" => \"person\",\n                \"person\" => [ \"email\" => \"mario@domain.com\" ],\n            ],\n        ]);\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getCreatedByById(\"abc\"));\n    }\n\n    public function test_get_created_by(): void\n    {\n        $prop = Properties\\CreatedBy::fromArray([\n            \"id\" => \"abc\",\n            \"type\" => \"created_by\",\n            \"created_by\" => [\n                \"id\" => \"62e1fd10-8b04-41eb-97c1-d2deddd160d4\",\n                \"name\" => \"Mario\",\n                \"avatar_url\" => null,\n                \"type\" => \"person\",\n                \"person\" => [ \"email\" => \"mario@domain.com\" ],\n            ],\n        ]);\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getCreatedBy(\"Name\"));\n    }\n\n    public function test_get_created_time_by_id(): void\n    {\n        $prop = Properties\\CreatedTime::fromArray([\n            \"id\" => \"abc\",\n            \"type\" => \"created_time\",\n            \"created_time\" => \"2021-01-01T00:00:00.000000Z\",\n        ]);\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getCreatedTimeById(\"abc\"));\n    }\n\n    public function test_get_created_time(): void\n    {\n        $prop = Properties\\CreatedTime::create(new DateTimeImmutable(\"2023-01-01\"));\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getCreatedTime(\"Name\"));\n    }\n\n    public function test_get_date_by_id(): void\n    {\n        $prop = Properties\\Date::fromArray([\n            \"id\"   => \"a7ede3b7-c7ae-4eb8-b415-a7f80ac4dfe5\",\n            \"type\" => \"date\",\n            \"date\" => [\n                \"start\" => \"2021-01-01T00:00:00.000000Z\",\n                \"end\"   => \"2021-12-31T00:00:00.000000Z\",\n            ],\n        ]);\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getDateById(\"a7ede3b7-c7ae-4eb8-b415-a7f80ac4dfe5\"));\n    }\n\n    public function test_get_date(): void\n    {\n        $prop = Properties\\Date::create(new DateTimeImmutable(\"2023-01-01\"));\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getDate(\"Name\"));\n    }\n\n    public function test_get_email_by_id(): void\n    {\n        $prop = Properties\\Email::fromArray([\n            \"id\" => \"abc\",\n            \"type\" => \"email\",\n            \"email\" => \"mario@domain.com\",\n        ]);\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getEmailById(\"abc\"));\n    }\n\n    public function test_get_email(): void\n    {\n        $prop = Properties\\Email::create(\"test@example.com\");\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getEmail(\"Name\"));\n    }\n\n    public function test_get_files_by_id(): void\n    {\n        $prop = Properties\\Files::fromArray([\n            \"id\" => \"abc\",\n            \"type\" => \"files\",\n            \"files\" => [\n                [\n                    \"type\" => \"external\",\n                    \"external\" => [\n                        \"url\"  => \"https://example.com/image.png\",\n                    ],\n                ],\n            ],\n        ]);\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getFilesById(\"abc\"));\n    }\n\n    public function test_get_files(): void\n    {\n        $prop = Properties\\Files::create();\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getFiles(\"Name\"));\n    }\n\n    public function test_get_formula_by_id(): void\n    {\n        $prop = Properties\\Formula::fromArray([\n            \"id\" => \"a7ede3b7-c7ae-4eb8-b415-a7f80ac4dfe5\",\n            \"type\" => \"formula\",\n            \"formula\" => [\n                \"type\" => \"number\",\n                \"number\" => 123,\n            ],\n        ]);\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getFormulaById(\"a7ede3b7-c7ae-4eb8-b415-a7f80ac4dfe5\"));\n    }\n\n    public function test_get_formula(): void\n    {\n        $prop = Properties\\Formula::fromArray([\n            \"id\" => \"a7ede3b7-c7ae-4eb8-b415-a7f80ac4dfe5\",\n            \"type\" => \"formula\",\n            \"formula\" => [\n                \"type\" => \"string\",\n                \"string\" => \"Formula result\",\n            ],\n        ]);\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getFormula(\"Name\"));\n    }\n\n    public function test_get_last_edited_by_by_id(): void\n    {\n        $prop = Properties\\LastEditedBy::fromArray([\n            \"id\" => \"abc\",\n            \"type\" => \"last_edited_by\",\n            \"last_edited_by\" => [\n                \"id\" => \"62e1fd10-8b04-41eb-97c1-d2deddd160d4\",\n                \"name\" => \"Mario\",\n                \"avatar_url\" => null,\n                \"type\" => \"person\",\n                \"person\" => [ \"email\" => \"mario@domain.com\" ],\n            ],\n        ]);\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getLastEditedByById(\"abc\"));\n    }\n\n    public function test_get_last_edited_by(): void\n    {\n        $prop = Properties\\LastEditedBy::fromArray([\n            \"id\" => \"abc\",\n            \"type\" => \"last_edited_by\",\n            \"last_edited_by\" => [\n                \"id\" => \"62e1fd10-8b04-41eb-97c1-d2deddd160d4\",\n                \"name\" => \"Mario\",\n                \"avatar_url\" => null,\n                \"type\" => \"person\",\n                \"person\" => [ \"email\" => \"mario@domain.com\" ],\n            ],\n        ]);\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getLastEditedBy(\"Name\"));\n    }\n\n    public function test_get_last_edited_time_by_id(): void\n    {\n        $prop = Properties\\LastEditedTime::fromArray([\n            \"id\" => \"abc\",\n            \"type\" => \"last_edited_time\",\n            \"last_edited_time\" => \"2021-01-01T00:00:00.000000Z\",\n        ]);\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getLastEditedTimeById(\"abc\"));\n    }\n\n    public function test_get_last_edited_time(): void\n    {\n        $prop = Properties\\LastEditedTime::fromArray([\n            \"id\" => \"abc\",\n            \"type\" => \"last_edited_time\",\n            \"last_edited_time\" => \"2021-01-01T00:00:00.000000Z\",\n        ]);\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getLastEditedTime(\"Name\"));\n    }\n\n    public function test_get_multi_select_by_id(): void\n    {\n        $prop = Properties\\MultiSelect::fromArray([\n            \"id\" => \"931db25b-f8af-4fc0-b7bf-eb9c29de6b87\",\n            \"type\" => \"multi_select\",\n            \"multi_select\" => [\n                [ \"name\" => \"Option A\", \"color\" => \"red\" ],\n                [ \"name\" => \"Option C\", \"color\" => \"blue\" ],\n            ],\n        ]);\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getMultiSelectById(\"931db25b-f8af-4fc0-b7bf-eb9c29de6b87\"));\n    }\n\n    public function test_get_multi_select(): void\n    {\n        $prop = Properties\\MultiSelect::fromNames(\"Orange\", \"Banana\");\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getMultiSelect(\"Name\"));\n    }\n\n    public function test_get_number_by_id(): void\n    {\n        $prop = Properties\\Number::fromArray([\n            \"id\"    => \"a7ede3b7-c7ae-4eb8-b415-a7f80ac4dfe5\",\n            \"type\"  => \"number\",\n            \"number\" => 123,\n        ]);\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getNumberById(\"a7ede3b7-c7ae-4eb8-b415-a7f80ac4dfe5\"));\n    }\n\n    public function test_get_number(): void\n    {\n        $prop = Properties\\Number::create(123);\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getNumber(\"Name\"));\n    }\n\n    public function test_get_people_by_id(): void\n    {\n        $prop = Properties\\People::fromArray([\n            \"id\" => \"abc\",\n            \"type\" => \"people\",\n            \"people\" => [\n                [\n                    \"id\" => \"f98bfb6a-08b3-4e65-861b-6f68fb0c7a48\",\n                    \"name\" => \"Mario\",\n                    \"avatar_url\" => null,\n                    \"type\" => \"person\",\n                    \"person\" => [ \"email\" => \"mario@website.domain\" ],\n                ],\n            ],\n        ]);\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getPeopleById(\"abc\"));\n    }\n\n    public function test_get_people(): void\n    {\n        $prop = Properties\\People::create();\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getPeople(\"Name\"));\n    }\n\n    public function test_get_phone_number_by_id(): void\n    {\n        $prop = Properties\\PhoneNumber::fromArray([\n            \"id\" => \"abc\",\n            \"type\" => \"phone_number\",\n            \"phone_number\" => \"415-000-1111\",\n        ]);\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getPhoneNumberById(\"abc\"));\n    }\n\n    public function test_get_phone_number(): void\n    {\n        $prop = Properties\\PhoneNumber::create(\"+551140028922\");\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getPhoneNumber(\"Name\"));\n    }\n\n    public function test_get_relation_by_id(): void\n    {\n        $prop = Properties\\Relation::fromArray([\n            \"id\" => \"abc\",\n            \"type\" => \"relation\",\n            \"relation\" => [\n                [ \"id\" => \"264f3f43-3d87-4bb2-bb66-11812ab74eae\" ],\n                [ \"id\" => \"f3902b7f-e9e2-4406-8c3f-5d07dbc87d66\" ],\n            ],\n        ]);\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getRelationById(\"abc\"));\n    }\n\n    public function test_get_relation(): void\n    {\n        $prop = Properties\\Relation::create();\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getRelation(\"Name\"));\n    }\n\n    public function test_get_rich_text_by_id(): void\n    {\n        $prop = Properties\\RichTextProperty::fromArray([\n            \"id\"    => \"a7ede3b7-c7ae-4eb8-b415-a7f80ac4dfe5\",\n            \"type\"  => \"rich_text\",\n            \"rich_text\" => [[\n                \"plain_text\" => \"Dummy text\",\n                \"href\" => null,\n                \"annotations\" => [\n                    \"bold\"          => false,\n                    \"italic\"        => false,\n                    \"strikethrough\" => false,\n                    \"underline\"     => false,\n                    \"code\"          => false,\n                    \"color\"         => \"default\",\n                ],\n                \"type\" => \"text\",\n                \"text\" => [\n                    \"content\" => \"Dummy text\",\n                ],\n            ]],\n        ]);\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getRichTextById(\"a7ede3b7-c7ae-4eb8-b415-a7f80ac4dfe5\"));\n    }\n\n    public function test_get_rich_text(): void\n    {\n        $prop = Properties\\RichTextProperty::fromString(\"Hi!\");\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getRichText(\"Name\"));\n    }\n\n    public function test_get_select_by_id(): void\n    {\n        $prop = Properties\\Select::fromArray([\n            \"id\"     => \"a7ede3b7-c7ae-4eb8-b415-a7f80ac4dfe5\",\n            \"type\"   => \"select\",\n            \"select\" => [\n                \"name\"  => \"Option A\",\n                \"id\"    => \"ad762674-9280-444b-96a7-3a0fb0aefff9\",\n                \"color\" => \"default\",\n            ],\n        ]);\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getSelectById(\"a7ede3b7-c7ae-4eb8-b415-a7f80ac4dfe5\"));\n    }\n\n    public function test_get_select(): void\n    {\n        $prop = Properties\\Select::fromName(\"Blue\");\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getSelect(\"Name\"));\n    }\n\n    public function test_get_status_by_id(): void\n    {\n        $prop = Properties\\Status::fromArray([\n            \"id\"     => \"ec27421d-cd03-4843-b8e9-ea08702d54ac\",\n            \"type\"   => \"status\",\n            \"status\" => [\n                \"id\"    => \"032b00eb-228c-4ee3-ba1d-fb6e8a42cc95\",\n                \"name\"  => \"Done\",\n                \"color\" => \"default\"\n            ],\n        ]);\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getStatusById(\"ec27421d-cd03-4843-b8e9-ea08702d54ac\"));\n    }\n\n    public function test_get_status(): void\n    {\n        $prop = Properties\\Status::fromName(\"Done\");\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getStatus(\"Name\"));\n    }\n\n    public function test_get_url_by_id(): void\n    {\n        $prop = Properties\\Url::fromArray([\n            \"id\" => \"abc\",\n            \"type\" => \"url\",\n            \"url\" => \"https://notion.so\",\n        ]);\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getUrlById(\"abc\"));\n    }\n\n    public function test_get_url(): void\n    {\n        $prop = Properties\\Url::create(\"https://example.com\");\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getUrl(\"Name\"));\n    }\n\n    public function test_get_unique_id_by_id(): void\n    {\n        $prop = Properties\\UniqueId::fromArray([\n            \"id\" => \"abc\",\n            \"type\" => \"unique_id\",\n            \"unique_id\" => [\n                \"number\" => 123,\n                \"prefix\" => \"ISSUE\",\n            ],\n        ]);\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getUniqueIdById(\"abc\"));\n    }\n\n    public function test_get_unique_id(): void\n    {\n        $prop = Properties\\UniqueId::fromArray([\n            \"id\" => \"abc\",\n            \"type\" => \"unique_id\",\n            \"unique_id\" => [\n                \"number\" => 123,\n                \"prefix\" => \"ISSUE\",\n            ],\n        ]);\n\n        $c = PropertyCollection::create([ \"Name\" => $prop ]);\n\n        $this->assertSame($prop, $c->getUniqueId(\"Name\"));\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Pages/Properties/RelationTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Pages\\Properties;\n\nuse Notion\\Pages\\Properties\\PropertyFactory;\nuse Notion\\Pages\\Properties\\PropertyType;\nuse Notion\\Pages\\Properties\\Relation;\nuse PHPUnit\\Framework\\TestCase;\n\nclass RelationTest extends TestCase\n{\n    public function test_create(): void\n    {\n        $id1 = \"5604389a-8de1-4ba6-a07f-ca346ff98f00\";\n        $id2 = \"03d71291-5ca3-4daa-a1e8-f012d513e8c8\";\n\n        $relation = Relation::create($id1, $id2);\n\n        $this->assertEquals([$id1, $id2], $relation->pageIds);\n        $this->assertTrue($relation->metadata()->type === PropertyType::Relation);\n    }\n\n    public function test_replace_relation(): void\n    {\n        $id1 = \"5604389a-8de1-4ba6-a07f-ca346ff98f00\";\n        $id2 = \"03d71291-5ca3-4daa-a1e8-f012d513e8c8\";\n\n        $relation = Relation::create($id1)->changeRelations($id2);\n\n        $this->assertEquals([$id2], $relation->pageIds);\n    }\n\n    public function test_add_relation(): void\n    {\n        $id1 = \"5604389a-8de1-4ba6-a07f-ca346ff98f00\";\n        $id2 = \"03d71291-5ca3-4daa-a1e8-f012d513e8c8\";\n\n        $relation = Relation::create($id1)->addRelation($id2);\n\n        $this->assertEquals([$id1, $id2], $relation->pageIds);\n    }\n\n    public function test_remove_relation(): void\n    {\n        $id1 = \"5604389a-8de1-4ba6-a07f-ca346ff98f00\";\n        $id2 = \"03d71291-5ca3-4daa-a1e8-f012d513e8c8\";\n\n        $relation = Relation::create($id1, $id2);\n        $relation = $relation->removeRelation($id2);\n\n        $this->assertEquals([$id1], $relation->pageIds);\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\" => \"abc\",\n            \"type\" => \"relation\",\n            \"relation\" => [\n                [ \"id\" => \"264f3f43-3d87-4bb2-bb66-11812ab74eae\" ],\n                [ \"id\" => \"f3902b7f-e9e2-4406-8c3f-5d07dbc87d66\" ],\n            ],\n        ];\n\n        $relation = Relation::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $relation->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Pages/Properties/RichTextPropertyTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Pages\\Properties;\n\nuse Notion\\Common\\RichText;\nuse Notion\\Pages\\Properties\\PropertyFactory;\nuse Notion\\Pages\\Properties\\PropertyType;\nuse Notion\\Pages\\Properties\\RichTextProperty;\nuse PHPUnit\\Framework\\TestCase;\n\nclass RichTextPropertyTest extends TestCase\n{\n    public function test_create(): void\n    {\n        $text = RichTextProperty::fromText(RichText::fromString(\"Dummy text\"));\n\n        $this->assertEquals(\"Dummy text\", $text->text[0]->text?->content);\n        $this->assertEquals(\"\", $text->metadata()->id);\n        $this->assertTrue($text->metadata()->type === PropertyType::RichText);\n    }\n\n    public function test_create_empty(): void\n    {\n        $text = RichTextProperty::createEmpty();\n\n        $this->assertTrue($text->isEmpty());\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\"    => \"a7ede3b7-c7ae-4eb8-b415-a7f80ac4dfe5\",\n            \"type\"  => \"rich_text\",\n            \"rich_text\" => [[\n                \"plain_text\" => \"Dummy text\",\n                \"href\" => null,\n                \"annotations\" => [\n                    \"bold\"          => false,\n                    \"italic\"        => false,\n                    \"strikethrough\" => false,\n                    \"underline\"     => false,\n                    \"code\"          => false,\n                    \"color\"         => \"default\",\n                ],\n                \"type\" => \"text\",\n                \"text\" => [\n                    \"content\" => \"Dummy text\",\n                ],\n            ]],\n        ];\n\n        $text = RichTextProperty::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $text->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n    }\n\n    public function test_string_conversion(): void\n    {\n        $text = RichTextProperty::fromText(RichText::fromString(\"Dummy text\"));\n        $this->assertEquals(\"Dummy text\", $text->toString());\n    }\n\n    public function test_change_text(): void\n    {\n        $text = RichTextProperty::fromText()->changeText(\n            RichText::fromString(\"Dummy text\")\n        );\n        $this->assertEquals(\"Dummy text\", $text->toString());\n    }\n\n    public function test_clear(): void\n    {\n        $text = RichTextProperty::fromString(\"Dummy text\")->clear();\n\n        $this->assertTrue($text->isEmpty());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Pages/Properties/SelectTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Pages\\Properties;\n\nuse Notion\\Common\\Color;\nuse Notion\\Databases\\Properties\\SelectOption;\nuse Notion\\Pages\\Properties\\PropertyFactory;\nuse Notion\\Pages\\Properties\\PropertyType;\nuse Notion\\Pages\\Properties\\Select;\nuse PHPUnit\\Framework\\TestCase;\n\nclass SelectTest extends TestCase\n{\n    public function test_create_from_option_id(): void\n    {\n        $select = Select::fromId(\"e69017d3-9027-46c4-9b6f-490d243e459b\");\n\n        $this->assertEquals(\"e69017d3-9027-46c4-9b6f-490d243e459b\", $select->option?->id);\n        $this->assertNull($select->option?->name);\n        $this->assertEquals(\"\", $select->metadata()->id);\n        $this->assertTrue($select->metadata()->type === PropertyType::Select);\n    }\n\n    public function test_create_from_option_name(): void\n    {\n        $select = Select::fromName(\"Option A\");\n\n        $this->assertEquals(\"Option A\", $select->option?->name);\n        $this->assertNull($select->option?->id);\n        $this->assertEquals(\"\", $select->metadata()->id);\n        $this->assertTrue($select->metadata()->type === PropertyType::Select);\n    }\n\n    public function test_create_from_option(): void\n    {\n        $option = SelectOption::fromId(\"abc\");\n\n        $select = Select::fromOption($option);\n\n        $this->assertEquals(\"abc\", $select->option?->id);\n    }\n\n    public function test_create_empty(): void\n    {\n        $select = Select::createEmpty();\n\n        $this->assertTrue($select->isEmpty());\n    }\n\n    public function test_change_option(): void\n    {\n        $optionA = SelectOption::fromId(\"abc\");\n        $optionB = SelectOption::fromId(\"def\");\n\n        $select = Select::fromOption($optionA);\n        $select = $select->changeOption($optionB);\n\n        $this->assertEquals($optionB, $select->option);\n    }\n\n    public function test_clear(): void\n    {\n        $option = SelectOption::fromId(\"abc\");\n\n        $select = Select::fromOption($option)->clear();\n\n        $this->assertTrue($select->isEmpty());\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\"     => \"a7ede3b7-c7ae-4eb8-b415-a7f80ac4dfe5\",\n            \"type\"   => \"select\",\n            \"select\" => [\n                \"name\"  => \"Option A\",\n                \"id\"    => \"ad762674-9280-444b-96a7-3a0fb0aefff9\",\n                \"color\" => \"default\",\n            ],\n        ];\n\n        $select = Select::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $select->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n    }\n\n    public function test_is_empty(): void\n    {\n        $array = [\n            \"id\"     => \"a7ede3b7-c7ae-4eb8-b415-a7f80ac4dfe5\",\n            \"type\"   => \"select\",\n            \"select\" => null,\n        ];\n\n        $select = Select::fromArray($array);\n\n        $this->assertTrue($select->isEmpty());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Pages/Properties/StatusTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Pages\\Properties;\n\nuse Notion\\Common\\Color;\nuse Notion\\Databases\\Properties\\StatusOption;\nuse Notion\\Pages\\Properties\\PropertyType;\nuse Notion\\Pages\\Properties\\Status;\nuse PHPUnit\\Framework\\TestCase;\n\nclass StatusTest extends TestCase\n{\n    public function test_from_id(): void\n    {\n        $id = \"507b5b2c-5dc8-438b-ac5b-51d0c781ba65\";\n        $status = Status::fromId($id);\n\n        $this->assertSame($id, $status->option->id);\n    }\n\n    public function test_from_name(): void\n    {\n        $name = \"Done\";\n        $status = Status::fromName($name);\n\n        $this->assertSame($name, $status->option->name);\n    }\n\n    public function test_from_option(): void\n    {\n        $option = StatusOption::fromName(\"Done\");\n        $status = Status::fromOption($option);\n\n        $this->assertSame($option, $status->option);\n    }\n\n    public function test_change_color(): void\n    {\n        $color = Color::Green;\n\n        $status = Status::fromId(\"5fdb657e-27d1-4832-842e-32231952a560\")\n            ->changeColor($color);\n\n        $this->assertSame($color, $status->option->color);\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\"     => \"ec27421d-cd03-4843-b8e9-ea08702d54ac\",\n            \"type\"   => \"status\",\n            \"status\" => [\n                \"id\"    => \"032b00eb-228c-4ee3-ba1d-fb6e8a42cc95\",\n                \"name\"  => \"Done\",\n                \"color\" => \"default\"\n            ],\n        ];\n\n        $status = Status::fromArray($array);\n\n        $this->assertEquals($array, $status->toArray());\n        $this->assertSame(PropertyType::Status, $status->metadata()->type);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Pages/Properties/TitleTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Pages\\Properties;\n\nuse Notion\\Common\\RichText;\nuse Notion\\Pages\\Properties\\PropertyType;\nuse Notion\\Pages\\Properties\\Title;\nuse PHPUnit\\Framework\\TestCase;\n\nclass TitleTest extends TestCase\n{\n    public function test_create(): void\n    {\n        $title = Title::fromText(RichText::fromString(\"Dummy title\"));\n\n        $this->assertEquals(\"Dummy title\", $title->title[0]->text?->content);\n        $this->assertEquals(\"title\", $title->metadata()->id);\n        $this->assertTrue($title->metadata()->type === PropertyType::Title);\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\"    => \"title\",\n            \"type\"  => \"title\",\n            \"title\" => [[\n                \"plain_text\" => \"Dummy title\",\n                \"href\" => null,\n                \"annotations\" => [\n                    \"bold\"          => false,\n                    \"italic\"        => false,\n                    \"strikethrough\" => false,\n                    \"underline\"     => false,\n                    \"code\"          => false,\n                    \"color\"         => \"default\",\n                ],\n                \"type\" => \"text\",\n                \"text\" => [\n                    \"content\" => \"Dummy title\",\n                ],\n            ]],\n        ];\n\n        $title = Title::fromArray($array);\n        $this->assertEquals($array, $title->toArray());\n    }\n\n    public function test_string_conversion(): void\n    {\n        $title = Title::fromText(RichText::fromString(\"Dummy title\"));\n        $this->assertEquals(\"Dummy title\", $title->toString());\n    }\n\n    public function test_change_text(): void\n    {\n        $title = Title::fromText()->change(\n            RichText::fromString(\"Dummy title\")\n        );\n        $this->assertEquals(\"Dummy title\", $title->toString());\n    }\n\n    public function test_is_empty_on_empty_string(): void\n    {\n        $this->assertTrue(Title::fromString(\"\")->isEmpty());\n    }\n\n    public function test_is_empty_on_no_rich_text(): void\n    {\n        $this->assertTrue(Title::fromText()->isEmpty());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Pages/Properties/UniqueIdTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Pages\\Properties;\n\nuse Notion\\Pages\\Properties\\PropertyFactory;\nuse Notion\\Pages\\Properties\\PropertyType;\nuse Notion\\Pages\\Properties\\UniqueId;\nuse PHPUnit\\Framework\\TestCase;\n\nclass UniqueIdTest extends TestCase\n{\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\" => \"abc\",\n            \"type\" => \"unique_id\",\n            \"unique_id\" => [\n                \"number\" => 3,\n                \"prefix\" => \"ISSUE\"\n            ]\n        ];\n\n        $prop = UniqueId::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $prop->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n\n        $this->assertSame(\"ISSUE\", $prop->prefix);\n        $this->assertSame(3, $prop->number);\n        $this->assertSame(PropertyType::UniqueId, $prop->metadata()->type);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Pages/Properties/UnknownTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Pages\\Properties;\n\nuse Notion\\Pages\\Properties\\PropertyFactory;\nuse Notion\\Pages\\Properties\\PropertyType;\nuse Notion\\Pages\\Properties\\Unknown;\nuse PHPUnit\\Framework\\TestCase;\n\nclass UnknownTest extends TestCase\n{\n    public function test_serialization(): void\n    {\n        $array = [\n            \"id\"    => \"abc\",\n            \"type\"  => \"blabla\",\n            \"blabla\" => new \\stdClass(),\n        ];\n        $property = Unknown::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $property->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n        $this->assertSame(PropertyType::Unknown, $property->metadata()->type);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Pages/Properties/UrlTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Pages\\Properties;\n\nuse Notion\\Pages\\Properties\\Url;\nuse Notion\\Pages\\Properties\\PropertyFactory;\nuse Notion\\Pages\\Properties\\PropertyType;\nuse PHPUnit\\Framework\\TestCase;\n\nclass UrlTest extends TestCase\n{\n    public function test_create(): void\n    {\n        $url = Url::create(\"https://notion.so\");\n\n        $this->assertTrue($url->metadata()->type === PropertyType::Url);\n        $this->assertEquals(\"https://notion.so\", $url->url);\n    }\n\n    public function test_create_empty(): void\n    {\n        $url = Url::createEmpty();\n\n        $this->assertTrue($url->isEmpty());\n    }\n\n    public function test_change_url(): void\n    {\n        $url = Url::create(\"https://notion.so\")->changeUrl(\"https://google.com\");\n\n        $this->assertEquals(\"https://google.com\", $url->url);\n    }\n\n    public function test_clear(): void\n    {\n        $url = Url::create(\"https://notion.so\")->clear();\n\n        $this->assertTrue($url->isEmpty());\n    }\n\n    public function test_array_conversion(): void\n    {\n        $array = [\n            \"id\" => \"abc\",\n            \"type\" => \"url\",\n            \"url\" => \"https://notion.so\",\n        ];\n\n        $url = Url::fromArray($array);\n        $fromFactory = PropertyFactory::fromArray($array);\n\n        $this->assertEquals($array, $url->toArray());\n        $this->assertEquals($array, $fromFactory->toArray());\n    }\n\n    public function test_is_empty(): void\n    {\n        $array = [\n            \"id\" => \"abc\",\n            \"type\" => \"url\",\n            \"url\" => null,\n        ];\n\n        $url = Url::fromArray($array);\n\n        $this->assertTrue($url->isEmpty());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Search/FilterTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Search;\n\nuse Notion\\Search\\Filter;\nuse Notion\\Search\\FilterProperty;\nuse Notion\\Search\\FilterValue;\nuse PHPUnit\\Framework\\TestCase;\n\nclass FilterTest extends TestCase\n{\n    public function test_by_pages(): void\n    {\n        $f = Filter::byPages();\n\n        $this->assertSame(FilterValue::Page, $f->value);\n        $this->assertSame(FilterProperty::Object, $f->property);\n    }\n\n    public function test_by_databases(): void\n    {\n        $f = Filter::byDatabases();\n\n        $this->assertSame(FilterValue::Database, $f->value);\n        $this->assertSame(FilterProperty::Object, $f->property);\n    }\n\n    public function test_to_array(): void\n    {\n        $f = Filter::byPages();\n\n        $expected = [\n            \"value\"    => \"page\",\n            \"property\" => \"object\",\n        ];\n        $this->assertSame($expected, $f->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Search/QueryTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Search;\n\nuse Notion\\Search\\FilterValue;\nuse Notion\\Search\\Query;\nuse Notion\\Search\\SortDirection;\nuse PHPUnit\\Framework\\TestCase;\n\nclass QueryTest extends TestCase\n{\n    public function test_term(): void\n    {\n        $q = Query::title(\"Search term\");\n\n        $this->assertSame(\"Search term\", $q->query);\n        $this->assertNull($q->filter);\n        $this->assertNull($q->sort);\n        $this->assertNull($q->startCursor);\n        $this->assertNull($q->pageSize);\n    }\n\n    public function test_query_all(): void\n    {\n        $q = Query::all();\n\n        $this->assertNull($q->query);\n    }\n\n    public function test_filter_by_pages(): void\n    {\n        $q = Query::title(\"Term\")->filterByPages();\n\n        $this->assertSame(FilterValue::Page, $q->filter?->value);\n    }\n\n    public function test_filter_by_databases(): void\n    {\n        $q = Query::title(\"Term\")->filterByDatabases();\n\n        $this->assertSame(FilterValue::Database, $q->filter?->value);\n    }\n\n    public function test_sort_ascending(): void\n    {\n        $q = Query::title(\"Term\")->sortByLastEditedTime(SortDirection::Ascending);\n\n        $this->assertSame(SortDirection::Ascending, $q->sort?->direction);\n    }\n\n    public function test_sort_descending(): void\n    {\n        $q = Query::title(\"Term\")->sortByLastEditedTime(SortDirection::Descending);\n\n        $this->assertSame(SortDirection::Descending, $q->sort?->direction);\n    }\n\n    public function test_change_start_cursor(): void\n    {\n        $q = Query::title(\"Term\")->changeStartCursor(\"abc123\");\n\n        $this->assertSame(\"abc123\", $q->startCursor);\n    }\n\n    public function test_change_page_size(): void\n    {\n        $q = Query::title(\"Term\")->changePageSize(3);\n\n        $this->assertSame(3, $q->pageSize);\n    }\n\n    public function test_to_array(): void\n    {\n        $q = Query::title(\"Term\")\n            ->filterByPages()\n            ->sortByLastEditedTime(SortDirection::Ascending)\n            ->changeStartCursor(\"abc123\")\n            ->changePageSize(10);\n\n        $expected = [\n            \"query\" => \"Term\",\n            \"filter\" => [\n                \"value\" => \"page\",\n                \"property\" => \"object\",\n            ],\n            \"sort\" => [\n                \"direction\" => \"ascending\",\n                \"timestamp\" => \"last_edited_time\",\n            ],\n            \"start_cursor\" => \"abc123\",\n            \"page_size\" => 10,\n        ];\n        $this->assertSame($expected, $q->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Search/SortTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Search;\n\nuse Notion\\Search\\Sort;\nuse Notion\\Search\\SortDirection;\nuse Notion\\Search\\SortTimestamp;\nuse PHPUnit\\Framework\\TestCase;\n\nclass SortTest extends TestCase\n{\n    public function test_create(): void\n    {\n        $s = Sort::create();\n\n        $this->assertSame(SortDirection::Descending, $s->direction);\n        $this->assertSame(SortTimestamp::LastEditedTime, $s->timestamp);\n    }\n\n    public function test_by_last_edited_time(): void\n    {\n        $s = Sort::create()->byLastEditedTime();\n\n        $this->assertSame(SortTimestamp::LastEditedTime, $s->timestamp);\n    }\n\n    public function test_ascending(): void\n    {\n        $s = Sort::create()->ascending();\n\n        $this->assertSame(SortDirection::Ascending, $s->direction);\n    }\n\n    public function test_descending(): void\n    {\n        $s = Sort::create()->descending();\n\n        $this->assertSame(SortDirection::Descending, $s->direction);\n    }\n\n    public function test_to_array(): void\n    {\n        $s = Sort::create();\n\n        $expected = [\n            \"direction\" => \"descending\",\n            \"timestamp\" => \"last_edited_time\",\n        ];\n        $this->assertSame($expected, $s->toArray());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Users/UserTest.php",
    "content": "<?php\n\nnamespace Notion\\Test\\Unit\\Users;\n\nuse Notion\\Users\\User;\nuse Notion\\Users\\UserType;\nuse PHPUnit\\Framework\\TestCase;\n\nclass UserTest extends TestCase\n{\n    public function test_person_from_array(): void\n    {\n        $array = [\n            \"object\"     => \"user\",\n            \"id\"         => \"b0688871-85db-4637-8fc9-043a240fcaec\",\n            \"name\"       => \"Mario Simao\",\n            \"avatar_url\" => \"http://example.com\",\n            \"type\"       => \"person\",\n            \"person\"     => [ \"email\" => \"mariosimao@email.com\" ],\n        ];\n\n        $user = User::fromArray($array);\n\n        $this->assertEquals($array, $user->toArray());\n        $this->assertTrue($user->isPerson());\n        $this->assertEquals(\"b0688871-85db-4637-8fc9-043a240fcaec\", $user->id);\n        $this->assertEquals(\"Mario Simao\", $user->name);\n        $this->assertEquals(UserType::Person, $user->type);\n        $this->assertEquals(\"mariosimao@email.com\", $user->person?->email);\n    }\n\n    public function test_bot_from_array(): void\n    {\n        $array = [\n            \"object\"     => \"user\",\n            \"id\"         => \"b0688871-85db-4637-8fc9-043a240fcaec\",\n            \"name\"       => \"Notion Bot\",\n            \"type\"       => \"bot\",\n            \"bot\"        => [\n                \"object\" => \"bot\",\n                \"workspace_limits\" => [\n                    \"max_file_upload_size_in_bytes\" => 104857600,\n                ],\n            ],\n        ];\n\n        $user = User::fromArray($array);\n\n        $this->assertEquals($array, $user->toArray());\n        $this->assertTrue($user->isBot());\n        $this->assertNotNull($user->bot);\n        $this->assertNull($user->avatarUrl);\n    }\n\n    public function test_invalid_type_from_array(): void\n    {\n        $array = [\n            \"object\"     => \"user\",\n            \"id\"         => \"b0688871-85db-4637-8fc9-043a240fcaec\",\n            \"name\"       => \"Invalid user\",\n            \"type\"       => \"wrong-type\",\n        ];\n\n        $this->expectException(\\ValueError::class);\n        /** @psalm-suppress InvalidArgument */\n        User::fromArray($array);\n    }\n}\n"
  }
]