Skip to content

Feature/notion models and commands #121

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Draft
wants to merge 82 commits into
base: dev
Choose a base branch
from

Conversation

johguentner
Copy link
Member

No description provided.

johguentner and others added 12 commits February 2, 2023 14:58
- command for creating NotionModels
- add Notion Models for simple interation with Notion databases
- add NotionQueryBuilder for simple query-behaviour for Notion databases (similar to eloquent)
- add filterbags instead of collections
- some entities have parents or expose user-ids, without additional information
- by resolving these within the "endpoint" (not a real notion enpoint) ``Resolve::class``, the additional information can be easily obtained
@what-the-diff
Copy link

what-the-diff bot commented Feb 16, 2023

  • Added a new artisan command to generate Notion Models
  • Fixed the issue with Text and Title properties not being converted to text when using asText() method on them
  • Created an abstract class for all notion models that will be generated by the artisan command in step 1 above, this is so we can have some common functionality between all of our models (like caching) without having to repeat ourselves over and over again.
  • Added a new class NotionQueryBuilder
  • This is used to query the notion database and return results as collections of models or json
  • The model classes can be configured with filters, sortings etc which are then applied when querying the db
  • Results from queries are cached for configurable durations (defaults to 1 hour) using Laravel's cache system

@johguentner johguentner added this to the 🍩 1.2.0 milestone Feb 16, 2023
johguentner and others added 27 commits May 2, 2023 14:12
- remove adding query to file-name
- instead add short hash of query to file-name
- or (if given) allow user to set a specific name for upcoming query
- additionally save header, method and payload within the snapshot
- polish `PestHttpRecorder::class`
- ... rebuild snapshots for comments (due to changed file-naming structure)
- specifically regarding type checking
@Gummibeer
Copy link
Contributor

Hey,
I'm working on something similar using calebporzio/sushi so a bit different approach. It loads the whole Notion database in SQLite and makes that one usable via Eloquent.

I have tried to find the spot where you cast the page properties to model attributes but can't find it!? 🤔
In case you are interested, here's my approach:

That's the trait with all the abstracted logic to link an eloquent model to a notion database.

<?php

namespace App\Models\Concerns;

use BackedEnum;
use FiveamCode\LaravelNotionApi\Endpoints\Database;
use FiveamCode\LaravelNotionApi\Entities\Collections\PageCollection;
use FiveamCode\LaravelNotionApi\Entities\Page;
use FiveamCode\LaravelNotionApi\Entities\Properties\Checkbox;
use FiveamCode\LaravelNotionApi\Entities\Properties\MultiSelect;
use FiveamCode\LaravelNotionApi\Entities\Properties\Number;
use FiveamCode\LaravelNotionApi\Entities\Properties\Property;
use FiveamCode\LaravelNotionApi\Entities\Properties\Relation;
use FiveamCode\LaravelNotionApi\Entities\Properties\Select;
use FiveamCode\LaravelNotionApi\Entities\Properties\Text;
use FiveamCode\LaravelNotionApi\Entities\Properties\Title;
use FiveamCode\LaravelNotionApi\Entities\Properties\Url;
use FiveamCode\LaravelNotionApi\Entities\PropertyItems\SelectItem;
use FiveamCode\LaravelNotionApi\Notion;
use FiveamCode\LaravelNotionApi\Query\StartCursor;
use Generator;
use GuzzleHttp\Psr7\Uri;
use Illuminate\Support\LazyCollection;
use OutOfRangeException;
use Ramsey\Uuid\Uuid;
use Sushi\Sushi;

trait HasNotionDatabase
{
    use Sushi;

    abstract protected function getNotionDatabaseId(): string;

    abstract protected function getNotionPageData(Page $page): array;

    protected function getNotionDatabase(): Database
    {
        return app(Notion::class)->database($this->getNotionDatabaseId());
    }

    protected function queryNotionDatabase(?StartCursor $cursor = null): PageCollection
    {
        return $cursor !== null
            ? $this->getNotionDatabase()->offset($cursor)->query()
            : $this->getNotionDatabase()->query();
    }

