Skip to content

Commit

Permalink
feat(decorators): add rename field decorator (#49)
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolasalexandre9 authored May 9, 2023
1 parent 7adecdb commit 2ea9821
Show file tree
Hide file tree
Showing 10 changed files with 606 additions and 2 deletions.
11 changes: 10 additions & 1 deletion src/DatasourceCustomizer/CollectionCustomizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,17 @@ public function importField(string $name, array $options)
{
}

public function renameField($oldName, string $newName)
/**
* Allow to rename a field of a given collection.
*
* @param string $oldName the current name of the field in a given collection
* @param string $newName the new name of the field
*/
public function renameField(string $oldName, string $newName)
{
$this->stack->renameField->getCollection($this->name)->renameField($oldName, $newName);

return $this;
}

public function removeField(...$names)
Expand Down
6 changes: 6 additions & 0 deletions src/DatasourceCustomizer/Decorators/DecoratorsStack.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\OperatorsEmulate\OperatorsEmulateCollection;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\OperatorsReplace\OperatorsReplaceCollection;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Relation\RelationCollection;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\RenameField\RenameFieldCollection;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Schema\SchemaCollection;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Search\SearchCollection;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Segment\SegmentCollection;
Expand Down Expand Up @@ -36,6 +37,7 @@ class DecoratorsStack
public DatasourceDecorator $write;
public DatasourceContract $validation;
public ChartDataSourceDecorator $chart;
public DatasourceDecorator $renameField;

public function __construct(DatasourceContract $dataSource)
{
Expand Down Expand Up @@ -69,6 +71,9 @@ public function __construct(DatasourceContract $dataSource)
$last = $this->write = new WriteDataSourceDecorator($last);
$last = $this->validation = new DatasourceDecorator($last, ValidationCollection::class);

// Step 4: Renaming must be either the very first or very last so that naming in customer code is consistent.
$last = $this->renameField = new DatasourceDecorator($last, RenameFieldCollection::class);

$this->dataSource = &$last;
}

Expand All @@ -90,6 +95,7 @@ public function build(): void
$this->schema->build();
$this->write->build();
$this->validation->build();
$this->renameField->build();
$this->dataSource->build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public function delete(Caller $caller, Filter $filter): void
public function aggregate(Caller $caller, Filter $filter, Aggregation $aggregation, ?int $limit = null)
{
if (! $this->returnsEmptySet($filter->getConditionTree())) {
return parent::aggregate($caller, $filter, $aggregation, $limit);
return $this->childCollection->aggregate($caller, $filter, $aggregation, $limit);
}

return [];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
<?php

namespace ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\RenameField;

use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\CollectionDecorator;
use ForestAdmin\AgentPHP\DatasourceToolkit\Components\Caller;
use ForestAdmin\AgentPHP\DatasourceToolkit\Components\Query\Aggregation;
use ForestAdmin\AgentPHP\DatasourceToolkit\Components\Query\Filters\Filter;
use ForestAdmin\AgentPHP\DatasourceToolkit\Components\Query\Filters\PaginatedFilter;
use ForestAdmin\AgentPHP\DatasourceToolkit\Components\Query\Projection\Projection;
use ForestAdmin\AgentPHP\DatasourceToolkit\Exceptions\ForestException;
use ForestAdmin\AgentPHP\DatasourceToolkit\Schema\ColumnSchema;
use ForestAdmin\AgentPHP\DatasourceToolkit\Schema\Relations\ManyToManySchema;
use ForestAdmin\AgentPHP\DatasourceToolkit\Schema\Relations\ManyToOneSchema;
use ForestAdmin\AgentPHP\DatasourceToolkit\Schema\Relations\OneToManySchema;
use ForestAdmin\AgentPHP\DatasourceToolkit\Schema\Relations\OneToOneSchema;
use ForestAdmin\AgentPHP\DatasourceToolkit\Schema\RelationSchema;
use Illuminate\Support\Collection as IlluminateCollection;
use Illuminate\Support\Str;

class RenameFieldCollection extends CollectionDecorator
{
protected array $fromChildCollection = [];
protected array $toChildCollection = [];

public function renameField(string $currentName, string $newName): void
{
if ($this->getFields()->get($currentName) === null) {
throw new ForestException("No such field '$currentName'");
}

$initialName = $currentName;

// Revert previous renaming (avoids conflicts and need to recurse on this.toSubCollection).
if (isset($this->toChildCollection[$currentName])) {
$childName = $this->toChildCollection[$currentName];
unset($this->toChildCollection[$currentName], $this->fromChildCollection[$childName]);
$initialName = $childName;
}

// Do not update arrays if renaming is a no-op (ie: customer is cancelling a previous rename).
if ($initialName !== $newName) {
$this->fromChildCollection[$initialName] = $newName;
$this->toChildCollection[$newName] = $initialName;
}
}

public function getFields(): IlluminateCollection
{
$fields = collect();

foreach ($this->childCollection->getFields() as $oldName => $schema) {
if ($schema instanceof ManyToOneSchema) {
$schema->setForeignKey($this->fromChildCollection[$schema->getForeignKey()] ?? $schema->getForeignKey());
} elseif ($schema instanceof OneToManySchema || $schema instanceof OneToOneSchema) {
/** @var self $relation */
$relation = $this->dataSource->getCollection($schema->getForeignCollection());
$schema->setOriginKey($relation->fromChildCollection[$schema->getOriginKey()] ?? $schema->getOriginKey());
} elseif ($schema instanceof ManyToManySchema) {
/** @var self $through */
$through = $this->dataSource->getCollection($schema->getThroughCollection());
$schema->setForeignKey($through->fromChildCollection[$schema->getForeignKey()] ?? $schema->getForeignKey());
$schema->setOriginKey($through->fromChildCollection[$schema->getOriginKey()] ?? $schema->getOriginKey());
}

$fields->put($this->fromChildCollection[$oldName] ?? $oldName, $schema);
}

return $fields;
}

public function create(Caller $caller, array $data)
{
$newRecord = $this->childCollection->create($caller, $this->recordToChildCollection($data));

return $this->recordFromChildCollection($newRecord);
}

public function list(Caller $caller, PaginatedFilter $filter, Projection $projection): array
{
$childProjection = $projection->replaceItem(fn ($field) => $this->pathToChildCollection($field));
$records = $this->childCollection->list($caller, $this->refineFilter($caller, $filter), $childProjection);
if ($childProjection->diff($projection)->isEmpty()) {
return $records;
}

return collect($records)->map(fn ($record) => $this->recordFromChildCollection($record))->toArray();
}

public function update(Caller $caller, Filter $filter, array $patch)
{
return $this->childCollection->update($caller, $this->refineFilter($caller, $filter), $this->recordToChildCollection($patch));
}

public function aggregate(Caller $caller, Filter $filter, Aggregation $aggregation, ?int $limit = null, ?string $chartType = null)
{
$rows = $this->childCollection->aggregate(
$caller,
$this->refineFilter($caller, $filter),
$aggregation->replaceFields(fn ($field) => $this->pathToChildCollection($field)),
$limit
);

return collect($rows)->map(
fn ($row) => [
'value' => $row['value'],
'group' => collect($row['group'] ?? [])->reduce(
fn ($memo, $value, $key) => array_merge($memo, [$this->pathFromChildCollection($key) => $value]),
[]
),
]
)
->toArray();
}

protected function refineFilter(Caller $caller, Filter|PaginatedFilter|null $filter): Filter|PaginatedFilter|null
{
if ($filter instanceof PaginatedFilter) {
return $filter?->override(
conditionTree: $filter->getConditionTree()?->replaceFields(fn ($field) => $this->pathToChildCollection($field)),
sort: $filter->getSort()?->replaceClauses(fn ($clause) => [
[
'field' => $this->pathToChildCollection($clause['field']),
'ascending' => $clause['ascending'],
],
])
);
} else {
return $filter?->override(
conditionTree: $filter->getConditionTree()?->replaceFields(fn ($field) => $this->pathToChildCollection($field)),
);
}
}

/** Convert field path from child collection to this collection */
private function pathFromChildCollection(string $childPath): string
{
if (Str::contains($childPath, ':')) {
$childField = Str::before($childPath, ':');
$thisField = $this->fromChildCollection[$childField] ?? $childField;
/** @var RelationSchema $relationSchema */
$relationSchema = $this->getFields()[$thisField];
/** @var self $relation */
$relation = $this->getDataSource()->getCollection($relationSchema->getForeignCollection());

return "$thisField:" . $relation->pathFromChildCollection(Str::after($childPath, ':'));
}

return $this->fromChildCollection[$childPath] ?? $childPath;
}

/** Convert field path from this collection to child collection */
private function pathToChildCollection(string $thisPath): string
{
if (Str::contains($thisPath, ':')) {
$thisField = Str::before($thisPath, ':');
/** @var RelationSchema $relationSchema */
$relationSchema = $this->getFields()[$thisField];
/** @var self $relation */
$relation = $this->getDataSource()->getCollection($relationSchema->getForeignCollection());
$childField = $this->toChildCollection[$thisField] ?? $thisField;

return "$childField:" . $relation->pathToChildCollection(Str::after($thisPath, ':'));
}

return $this->toChildCollection[$thisPath] ?? $thisPath;
}

/** Convert record from this collection to the child collection */
private function recordToChildCollection(array $thisRecord): array
{
$childRecord = [];
foreach ($thisRecord as $thisField => $value) {
$childRecord[$this->toChildCollection[$thisField] ?? $thisField] = $value;
}

return $childRecord;
}

/** Convert record from the child collection to this collection */
private function recordFromChildCollection(array $childRecord): array
{
$thisRecord = [];

foreach ($childRecord as $childField => $value) {
$thisField = $this->fromChildCollection[$childField] ?? $childField;
$fieldSchema = $this->getFields()[$thisField];

// Perform the mapping, recurse for relations.
if ($fieldSchema instanceof ColumnSchema || $value === null) {
$thisRecord[$thisField] = $value;
} else {
/** @var self $relation */
$relation = $this->getDataSource()->getCollection($fieldSchema->getForeignCollection());
$thisRecord[$thisField] = $relation->recordFromChildCollection($value);
}
}

return $thisRecord;
}
}
20 changes: 20 additions & 0 deletions src/DatasourceToolkit/Components/Query/Aggregation.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace ForestAdmin\AgentPHP\DatasourceToolkit\Components\Query;

use Closure;
use ForestAdmin\AgentPHP\DatasourceToolkit\Components\Query\Projection\Projection;
use ForestAdmin\AgentPHP\DatasourceToolkit\Exceptions\ForestException;
use ForestAdmin\AgentPHP\DatasourceToolkit\Utils\Record;
Expand Down Expand Up @@ -53,6 +54,25 @@ public function getProjection()
return new Projection($aggregateFields);
}

public function replaceFields(Closure $handler): self
{
$result = clone $this;

if ($result->field) {
$result->field = $handler($result->field);
}

$result->groups = collect($result->groups)->map(
fn ($group) => [
'field' => $handler($group['field']),
'operation' => $group['operation'] ?? null,
]
)
->toArray();

return $result;
}

public function override(...$args): self
{
return new self(...array_merge($this->toArray(), $args));
Expand Down
5 changes: 5 additions & 0 deletions src/DatasourceToolkit/Schema/Relations/ManyRelationSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,9 @@ public function getForeignKeyTarget(): string
{
return $this->foreignKeyTarget;
}

public function setForeignKey(string $foreignKey): void
{
$this->foreignKey = $foreignKey;
}
}
5 changes: 5 additions & 0 deletions src/DatasourceToolkit/Schema/Relations/ManyToManySchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ public function __construct(
parent::__construct($foreignKey, $foreignKeyTarget, $foreignCollection, 'ManyToMany');
}

public function setOriginKey(string $originKey): void
{
$this->originKey = $originKey;
}

public function getThroughCollection(): string
{
return $this->throughCollection;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ public function __construct(
parent::__construct($foreignCollection, $type);
}

public function setOriginKey(string $originKey): void
{
$this->originKey = $originKey;
}

public function getOriginKey(): string
{
return $this->originKey;
Expand Down
11 changes: 11 additions & 0 deletions tests/DatasourceCustomizer/CollectionCustomizerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -436,3 +436,14 @@ function factoryCollectionCustomizer($collectionName = 'Book')

expect(invokeProperty($writeReplaceCollection, 'handlers'))->toEqual([$field => $condition]);
});

test('renameField() should rename a field in collection', function () {
[$customizer, $datasourceCustomizer] = factoryCollectionCustomizer();
$customizer->renameField('title', 'newTitle');
/** @var ComputedCollection $computedCollection */
$computedCollection = $datasourceCustomizer->getStack()->renameField->getCollection('Book');

expect($computedCollection->getFields())
->toHaveKey('newTitle')
->not->toHaveKey('title');
});
Loading

0 comments on commit 2ea9821

Please sign in to comment.