Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feature/notion models and commands #121

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants