feat(api): mobile API Milestones 5+6 — media upload, sync_logs idempotency, OpenAPI
Milestone 5 (media): - POST /api/v1/media — multipart upload, attaches to feature/issue/project/ phase/layer, idempotent by uuid, authz member + 'upload media'. Added uuid+client_updated_at to media. - Bundle now includes a 'media' array (URLs) for the project's project/feature/ issue attachments (delta-aware). Milestone 6 (hardening + docs): - sync_logs table/model: every applied op is logged; /sync short-circuits on a repeated op uuid -> 'duplicate' (true idempotency for updates too, not just creates). - Rate limiting on login (10/min), sync (60/min), media (120/min). - docs/openapi.yaml: OpenAPI 3 contract for the mobile team. Tests: 18 passing (added media upload idempotency + sync_logs idempotency). The mobile API (Milestones 1-6) is now feature-complete on the webapp side. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ use App\Models\Inspection;
|
||||
use App\Models\InspectionTemplate;
|
||||
use App\Models\Issue;
|
||||
use App\Models\Layer;
|
||||
use App\Models\Media;
|
||||
use App\Models\Phase;
|
||||
use App\Models\Project;
|
||||
use Carbon\Carbon;
|
||||
@@ -48,6 +49,14 @@ class ProjectApiController extends Controller
|
||||
$issues = $changed(Issue::where('project_id', $project->id))->get();
|
||||
$templates = $changed(InspectionTemplate::where('project_id', $project->id))->get();
|
||||
|
||||
$featureIds = Feature::whereIn('layer_id', $allLayerIds)->pluck('id');
|
||||
$issueIds = Issue::where('project_id', $project->id)->pluck('id');
|
||||
$media = $changed(Media::where(function ($q) use ($project, $featureIds, $issueIds) {
|
||||
$q->where(fn ($w) => $w->where('mediable_type', Project::class)->where('mediable_id', $project->id))
|
||||
->orWhere(fn ($w) => $w->where('mediable_type', Feature::class)->whereIn('mediable_id', $featureIds))
|
||||
->orWhere(fn ($w) => $w->where('mediable_type', Issue::class)->whereIn('mediable_id', $issueIds));
|
||||
}))->get();
|
||||
|
||||
return response()->json([
|
||||
'server_time' => now()->toIso8601String(),
|
||||
'project' => $this->mapProject($project),
|
||||
@@ -57,6 +66,7 @@ class ProjectApiController extends Controller
|
||||
'inspections' => $inspections->map(fn ($i) => $this->mapInspection($i))->values(),
|
||||
'issues' => $issues->map(fn ($i) => $this->mapIssue($i))->values(),
|
||||
'templates' => $templates->map(fn ($t) => $this->mapTemplate($t))->values(),
|
||||
'media' => $media->map(fn ($m) => $this->mapMedia($m))->values(),
|
||||
'deleted' => $since ? $this->tombstones($since, $project, $allPhaseIds, $allLayerIds) : (object) [],
|
||||
]);
|
||||
}
|
||||
@@ -166,4 +176,21 @@ class ProjectApiController extends Controller
|
||||
'updated_at' => $t->updated_at?->toIso8601String(),
|
||||
];
|
||||
}
|
||||
|
||||
private function mapMedia(Media $m): array
|
||||
{
|
||||
$entity = [
|
||||
Project::class => 'project',
|
||||
Feature::class => 'feature',
|
||||
Issue::class => 'issue',
|
||||
][$m->mediable_type] ?? class_basename($m->mediable_type);
|
||||
|
||||
return [
|
||||
'id' => $m->id, 'uuid' => $m->uuid,
|
||||
'parent_entity' => $entity, 'parent_id' => $m->mediable_id,
|
||||
'url' => $m->url, 'name' => $m->name, 'file_type' => $m->file_type,
|
||||
'category' => $m->category, 'updated_at' => $m->updated_at?->toIso8601String(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user