Skip to content

Commit

Permalink
Merge pull request #70 from thekid/refactor/import
Browse files Browse the repository at this point in the history
Speed up importing from local directory
  • Loading branch information
thekid authored Dec 31, 2024
2 parents d4227c2 + d3fcdfc commit fccf361
Show file tree
Hide file tree
Showing 18 changed files with 508 additions and 221 deletions.
2 changes: 1 addition & 1 deletion src/main/php/de/thekid/dialog/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public function routes() {
'/' => new Frontend(
new HandlersIn('de.thekid.dialog.web', $inject->get(...)),
new Handlebars($this->environment->path('src/main/handlebars'), [
new Dates(TimeZone::getByName('Europe/Berlin')),
new Dates(new TimeZone('Europe/Berlin')),
new Numbers(),
new Assets($manifest),
$inject->get(Helpers::class),
Expand Down
11 changes: 8 additions & 3 deletions src/main/php/de/thekid/dialog/Repository.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php namespace de\thekid\dialog;

use com\mongodb\result\{Cursor, Update, Modification};
use com\mongodb\result\{Cursor, Update, Modification, Delete};
use com\mongodb\{Database, Document};
use text\hash\Hashing;
use util\{Date, Secret};
Expand Down Expand Up @@ -135,9 +135,9 @@ public function entry(string $slug, bool $published= true): ?Document {
}

/** Returns an entry's children, latest first */
public function children(string $slug, array<string, mixed> $sort= ['date' => -1]): Cursor {
public function children(string $slug, bool $published= true, array<string, mixed> $sort= ['date' => -1]): Cursor {
return $this->database->collection('entries')->aggregate([
['$match' => ['parent' => $slug, 'published' => ['$lt' => Date::now()]]],
['$match' => ['parent' => $slug] + ($published ? ['published' => ['$lt' => Date::now()]] : [])],
['$unset' => '_searchable'],
['$sort' => $sort],
]);
Expand All @@ -159,4 +159,9 @@ public function modify(string $slug, array<string, mixed> $statements): Update {
$statements,
);
}

/** Delete an entry identified by a given slug */
public function delete(string $slug): Delete {
return $this->database->collection('entries')->delete(['slug' => $slug]);
}
}
76 changes: 44 additions & 32 deletions src/main/php/de/thekid/dialog/api/Entries.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,33 @@
use de\thekid\dialog\{Repository, Storage};
use io\File;
use util\Date;
use web\rest\{Async, Delete, Entity, Put, Resource, Request, Response, Value};
use web\rest\{Async, Delete, Entity, Patch, Put, Resource, Param, Request, Response, Value, SeparatedBy};

#[Resource('/api/entries')]
class Entries {

public function __construct(private Repository $repository, private Storage $storage) { }

#[Put('/{id:.+(/.+)?}')]
public function create(#[Value] $user, string $id, #[Entity] array<string, mixed> $attributes) {

// Join places, autocomplete seems to only work for strings
$suggest= '';
foreach ($attributes['locations'] as $location) {
$suggest.= ' '.$location['name'];
}

$result= $this->repository->replace($id, [
'parent' => $attributes['parent'] ?? null,
'date' => new Date($attributes['date']),
'title' => $attributes['title'],
'keywords' => $attributes['keywords'],
'locations' => $attributes['locations'],
'content' => $attributes['content'],
'is' => $attributes['is'],
'_searchable' => [
'boost' => isset($attributes['is']['journey']) ? 2.0 : 1.0,
'suggest' => trim($suggest),
'content' => strip_tags(strtr($attributes['content'], ['<br>' => "\n", '</p><p>' => "\n"]))
],
]);
public function upsert(#[Value] $user, string $id, #[Param, SeparatedBy(',')] array<string> $expand= []) {
if ($document= $this->repository->entry($id, published: false)) {

// Expand requested properties by performing lookups
foreach ($expand as $selector) {
$document[$selector]= match ($selector) {
'$children' => $this->repository->children($document['slug'], published: false)->all(),
};
}
return $document;
} else {
$result= $this->repository->replace($id, ['modified' => time()]);

// Ensure storage directory is created
if ($result->upserted()) {
$this->storage->folder($id)->create();
// Ensure storage directory is created
if ($result->upserted()) {
$this->storage->folder($id)->create();
}
return $result->document();
}

return $result->document();
}

#[Put('/{id:.+(/.+)?}/images/{name}')]
Expand Down Expand Up @@ -101,8 +91,8 @@ public function remove(#[Value] $user, string $id, string $name) {
return $deleted;
}

#[Put('/{id:.+(/.+)?}/published')]
public function publish(#[Value] $user, string $id, #[Entity] Date $date) {
#[Patch('/{id:.+(/.+)?}')]
public function update(#[Value] $user, string $id, #[Entity] Entry $source) {

// If this entry does not contain any images, use the first image of the latest
// child element as the preview image. This will update the preview image every
Expand All @@ -114,8 +104,30 @@ public function publish(#[Value] $user, string $id, #[Entity] Date $date) {
} else {
$preview= ['slug' => $id, ...$entry['images'][0]];
}
$this->repository->modify($id, ['$set' => ['published' => $date, 'preview' => $preview]]);

return ['published' => $date];
// Join places, autocomplete seems to only work for strings
$suggest= '';
foreach ($source->locations as $location) {
$suggest.= ' '.$location['name'];
}
$changes= $source->attributes() + [
'preview' => $preview,
'modified' => time(),
'_searchable' => [
'boost' => isset($source->is['journey']) ? 2.0 : 1.0,
'suggest' => trim($suggest),
'content' => strip_tags(strtr($source->content, ['<br>' => "\n", '</p><p>' => "\n"]))
],
];
$n= $this->repository->modify($id, ['$set' => $changes])->modified();
return ['updated' => $n];
}

#[Delete('/{id:.+(/.+)?}')]
public function delete(#[Value] $user, string $id) {
if ($n= $this->repository->delete($id)->deleted()) {
$this->storage->folder($id)->unlink();
}
return ['deleted' => $n];
}
}
78 changes: 78 additions & 0 deletions src/main/php/de/thekid/dialog/api/Entry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php namespace de\thekid\dialog\api;

use lang\Value;
use util\{Date, Objects};

/** Represents an entry passed to the /entries API */
class Entry implements Value {
private $attributes= [];

public string $slug {
get => $this->attributes['slug'];
set { $this->attributes['slug']= $value; }
}

public Date $date {
get => $this->attributes['date'];
set { $this->attributes['date']= $value; }
}

public string $title {
get => $this->attributes['title'];
set { $this->attributes['title']= $value; }
}

public string $content {
get => $this->attributes['content'];
set { $this->attributes['content']= $value; }
}

public array<string, mixed> $is {
get => $this->attributes['is'];
set { $this->attributes['is']= $value; }
}

public ?string $parent {
get => $this->attributes['parent'] ?? null;
set { $this->attributes['parent']= $value; }
}

public array<string> $keywords {
get => $this->attributes['keywords'] ?? [];
set { $this->attributes['keywords']= $value; }
}

public array<array<mixed>> $locations {
get => $this->attributes['locations'] ?? [];
set { $this->attributes['locations']= $value; }
}

public array<string, mixed> $weather {
get => $this->attributes['weather'];
set { $this->attributes['weather']= $value; }
}

public ?Date $published {
get => $this->attributes['published'] ?? null;
set { $this->attributes['published']= $value; }
}

/** Returns all attributes as a map */
public function attributes(): array<string, mixed> { return $this->attributes; }

/** @return string */
public function hashCode() { return 'E'.Objects::hashOf($this->attributes); }

/** @return string */
public function toString() { return nameof($this).'@'.Objects::stringOf($this->attributes); }

/**
* Comparison
*
* @param var $value
* @return int
*/
public function compareTo($value) {
return $value instanceof self ? Objects::compare($this->attributes, $value->attributes) : 1;
}
}
24 changes: 24 additions & 0 deletions src/main/php/de/thekid/dialog/import/Content.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php namespace de\thekid\dialog\import;

use de\thekid\dialog\processing\Files;

/** Imports contents */
class Content extends Source {

public function entryFrom(Description $description): array<string, mixed> {
return [
'slug' => $this->name(),
'parent' => $this->parent(),
'date' => $description->meta['date'],
'title' => $description->meta['title'],
'keywords' => $description->meta['keywords'] ?? [],
'content' => $description->content,
'locations' => [...$description->locations($description->meta['date']->getTimeZone())],
'is' => ['content' => true],
];
}

public function contentsIn(Files $files): iterable {
yield from $this->mediaIn($files);
}
}
24 changes: 24 additions & 0 deletions src/main/php/de/thekid/dialog/import/Cover.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php namespace de\thekid\dialog\import;

use de\thekid\dialog\processing\Files;

/** Imports the cover image */
class Cover extends Source {

public function entryFrom(Description $description): array<string, mixed> {
return [
'slug' => '@cover',
'parent' => '~',
'date' => $description->meta['date'],
'title' => $description->meta['title'],
'keywords' => $description->meta['keywords'] ?? [],
'content' => $description->content,
'locations' => [...$description->locations($description->meta['date']->getTimeZone())],
'is' => ['cover' => true],
];
}

public function contentsIn(Files $files): iterable {
yield from $this->mediaIn($files);
}
}
14 changes: 14 additions & 0 deletions src/main/php/de/thekid/dialog/import/DeleteEntry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php namespace de\thekid\dialog\import;

use webservices\rest\Endpoint;

class DeleteEntry extends Task {

public function __construct(private string $slug) { }

public function execute(Endpoint $api) {
return $api->resource('entries/{0}', [$this->slug])->delete();
}

public function description(): string { return "Removing entry {$this->slug}"; }
}
16 changes: 16 additions & 0 deletions src/main/php/de/thekid/dialog/import/DeleteMedia.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php namespace de\thekid\dialog\import;

use webservices\rest\Endpoint;

class DeleteMedia extends Task {

public function __construct(private string $slug, private string $name) { }

public function execute(Endpoint $api) {
return $api->resource('entries/{0}/images/{1}', [$this->slug, $this->name])->delete();
}

/** @return string */
public function description(): string { return "Deleting media {$this->slug}/{$this->name}"; }

}
17 changes: 17 additions & 0 deletions src/main/php/de/thekid/dialog/import/FetchEntry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php namespace de\thekid\dialog\import;

use webservices\rest\Endpoint;

class FetchEntry extends Task {

public function __construct(private string $slug) { }

public function execute(Endpoint $api) {
return $api->resource('entries/{0}?expand=$children', [$this->slug])
->put([])
->value()
;
}

public function description(): string { return "Fetching entry {$this->slug}"; }
}
54 changes: 54 additions & 0 deletions src/main/php/de/thekid/dialog/import/Journey.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php namespace de\thekid\dialog\import;

use de\thekid\dialog\processing\Files;
use io\File;

/** Imports journeys */
class Journey extends Source {

/** Subfolders of a journey form its child contents */
private function childrenIn(Files $files): iterable {
$children= [];
foreach ($this->entry['$children'] as $child) {
$children[$child['slug']]= $child;
}

foreach ($this->origin->entries() as $path) {
if ($path->isFolder()) {
$folder= $path->asFolder();
$slug= $this->entry['slug'].'/'.$folder->dirname;

yield from new Content($folder, new File($folder, 'content.md'), $children[$slug] ?? null)
->nestedIn($this->entry['slug'])
->synchronize($files)
;
unset($children[$slug]);
}
}

foreach ($children as $rest) {
yield new DeleteEntry($rest['slug']);
}
}

public function entryFrom(Description $description): array<string, mixed> {
return [
'slug' => $this->name(),
'date' => $description->meta['from'],
'title' => $description->meta['title'],
'keywords' => $description->meta['keywords'] ?? [],
'content' => $description->content,
'locations' => [...$description->locations($description->meta['from']->getTimeZone())],
'is' => [
'journey' => true,
'from' => $description->meta['from'],
'until' => $description->meta['until'],
],
];
}

public function contentsIn(Files $files): iterable {
yield from $this->mediaIn($files);
yield from $this->childrenIn($files);
}
}
Loading

0 comments on commit fccf361

Please sign in to comment.