Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions src/Commands/DevCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class DevCommand extends Command

protected $signature = 'restify:dev {--path= : The path to the root local directory.}
{--git : Use the latest vcs git repository}
{--revert : Revert composer.json to remove local development setup}
';

protected $description = 'Add laravel-restify from a local directory.';
Expand All @@ -23,6 +24,10 @@ public function handle()
return true;
}

if ($this->option('revert')) {
return $this->revert();
}

$this->addRepositoryToRootComposer();

$this->info('Added local path to repositories.');
Expand Down Expand Up @@ -127,4 +132,63 @@ private function resolveDefaultPath()
? 'git@github.com:BinarCode/laravel-restify.git'
: '../../binarcode/laravel-restify';
}

protected function revert(): int
{
$this->removeRepositoryFromComposer();

$this->info('Removed local path from repositories.');

$this->restorePackageVersion();

$this->info('Restored package version in composer.json.');

$this->composerUpdate();

$this->info('Composer updated. Development setup reverted.');

return 0;
}

protected function removeRepositoryFromComposer(): void
{
$composer = json_decode(file_get_contents(base_path('composer.json')), true);

if (! array_key_exists('repositories', $composer)) {
return;
}

$composer['repositories'] = collect($composer['repositories'])->filter(function ($repository) {
if (! array_key_exists('url', $repository)) {
return true;
}

return ! Str::contains($repository['url'], 'laravel-restify');
})->values()->toArray();

if (empty($composer['repositories'])) {
unset($composer['repositories']);
}

file_put_contents(
base_path('composer.json'),
json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);
}

protected function restorePackageVersion(): void
{
$composer = json_decode(file_get_contents(base_path('composer.json')), true);

if (! array_key_exists('require', $composer) || ! array_key_exists('binaryk/laravel-restify', $composer['require'])) {
return;
}

$composer['require']['binaryk/laravel-restify'] = '^10.0';

file_put_contents(
base_path('composer.json'),
json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);
}
}
7 changes: 7 additions & 0 deletions src/Fields/FieldCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -198,4 +198,11 @@ public function inList(array $columns = []): self
->filter(fn (Field $field) => in_array($field->getAttribute(), $columns, true))
->values();
}

public function areFiles(): self
{
return $this
->filter(fn (Field $field) => $field instanceof File)
->values();
}
}
41 changes: 34 additions & 7 deletions src/Fields/File.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
use Carbon\CarbonInterface;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use RuntimeException;

class File extends Field implements DeletableContract, StorableContract
{
Expand Down Expand Up @@ -81,8 +83,11 @@ public function storeAs($storeAs): self
*
* @return $this
*/
public function resolveUsingTemporaryUrl(bool $resolveTemporaryUrl = true, ?CarbonInterface $expiration = null, array $options = []): self
{
public function resolveUsingTemporaryUrl(
bool $resolveTemporaryUrl = true,
?CarbonInterface $expiration = null,
array $options = []
): self {
if (! $resolveTemporaryUrl) {
return $this;
}
Expand Down Expand Up @@ -161,13 +166,32 @@ public function storeSize($column)
return $this;
}

protected function resolveFileFromRequest(Request $request): ?UploadedFile
{
if (($file = $request->input($this->attribute)) instanceof UploadedFile && $file->isValid()) {
return $file;
}

if (($file = $request->file($this->attribute)) && $file->isValid()) {
return $file;
}

return null;
}

protected function storeFile(Request $request, string $requestAttribute)
{
$file = $this->resolveFileFromRequest($request);

if (! $file) {
throw new RuntimeException("No valid file found in the request for attribute {$requestAttribute}");
}

if (! $this->storeAs) {
return $request->file($requestAttribute)->store($this->getStorageDir(), $this->getStorageDisk());
return $file->store($this->getStorageDir(), $this->getStorageDisk());
}

return $request->file($requestAttribute)->storeAs(
return $file->storeAs(
$this->getStorageDir(),
is_callable($this->storeAs) ? call_user_func($this->storeAs, $request) : $this->storeAs,
$this->getStorageDisk()
Expand Down Expand Up @@ -196,7 +220,7 @@ public function store($storageCallback): self
*/
protected function mergeExtraStorageColumns($request, array $attributes): array
{
$file = $request->file($this->attribute);
$file = $this->resolveFileFromRequest($request);

if ($this->originalNameColumn) {
$attributes[$this->originalNameColumn] = $file->getClientOriginalName();
Expand Down Expand Up @@ -229,6 +253,10 @@ protected function columnsThatShouldBeDeleted(): array

public function fillAttribute(RestifyRequest $request, $model, ?int $bulkRow = null)
{
if ($this->storeCallback instanceof Closure) {
return call_user_func($this->storeCallback, $request, $model, $this->attribute);
}

// Handle URL input first
if ($request->has($this->attribute) && is_string($request->input($this->attribute))) {
$url = $request->input($this->attribute);
Expand All @@ -254,8 +282,7 @@ public function fillAttribute(RestifyRequest $request, $model, ?int $bulkRow = n
}
}

// Existing file upload logic
if (is_null($file = $request->file($this->attribute)) || ! $file->isValid()) {
if (! $this->resolveFileFromRequest($request)) {
return $this;
}

Expand Down
6 changes: 3 additions & 3 deletions src/MCP/Actions/SchemaAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -955,7 +955,7 @@ public function validateFile(string $attribute, $schema, array $parameters)
return $existing;
}

return $schema->string()->description('Must be a valid file');
return $schema->string()->description('Must be a valid file, or file absolute path');
}

/**
Expand Down Expand Up @@ -1266,7 +1266,7 @@ public function validateMimes(string $attribute, $schema, array $parameters)
return $schema->string()->description("Allowed file extensions: {$extensions}");
}

return $schema->string()->description('Must be a valid file with allowed extension');
return $schema->string()->description('Must be a valid file, or file absolute path with allowed extension');
}

/**
Expand All @@ -1289,7 +1289,7 @@ public function validateMimetypes(string $attribute, $schema, array $parameters)
return $schema->string()->description("Allowed MIME types: {$types}");
}

return $schema->string()->description('Must be a valid file with allowed MIME type');
return $schema->string()->description('Must be a valid file, or file absolute path with allowed MIME type');
}

/**
Expand Down
15 changes: 8 additions & 7 deletions src/MCP/Concerns/FieldMcpSchemaDetection.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,15 @@ public function getDescription(RestifyRequest $request, Repository $repository):
}
}

$partialDescription = '';

if ($this instanceof File) {
$partialDescription = ' -- Important: This should be an absolute path or URL to the file that can be read.';
}

if ($description = data_get($this->jsonSchema()?->toArray(), 'description')) {
if (is_string($description)) {
return $description;
return $description.$partialDescription;
}
}

Expand All @@ -78,11 +84,6 @@ public function getDescription(RestifyRequest $request, Repository $repository):
}
}

// Add file information for file fields
if ($this instanceof File) {
$description .= '. Upload a file';
}

// Add examples based on field type and name
if ($this->jsonSchema instanceof Type) {
$examples = $this->generateFieldExamples($this->jsonSchema);
Expand All @@ -92,7 +93,7 @@ public function getDescription(RestifyRequest $request, Repository $repository):
}
}

return $description;
return $description.' '.$partialDescription;
}

/**
Expand Down
36 changes: 36 additions & 0 deletions src/MCP/Concerns/McpStoreTool.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
namespace Binaryk\LaravelRestify\MCP\Concerns;

use Binaryk\LaravelRestify\Fields\Field;
use Binaryk\LaravelRestify\Fields\File;
use Binaryk\LaravelRestify\MCP\Requests\McpStoreRequest;
use Illuminate\Http\UploadedFile;
use Illuminate\JsonSchema\JsonSchema;

/**
Expand All @@ -13,6 +15,40 @@ trait McpStoreTool
{
public function storeTool(McpStoreRequest $request): array
{
$this->collectFields($request)
->forStore($request, $this)
->areFiles()
->each(function (File $file) use ($request) {
if (! $request->has($file->attribute)) {
return;
}

$filePath = $request->input($file->attribute);
$actualPath = null;
$fileName = null;

if (file_exists($filePath) && is_readable($filePath)) {
$actualPath = $filePath;
$fileName = basename($filePath);
} elseif (filter_var($filePath, FILTER_VALIDATE_URL)) {
$actualPath = tempnam(sys_get_temp_dir(), 'upload_');
file_put_contents($actualPath, file_get_contents($filePath));
$fileName = basename(parse_url($filePath, PHP_URL_PATH));
}

if ($actualPath) {
$uploadedFile = new UploadedFile(
$actualPath,
$fileName,
mime_content_type($actualPath),
null,
true // Mark it as test mode to allow local files
);

$request->merge([$file->attribute => $uploadedFile]);
}
});

return $this
->allowToStore($request)
->store($request)
Expand Down