├── .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 |
2 |
3 |
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 |