Skip to content

Commit

Permalink
feat: implement a way of matching the fields to the jsonapi spec
Browse files Browse the repository at this point in the history
  • Loading branch information
CoolGoose committed Dec 6, 2024
1 parent 5c98ec6 commit cc074ba
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 12 deletions.
19 changes: 19 additions & 0 deletions config/query-builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,23 @@
* GET /users?fields[userOwner]=id,name
*/
'convert_relation_names_to_snake_case_plural' => true,

/*
* By default, the package expects relationship names to be snake case plural when using fields[relationship].
* For example, fetching the id and name for a userOwner relation would look like this:
* GET /users?fields[user_owner]=id,name
*
* Set this to one of `snake_case`, `camelCase` or `none` if you want to enable table name resolution in addition to the relation name resolution
* GET /users?include=topOrders&fields[orders]=id,name
*/
'convert_relation_table_name_strategy' => false,

/*
* By default, the package expects the field names to match the database names
* For example, fetching the field named firstName would look like this:
* GET /users?fields=firstName
*
* Set this to `true` if you want to convert the firstName into first_name for the underlying query
*/
'convert_field_names_to_snake_case' => false,
];
2 changes: 1 addition & 1 deletion database/factories/AppendModelFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

namespace Spatie\QueryBuilder\Database\Factories;

use Spatie\QueryBuilder\Tests\TestClasses\Models\AppendModel;
use Illuminate\Database\Eloquent\Factories\Factory;
use Spatie\QueryBuilder\Tests\TestClasses\Models\AppendModel;

class AppendModelFactory extends Factory
{
Expand Down
1 change: 0 additions & 1 deletion database/factories/TestModelFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,3 @@ public function definition()
];
}
}

43 changes: 35 additions & 8 deletions src/Concerns/AddsFieldsToQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ protected function addRequestedModelFieldsToQuery(): void

$fields = $this->request->fields();

if (! $fields->isEmpty() && config('query-builder.convert_field_names_to_snake_case', false)) {
$fields = $fields->mapWithKeys(fn ($fields, $table) => [$table => collect($fields)->map(fn ($field) => Str::snake($field))->toArray()]);
}

$modelFields = $fields->has($modelTableName) ? $fields->get($modelTableName) : $fields->get('_');

if (empty($modelFields)) {
Expand All @@ -49,23 +53,46 @@ protected function addRequestedModelFieldsToQuery(): void
$this->select($prependedFields);
}

public function getRequestedFieldsForRelatedTable(string $relation): array
public function getRequestedFieldsForRelatedTable(string $relation, ?string $tableName = null): array
{
$tableOrRelation = config('query-builder.convert_relation_names_to_snake_case_plural', true)
? Str::plural(Str::snake($relation))
: $relation;
// Possible table names to check
$possibleRelatedNames = [
// Preserve existing relation name conversion logic
config('query-builder.convert_relation_names_to_snake_case_plural', true)
? Str::plural(Str::snake($relation))
: $relation,
];

$strategy = config('query-builder.convert_relation_table_name_strategy', false);

// Apply additional table name conversion based on strategy
if ($strategy === 'snake_case' && $tableName) {
$possibleRelatedNames[] = Str::snake($tableName);
} elseif ($strategy === 'camelCase' && $tableName) {
$possibleRelatedNames[] = Str::camel($tableName);
} elseif ($strategy === 'none') {
$possibleRelatedNames = $tableName;
}

// Remove any null values
$possibleRelatedNames = array_filter($possibleRelatedNames);

$fields = $this->request->fields()
->mapWithKeys(fn ($fields, $table) => [$table => $fields])
->get($tableOrRelation);
->mapWithKeys(fn ($fields, $table) => [$table => collect($fields)->map(fn ($field) => config('query-builder.convert_field_names_to_snake_case', false) ? Str::snake($field) : $field)])
->filter(fn ($value, $table) => in_array($table, $possibleRelatedNames))
->first();

if (! $fields) {
return [];
}

if (! $this->allowedFields instanceof Collection) {
// We have requested fields but no allowed fields (yet?)
$fields = $fields->toArray();

if ($tableName !== null) {
$fields = $this->prependFieldsWithTableName($fields, $tableName);
}

if (! $this->allowedFields instanceof Collection) {
throw new UnknownIncludedFieldsQuery($fields);
}

Expand Down
21 changes: 19 additions & 2 deletions src/Includes/IncludedRelationship.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Spatie\QueryBuilder\Includes;

use Closure;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;

Expand All @@ -16,11 +17,27 @@ public function __invoke(Builder $query, string $relationship)
$relatedTables = collect(explode('.', $relationship));

$withs = $relatedTables
->mapWithKeys(function ($table, $key) use ($relatedTables) {
->mapWithKeys(function ($table, $key) use ($relatedTables, $query) {
$fullRelationName = $relatedTables->slice(0, $key + 1)->implode('.');

if ($this->getRequestedFieldsForRelatedTable) {
$fields = ($this->getRequestedFieldsForRelatedTable)($fullRelationName);

$tableName = null;
$strategy = config('query-builder.convert_relation_table_name_strategy', false);

if ($strategy !== false) {
// Try to resolve the related model's table name
try {
// Use the current query's model to resolve the relationship
$relatedModel = $query->getModel()->{$fullRelationName}()->getRelated();
$tableName = $relatedModel->getTable();
} catch (Exception $e) {
// If we can not figure out the table don't do anything
$tableName = null;
}
}

$fields = ($this->getRequestedFieldsForRelatedTable)($fullRelationName, $tableName);
}

if (empty($fields)) {
Expand Down

0 comments on commit cc074ba

Please sign in to comment.