    protected function getNotionPages(): LazyCollection
    {
        return LazyCollection::make(function (): Generator {
            $cursor = null;

            do {
                $response = $this->queryNotionDatabase($cursor);
                $cursor = $response->nextCursor();
                $pages = $response->asCollection();

                foreach ($pages as $page) {
                    yield $page;
                }
            } while ($response->hasMoreEntries());
        });
    }

    protected function getNotionPropertyValue(Page $page, string $key): mixed
    {
        $property = $page->getProperty($key);

        return $property === null
            ? null
            : $this->castNotionProperty($property);
    }

    protected function castNotionProperty(Property $property): mixed
    {
        return match ($property::class) {
            Checkbox::class => $property->getContent(),
            Select::class => $property->getContent()->getName(),
            MultiSelect::class => $property->getContent()
                ->map(fn (SelectItem $item) => $item->getName())
                ->all(),
            Title::class, Text::class => $property->getContent()->getPlainText() ?: null,
            Url::class => new Uri($property->getContent()),
            Number::class => $property->getContent(),
            Relation::class => $property->getContent()
                ->pluck('id')
                ->map(fn (string $id) => Uuid::fromString($id))
                ->all(),
            default => throw new OutOfRangeException('Missing notion property cast for: '.$property::class),
        };
    }

    public function getRows(): array
    {
        return $this->getNotionPages()
            ->map($this->getNotionPageData(...))
            ->map(static function (array $attributes): array {
                return collect($attributes)
                    ->map(static function (mixed $value) {
                        if ($value === null) {
                            return null;
                        }

                        if (is_array($value)) {
                            return json_encode($value);
                        }

                        if ($value instanceof Uri) {
                            return (string) $value;
                        }

                        if ($value instanceof BackedEnum) {
                            return $value->value;
                        }

                        return $value;
                    })
                    ->all();
            })
            ->collect()
            ->all();
    }
}

And here's an example model - with working relation, casting and all we love about Eloquent:

<?php

namespace App\Models;

use App\Enums\Alignment;
use App\Enums\Race;
use App\Enums\Sex;
use App\Models\Concerns\HasNotionDatabase;
use App\Renderers\PageRenderer;
use Carbon\CarbonInterval;
use FiveamCode\LaravelNotionApi\Entities\Page;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;

class Character extends Model
{
    use HasNotionDatabase;

    protected $casts = [
        'aliases' => 'array',
        'race' => Race::class,
        'sex' => Sex::class,
        'alignment' => Alignment::class,
        'is_deceased' => 'bool',
        'born_at' => 'int',
        'died_at' => 'int',
    ];

    public function partner(): BelongsTo
    {
        return $this->belongsTo(Character::class, 'partner_id', 'id');
    }

    public function getNotionDatabaseId(): string
    {
        return 'c10fea27034e4de993201eb9323cd885';
    }

    public function getNotionPageData(Page $page): array
    {
        return [
            'id' => $page->getId(),
            'name' => $page->getTitle(),
            'aliases' => Str::of($this->getNotionPropertyValue($page, 'Alias'))
                ->explode(',')
                ->map(fn (string $alias): string => trim($alias))
                ->filter()
                ->all(),
            'race' => $this->getNotionPropertyValue($page, 'Race'),
            'sex' => $this->getNotionPropertyValue($page, 'Sex'),
            'alignment' => $this->getNotionPropertyValue($page, 'Alignment'),
            'is_deceased' => $this->getNotionPropertyValue($page, 'Deceased'),
            'born_at' => (int) $this->getNotionPropertyValue($page, 'Born at'),
            'died_at' => (int) $this->getNotionPropertyValue($page, 'Died at'),
            // relations
            'partner_id' => Arr::first($this->getNotionPropertyValue($page, 'Partner')),
            // links
            '5etools_url' => $this->getNotionPropertyValue($page, '5e.tools'),
            'fandom_url' => $this->getNotionPropertyValue($page, 'Fandom'),
            // content
            'content' => Cache::remember(
                key: "{$this->getTable()}.{$page->getId()}.content",
                ttl: CarbonInterval::day(),
                callback: fn () => PageRenderer::make($page)->render()
            ),
        ];
    }
}

@mechelon mechelon modified the milestones: 🍩 1.2.0, future-features Mar 5, 2025
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants