├── .gitignore ├── src ├── Exceptions │ ├── BindingException.php │ ├── RouteDiscoveryException.php │ ├── MaxNestedDepthExceededException.php │ └── MaxPaginationLimitExceededException.php ├── Http │ ├── Routing │ │ ├── MorphManyRelationResourceRegistrar.php │ │ ├── MorphOneRelationResourceRegistrar.php │ │ ├── MorphToRelationResourceRegistrar.php │ │ ├── HasManyThroughRelationResourceRegistrar.php │ │ ├── HasOneThroughRelationResourceRegistrar.php │ │ ├── MorphToManyRelationResourceRegistrar.php │ │ ├── HasOneRelationResourceRegistrar.php │ │ ├── BelongsToRelationResourceRegistrar.php │ │ └── HasManyRelationResourceRegistrar.php │ ├── Resources │ │ ├── Resource.php │ │ └── CollectionResource.php │ ├── Middleware │ │ └── EnforceExpectsJson.php │ ├── Rules │ │ ├── WhitelistedQueryFields.php │ │ └── WhitelistedField.php │ └── Controllers │ │ └── Controller.php ├── Contracts │ ├── Specs │ │ ├── Parser.php │ │ └── Formatter.php │ ├── SearchBuilder.php │ ├── Paginator.php │ ├── KeyResolver.php │ ├── ComponentsResolver.php │ ├── ParamsValidator.php │ └── RelationsResolver.php ├── Specs │ ├── Builders │ │ ├── Components │ │ │ ├── SharedComponentBuilder.php │ │ │ ├── ModelComponentBuilder.php │ │ │ └── Shared │ │ │ │ ├── ResourceLinksComponentBuilder.php │ │ │ │ ├── SecurityComponentBuilder.php │ │ │ │ └── ResourceMetaComponentBuilder.php │ │ ├── ServersBuilder.php │ │ ├── SecurityBuilder.php │ │ ├── PropertyBuilder.php │ │ ├── Partials │ │ │ └── RequestBody │ │ │ │ ├── SearchPartialBuilder.php │ │ │ │ └── Search │ │ │ │ ├── IncludesBuilder.php │ │ │ │ ├── SortBuilder.php │ │ │ │ ├── SearchBuilder.php │ │ │ │ ├── ScopesBuilder.php │ │ │ │ ├── AggregatesBuilder.php │ │ │ │ └── FiltersBuilder.php │ │ ├── TagsBuilder.php │ │ ├── RelationOperationBuilder.php │ │ ├── Operations │ │ │ ├── ShowOperationBuilder.php │ │ │ ├── DestroyOperationBuilder.php │ │ │ ├── RestoreOperationBuilder.php │ │ │ ├── IndexOperationBuilder.php │ │ │ ├── Batch │ │ │ │ ├── BatchDestroyOperationBuilder.php │ │ │ │ ├── BatchRestoreOperationBuilder.php │ │ │ │ ├── BatchStoreOperationBuilder.php │ │ │ │ └── BatchUpdateOperationBuilder.php │ │ │ ├── Relations │ │ │ │ ├── OneToMany │ │ │ │ │ ├── AssociateOperationBuilder.php │ │ │ │ │ └── DissociateOperationBuilder.php │ │ │ │ └── ManyToMany │ │ │ │ │ ├── UpdatePivotOperationBuilder.php │ │ │ │ │ ├── SyncOperationBuilder.php │ │ │ │ │ ├── AttachOperationBuilder.php │ │ │ │ │ ├── DetachOperationBuilder.php │ │ │ │ │ └── ToggleOperationBuilder.php │ │ │ ├── StoreOperationBuilder.php │ │ │ ├── SearchOperationBuilder.php │ │ │ └── UpdateOperationBuilder.php │ │ ├── InfoBuilder.php │ │ └── Builder.php │ ├── Parsers │ │ ├── YamlParser.php │ │ └── JsonParser.php │ ├── Formatters │ │ ├── YamlFormatter.php │ │ └── JsonFormatter.php │ ├── ResourcesCacheStore.php │ └── Factories │ │ ├── OperationBuilderFactory.php │ │ └── RelationOperationBuilderFactory.php ├── Concerns │ ├── DisableAuthorization.php │ ├── DisableRouteDiscovery.php │ ├── DisablePagination.php │ ├── InteractsWithHooks.php │ ├── ExtendsResources.php │ ├── InteractsWithBatchResources.php │ ├── HandlesAuthorization.php │ ├── InteractsWithSoftDeletes.php │ ├── HandlesTransactions.php │ └── BuildsResponses.php ├── ValueObjects │ ├── Specs │ │ ├── Request.php │ │ ├── Schema │ │ │ ├── Properties │ │ │ │ ├── BooleanSchemaProperty.php │ │ │ │ ├── IntegerSchemaProperty.php │ │ │ │ ├── NumberSchemaProperty.php │ │ │ │ ├── StringSchemaProperty.php │ │ │ │ ├── AnySchemaProperty.php │ │ │ │ ├── DateSchemaProperty.php │ │ │ │ ├── BinarySchemaProperty.php │ │ │ │ ├── ObjectSchemaProperty.php │ │ │ │ ├── DateTimeSchemaProperty.php │ │ │ │ └── ArraySchemaProperty.php │ │ │ └── SchemaProperty.php │ │ ├── Responses │ │ │ ├── Error │ │ │ │ ├── UnauthorizedResponse.php │ │ │ │ ├── UnauthenticatedResponse.php │ │ │ │ ├── ValidationErrorResponse.php │ │ │ │ └── ResourceNotFoundResponse.php │ │ │ └── Success │ │ │ │ ├── CollectionResponse.php │ │ │ │ ├── Relation │ │ │ │ └── ManyToMany │ │ │ │ │ ├── AttachResponse.php │ │ │ │ │ ├── DetachResponse.php │ │ │ │ │ ├── UpdatePivotResponse.php │ │ │ │ │ └── ToggleResponse.php │ │ │ │ ├── EntityResponse.php │ │ │ │ └── PaginatedCollectionResponse.php │ │ ├── SecuritySchemesComponent.php │ │ ├── ModelResourceComponent.php │ │ ├── Response.php │ │ ├── Component.php │ │ ├── Path.php │ │ ├── Requests │ │ │ ├── StoreRequest.php │ │ │ ├── UpdateRequest.php │ │ │ ├── Relations │ │ │ │ └── ManyToMany │ │ │ │ │ ├── UpdatePivotRequest.php │ │ │ │ │ ├── SyncRequest.php │ │ │ │ │ ├── AttachRequest.php │ │ │ │ │ ├── DetachRequest.php │ │ │ │ │ └── ToggleRequest.php │ │ │ └── Batch │ │ │ │ ├── BatchStoreRequest.php │ │ │ │ ├── BatchUpdateRequest.php │ │ │ │ ├── BatchDestroyRequest.php │ │ │ │ └── BatchRestoreRequest.php │ │ └── Operation.php │ └── RegisteredResource.php ├── Drivers │ └── Standard │ │ ├── SearchBuilder.php │ │ ├── KeyResolver.php │ │ └── Paginator.php ├── Helpers │ └── ArrayHelper.php └── Testing │ ├── InteractsWithJsonFields.php │ └── InteractsWithAuthorization.php ├── tests ├── Unit │ ├── TestCase.php │ ├── Drivers │ │ └── Standard │ │ │ ├── Stubs │ │ │ ├── StubRequest.php │ │ │ ├── StubResource.php │ │ │ ├── ControllerStub.php │ │ │ └── StubCollectionResource.php │ │ │ └── PaginatorTest.php │ ├── Http │ │ ├── Requests │ │ │ └── Stubs │ │ │ │ ├── ControllerStub.php │ │ │ │ └── RelationControllerStub.php │ │ ├── Controllers │ │ │ ├── Stubs │ │ │ │ ├── RelationControllerStubWithoutRelation.php │ │ │ │ ├── ControllerStub.php │ │ │ │ ├── BaseControllerStubWithoutModel.php │ │ │ │ ├── BaseControllerStubWithoutComponents.php │ │ │ │ ├── BaseControllerStub.php │ │ │ │ ├── RelationControllerStub.php │ │ │ │ └── BaseControllerStubWithWhitelistedFieldsAndRelations.php │ │ │ └── ControllerTest.php │ │ ├── Middleware │ │ │ └── EnforceExpectsJsonTest.php │ │ └── Rules │ │ │ └── WhitelistedFieldTest.php │ ├── Specs │ │ └── Builders │ │ │ ├── PathsBuilderTest.php │ │ │ └── PropertyBuilderTest.php │ └── Concerns │ │ ├── ExtendsResourcesTest.php │ │ └── DisableRouteDiscoveryTest.php ├── Fixtures │ ├── app │ │ ├── Http │ │ │ ├── Controllers │ │ │ │ ├── DummyController.php │ │ │ │ ├── UserPostsController.php │ │ │ │ ├── PostPostImageController.php │ │ │ │ ├── AccessKeysController.php │ │ │ │ ├── CommentsController.php │ │ │ │ ├── TeamsController.php │ │ │ │ ├── PostPostMetaController.php │ │ │ │ ├── UserNotificationsController.php │ │ │ │ ├── CompanyTeamsController.php │ │ │ │ ├── PostCategoryController.php │ │ │ │ ├── AccessKeyAccessKeyScopesController.php │ │ │ │ ├── PostUserController.php │ │ │ │ ├── UsersController.php │ │ │ │ ├── UserRolesController.php │ │ │ │ └── PostsController.php │ │ │ ├── Resources │ │ │ │ ├── SampleResource.php │ │ │ │ └── SampleCollectionResource.php │ │ │ └── Requests │ │ │ │ ├── PostMetaRequest.php │ │ │ │ ├── TeamRequest.php │ │ │ │ ├── PostRequest.php │ │ │ │ ├── UserRequest.php │ │ │ │ └── RoleRequest.php │ │ ├── Models │ │ │ ├── UserRole.php │ │ │ ├── Notification.php │ │ │ ├── Comment.php │ │ │ ├── Company.php │ │ │ ├── AccessKey.php │ │ │ ├── PostMeta.php │ │ │ ├── Team.php │ │ │ ├── AccessKeyScope.php │ │ │ ├── PostImage.php │ │ │ ├── Category.php │ │ │ ├── Role.php │ │ │ └── User.php │ │ ├── Traits │ │ │ └── AppliesDefaultOrder.php │ │ ├── Drivers │ │ │ └── TwoRouteParameterKeyResolver.php │ │ ├── Providers │ │ │ └── OrionServiceProvider.php │ │ └── Policies │ │ │ ├── GreenPolicy.php │ │ │ └── RedPolicy.php │ └── database │ │ ├── factories │ │ ├── TeamFactory.php │ │ ├── RoleFactory.php │ │ ├── CommentFactory.php │ │ ├── CompanyFactory.php │ │ ├── PostMetaFactory.php │ │ ├── UserFactory.php │ │ ├── CategoryFactory.php │ │ ├── NotificationFactory.php │ │ ├── PostFactory.php │ │ ├── PostImageFactory.php │ │ ├── AccessKeyFactory.php │ │ └── AccessKeyScopeFactory.php │ │ └── migrations │ │ ├── 2023_01_24_095025_create_comments_table.php │ │ ├── 2019_01_06_040512_create_companies_table.php │ │ ├── 2014_10_12_100000_create_password_resets_table.php │ │ ├── 2019_01_06_063445_create_categories_table.php │ │ ├── 2019_03_05_126414_create_notifications_table.php │ │ ├── 2019_03_06_126414_create_access_keys_table.php │ │ ├── 2019_03_05_124414_create_roles_table.php │ │ ├── 2019_03_04_185404_create_post_images_table.php │ │ ├── 2019_01_06_051132_create_teams_table.php │ │ ├── 2019_03_06_126415_create_access_key_scopes_table.php │ │ ├── 2019_03_04_184404_create_post_metas_table.php │ │ ├── 2019_01_06_060001_create_users_table.php │ │ ├── 2019_03_05_127449_create_notification_user_pivot_table.php │ │ ├── 2019_03_05_125449_create_role_user_pivot_table.php │ │ └── 2019_01_06_113445_create_posts_table.php ├── Feature │ └── Relations │ │ └── BelongsToMany │ │ └── BelongsToManyRelationStandardIndexSortingOperationsTest.php └── TestCase.php ├── phpunit.xml.dist ├── LICENSE.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /vendor 3 | /.vscode 4 | .phpunit.result.cache 5 | phpunit.xml 6 | composer.lock 7 | .DS_Store -------------------------------------------------------------------------------- /src/Exceptions/BindingException.php: -------------------------------------------------------------------------------- 1 | define(Team::class, function (Faker $faker) { 7 | return [ 8 | 'name' => $faker->word, 9 | ]; 10 | }); 11 | -------------------------------------------------------------------------------- /tests/Fixtures/database/factories/RoleFactory.php: -------------------------------------------------------------------------------- 1 | define(Role::class, function (Faker $faker) { 7 | return [ 8 | 'name' => $faker->words(3, true) 9 | ]; 10 | }); -------------------------------------------------------------------------------- /tests/Fixtures/database/factories/CommentFactory.php: -------------------------------------------------------------------------------- 1 | define(Comment::class, function (Faker $faker) { 7 | return [ 8 | 'body' => $faker->text(), 9 | ]; 10 | }); 11 | -------------------------------------------------------------------------------- /tests/Fixtures/database/factories/CompanyFactory.php: -------------------------------------------------------------------------------- 1 | define(Company::class, function (Faker $faker) { 7 | return [ 8 | 'name' => $faker->word, 9 | ]; 10 | }); 11 | -------------------------------------------------------------------------------- /src/Http/Resources/CollectionResource.php: -------------------------------------------------------------------------------- 1 | define(PostMeta::class, function (Faker $faker) { 8 | return [ 9 | 'notes' => $faker->text() 10 | ]; 11 | }); -------------------------------------------------------------------------------- /src/ValueObjects/Specs/Request.php: -------------------------------------------------------------------------------- 1 | schemes; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/ValueObjects/Specs/Responses/Error/UnauthenticatedResponse.php: -------------------------------------------------------------------------------- 1 | toArrayWithMerge($request, [ 12 | 'test-field-from-resource' => 'test-value' 13 | ]); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/Fixtures/database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | define(User::class, function (Faker $faker) { 8 | return [ 9 | 'name' => $faker->name, 10 | 'email' => $faker->safeEmail, 11 | 'password' => Hash::make($faker->words(3, true)) 12 | ]; 13 | }); 14 | -------------------------------------------------------------------------------- /src/Contracts/KeyResolver.php: -------------------------------------------------------------------------------- 1 | $this->title, 14 | ], 15 | $this->properties 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Specs/Formatters/YamlFormatter.php: -------------------------------------------------------------------------------- 1 | define(Category::class, function (Faker $faker) { 8 | return [ 9 | 'name' => $faker->word, 10 | ]; 11 | }); 12 | 13 | 14 | $factory->state(Category::class, 'trashed', function (Faker $faker) { 15 | return [ 16 | 'deleted_at' => Carbon::now() 17 | ]; 18 | }); -------------------------------------------------------------------------------- /src/Specs/Builders/SecurityBuilder.php: -------------------------------------------------------------------------------- 1 | []] 13 | ]; 14 | 15 | if (class_exists('Laravel\\Passport\\PassportServiceProvider')) { 16 | $schemes[] = ['OAuth2' => []]; 17 | } 18 | 19 | return $schemes; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/Fixtures/app/Http/Controllers/UserPostsController.php: -------------------------------------------------------------------------------- 1 | searchableBy = $searchableBy; 15 | } 16 | 17 | public function searchableBy(): array 18 | { 19 | return $this->searchableBy; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Specs/Parsers/JsonParser.php: -------------------------------------------------------------------------------- 1 | define(Notification::class, function (Faker $faker) { 8 | return [ 9 | 'text' => $faker->words(3, true) 10 | ]; 11 | }); 12 | 13 | $factory->state(Notification::class, 'trashed', function (Faker $faker) { 14 | return [ 15 | 'deleted_at' => Carbon::now() 16 | ]; 17 | }); 18 | -------------------------------------------------------------------------------- /tests/Fixtures/app/Traits/AppliesDefaultOrder.php: -------------------------------------------------------------------------------- 1 | orderBy($builder->getModel()->getTable().'.id', 'asc'); 15 | }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Fixtures/database/factories/PostFactory.php: -------------------------------------------------------------------------------- 1 | define(Post::class, function (Faker $faker) { 8 | return [ 9 | 'title' => $faker->words(5, true), 10 | 'body' => $faker->text(), 11 | ]; 12 | }); 13 | 14 | $factory->state(Post::class, 'trashed', function (Faker $faker) { 15 | return [ 16 | 'deleted_at' => Carbon::now() 17 | ]; 18 | }); 19 | -------------------------------------------------------------------------------- /tests/Fixtures/database/factories/PostImageFactory.php: -------------------------------------------------------------------------------- 1 | define(PostImage::class, function (Faker $faker) { 8 | return [ 9 | 'path' => "{$faker->uuid}/{$faker->uuid}.{$faker->fileExtension}" 10 | ]; 11 | }); 12 | 13 | $factory->state(PostImage::class, 'trashed', function (Faker $faker) { 14 | return [ 15 | 'deleted_at' => Carbon::now() 16 | ]; 17 | }); -------------------------------------------------------------------------------- /src/ValueObjects/Specs/Response.php: -------------------------------------------------------------------------------- 1 | $this->description, 20 | ]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Fixtures/app/Http/Controllers/AccessKeysController.php: -------------------------------------------------------------------------------- 1 | define(AccessKey::class, function (Faker $faker) { 8 | return [ 9 | 'key' => \Illuminate\Support\Str::random(), 10 | 'name' => $faker->words(3, true) 11 | ]; 12 | }); 13 | 14 | $factory->state(AccessKey::class, 'trashed', function (Faker $faker) { 15 | return [ 16 | 'deleted_at' => Carbon::now() 17 | ]; 18 | }); 19 | -------------------------------------------------------------------------------- /tests/Unit/Http/Controllers/ControllerTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(Post::class, $stub->resolveResourceModelClass()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Specs/Formatters/JsonFormatter.php: -------------------------------------------------------------------------------- 1 | 'date', 19 | ], 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ValueObjects/Specs/Schema/Properties/BinarySchemaProperty.php: -------------------------------------------------------------------------------- 1 | 'binary', 19 | ], 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Specs/ResourcesCacheStore.php: -------------------------------------------------------------------------------- 1 | resources[] = $resource; 17 | 18 | return $this; 19 | } 20 | 21 | public function getResources(): array 22 | { 23 | return $this->resources; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/ValueObjects/Specs/Schema/Properties/ObjectSchemaProperty.php: -------------------------------------------------------------------------------- 1 | $this->type, 17 | 'additionalProperties' => true, 18 | ]; 19 | 20 | return $descriptor; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Fixtures/app/Models/Notification.php: -------------------------------------------------------------------------------- 1 | belongsToMany(User::class)->withPivot('meta'); 20 | } 21 | } -------------------------------------------------------------------------------- /src/ValueObjects/Specs/Schema/Properties/DateTimeSchemaProperty.php: -------------------------------------------------------------------------------- 1 | 'date-time', 19 | ], 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Fixtures/app/Http/Controllers/TeamsController.php: -------------------------------------------------------------------------------- 1 | define(AccessKeyScope::class, function (Faker $faker) { 8 | return [ 9 | 'scope' => \Illuminate\Support\Str::slug($faker->words(3, true)), 10 | 'description' => $faker->words(3, true) 11 | ]; 12 | }); 13 | 14 | $factory->state(AccessKeyScope::class, 'trashed', function (Faker $faker) { 15 | return [ 16 | 'deleted_at' => Carbon::now() 17 | ]; 18 | }); 19 | -------------------------------------------------------------------------------- /tests/Fixtures/app/Http/Controllers/PostPostMetaController.php: -------------------------------------------------------------------------------- 1 | get('resources', []); 16 | if (array_keys($resources) !== range(0, count($resources) - 1)) { 17 | return array_keys($resources); 18 | } 19 | 20 | return array_values($resources); 21 | } 22 | } -------------------------------------------------------------------------------- /tests/Fixtures/app/Http/Controllers/CompanyTeamsController.php: -------------------------------------------------------------------------------- 1 | header('Accept'), 'application/json')) { 18 | $request->headers->set('Accept', 'application/json, ' . $request->header('Accept')); 19 | } 20 | 21 | return $next($request); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/ValueObjects/Specs/Component.php: -------------------------------------------------------------------------------- 1 | $this->title, 24 | 'type' => $this->type, 25 | 'properties' => $this->properties, 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Fixtures/app/Http/Resources/SampleCollectionResource.php: -------------------------------------------------------------------------------- 1 | $this->collection, 20 | 'test-field-from-resource' => 'test-value' 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/Unit/Http/Controllers/Stubs/BaseControllerStubWithoutModel.php: -------------------------------------------------------------------------------- 1 | morphTo(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Fixtures/app/Models/Company.php: -------------------------------------------------------------------------------- 1 | hasMany(Team::class); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Http/Rules/WhitelistedQueryFields.php: -------------------------------------------------------------------------------- 1 | hasMany(AccessKeyScope::class); 27 | } 28 | } -------------------------------------------------------------------------------- /tests/Fixtures/app/Models/PostMeta.php: -------------------------------------------------------------------------------- 1 | belongsTo(Post::class); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Fixtures/app/Models/Team.php: -------------------------------------------------------------------------------- 1 | belongsTo(Company::class); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/ValueObjects/Specs/Schema/SchemaProperty.php: -------------------------------------------------------------------------------- 1 | $this->type, 22 | ]; 23 | 24 | if ($this->nullable) { 25 | $descriptor['nullable'] = true; 26 | } 27 | 28 | return $descriptor; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Fixtures/app/Drivers/TwoRouteParameterKeyResolver.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 15 | 16 | $table->text('body'); 17 | 18 | $table->unsignedBigInteger('commentable_id'); 19 | $table->string('commentable_type'); 20 | 21 | $table->timestamps(); 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Fixtures/app/Models/AccessKeyScope.php: -------------------------------------------------------------------------------- 1 | belongsTo(AccessKey::class); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Specs/Builders/PropertyBuilder.php: -------------------------------------------------------------------------------- 1 | name = $column['name']; 22 | $property->nullable = $column['nullable']; 23 | 24 | return $property; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Fixtures/app/Models/PostImage.php: -------------------------------------------------------------------------------- 1 | belongsTo(Post::class); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Fixtures/app/Http/Controllers/UsersController.php: -------------------------------------------------------------------------------- 1 | $this->type, 17 | ]; 18 | 19 | if ($this->nullable) { 20 | $descriptor['nullable'] = true; 21 | } 22 | 23 | if ($this->type === 'array') { 24 | $descriptor['items'] = (object) []; 25 | } 26 | 27 | return $descriptor; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ValueObjects/Specs/Path.php: -------------------------------------------------------------------------------- 1 | path = $path; 26 | $this->operations = collect([]); 27 | } 28 | 29 | public function toArray(): array 30 | { 31 | return $this->operations->toArray(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Unit/Http/Controllers/Stubs/BaseControllerStubWithoutComponents.php: -------------------------------------------------------------------------------- 1 | getModel(); 22 | } 23 | 24 | public function getResourceQueryBuilder(): QueryBuilder 25 | { 26 | return $this->getQueryBuilder(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Specs/Builders/Partials/RequestBody/SearchPartialBuilder.php: -------------------------------------------------------------------------------- 1 | controller = app()->make($controller); 24 | } 25 | 26 | abstract public function build(): ?array; 27 | } 28 | -------------------------------------------------------------------------------- /tests/Fixtures/app/Models/Category.php: -------------------------------------------------------------------------------- 1 | hasMany(Post::class); 29 | } 30 | } -------------------------------------------------------------------------------- /tests/Fixtures/database/migrations/2019_01_06_040512_create_companies_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('name'); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::dropIfExists('role_user'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Unit/Specs/Builders/PathsBuilderTest.php: -------------------------------------------------------------------------------- 1 | pathsBuilder = app()->make(PathsBuilder::class); 24 | } 25 | 26 | /** @test */ 27 | public function building_paths(): void 28 | { 29 | // $specs = app()->make(Builder::class)->build(); 30 | // dd(app()->make(YamlWriter::class)->format($specs)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Fixtures/app/Models/Role.php: -------------------------------------------------------------------------------- 1 | 'boolean' 21 | ]; 22 | 23 | /** 24 | * The roles that belong to the user. 25 | */ 26 | public function users() 27 | { 28 | return $this->belongsToMany(User::class) 29 | ->withPivot('meta', 'references', 'custom_name') 30 | ->withTimestamps() 31 | ->using(UserRole::class); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | getModel(); 21 | } 22 | 23 | /** 24 | * Retrieves the query builder used to query the end-resource. 25 | * 26 | * @return QueryBuilder 27 | */ 28 | public function getResourceQueryBuilder(): QueryBuilder 29 | { 30 | return $this->getQueryBuilder(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Fixtures/database/migrations/2014_10_12_100000_create_password_resets_table.php: -------------------------------------------------------------------------------- 1 | string('email')->index(); 18 | $table->string('token'); 19 | $table->timestamp('created_at')->nullable(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::dropIfExists('password_resets'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Fixtures/database/migrations/2019_01_06_063445_create_categories_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | 19 | $table->string('name'); 20 | 21 | $table->timestamps(); 22 | $table->softDeletes(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::dropIfExists('categories'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Fixtures/database/migrations/2019_03_05_126414_create_notifications_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->text('text'); 19 | $table->timestamps(); 20 | $table->softDeletes(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | * @return void 28 | */ 29 | public function down() 30 | { 31 | Schema::dropIfExists('notifications'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Specs/Builders/Components/ModelComponentBuilder.php: -------------------------------------------------------------------------------- 1 | schemaManager = $schemaManager; 30 | $this->propertyBuilder = $propertyBuilder; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /tests/Fixtures/database/migrations/2019_03_06_126414_create_access_keys_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('key'); 19 | $table->string('name'); 20 | $table->timestamps(); 21 | $table->softDeletes(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | * 28 | * @return void 29 | */ 30 | public function down() 31 | { 32 | Schema::dropIfExists('access_keys'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 16 | 17 | 18 | ./src 19 | 20 | 21 | 22 | 23 | ./tests 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Concerns/HandlesAuthorization.php: -------------------------------------------------------------------------------- 1 | bound('orion.authorizationRequired')) { 29 | return app()->make('orion.authorizationRequired'); 30 | } 31 | 32 | return !property_exists($this, 'authorizationDisabled'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Fixtures/database/migrations/2019_03_05_124414_create_roles_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('name'); 19 | $table->string('description')->nullable(); 20 | $table->boolean('deprecated')->default(false); 21 | $table->timestamps(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | * 28 | * @return void 29 | */ 30 | public function down() 31 | { 32 | Schema::dropIfExists('roles'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Fixtures/app/Http/Requests/PostMetaRequest.php: -------------------------------------------------------------------------------- 1 | ['string', 'required', 'min:2'] 28 | ]; 29 | } 30 | 31 | /** 32 | * Rules for the "update" (PATCH|PUT) endpoint. 33 | * 34 | * @return array 35 | */ 36 | public function updateRules() : array 37 | { 38 | return [ 39 | 'notes' => ['string', 'required', 'min:4'] 40 | ]; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Specs/Builders/TagsBuilder.php: -------------------------------------------------------------------------------- 1 | resourcesCacheStore = $resourcesCacheStore; 19 | } 20 | 21 | public function build(): array 22 | { 23 | $resources = $this->resourcesCacheStore->getResources(); 24 | $tags = collect(config('orion.specs.tags')); 25 | 26 | foreach ($resources as $resource) { 27 | if (!$tags->contains('name', $resource->tag)) $tags[] = [ 28 | 'name' => $resource->tag, 29 | 'description' => "API documentation for {$resource->tag}", 30 | ]; 31 | } 32 | 33 | return $tags->toArray(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Fixtures/app/Http/Controllers/UserRolesController.php: -------------------------------------------------------------------------------- 1 | resourceComponentBaseName = $resourceComponentBaseName; 19 | } 20 | 21 | public function toArray(): array 22 | { 23 | return array_merge( 24 | parent::toArray(), 25 | [ 26 | 'content' => [ 27 | 'application/json' => [ 28 | 'schema' => [ 29 | '$ref' => "#/components/schemas/{$this->resourceComponentBaseName}", 30 | ], 31 | ], 32 | ], 33 | ] 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/ValueObjects/Specs/Requests/UpdateRequest.php: -------------------------------------------------------------------------------- 1 | resourceComponentBaseName = $resourceComponentBaseName; 19 | } 20 | 21 | public function toArray(): array 22 | { 23 | return array_merge( 24 | parent::toArray(), 25 | [ 26 | 'content' => [ 27 | 'application/json' => [ 28 | 'schema' => [ 29 | '$ref' => "#/components/schemas/{$this->resourceComponentBaseName}", 30 | ], 31 | ], 32 | ], 33 | ] 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Fixtures/app/Http/Requests/TeamRequest.php: -------------------------------------------------------------------------------- 1 | ['string', 'nullable'] 18 | ]; 19 | } 20 | 21 | /** 22 | * Rules for the "store" (POST) endpoint. 23 | * 24 | * @return array 25 | */ 26 | public function storeRules() : array 27 | { 28 | return [ 29 | 'name' => ['string', 'required'] 30 | ]; 31 | } 32 | 33 | /** 34 | * Rules for the "update" (PATCH|PUT) endpoint. 35 | * 36 | * @return array 37 | */ 38 | public function updateRules() : array 39 | { 40 | return [ 41 | 'name' => ['string', 'required'] 42 | ]; 43 | } 44 | } -------------------------------------------------------------------------------- /tests/Fixtures/app/Http/Requests/PostRequest.php: -------------------------------------------------------------------------------- 1 | ['string', 'required', 'max:255'], 28 | 'body' => ['string', 'required'] 29 | ]; 30 | } 31 | 32 | /** 33 | * Rules for the "update" (PATCH|PUT) endpoint. 34 | * 35 | * @return array 36 | */ 37 | public function updateRules() : array 38 | { 39 | return [ 40 | 'title' => ['string', 'required', 'max:255'] 41 | ]; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/Fixtures/app/Http/Requests/UserRequest.php: -------------------------------------------------------------------------------- 1 | ['string', 'required', 'max:255'], 28 | 'email' => ['string', 'email', 'required'] 29 | ]; 30 | } 31 | 32 | /** 33 | * Rules for the "update" (PATCH|PUT) endpoint. 34 | * 35 | * @return array 36 | */ 37 | public function updateRules() : array 38 | { 39 | return [ 40 | 'name' => ['string', 'required', 'max:255'] 41 | ]; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/Unit/Concerns/ExtendsResourcesTest.php: -------------------------------------------------------------------------------- 1 | 'test'])); 17 | $this->assertSame( 18 | [ 19 | 'title' => 'test', 20 | 'additional-value' => 'test', 21 | ], 22 | $stub->toArrayWithMerge( 23 | new Request(), 24 | [ 25 | 'additional-value' => 'test', 26 | ] 27 | ) 28 | ); 29 | } 30 | } 31 | 32 | class ExtendsResourcesStub extends JsonResource 33 | { 34 | use ExtendsResources; 35 | } 36 | -------------------------------------------------------------------------------- /src/Contracts/ParamsValidator.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->text('path'); 19 | $table->unsignedBigInteger('post_id'); 20 | $table->foreign('post_id')->references('id')->on('posts')->onDelete('cascade'); 21 | $table->timestamps(); 22 | $table->softDeletes(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::dropIfExists('post_images'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/ValueObjects/Specs/Requests/Relations/ManyToMany/UpdatePivotRequest.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'application/json' => [ 18 | 'schema' => [ 19 | 'type' => 'object', 20 | 'properties' => [ 21 | 'pivot' => [ 22 | 'type' => 'object', 23 | 'description' => 'Pivot fields' 24 | ] 25 | ], 26 | ], 27 | ], 28 | ] 29 | ] 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Specs/Builders/RelationOperationBuilder.php: -------------------------------------------------------------------------------- 1 | make($this->resource->controller)->getModel(); 21 | /** @var Model $parentResourceModel */ 22 | $parentResourceModel = app()->make($parentResourceModelClass); 23 | 24 | $resourceName = Str::lower(str_replace('_', ' ', $parentResourceModel->getTable())); 25 | 26 | if (!$pluralize) { 27 | return Str::singular($resourceName); 28 | } 29 | 30 | return $resourceName; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Specs/Factories/OperationBuilderFactory.php: -------------------------------------------------------------------------------- 1 | makeWith( 25 | $operationBuilderClass, 26 | [ 27 | 'resource' => $resource, 28 | 'operation' => $operation, 29 | 'route' => $route, 30 | ] 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Unit/Concerns/DisableRouteDiscoveryTest.php: -------------------------------------------------------------------------------- 1 | assertFalse($controller->routeDiscoveryEnabled()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Aleksei Zarubin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /src/Specs/Factories/RelationOperationBuilderFactory.php: -------------------------------------------------------------------------------- 1 | makeWith( 25 | $operationBuilderClass, 26 | [ 27 | 'resource' => $resource, 28 | 'operation' => $operation, 29 | 'route' => $route, 30 | ] 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Fixtures/database/migrations/2019_01_06_051132_create_teams_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('name'); 19 | $table->string('description')->nullable(); 20 | $table->boolean('active')->default(true); 21 | 22 | $table->unsignedBigInteger('company_id')->nullable(); 23 | $table->foreign('company_id')->references('id')->on('companies'); 24 | 25 | $table->timestamps(); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | * 32 | * @return void 33 | */ 34 | public function down() 35 | { 36 | Schema::dropIfExists('teams'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Fixtures/database/migrations/2019_03_06_126415_create_access_key_scopes_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('scope'); 19 | $table->string('description')->nullable(); 20 | $table->unsignedBigInteger('access_key_id'); 21 | $table->foreign('access_key_id')->references('id')->on('access_keys')->onDelete('cascade'); 22 | $table->timestamps(); 23 | $table->softDeletes(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('access_key_scopes'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Specs/Builders/Operations/ShowOperationBuilder.php: -------------------------------------------------------------------------------- 1 | makeBaseOperation(); 21 | $operation->summary = "Get {$this->resolveResourceName()}"; 22 | 23 | return $operation; 24 | } 25 | 26 | /** 27 | * @return array 28 | * @throws BindingResolutionException 29 | */ 30 | protected function responses(): array 31 | { 32 | return array_merge( 33 | [ 34 | new EntityResponse($this->resolveResourceComponentBaseName()), 35 | ], 36 | parent::responses() 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Specs/Builders/Partials/RequestBody/Search/IncludesBuilder.php: -------------------------------------------------------------------------------- 1 | controller->includes())) { 14 | return null; 15 | } 16 | 17 | return [ 18 | 'type' => 'array', 19 | 'items' => [ 20 | 'type' => 'object', 21 | 'properties' => [ 22 | 'relation' => [ 23 | 'type' => 'string', 24 | 'enum' => $this->controller->includes(), 25 | ], 26 | 'filters' => [ 27 | 'type' => 'object', 28 | 'properties' => app()->makeWith(FiltersBuilder::class, ['controller' => get_class($this->controller)])->build(), 29 | ], 30 | ] 31 | ] 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Specs/Builders/Partials/RequestBody/Search/SortBuilder.php: -------------------------------------------------------------------------------- 1 | controller->sortableBy())) { 14 | return null; 15 | } 16 | 17 | return [ 18 | 'type' => 'array', 19 | 'items' => [ 20 | 'type' => 'object', 21 | 'properties' => [ 22 | 'field' => [ 23 | 'type' => 'string', 24 | 'enum' => $this->controller->sortableBy(), 25 | ], 26 | 'direction' => [ 27 | 'type' => 'string', 28 | 'enum' => ['asc', 'desc'] 29 | ] 30 | ], 31 | 'required' => [ 32 | 'field' 33 | ] 34 | ] 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Specs/Builders/Operations/DestroyOperationBuilder.php: -------------------------------------------------------------------------------- 1 | makeBaseOperation(); 21 | $operation->summary = "Delete {$this->resolveResourceName()}"; 22 | 23 | return $operation; 24 | } 25 | 26 | /** 27 | * @return array 28 | * @throws BindingResolutionException 29 | */ 30 | protected function responses(): array 31 | { 32 | return array_merge( 33 | [ 34 | new EntityResponse($this->resolveResourceComponentBaseName()), 35 | ], 36 | parent::responses() 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Specs/Builders/Operations/RestoreOperationBuilder.php: -------------------------------------------------------------------------------- 1 | makeBaseOperation(); 21 | $operation->summary = "Restore {$this->resolveResourceName()}"; 22 | 23 | return $operation; 24 | } 25 | 26 | /** 27 | * @return array 28 | * @throws BindingResolutionException 29 | */ 30 | protected function responses(): array 31 | { 32 | return array_merge( 33 | [ 34 | new EntityResponse($this->resolveResourceComponentBaseName()), 35 | ], 36 | parent::responses() 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Specs/Builders/Components/Shared/ResourceLinksComponentBuilder.php: -------------------------------------------------------------------------------- 1 | title = 'ResourceLinks'; 16 | $component->type = 'object'; 17 | $component->properties = [ 18 | 'first' => [ 19 | 'type' => 'string', 20 | 'format' => 'uri', 21 | ], 22 | 'last' => [ 23 | 'type' => 'string', 24 | 'format' => 'uri', 25 | ], 26 | 'prev' => [ 27 | 'type' => 'string', 28 | 'format' => 'uri', 29 | ], 30 | 'next' => [ 31 | 'type' => 'string', 32 | 'format' => 'uri', 33 | ], 34 | ]; 35 | 36 | return $component; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Fixtures/database/migrations/2019_03_04_184404_create_post_metas_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('title')->nullable(); 19 | $table->string('name')->nullable(); 20 | $table->text('notes'); 21 | $table->boolean('comments_enabled')->default(true); 22 | $table->unsignedBigInteger('post_id'); 23 | $table->foreign('post_id')->references('id')->on('posts')->onDelete('cascade'); 24 | $table->timestamps(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::dropIfExists('post_metas'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Specs/Builders/Partials/RequestBody/Search/SearchBuilder.php: -------------------------------------------------------------------------------- 1 | controller->searchableBy())) { 14 | return null; 15 | } 16 | 17 | return [ 18 | 'type' => 'object', 19 | 'properties' => [ 20 | 'value' => [ 21 | 'type' => 'string', 22 | 'description' => 'A search for the given value will be performed on the following fields: ' . collect( 23 | $this->controller->searchableBy() 24 | )->join(', ') 25 | ], 26 | 'case_sensitive' => [ 27 | 'type' => 'boolean', 28 | 'description' => '(default: true) Set it to false to perform search in case-insensitive way' 29 | ] 30 | ] 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Testing/InteractsWithJsonFields.php: -------------------------------------------------------------------------------- 1 | map( 15 | function ($value) { 16 | if (is_array($value) || $value instanceof Jsonable) { 17 | return $this->castFieldToJson($value); 18 | } 19 | 20 | return $value; 21 | } 22 | )->toArray(); 23 | } 24 | 25 | protected function castFieldToJson($value) 26 | { 27 | if ($value instanceof Jsonable) { 28 | $value = $value->toJson(); 29 | } else { 30 | $value = json_encode($value); 31 | } 32 | 33 | if (config('database.default') === 'mysql') { 34 | $value = DB::raw("CAST('{$value}' AS JSON)"); 35 | } 36 | if (config('database.default') === 'pgsql') { 37 | $value = DB::raw("'{$value}'::jsonb"); 38 | } 39 | 40 | return $value; 41 | } 42 | } -------------------------------------------------------------------------------- /tests/Fixtures/database/migrations/2019_01_06_060001_create_users_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | 19 | $table->unsignedBigInteger('team_id')->nullable(); 20 | $table->foreign('team_id')->references('id')->on('teams')->onDelete('cascade'); 21 | 22 | $table->string('name'); 23 | $table->string('email')->unique(); 24 | $table->timestamp('email_verified_at')->nullable(); 25 | $table->string('password'); 26 | $table->rememberToken(); 27 | $table->timestamps(); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | * 34 | * @return void 35 | */ 36 | public function down() 37 | { 38 | Schema::dropIfExists('users'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Fixtures/database/migrations/2019_03_05_127449_create_notification_user_pivot_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->unsignedBigInteger('notification_id'); 19 | $table->foreign('notification_id')->references('id')->on('notifications')->onDelete('cascade'); 20 | $table->unsignedBigInteger('user_id'); 21 | $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); 22 | $table->jsonb('meta')->nullable(); 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('notification_user'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Unit/Http/Middleware/EnforceExpectsJsonTest.php: -------------------------------------------------------------------------------- 1 | handle( 17 | $request, 18 | function ($processedRequest) { 19 | $this->assertTrue($processedRequest->expectsJson()); 20 | } 21 | ); 22 | } 23 | 24 | /** @test */ 25 | public function preserving_existing_accept_header_content_types(): void 26 | { 27 | $request = Request::create('/api/posts'); 28 | $request->headers->set('Accept', 'application/xml'); 29 | 30 | (new EnforceExpectsJson())->handle( 31 | $request, 32 | function ($processedRequest) { 33 | $this->assertSame('application/json, application/xml', $processedRequest->header('Accept')); 34 | } 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Specs/Builders/Partials/RequestBody/Search/ScopesBuilder.php: -------------------------------------------------------------------------------- 1 | controller->exposedScopes())) { 14 | return null; 15 | } 16 | 17 | return [ 18 | 'type' => 'array', 19 | 'items' => [ 20 | 'type' => 'object', 21 | 'properties' => [ 22 | 'name' => [ 23 | 'type' => 'string', 24 | 'enum' => $this->controller->exposedScopes(), 25 | ], 26 | 'parameters' => [ 27 | 'type' => 'array', 28 | 'items' => [ 29 | 'type' => 'string' 30 | ] 31 | ] 32 | ], 33 | 'required' => [ 34 | 'name' 35 | ] 36 | ] 37 | ]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Fixtures/app/Models/User.php: -------------------------------------------------------------------------------- 1 | hasMany(Post::class); 33 | } 34 | 35 | public function roles() 36 | { 37 | return $this->belongsToMany(Role::class) 38 | ->withPivot('meta', 'references', 'custom_name') 39 | ->withTimestamps() 40 | ->using(UserRole::class); 41 | } 42 | 43 | public function notifications() 44 | { 45 | return $this->belongsToMany(Notification::class)->withPivot('meta'); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/Fixtures/app/Http/Requests/RoleRequest.php: -------------------------------------------------------------------------------- 1 | ['string', 'required', 'max:255'], 20 | ]; 21 | } 22 | 23 | /** 24 | * Rules for the "store" (POST) endpoint. 25 | * 26 | * @return array 27 | */ 28 | public function storeRules() : array 29 | { 30 | return [ 31 | 'description' => ['string', 'required', 'min:5'], 32 | 'pivot.custom_name' => ['string', 'required', 'min:5'] 33 | ]; 34 | } 35 | 36 | /** 37 | * Rules for the "update" (PATCH|PUT) endpoint. 38 | * 39 | * @return array 40 | */ 41 | public function updateRules() : array 42 | { 43 | return [ 44 | 'description' => ['string', 'required', 'min:2'], 45 | 'pivot.custom_name' => ['string', 'required', 'min:2'] 46 | ]; 47 | } 48 | } -------------------------------------------------------------------------------- /src/Specs/Builders/Components/Shared/SecurityComponentBuilder.php: -------------------------------------------------------------------------------- 1 | title = 'securitySchemes'; 16 | $component->schemes = [ 17 | 'BearerAuth' => [ 18 | 'type' => 'http', 19 | 'scheme' => 'bearer' 20 | ] 21 | ]; 22 | 23 | if (class_exists('Laravel\\Passport\\PassportServiceProvider')) { 24 | $component->schemes['OAuth2'] = [ 25 | 'type' => 'oauth2', 26 | 'flows' => [ 27 | 'authorizationCode' => [ 28 | 'authorizationUrl' => route('passport.authorizations.authorize'), 29 | 'tokenUrl' => route('passport.token') 30 | ] 31 | ] 32 | ]; 33 | } 34 | 35 | return $component; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/ValueObjects/Specs/Requests/Relations/ManyToMany/SyncRequest.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'application/json' => [ 18 | 'schema' => [ 19 | 'type' => 'object', 20 | 'properties' => [ 21 | 'resources' => [ 22 | 'type' => 'array', 23 | 'items' => [ 24 | 'type' => 'object', 25 | 'description' => 'A key-value pairs, where keys are relation resource IDs and values are objects representing pivot fields' 26 | ] 27 | ] 28 | ] 29 | ], 30 | ], 31 | ], 32 | ] 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Fixtures/database/migrations/2019_03_05_125449_create_role_user_pivot_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->unsignedBigInteger('role_id'); 19 | $table->foreign('role_id')->references('id')->on('roles')->onDelete('cascade'); 20 | $table->unsignedBigInteger('user_id'); 21 | $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); 22 | $table->jsonb('meta')->nullable(); 23 | $table->jsonb('references')->nullable(); 24 | $table->string('custom_name')->nullable(); 25 | $table->timestamps(); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | * 32 | * @return void 33 | */ 34 | public function down() 35 | { 36 | Schema::dropIfExists('role_user'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/ValueObjects/Specs/Requests/Relations/ManyToMany/AttachRequest.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'application/json' => [ 18 | 'schema' => [ 19 | 'type' => 'object', 20 | 'properties' => [ 21 | 'resources' => [ 22 | 'type' => 'array', 23 | 'items' => [ 24 | 'type' => 'object', 25 | 'description' => 'A key-value pairs, where keys are relation resource IDs and values are objects representing pivot fields' 26 | ] 27 | ] 28 | ] 29 | ], 30 | ], 31 | ], 32 | ] 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/ValueObjects/Specs/Requests/Relations/ManyToMany/DetachRequest.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'application/json' => [ 18 | 'schema' => [ 19 | 'type' => 'object', 20 | 'properties' => [ 21 | 'resources' => [ 22 | 'type' => 'array', 23 | 'items' => [ 24 | 'type' => 'object', 25 | 'description' => 'A key-value pairs, where keys are relation resource IDs and values are objects representing pivot fields' 26 | ] 27 | ] 28 | ] 29 | ], 30 | ], 31 | ], 32 | ] 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/ValueObjects/Specs/Requests/Relations/ManyToMany/ToggleRequest.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'application/json' => [ 18 | 'schema' => [ 19 | 'type' => 'object', 20 | 'properties' => [ 21 | 'resources' => [ 22 | 'type' => 'array', 23 | 'items' => [ 24 | 'type' => 'object', 25 | 'description' => 'A key-value pairs, where keys are relation resource IDs and values are objects representing pivot fields' 26 | ] 27 | ] 28 | ] 29 | ], 30 | ], 31 | ], 32 | ] 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Specs/Builders/Components/Shared/ResourceMetaComponentBuilder.php: -------------------------------------------------------------------------------- 1 | title = 'ResourceMeta'; 16 | $component->type = 'object'; 17 | $component->properties = [ 18 | 'current_page' => [ 19 | 'type' => 'integer', 20 | ], 21 | 'from' => [ 22 | 'type' => 'integer', 23 | ], 24 | 'last_page' => [ 25 | 'type' => 'integer', 26 | ], 27 | 'path' => [ 28 | 'type' => 'string', 29 | ], 30 | 'per_page' => [ 31 | 'type' => 'integer', 32 | ], 33 | 'to' => [ 34 | 'type' => 'integer', 35 | ], 36 | 'total' => [ 37 | 'type' => 'integer', 38 | ], 39 | ]; 40 | 41 | return $component; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/Unit/Http/Controllers/Stubs/BaseControllerStub.php: -------------------------------------------------------------------------------- 1 | getModel(); 40 | } 41 | 42 | public function getResourceQueryBuilder(): QueryBuilder 43 | { 44 | return $this->getQueryBuilder(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/ValueObjects/Specs/Operation.php: -------------------------------------------------------------------------------- 1 | $this->parameters, 30 | 'summary' => $this->summary, 31 | 'responses' => collect($this->responses)->mapWithKeys( 32 | function (Response $response) { 33 | return [(string)$response->statusCode => $response->toArray()]; 34 | } 35 | )->toArray(), 36 | 'tags' => $this->tags 37 | ]; 38 | 39 | if ($this->request) { 40 | $operation['requestBody'] = $this->request->toArray(); 41 | } 42 | 43 | ksort($operation); 44 | 45 | return $operation; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/Unit/Specs/Builders/PropertyBuilderTest.php: -------------------------------------------------------------------------------- 1 | pathsBuilder = app()->make(PathsBuilder::class); 24 | } 25 | 26 | /** @test */ 27 | public function building_property(): void 28 | { 29 | $column = [ 30 | 'name' => 'example_column', 31 | 'nullable' => true, 32 | ]; 33 | $concretePropertyClass = 'Orion\ValueObjects\Specs\Schema\SchemaProperty'; 34 | 35 | $propertyBuilder = new PropertyBuilder(); 36 | $property = $propertyBuilder->build($column, $concretePropertyClass); 37 | 38 | $this->assertInstanceOf(SchemaProperty::class, $property); 39 | $this->assertEquals($column['name'], $property->name); 40 | $this->assertEquals($column['nullable'], $property->nullable); 41 | } 42 | } 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/Specs/Builders/Operations/IndexOperationBuilder.php: -------------------------------------------------------------------------------- 1 | makeBaseOperation(); 23 | $operation->summary = "Get a list of {$this->resolveResourceName(true)}"; 24 | 25 | return $operation; 26 | } 27 | 28 | /** 29 | * @return array 30 | * @throws BindingResolutionException 31 | */ 32 | protected function responses(): array 33 | { 34 | return [ 35 | new PaginatedCollectionResponse($this->resolveResourceComponentBaseName()), 36 | new UnauthenticatedResponse(), 37 | new UnauthorizedResponse(), 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Specs/Builders/Partials/RequestBody/Search/AggregatesBuilder.php: -------------------------------------------------------------------------------- 1 | controller->aggregates())) { 14 | return null; 15 | } 16 | 17 | return [ 18 | 'type' => 'array', 19 | 'items' => [ 20 | 'type' => 'object', 21 | 'properties' => [ 22 | 'type' => [ 23 | 'type' => 'string', 24 | 'enum' => ['count', 'min', 'max', 'avg', 'sum', 'exists'], 25 | ], 26 | 'relation' => [ 27 | 'type' => 'string', 28 | 'enum' => $this->controller->aggregates(), 29 | ], 30 | 'filters' => [ 31 | 'type' => 'object', 32 | 'properties' => app()->makeWith(FiltersBuilder::class, ['controller' => get_class($this->controller)])->build(), 33 | ], 34 | ] 35 | ] 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/ValueObjects/Specs/Responses/Success/CollectionResponse.php: -------------------------------------------------------------------------------- 1 | resourceComponentBaseName = $resourceComponentBaseName; 16 | } 17 | 18 | public function toArray(): array 19 | { 20 | return array_merge( 21 | parent::toArray(), 22 | [ 23 | 'content' => [ 24 | 'application/json' => [ 25 | 'schema' => [ 26 | 'type' => 'object', 27 | 'properties' => [ 28 | 'data' => [ 29 | 'type' => 'array', 30 | 'items' => [ 31 | '$ref' => "#/components/schemas/{$this->resourceComponentBaseName}Resource", 32 | ], 33 | ], 34 | ], 35 | ], 36 | ], 37 | ], 38 | ] 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Unit/Http/Controllers/Stubs/RelationControllerStub.php: -------------------------------------------------------------------------------- 1 | resourceComponentBaseName = $resourceComponentBaseName; 19 | } 20 | 21 | public function toArray(): array 22 | { 23 | return array_merge( 24 | parent::toArray(), 25 | [ 26 | 'content' => [ 27 | 'application/json' => [ 28 | 'schema' => [ 29 | 'type' => 'object', 30 | 'properties' => [ 31 | 'resources' => [ 32 | 'type' => 'array', 33 | 'items' => [ 34 | '$ref' => "#/components/schemas/{$this->resourceComponentBaseName}", 35 | ] 36 | ] 37 | ] 38 | ], 39 | ], 40 | ], 41 | ] 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Concerns/InteractsWithSoftDeletes.php: -------------------------------------------------------------------------------- 1 | query('force', false), FILTER_VALIDATE_BOOLEAN); 32 | } 33 | 34 | /** 35 | * Determines, if the resource is considered trashed for the current request. 36 | * 37 | * @param Model|SoftDeletes $entity 38 | * @param bool $softDeletes 39 | * @param bool $forceDeletes 40 | * @return bool 41 | */ 42 | protected function isResourceTrashed(Model $entity, bool $softDeletes, bool $forceDeletes): bool 43 | { 44 | return !$forceDeletes && $softDeletes && $entity->trashed(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Drivers/Standard/Paginator.php: -------------------------------------------------------------------------------- 1 | defaultLimit = $defaultLimit; 29 | $this->maxLimit = $maxLimit; 30 | } 31 | 32 | /** 33 | * Determine the pagination limit based on the "limit" query parameter or the default, specified by developer. 34 | * 35 | * @param Request $request 36 | * @return int 37 | */ 38 | public function resolvePaginationLimit(Request $request): int 39 | { 40 | $limit = (int) $request->get('limit'); 41 | 42 | return tap($limit > 0 ? $limit : $this->defaultLimit, function ($limit) { 43 | if ($this->maxLimit && $limit > $this->maxLimit) { 44 | throw new MaxPaginationLimitExceededException(422, __("Pagination limit of :max is exceeded. Current: :limit", ['max' => $this->maxLimit, 'limit' => $limit])); 45 | } 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/ValueObjects/Specs/Requests/Batch/BatchUpdateRequest.php: -------------------------------------------------------------------------------- 1 | resourceComponentBaseName = $resourceComponentBaseName; 19 | } 20 | 21 | public function toArray(): array 22 | { 23 | return array_merge( 24 | parent::toArray(), 25 | [ 26 | 'content' => [ 27 | 'application/json' => [ 28 | 'schema' => [ 29 | 'type' => 'object', 30 | 'properties' => [ 31 | 'resources' => [ 32 | 'type' => 'object', 33 | 'properties' => [ 34 | '{key}' => [ 35 | '$ref' => "#/components/schemas/{$this->resourceComponentBaseName}", 36 | ], 37 | ], 38 | ], 39 | ], 40 | ], 41 | ], 42 | ], 43 | ] 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/ValueObjects/Specs/Responses/Success/Relation/ManyToMany/AttachResponse.php: -------------------------------------------------------------------------------- 1 | resourceModel = $resourceModel; 23 | } 24 | 25 | public function toArray(): array 26 | { 27 | return array_merge( 28 | parent::toArray(), 29 | [ 30 | 'content' => [ 31 | 'application/json' => [ 32 | 'schema' => [ 33 | 'type' => 'object', 34 | 'properties' => [ 35 | 'attached' => [ 36 | 'type' => 'array', 37 | 'items' => [ 38 | 'type' => $this->resourceModel->getKeyType() === 'int' ? 'integer' : 'string', 39 | ], 40 | ], 41 | ], 42 | ], 43 | ], 44 | ], 45 | ] 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/ValueObjects/Specs/Responses/Success/Relation/ManyToMany/DetachResponse.php: -------------------------------------------------------------------------------- 1 | resourceModel = $resourceModel; 23 | } 24 | 25 | public function toArray(): array 26 | { 27 | return array_merge( 28 | parent::toArray(), 29 | [ 30 | 'content' => [ 31 | 'application/json' => [ 32 | 'schema' => [ 33 | 'type' => 'object', 34 | 'properties' => [ 35 | 'detached' => [ 36 | 'type' => 'array', 37 | 'items' => [ 38 | 'type' => $this->resourceModel->getKeyType() === 'int' ? 'integer' : 'string', 39 | ], 40 | ], 41 | ], 42 | ], 43 | ], 44 | ], 45 | ] 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | Latest Version on Packagist 7 | Build Status 8 |

9 | 10 | ## Introduction 11 | 12 | Orion for Laravel allows you to build a fully featured REST API based on your Eloquent models and relationships with simplicity of Laravel as you love it. 13 | 14 | ## Documentation 15 | 16 | Documentation can be found on the [website](https://orion.tailflow.org). 17 | 18 | ## Supported By 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ## License 29 | 30 | The Laravel Orion is open-source software licensed under the [MIT license](https://opensource.org/licenses/MIT). 31 | -------------------------------------------------------------------------------- /src/ValueObjects/Specs/Responses/Success/Relation/ManyToMany/UpdatePivotResponse.php: -------------------------------------------------------------------------------- 1 | resourceModel = $resourceModel; 23 | } 24 | 25 | public function toArray(): array 26 | { 27 | return array_merge( 28 | parent::toArray(), 29 | [ 30 | 'content' => [ 31 | 'application/json' => [ 32 | 'schema' => [ 33 | 'type' => 'object', 34 | 'properties' => [ 35 | 'updated' => [ 36 | 'type' => 'array', 37 | 'items' => [ 38 | 'type' => $this->resourceModel->getKeyType() === 'int' ? 'integer' : 'string', 39 | ], 40 | ], 41 | ], 42 | ], 43 | ], 44 | ], 45 | ] 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/Fixtures/database/migrations/2019_01_06_113445_create_posts_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | 19 | $table->string('title'); 20 | $table->text('body'); 21 | $table->float('stars')->nullable(); 22 | $table->string('tracking_id')->nullable(); 23 | $table->jsonb('meta')->nullable(); 24 | $table->jsonb('options')->nullable(); 25 | $table->unsignedInteger('position')->default(0); 26 | 27 | $table->unsignedBigInteger('user_id')->nullable(); 28 | $table->unsignedBigInteger('category_id')->nullable(); 29 | 30 | $table->timestamp('publish_at')->nullable(); 31 | $table->timestamps(); 32 | $table->softDeletes(); 33 | 34 | $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); 35 | $table->foreign('category_id')->references('id')->on('categories')->onDelete('cascade'); 36 | }); 37 | } 38 | 39 | /** 40 | * Reverse the migrations. 41 | * 42 | * @return void 43 | */ 44 | public function down() 45 | { 46 | Schema::dropIfExists('posts'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Contracts/RelationsResolver.php: -------------------------------------------------------------------------------- 1 | resourceComponentBaseName = $resourceComponentBaseName; 17 | $this->statusCode = $statusCode; 18 | } 19 | 20 | public function toArray(): array 21 | { 22 | return array_merge( 23 | parent::toArray(), 24 | [ 25 | 'content' => [ 26 | 'application/json' => [ 27 | 'schema' => JsonResource::$wrap ? 28 | [ 29 | 'type' => 'object', 30 | 'properties' => [ 31 | JsonResource::$wrap => [ 32 | '$ref' => "#/components/schemas/{$this->resourceComponentBaseName}Resource", 33 | ], 34 | ], 35 | ] : 36 | [ 37 | '$ref' => "#/components/schemas/{$this->resourceComponentBaseName}Resource", 38 | ], 39 | ], 40 | ], 41 | ] 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Specs/Builders/Operations/Batch/BatchDestroyOperationBuilder.php: -------------------------------------------------------------------------------- 1 | makeBaseOperation(); 24 | $operation->summary = "Delete a batch of {$this->resolveResourceName(true)}"; 25 | 26 | return $operation; 27 | } 28 | 29 | /** 30 | * @return Request|null 31 | */ 32 | protected function request(): ?Request 33 | { 34 | return new BatchDestroyRequest($this->resource); 35 | } 36 | 37 | /** 38 | * @return array 39 | * @throws BindingResolutionException 40 | */ 41 | protected function responses(): array 42 | { 43 | return array_merge( 44 | [ 45 | new CollectionResponse($this->resolveResourceComponentBaseName()), 46 | new ValidationErrorResponse(), 47 | ], 48 | parent::responses() 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Specs/Builders/Operations/Batch/BatchRestoreOperationBuilder.php: -------------------------------------------------------------------------------- 1 | makeBaseOperation(); 24 | $operation->summary = "Restore a batch of {$this->resolveResourceName(true)}"; 25 | 26 | return $operation; 27 | } 28 | 29 | /** 30 | * @return Request|null 31 | */ 32 | protected function request(): ?Request 33 | { 34 | return new BatchRestoreRequest($this->resource); 35 | } 36 | 37 | /** 38 | * @return array 39 | * @throws BindingResolutionException 40 | */ 41 | protected function responses(): array 42 | { 43 | return array_merge( 44 | [ 45 | new CollectionResponse($this->resolveResourceComponentBaseName()), 46 | new ValidationErrorResponse(), 47 | ], 48 | parent::responses() 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Specs/Builders/Operations/Relations/OneToMany/AssociateOperationBuilder.php: -------------------------------------------------------------------------------- 1 | makeBaseOperation(); 24 | $operation->summary = "Associate {$this->resolveResourceName(false)} with {$this->resolveParentResourceName(false)}"; 25 | 26 | return $operation; 27 | } 28 | 29 | /** 30 | * @return array 31 | * @throws BindingResolutionException 32 | */ 33 | protected function responses(): array 34 | { 35 | return [ 36 | new EntityResponse($this->resolveResourceComponentBaseName()), 37 | new UnauthenticatedResponse(), 38 | new UnauthorizedResponse(), 39 | new ResourceNotFoundResponse(), 40 | new ValidationErrorResponse(), 41 | ]; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Specs/Builders/Operations/Relations/OneToMany/DissociateOperationBuilder.php: -------------------------------------------------------------------------------- 1 | makeBaseOperation(); 24 | $operation->summary = "Dissociate {$this->resolveResourceName(false)} from {$this->resolveParentResourceName(false)}"; 25 | 26 | return $operation; 27 | } 28 | 29 | /** 30 | * @return array 31 | * @throws BindingResolutionException 32 | */ 33 | protected function responses(): array 34 | { 35 | return [ 36 | new EntityResponse($this->resolveResourceComponentBaseName()), 37 | new UnauthenticatedResponse(), 38 | new UnauthorizedResponse(), 39 | new ResourceNotFoundResponse(), 40 | new ValidationErrorResponse(), 41 | ]; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Specs/Builders/Partials/RequestBody/Search/FiltersBuilder.php: -------------------------------------------------------------------------------- 1 | controller->filterableBy())) { 14 | return null; 15 | } 16 | 17 | $filters = [ 18 | 'type' => [ 19 | 'type' => 'string', 20 | 'enum' => ['and', 'or'], 21 | ], 22 | 'field' => [ 23 | 'type' => 'string', 24 | 'enum' => $this->controller->filterableBy(), 25 | ], 26 | 'operator' => [ 27 | 'type' => 'string', 28 | 'enum' => ['<','<=','>','>=','=','!=','like','not like','ilike','not ilike','in','not in', 'all in', 'any in'], 29 | ], 30 | 'value' => [ 31 | 'type' => 'string', 32 | ] 33 | ]; 34 | 35 | return [ 36 | 'type' => 'array', 37 | 'items' => [ 38 | 'type' => 'object', 39 | 'properties' => array_merge($filters, [ 40 | 'nested' => [ 41 | 'type' => 'array', 42 | 'items' => [ 43 | 'type' => 'object', 44 | 'properties' => $filters 45 | ] 46 | ] 47 | ]) 48 | ] 49 | ]; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Specs/Builders/Operations/Batch/BatchStoreOperationBuilder.php: -------------------------------------------------------------------------------- 1 | makeBaseOperation(); 24 | $operation->summary = "Create a batch of {$this->resolveResourceName(true)}"; 25 | 26 | return $operation; 27 | } 28 | 29 | /** 30 | * @return Request|null 31 | * @throws BindingResolutionException 32 | */ 33 | protected function request(): ?Request 34 | { 35 | return new BatchStoreRequest($this->resolveResourceComponentBaseName()); 36 | } 37 | 38 | /** 39 | * @return array 40 | * @throws BindingResolutionException 41 | */ 42 | protected function responses(): array 43 | { 44 | return array_merge( 45 | [ 46 | new CollectionResponse($this->resolveResourceComponentBaseName()), 47 | new ValidationErrorResponse(), 48 | ], 49 | parent::responses() 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Specs/Builders/Operations/Batch/BatchUpdateOperationBuilder.php: -------------------------------------------------------------------------------- 1 | makeBaseOperation(); 24 | $operation->summary = "Update a batch of {$this->resolveResourceName(true)}"; 25 | 26 | return $operation; 27 | } 28 | 29 | /** 30 | * @return Request|null 31 | * @throws BindingResolutionException 32 | */ 33 | protected function request(): ?Request 34 | { 35 | return new BatchUpdateRequest($this->resolveResourceComponentBaseName()); 36 | } 37 | 38 | /** 39 | * @return array 40 | * @throws BindingResolutionException 41 | */ 42 | protected function responses(): array 43 | { 44 | return array_merge( 45 | [ 46 | new CollectionResponse($this->resolveResourceComponentBaseName()), 47 | new ValidationErrorResponse(), 48 | ], 49 | parent::responses() 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/Unit/Http/Rules/WhitelistedFieldTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($rule->passes('', 'any-field')); 15 | } 16 | 17 | /** @test */ 18 | public function exact_match_root_level_valid_field() 19 | { 20 | $rule = new WhitelistedField(['some-field', 'another-field']); 21 | $this->assertTrue($rule->passes('', 'some-field')); 22 | } 23 | 24 | /** @test */ 25 | public function exact_match_root_level_invalid_field() 26 | { 27 | $rule = new WhitelistedField(['some-field', 'another-field']); 28 | $this->assertFalse($rule->passes('', 'some-other-field')); 29 | } 30 | 31 | /** @test */ 32 | public function wildcard_match_2nd_level_nested_field() 33 | { 34 | $rule = new WhitelistedField(['parent.*']); 35 | $this->assertTrue($rule->passes('', 'parent.some-field')); 36 | } 37 | 38 | /** @test */ 39 | public function exact_match_2nd_level_nested_valid_field() 40 | { 41 | $rule = new WhitelistedField(['parent.some-field']); 42 | $this->assertTrue($rule->passes('', 'parent.some-field')); 43 | } 44 | 45 | /** @test */ 46 | public function exact_match_2nd_level_nested_invalid_field() 47 | { 48 | $rule = new WhitelistedField(['parent.some-field']); 49 | $this->assertFalse($rule->passes('', 'parent.some-other-field')); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/Unit/Http/Controllers/Stubs/BaseControllerStubWithWhitelistedFieldsAndRelations.php: -------------------------------------------------------------------------------- 1 | getModel(); 22 | } 23 | 24 | public function exposedScopes(): array 25 | { 26 | return ['testScope']; 27 | } 28 | 29 | public function filterableBy(): array 30 | { 31 | return ['test_filterable_field']; 32 | } 33 | 34 | public function sortableBy(): array 35 | { 36 | return ['test_sortable_field']; 37 | } 38 | 39 | public function searchableBy(): array 40 | { 41 | return ['test_searchable_field']; 42 | } 43 | 44 | public function includes(): array 45 | { 46 | return ['testRelation']; 47 | } 48 | 49 | public function aggregates(): array 50 | { 51 | return ['test_aggregatable_field']; 52 | } 53 | 54 | public function alwaysIncludes(): array 55 | { 56 | return ['testAlwaysIncludedRelation']; 57 | } 58 | 59 | protected function bindComponents(): void 60 | { 61 | } 62 | 63 | public function getResourceQueryBuilder(): QueryBuilder 64 | { 65 | return $this->getQueryBuilder(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/ValueObjects/Specs/Responses/Success/PaginatedCollectionResponse.php: -------------------------------------------------------------------------------- 1 | resourceComponentBaseName = $resourceComponentBaseName; 16 | } 17 | 18 | public function toArray(): array 19 | { 20 | return array_merge( 21 | parent::toArray(), 22 | [ 23 | 'content' => [ 24 | 'application/json' => [ 25 | 'schema' => [ 26 | 'type' => 'object', 27 | 'properties' => [ 28 | 'data' => [ 29 | 'type' => 'array', 30 | 'items' => [ 31 | '$ref' => "#/components/schemas/{$this->resourceComponentBaseName}Resource", 32 | ], 33 | ], 34 | 'links' => [ 35 | '$ref' => "#/components/schemas/ResourceLinks", 36 | ], 37 | 'meta' => [ 38 | '$ref' => "#/components/schemas/ResourceMeta", 39 | ], 40 | ], 41 | ], 42 | ], 43 | ], 44 | ] 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/Fixtures/app/Http/Controllers/PostsController.php: -------------------------------------------------------------------------------- 1 | user()) { 24 | $entity->user()->associate($user); 25 | } 26 | } 27 | 28 | public function sortableBy(): array 29 | { 30 | return ['title', 'user.name', 'user.email', 'meta->nested_field']; 31 | } 32 | 33 | public function filterableBy(): array 34 | { 35 | return [ 36 | 'title', 37 | 'position', 38 | 'publish_at', 39 | 'user.name', 40 | 'user.roles.name', 41 | 'meta.name', 42 | 'meta.title', 43 | 'meta->nested_field', 44 | 'options', 45 | 'options->nested_field', 46 | ]; 47 | } 48 | 49 | public function searchableBy(): array 50 | { 51 | return ['title', 'meta.title', 'meta.name', 'user.name']; 52 | } 53 | 54 | public function exposedScopes(): array 55 | { 56 | return ['published', 'publishedAt']; 57 | } 58 | 59 | /** 60 | * @return array 61 | */ 62 | public function includes(): array 63 | { 64 | return ['user', 'user.roles', 'image.*']; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/Unit/Drivers/Standard/PaginatorTest.php: -------------------------------------------------------------------------------- 1 | assertSame(15, $paginator->resolvePaginationLimit(Request::create('/api/posts'))); 18 | } 19 | 20 | /** @test */ 21 | public function resolving_pagination_limit_from_request(): void 22 | { 23 | $paginator = new Paginator(15, 500); 24 | $request = Request::create('/api/posts'); 25 | $request->query->set('limit', 30); 26 | 27 | $this->assertSame(30, $paginator->resolvePaginationLimit($request)); 28 | } 29 | 30 | /** @test */ 31 | public function falling_back_to_default_pagination_limit(): void 32 | { 33 | $paginator = new Paginator(15, 500); 34 | $request = Request::create('/api/posts'); 35 | $request->query->set('limit', 0); 36 | 37 | $this->assertSame(15, $paginator->resolvePaginationLimit($request)); 38 | } 39 | 40 | /** @test */ 41 | public function getting_a_list_of_resources_with_exceeded_pagination_limit(): void 42 | { 43 | $paginator = new Paginator(15, 500); 44 | $request = Request::create('/api/posts'); 45 | $request->query->set('limit', 501); 46 | 47 | $this->expectException(MaxPaginationLimitExceededException::class); 48 | 49 | $paginator->resolvePaginationLimit($request); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Specs/Builders/Operations/StoreOperationBuilder.php: -------------------------------------------------------------------------------- 1 | makeBaseOperation(); 26 | $operation->summary = "Create {$this->resolveResourceName()}"; 27 | 28 | return $operation; 29 | } 30 | 31 | /** 32 | * @return Request|null 33 | * @throws BindingResolutionException 34 | */ 35 | protected function request(): ?Request 36 | { 37 | return new StoreRequest($this->resolveResourceComponentBaseName()); 38 | } 39 | 40 | /** 41 | * @return array 42 | * @throws BindingResolutionException 43 | */ 44 | protected function responses(): array 45 | { 46 | return [ 47 | new EntityResponse($this->resolveResourceComponentBaseName(), 201), 48 | new UnauthenticatedResponse(), 49 | new UnauthorizedResponse(), 50 | new ValidationErrorResponse(), 51 | ]; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Specs/Builders/InfoBuilder.php: -------------------------------------------------------------------------------- 1 | config('orion.specs.info.title') 13 | ]; 14 | 15 | $optionalFields = [ 16 | 'description' => config('orion.specs.info.description'), 17 | 'termsOfService' => config('orion.specs.info.terms_of_service'), 18 | 'contact' => [ 19 | 'name' => config('orion.specs.info.contact.name'), 20 | 'url' => config('orion.specs.info.contact.url'), 21 | 'email' => config('orion.specs.info.contact.email'), 22 | ], 23 | 'license' => [ 24 | 'name' => config('orion.specs.info.license.name'), 25 | 'url' => config('orion.specs.info.license.url'), 26 | ], 27 | 'version' => config('orion.specs.info.version'), 28 | ]; 29 | 30 | $optionalFields = $this->resolveOptionalFields($optionalFields); 31 | 32 | return array_merge($info, $optionalFields); 33 | } 34 | 35 | protected function resolveOptionalFields(array $optionalFields): array 36 | { 37 | $fields = []; 38 | 39 | foreach ($optionalFields as $optionalField => $value) { 40 | if (!$value) { 41 | continue; 42 | } 43 | 44 | if (is_array($value)) { 45 | $value = $this->resolveOptionalFields($value); 46 | if (!$value) { 47 | continue; 48 | } 49 | } 50 | 51 | $fields[$optionalField] = $value; 52 | } 53 | 54 | return $fields; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/ValueObjects/RegisteredResource.php: -------------------------------------------------------------------------------- 1 | controller = $this->qualifyControllerClass($controller); 23 | $this->operations = $operations; 24 | $this->tag = Str::title( 25 | str_replace( 26 | '_', 27 | ' ', 28 | Str::snake(str_replace('Controller', '', class_basename($controller))) 29 | ) 30 | ); 31 | } 32 | 33 | /** 34 | * @return string 35 | * @throws BindingResolutionException 36 | */ 37 | public function getKeyType(): string 38 | { 39 | /** @var Controller $controller */ 40 | $controller = app()->make($this->controller); 41 | 42 | $model = app()->make($controller->resolveResourceModelClass()); 43 | 44 | return $model->getKeyType() === 'int' ? 'integer' : $model->getKeyType(); 45 | } 46 | 47 | /** 48 | * @param string $controller 49 | * @return string 50 | */ 51 | protected function qualifyControllerClass(string $controller): string 52 | { 53 | if (class_exists($controller) || Str::startsWith($controller, config('orion.namespaces.controllers'))) { 54 | return $controller; 55 | } 56 | 57 | return Str::finish(config('orion.namespaces.controllers'), '\\') . $controller; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/ValueObjects/Specs/Requests/Batch/BatchDestroyRequest.php: -------------------------------------------------------------------------------- 1 | registeredResource = $registeredResource; 26 | } 27 | 28 | /** 29 | * @return array 30 | * @throws BindingResolutionException 31 | */ 32 | public function toArray(): array 33 | { 34 | return array_merge( 35 | parent::toArray(), 36 | [ 37 | 'content' => [ 38 | 'application/json' => [ 39 | 'schema' => [ 40 | 'type' => 'object', 41 | 'properties' => [ 42 | 'resources' => [ 43 | 'type' => 'array', 44 | 'items' => [ 45 | 'type' => $this->registeredResource->getKeyType(), 46 | 'description' => 'A list of resource IDs' 47 | ] 48 | ] 49 | ] 50 | ], 51 | ], 52 | ], 53 | ] 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/ValueObjects/Specs/Requests/Batch/BatchRestoreRequest.php: -------------------------------------------------------------------------------- 1 | registeredResource = $registeredResource; 26 | } 27 | 28 | /** 29 | * @return array 30 | * @throws BindingResolutionException 31 | */ 32 | public function toArray(): array 33 | { 34 | return array_merge( 35 | parent::toArray(), 36 | [ 37 | 'content' => [ 38 | 'application/json' => [ 39 | 'schema' => [ 40 | 'type' => 'object', 41 | 'properties' => [ 42 | 'resources' => [ 43 | 'type' => 'array', 44 | 'items' => [ 45 | 'type' => $this->registeredResource->getKeyType(), 46 | 'description' => 'A list of resource IDs' 47 | ] 48 | ] 49 | ] 50 | ], 51 | ], 52 | ], 53 | ] 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/Feature/Relations/BelongsToMany/BelongsToManyRelationStandardIndexSortingOperationsTest.php: -------------------------------------------------------------------------------- 1 | version() <= 8.0) { 19 | $this->markTestSkipped('Unsupported framework version'); 20 | } 21 | 22 | /** @var User $user */ 23 | $user = factory(User::class)->create(); 24 | 25 | $roleA = factory(Role::class)->create(); 26 | $roleB = factory(Role::class)->create(); 27 | $roleC = factory(Role::class)->create(); 28 | 29 | $user->roles()->attach($roleA, ['custom_name' => 'a']); 30 | $user->roles()->attach($roleB, ['custom_name' => 'b']); 31 | $user->roles()->attach($roleC, ['custom_name' => 'c']); 32 | 33 | Gate::policy(User::class, GreenPolicy::class); 34 | Gate::policy(Role::class, GreenPolicy::class); 35 | 36 | $response = $this->withoutExceptionHandling()->post("/api/users/{$user->id}/roles/search", [ 37 | 'sort' => [ 38 | ['field' => 'pivot.custom_name', 'direction' => 'desc'] 39 | ] 40 | ]); 41 | 42 | $this->assertResourcesPaginated( 43 | $response, 44 | $this->makePaginator($user->roles()->get()->reverse()->toArray(), "users/{$user->id}/roles/search") 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Http/Routing/HasManyRelationResourceRegistrar.php: -------------------------------------------------------------------------------- 1 | getNestedResourceUriWithoutNestedParameter($name, $base).'/associate'; 28 | 29 | $action = $this->getResourceAction($name, $controller, 'associate', $options); 30 | 31 | return $this->router->post($uri, $action); 32 | } 33 | 34 | /** 35 | * Add the dissociate method for a resourceful route. 36 | * 37 | * @param string $name 38 | * @param string $base 39 | * @param string $controller 40 | * @param array $options 41 | * @return Route 42 | */ 43 | protected function addResourceDissociate(string $name, string $base, string $controller, array $options): Route 44 | { 45 | $uri = $this->getNestedResourceUriWithNestedParameter($name, $base).'/dissociate'; 46 | 47 | $action = $this->getResourceAction($name, $controller, 'dissociate', $options); 48 | 49 | return $this->router->delete($uri, $action); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Specs/Builders/Builder.php: -------------------------------------------------------------------------------- 1 | infoBuilder = $infoBuilder; 33 | $this->serversBuilder = $serversBuilder; 34 | $this->pathsBuilder = $pathsBuilder; 35 | $this->componentsBuilder = $componentsBuilder; 36 | $this->securityBuilder = $securityBuilder; 37 | $this->tagsBuilder = $tagsBuilder; 38 | } 39 | 40 | /** 41 | * @return array 42 | * @throws BindingResolutionException 43 | */ 44 | public function build(): array 45 | { 46 | return [ 47 | 'openapi' => '3.0.3', 48 | 'info' => $this->infoBuilder->build(), 49 | 'servers' => $this->serversBuilder->build(), 50 | 'security' => $this->securityBuilder->build(), 51 | 'paths' => $this->pathsBuilder->build(), 52 | 'components' => $this->componentsBuilder->build(), 53 | 'tags' => $this->tagsBuilder->build(), 54 | ]; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Specs/Builders/Operations/SearchOperationBuilder.php: -------------------------------------------------------------------------------- 1 | makeBaseOperation(); 25 | $operation->summary = "Search for {$this->resolveResourceName(true)}"; 26 | 27 | return $operation; 28 | } 29 | 30 | /** 31 | * @return SearchRequest 32 | * @throws BindingResolutionException 33 | */ 34 | protected function request(): ?Request 35 | { 36 | $request = new SearchRequest($this->resource->controller); 37 | $descriptor = $request->toArray(); 38 | 39 | if (!($descriptor['content']['application/json']['schema']['properties'])) { 40 | return null; 41 | } 42 | 43 | return $request; 44 | } 45 | 46 | /** 47 | * @return array 48 | * @throws BindingResolutionException 49 | */ 50 | protected function responses(): array 51 | { 52 | return [ 53 | new PaginatedCollectionResponse($this->resolveResourceComponentBaseName()), 54 | new UnauthenticatedResponse(), 55 | new UnauthorizedResponse(), 56 | ]; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/ValueObjects/Specs/Responses/Success/Relation/ManyToMany/ToggleResponse.php: -------------------------------------------------------------------------------- 1 | resourceModel = $resourceModel; 23 | } 24 | 25 | public function toArray(): array 26 | { 27 | $itemsType = $this->resourceModel->getKeyType() === 'int' ? 'integer' : 'string'; 28 | 29 | return array_merge( 30 | parent::toArray(), 31 | [ 32 | 'content' => [ 33 | 'application/json' => [ 34 | 'schema' => [ 35 | 'type' => 'object', 36 | 'properties' => [ 37 | 'attached' => [ 38 | 'type' => 'array', 39 | 'items' => [ 40 | 'type' => $itemsType, 41 | ], 42 | ], 43 | 'detached' => [ 44 | 'type' => 'array', 45 | 'items' => [ 46 | 'type' => $itemsType, 47 | ], 48 | ], 49 | ], 50 | ], 51 | ], 52 | ], 53 | ] 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Concerns/HandlesTransactions.php: -------------------------------------------------------------------------------- 1 | transactionsAreEnabled() !== true) { 18 | return; 19 | } 20 | 21 | DB::beginTransaction(); 22 | } 23 | 24 | /** 25 | * Commit changes to database and finish database 26 | * transaction 27 | * 28 | * @return void 29 | */ 30 | protected function commitTransaction(): void 31 | { 32 | if ($this->transactionsAreEnabled() !== true) { 33 | return; 34 | } 35 | 36 | DB::commit(); 37 | } 38 | 39 | /** 40 | * Rollback changes made to database and finish 41 | * database transaction 42 | * 43 | * @return void 44 | */ 45 | protected function rollbackTransaction(): void 46 | { 47 | if ($this->transactionsAreEnabled() !== true) { 48 | return; 49 | } 50 | 51 | DB::rollBack(); 52 | } 53 | 54 | /** 55 | * Rollback changes made to database and finish 56 | * database transaction and finally raise an exception 57 | * 58 | * @param Exception $exception 59 | * @return void 60 | * 61 | * @throws Exception 62 | */ 63 | protected function rollbackTransactionAndRaise(Exception $exception): void 64 | { 65 | $this->rollbackTransaction(); 66 | 67 | throw $exception; 68 | } 69 | 70 | /** 71 | * Return configuration value 72 | * 73 | * @return boolean 74 | */ 75 | protected function transactionsAreEnabled(): bool 76 | { 77 | return (bool)config('orion.transactions.enabled', false); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Specs/Builders/Operations/UpdateOperationBuilder.php: -------------------------------------------------------------------------------- 1 | makeBaseOperation(); 27 | $operation->summary = "Update {$this->resolveResourceName()}"; 28 | $operation->method = 'patch'; 29 | 30 | return $operation; 31 | } 32 | 33 | /** 34 | * @return Request|null 35 | * @throws BindingResolutionException 36 | */ 37 | protected function request(): ?Request 38 | { 39 | return new UpdateRequest($this->resolveResourceComponentBaseName()); 40 | } 41 | 42 | /** 43 | * @return array 44 | * @throws BindingResolutionException 45 | */ 46 | protected function responses(): array 47 | { 48 | return [ 49 | new EntityResponse($this->resolveResourceComponentBaseName()), 50 | new UnauthenticatedResponse(), 51 | new UnauthorizedResponse(), 52 | new ResourceNotFoundResponse(), 53 | new ValidationErrorResponse(), 54 | ]; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | withFactories(__DIR__ . '/Fixtures/database/factories'); 18 | } 19 | 20 | /** 21 | * Refresh the in-memory database. 22 | * 23 | * @return void 24 | */ 25 | protected function refreshInMemoryDatabase() 26 | { 27 | $this->artisan('migrate', ['--path' => __DIR__ . '/Fixtures/database/migrations', '--realpath' => true]); 28 | 29 | $this->app[Kernel::class]->setArtisan(null); 30 | } 31 | 32 | /** 33 | * Refresh a conventional test database. 34 | * 35 | * @return void 36 | */ 37 | protected function refreshTestDatabase() 38 | { 39 | if (!RefreshDatabaseState::$migrated) { 40 | $this->artisan('migrate', ['--path' => __DIR__ . '/Fixtures/database/migrations', '--realpath' => true]); 41 | 42 | $this->app[Kernel::class]->setArtisan(null); 43 | 44 | RefreshDatabaseState::$migrated = true; 45 | } 46 | 47 | $this->beginDatabaseTransaction(); 48 | } 49 | 50 | protected function getPackageProviders($app) 51 | { 52 | return [ 53 | 'Orion\Tests\Fixtures\App\Providers\OrionServiceProvider', 54 | ]; 55 | } 56 | 57 | protected function getEnvironmentSetUp($app) 58 | { 59 | $app['config']->set('auth.guards.api', [ 60 | 'driver' => 'token', 61 | 'provider' => 'users', 62 | ]); 63 | } 64 | 65 | public function getAnnotations(): array 66 | { 67 | return []; // orchestra/testbench 5 bug 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Testing/InteractsWithAuthorization.php: -------------------------------------------------------------------------------- 1 | actingAs($user ?? factory($this->resolveUserModelClass())->create(), $driver); 19 | } 20 | 21 | /** 22 | * Returns user model class used to create user instance for authentication. 23 | * 24 | * @return string|null 25 | */ 26 | protected function resolveUserModelClass(): ?string 27 | { 28 | return null; 29 | } 30 | 31 | /** 32 | * Forces authorization for the current request. 33 | * 34 | * @return $this 35 | */ 36 | protected function requireAuthorization() 37 | { 38 | app()->bind( 39 | 'orion.authorizationRequired', 40 | function () { 41 | return true; 42 | } 43 | ); 44 | 45 | return $this; 46 | } 47 | 48 | /** 49 | * Disables authorization for the current request. 50 | * 51 | * @return $this 52 | */ 53 | protected function bypassAuthorization() 54 | { 55 | app()->bind( 56 | 'orion.authorizationRequired', 57 | function () { 58 | return false; 59 | } 60 | ); 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * 67 | * @param \Illuminate\Testing\TestResponse|\Illuminate\Foundation\Testing\TestResponse $response 68 | */ 69 | protected function assertUnauthorizedResponse($response): void 70 | { 71 | $response->assertStatus(403); 72 | $response->assertJson(['message' => 'This action is unauthorized.']); 73 | } 74 | } -------------------------------------------------------------------------------- /src/Specs/Builders/Operations/Relations/ManyToMany/UpdatePivotOperationBuilder.php: -------------------------------------------------------------------------------- 1 | makeBaseOperation(); 27 | $operation->summary = "Update pivot"; 28 | 29 | return $operation; 30 | } 31 | 32 | protected function request(): ?Request 33 | { 34 | return new UpdatePivotRequest(); 35 | } 36 | 37 | /** 38 | * @return array 39 | * @throws BindingResolutionException 40 | */ 41 | protected function responses(): array 42 | { 43 | /** @var RelationController $controller */ 44 | $controller = app()->make($this->getResource()->controller); 45 | $resourceModel = app()->make($controller->resolveResourceModelClass()); 46 | 47 | return [ 48 | new UpdatePivotResponse($resourceModel), 49 | new UnauthenticatedResponse(), 50 | new UnauthorizedResponse(), 51 | new ResourceNotFoundResponse(), 52 | new ValidationErrorResponse(), 53 | ]; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Concerns/BuildsResponses.php: -------------------------------------------------------------------------------- 1 | getResource(); 23 | 24 | /** @var Resource $resource */ 25 | $resource = new $resourceClass($entity); 26 | 27 | return $this->addMetaToResource($resource); 28 | } 29 | 30 | /** 31 | * @param LengthAwarePaginator|Collection $entities 32 | * @return ResourceCollection 33 | */ 34 | public function collectionResponse($entities): ResourceCollection 35 | { 36 | if ($collectionResourceClass = $this->getCollectionResource()) { 37 | $collectionResource = new $collectionResourceClass($entities); 38 | } else { 39 | $resource = $this->getResource(); 40 | 41 | $collectionResource = $resource::collection($entities); 42 | } 43 | 44 | 45 | return $this->addMetaToResource($collectionResource); 46 | } 47 | 48 | public function withMeta(string $key, $value): self 49 | { 50 | $this->meta[$key] = $value; 51 | 52 | return $this; 53 | } 54 | 55 | /** 56 | * @param JsonResource|ResourceCollection $resource 57 | * @return JsonResource|ResourceCollection 58 | */ 59 | protected function addMetaToResource($resource) 60 | { 61 | if (count($this->meta)) { 62 | $resource->additional( 63 | [ 64 | 'meta' => $this->meta, 65 | ] 66 | ); 67 | } 68 | 69 | return $resource; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Http/Rules/WhitelistedField.php: -------------------------------------------------------------------------------- 1 | constraints = $constraints; 22 | } 23 | 24 | /** 25 | * Determine if the validation rule passes. 26 | * 27 | * @param string $_ 28 | * @param string $field 29 | * @return bool 30 | */ 31 | public function passes($_, $field): bool 32 | { 33 | if (in_array('*', $this->constraints, true)) { 34 | return true; 35 | } 36 | if (in_array($field, $this->constraints, true)) { 37 | return true; 38 | } 39 | 40 | if (strpos($field, '.') === false) { 41 | return false; 42 | } 43 | 44 | $nestedParamConstraints = array_filter( 45 | $this->constraints, 46 | function ($paramConstraint) { 47 | return strpos($paramConstraint, '.*') !== false; 48 | } 49 | ); 50 | 51 | foreach ($nestedParamConstraints as $nestedParamConstraint) { 52 | if (preg_match($this->convertConstraintToRegex($nestedParamConstraint), $field)) { 53 | return true; 54 | } 55 | } 56 | 57 | return false; 58 | } 59 | 60 | /** 61 | * @param string $constraint 62 | * @return string 63 | */ 64 | protected function convertConstraintToRegex(string $constraint): string 65 | { 66 | return '/'.str_replace('.*', '\.(\w+)', $constraint).'/'; 67 | } 68 | 69 | /** 70 | * Get the validation error message. 71 | * 72 | * @return string 73 | */ 74 | public function message() 75 | { 76 | return 'The :input field is not whitelisted.'; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Specs/Builders/Operations/Relations/ManyToMany/SyncOperationBuilder.php: -------------------------------------------------------------------------------- 1 | makeBaseOperation(); 28 | $operation->summary = "Sync {$this->resolveResourceName(true)}"; 29 | 30 | return $operation; 31 | } 32 | 33 | /** 34 | * @return Request|null 35 | */ 36 | protected function request(): ?Request 37 | { 38 | return new SyncRequest(); 39 | } 40 | 41 | /** 42 | * @return array 43 | * @throws BindingResolutionException 44 | */ 45 | protected function responses(): array 46 | { 47 | /** @var RelationController $controller */ 48 | $controller = app()->make($this->getResource()->controller); 49 | $resourceModel = app()->make($controller->resolveResourceModelClass()); 50 | 51 | return [ 52 | new SyncResponse($resourceModel), 53 | new UnauthenticatedResponse(), 54 | new UnauthorizedResponse(), 55 | new ResourceNotFoundResponse(), 56 | new ValidationErrorResponse(), 57 | ]; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/Fixtures/app/Providers/OrionServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->bind('orion', Orion::class); 29 | $this->app->bind(QueryBuilder::class, \Orion\Drivers\Standard\QueryBuilder::class); 30 | $this->app->bind(RelationsResolver::class, \Orion\Drivers\Standard\RelationsResolver::class); 31 | $this->app->bind(ParamsValidator::class, \Orion\Drivers\Standard\ParamsValidator::class); 32 | $this->app->bind(Paginator::class, \Orion\Drivers\Standard\Paginator::class); 33 | $this->app->bind(SearchBuilder::class, \Orion\Drivers\Standard\SearchBuilder::class); 34 | $this->app->bind(ComponentsResolver::class, \Orion\Drivers\Standard\ComponentsResolver::class); 35 | $this->app->bind(KeyResolver::class, \Orion\Drivers\Standard\KeyResolver::class); 36 | 37 | $this->app->singleton(ResourcesCacheStore::class); 38 | } 39 | 40 | /** 41 | * Perform post-registration booting of services. 42 | * 43 | * @return void 44 | * @throws \Illuminate\Contracts\Container\BindingResolutionException 45 | */ 46 | public function boot() 47 | { 48 | app()->make(Kernel::class)->pushMiddleware(EnforceExpectsJson::class); 49 | 50 | $this->mergeConfigFrom(__DIR__ . '/../../../../config/orion.php', 'orion'); 51 | 52 | $this->loadRoutesFrom(__DIR__.'/../../routes/api.php'); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Specs/Builders/Operations/Relations/ManyToMany/AttachOperationBuilder.php: -------------------------------------------------------------------------------- 1 | makeBaseOperation(); 28 | $operation->summary = "Attach {$this->resolveResourceName(true)}"; 29 | 30 | return $operation; 31 | } 32 | 33 | /** 34 | * @return Request|null 35 | */ 36 | protected function request(): ?Request 37 | { 38 | return new AttachRequest(); 39 | } 40 | 41 | /** 42 | * @return array 43 | * @throws BindingResolutionException 44 | */ 45 | protected function responses(): array 46 | { 47 | /** @var RelationController $controller */ 48 | $controller = app()->make($this->getResource()->controller); 49 | $resourceModel = app()->make($controller->resolveResourceModelClass()); 50 | 51 | return [ 52 | new AttachResponse($resourceModel), 53 | new UnauthenticatedResponse(), 54 | new UnauthorizedResponse(), 55 | new ResourceNotFoundResponse(), 56 | new ValidationErrorResponse(), 57 | ]; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Specs/Builders/Operations/Relations/ManyToMany/DetachOperationBuilder.php: -------------------------------------------------------------------------------- 1 | makeBaseOperation(); 28 | $operation->summary = "Detach {$this->resolveResourceName(true)}"; 29 | 30 | return $operation; 31 | } 32 | 33 | /** 34 | * @return Request|null 35 | */ 36 | protected function request(): ?Request 37 | { 38 | return new DetachRequest(); 39 | } 40 | 41 | /** 42 | * @return array 43 | * @throws BindingResolutionException 44 | */ 45 | protected function responses(): array 46 | { 47 | /** @var RelationController $controller */ 48 | $controller = app()->make($this->getResource()->controller); 49 | $resourceModel = app()->make($controller->resolveResourceModelClass()); 50 | 51 | return [ 52 | new DetachResponse($resourceModel), 53 | new UnauthenticatedResponse(), 54 | new UnauthorizedResponse(), 55 | new ResourceNotFoundResponse(), 56 | new ValidationErrorResponse(), 57 | ]; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Specs/Builders/Operations/Relations/ManyToMany/ToggleOperationBuilder.php: -------------------------------------------------------------------------------- 1 | makeBaseOperation(); 28 | $operation->summary = "Toggle {$this->resolveResourceName(true)}"; 29 | 30 | return $operation; 31 | } 32 | 33 | /** 34 | * @return Request|null 35 | */ 36 | protected function request(): ?Request 37 | { 38 | return new ToggleRequest(); 39 | } 40 | 41 | /** 42 | * @return array 43 | * @throws BindingResolutionException 44 | */ 45 | protected function responses(): array 46 | { 47 | /** @var RelationController $controller */ 48 | $controller = app()->make($this->getResource()->controller); 49 | $resourceModel = app()->make($controller->resolveResourceModelClass()); 50 | 51 | return [ 52 | new ToggleResponse($resourceModel), 53 | new UnauthenticatedResponse(), 54 | new UnauthorizedResponse(), 55 | new ResourceNotFoundResponse(), 56 | new ValidationErrorResponse(), 57 | ]; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/Fixtures/app/Policies/GreenPolicy.php: -------------------------------------------------------------------------------- 1 |