laravel-notion-api icon indicating copy to clipboard operation
laravel-notion-api copied to clipboard

Feature/notion models and commands

Open johguentner opened this issue 2 years ago • 2 comments
trafficstars

johguentner avatar Feb 16 '23 12:02 johguentner

  • 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

what-the-diff[bot] avatar Feb 16 '23 12:02 what-the-diff[bot]

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()
            ),
        ];
    }
}

Gummibeer avatar Jun 22 '23 11:06 Gummibeer