resourceNotFoundResponse($resource); } $modelClass = $config['model']; /** @var Builder $query */ $query = $modelClass::query(); $relations = SupportApiRegistry::withRelations($resource); if ($relations !== []) { $query->with($relations); } $this->applySearch($request, $query, $resource); $this->applySorting($request, $query, $resource); $perPage = $this->resolvePerPage($request); $paginator = $query->paginate($perPage); return response()->json([ 'data' => $paginator->items(), 'meta' => [ 'page' => $paginator->currentPage(), 'per_page' => $paginator->perPage(), 'total' => $paginator->total(), 'last_page' => $paginator->lastPage(), ], ]); } public function show(Request $request, string $resource, string $record): JsonResponse { if ($response = SupportApiAuthorizer::authorizeResource($request, $resource, 'read')) { return $response; } $model = $this->resolveRecord($resource, $record); if (! $model) { return $this->resourceNotFoundResponse($resource, $record); } return response()->json([ 'data' => $model, ]); } public function store(SupportResourceRequest $request, string $resource): JsonResponse { if ($response = SupportApiAuthorizer::authorizeResource($request, $resource, 'write')) { return $response; } if (! SupportApiRegistry::allowsMutation($resource, 'create')) { return $this->mutationNotAllowedResponse($resource, 'create'); } $config = SupportApiRegistry::get($resource); if (! $config) { return $this->resourceNotFoundResponse($resource); } $modelClass = $config['model']; /** @var Model $model */ $model = new $modelClass; $payload = $this->validatedPayload($request, $resource, 'create', $model); if ($payload instanceof JsonResponse) { return $payload; } if ($payload === []) { return $this->emptyPayloadResponse($resource); } if ($resource === 'data-exports') { $payload = $this->normalizeDataExportPayload($request, $payload); } $record = $modelClass::query()->create($payload); if ($resource === 'data-exports') { GenerateDataExport::dispatch($record->id); } return response()->json([ 'data' => $record, ], 201); } public function update(SupportResourceRequest $request, string $resource, string $record): JsonResponse { if ($response = SupportApiAuthorizer::authorizeResource($request, $resource, 'write')) { return $response; } if (! SupportApiRegistry::allowsMutation($resource, 'update')) { return $this->mutationNotAllowedResponse($resource, 'update'); } $model = $this->resolveRecord($resource, $record); if (! $model) { return $this->resourceNotFoundResponse($resource, $record); } $payload = $this->validatedPayload($request, $resource, 'update', $model); if ($payload instanceof JsonResponse) { return $payload; } if ($payload === []) { return $this->emptyPayloadResponse($resource); } $model->fill($payload); $model->save(); return response()->json([ 'data' => $model->refresh(), ]); } public function destroy(Request $request, string $resource, string $record): JsonResponse { if ($response = SupportApiAuthorizer::authorizeResource($request, $resource, 'write')) { return $response; } if (! SupportApiRegistry::allowsMutation($resource, 'delete')) { return $this->mutationNotAllowedResponse($resource, 'delete'); } $model = $this->resolveRecord($resource, $record); if (! $model) { return $this->resourceNotFoundResponse($resource, $record); } $model->delete(); return response()->json(['ok' => true]); } private function resolveRecord(string $resource, string $record): ?Model { $config = SupportApiRegistry::get($resource); if (! $config) { return null; } $modelClass = $config['model']; $query = $modelClass::query(); if (is_numeric($record)) { return $query->find($record); } $keyName = (new $modelClass)->getKeyName(); return $query->where($keyName, $record)->first(); } private function validatedPayload(SupportResourceRequest $request, string $resource, string $action, Model $model): array|JsonResponse { $payload = $request->validated('data'); if (! is_array($payload)) { return []; } $validationClass = SupportApiRegistry::validationClass($resource, $action); if ($validationClass && is_subclass_of($validationClass, SupportResourceFormRequest::class)) { $allowedFields = $validationClass::allowedFields($action); if ($allowedFields !== []) { $unexpected = array_diff(array_keys($payload), $allowedFields); if ($unexpected !== []) { return $this->invalidFieldResponse($resource, $unexpected); } } $rules = $validationClass::rulesFor($action, $model); if ($rules !== []) { $payload = Validator::make($payload, $rules)->validate(); } if ($allowedFields !== []) { $payload = Arr::only($payload, $allowedFields); } } $fillable = $model->getFillable(); if ($fillable === [] && method_exists($model, 'getGuarded') && $model->getGuarded() !== ['*']) { $columns = Schema::getColumnListing($model->getTable()); return Arr::only($payload, $columns); } if ($fillable === []) { return []; } return Arr::only($payload, $fillable); } private function applySearch(Request $request, Builder $query, string $resource): void { $term = $request->string('search')->trim()->value(); if ($term === '') { return; } $fields = SupportApiRegistry::searchFields($resource); if ($fields === []) { return; } $columns = Schema::getColumnListing($query->getModel()->getTable()); $fields = array_values(array_intersect($fields, $columns)); if ($fields === []) { return; } $query->where(function (Builder $builder) use ($fields, $term): void { foreach ($fields as $field) { if ($field === 'id' && is_numeric($term)) { $builder->orWhere($field, (int) $term); } else { $builder->orWhere($field, 'like', "%{$term}%"); } } }); } private function applySorting(Request $request, Builder $query, string $resource): void { $sort = $request->string('sort')->trim()->value(); if ($sort === '') { return; } $direction = 'asc'; $field = $sort; if (str_starts_with($sort, '-')) { $direction = 'desc'; $field = ltrim($sort, '-'); } $allowed = SupportApiRegistry::searchFields($resource); $allowed[] = 'id'; $columns = Schema::getColumnListing($query->getModel()->getTable()); $allowed = array_values(array_intersect($allowed, $columns)); if (! in_array($field, $allowed, true)) { return; } $query->orderBy($field, $direction); } private function resolvePerPage(Request $request): int { $default = (int) config('support-api.pagination.default_per_page', 50); $max = (int) config('support-api.pagination.max_per_page', 200); $perPage = (int) $request->input('per_page', $default); if ($perPage < 1) { $perPage = $default; } return min($perPage, $max); } private function mutationNotAllowedResponse(string $resource, string $action): JsonResponse { return ApiError::response( 'support_mutation_not_allowed', 'Mutation Not Allowed', "{$resource} does not allow {$action} operations in support API.", 403 ); } private function emptyPayloadResponse(string $resource): JsonResponse { return ApiError::response( 'support_invalid_payload', 'Invalid Payload', "No mutable fields provided for {$resource}.", 422 ); } private function invalidFieldResponse(string $resource, array $fields): JsonResponse { return ApiError::response( 'support_invalid_fields', 'Invalid Fields', "Unsupported fields provided for {$resource}.", 422, [ 'fields' => array_values($fields), ] ); } private function resourceNotFoundResponse(string $resource, ?string $record = null): JsonResponse { $message = $record ? "{$resource} record not found." : "Support resource {$resource} is not registered."; return ApiError::response( 'support_resource_not_found', 'Not Found', $message, 404 ); } private function normalizeDataExportPayload(Request $request, array $payload): array { $payload['user_id'] = $request->user()?->id; $payload['status'] = DataExport::STATUS_PENDING; if (($payload['scope'] ?? null) !== DataExportScope::EVENT->value) { $payload['event_id'] = null; } return $payload; } }