feat(api): Implement API Resource Standard compliance
- Create BaseResource with formatDate() and formatDecimal() utilities - Create 11 API Resource classes for all models - Update all 6 controllers to return wrapped responses via wrapResource() - Update frontend API client with unwrapResponse() helper - Update all 63+ backend tests to expect 'data' wrapper - Regenerate Scribe API documentation BREAKING CHANGE: All API responses now wrap data in 'data' key per architecture spec. Backend Tests: 70 passed, 5 failed (unrelated to data wrapper) Frontend Unit: 10 passed E2E Tests: 102 passed, 20 skipped API Docs: Generated successfully Refs: openspec/changes/api-resource-standard
This commit is contained in:
@@ -260,16 +260,19 @@ fetch(url, {
|
||||
<pre>
|
||||
|
||||
<code class="language-json" style="max-height: 300px;">{
|
||||
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
|
||||
"refresh_token": "abc123def456",
|
||||
"token_type": "bearer",
|
||||
"expires_in": 3600,
|
||||
"user": {
|
||||
"data": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "Alice Johnson",
|
||||
"email": "user@example.com",
|
||||
"role": "manager"
|
||||
}
|
||||
"role": "manager",
|
||||
"active": true,
|
||||
"created_at": "2026-01-01T00:00:00Z",
|
||||
"updated_at": "2026-01-01T00:00:00Z"
|
||||
},
|
||||
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
|
||||
"refresh_token": "abc123def456",
|
||||
"token_type": "bearer",
|
||||
"expires_in": 3600
|
||||
}</code>
|
||||
</pre>
|
||||
<blockquote>
|
||||
@@ -457,6 +460,15 @@ fetch(url, {
|
||||
<pre>
|
||||
|
||||
<code class="language-json" style="max-height: 300px;">{
|
||||
"data": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "Alice Johnson",
|
||||
"email": "user@example.com",
|
||||
"role": "manager",
|
||||
"active": true,
|
||||
"created_at": "2026-01-01T00:00:00Z",
|
||||
"updated_at": "2026-01-01T00:00:00Z"
|
||||
},
|
||||
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
|
||||
"refresh_token": "newtoken123",
|
||||
"token_type": "bearer",
|
||||
@@ -758,15 +770,20 @@ fetch(url, {
|
||||
<pre>
|
||||
|
||||
<code class="language-json" style="max-height: 300px;">{
|
||||
"person_days": 18.5,
|
||||
"hours": 148,
|
||||
"details": [
|
||||
{
|
||||
"date": "2026-02-02",
|
||||
"availability": 1,
|
||||
"is_pto": false
|
||||
}
|
||||
]
|
||||
"data": {
|
||||
"team_member_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"month": "2026-02",
|
||||
"working_days": 20,
|
||||
"person_days": 18.5,
|
||||
"hours": 148,
|
||||
"details": [
|
||||
{
|
||||
"date": "2026-02-02",
|
||||
"availability": 1,
|
||||
"is_pto": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}</code>
|
||||
</pre>
|
||||
</span>
|
||||
@@ -944,17 +961,19 @@ fetch(url, {
|
||||
<pre>
|
||||
|
||||
<code class="language-json" style="max-height: 300px;">{
|
||||
"month": "2026-02",
|
||||
"person_days": 180.5,
|
||||
"hours": 1444,
|
||||
"members": [
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "Ada Lovelace",
|
||||
"person_days": 18.5,
|
||||
"hours": 148
|
||||
}
|
||||
]
|
||||
"data": {
|
||||
"month": "2026-02",
|
||||
"total_person_days": 180.5,
|
||||
"total_hours": 1444,
|
||||
"members": [
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "Ada Lovelace",
|
||||
"person_days": 18.5,
|
||||
"hours": 148
|
||||
}
|
||||
]
|
||||
}
|
||||
}</code>
|
||||
</pre>
|
||||
</span>
|
||||
@@ -1108,8 +1127,19 @@ fetch(url, {
|
||||
<pre>
|
||||
|
||||
<code class="language-json" style="max-height: 300px;">{
|
||||
"month": "2026-02",
|
||||
"possible_revenue": 21500.25
|
||||
"data": {
|
||||
"month": "2026-02",
|
||||
"possible_revenue": 21500.25,
|
||||
"member_revenues": [
|
||||
{
|
||||
"team_member_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"team_member_name": "Ada Lovelace",
|
||||
"hours": 148,
|
||||
"hourly_rate": 150,
|
||||
"revenue": 22200
|
||||
}
|
||||
]
|
||||
}
|
||||
}</code>
|
||||
</pre>
|
||||
</span>
|
||||
@@ -1262,14 +1292,16 @@ fetch(url, {
|
||||
</blockquote>
|
||||
<pre>
|
||||
|
||||
<code class="language-json" style="max-height: 300px;">[
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"date": "2026-02-14",
|
||||
"name": "Company Holiday",
|
||||
"description": "Office closed"
|
||||
}
|
||||
]</code>
|
||||
<code class="language-json" style="max-height: 300px;">{
|
||||
"data": [
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"date": "2026-02-14",
|
||||
"name": "Company Holiday",
|
||||
"description": "Office closed"
|
||||
}
|
||||
]
|
||||
}</code>
|
||||
</pre>
|
||||
</span>
|
||||
<span id="execution-results-GETapi-holidays" hidden>
|
||||
@@ -1426,10 +1458,12 @@ fetch(url, {
|
||||
<pre>
|
||||
|
||||
<code class="language-json" style="max-height: 300px;">{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"date": "2026-02-14",
|
||||
"name": "Presidents' Day",
|
||||
"description": "Office closed"
|
||||
"data": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"date": "2026-02-14",
|
||||
"name": "Presidents' Day",
|
||||
"description": "Office closed"
|
||||
}
|
||||
}</code>
|
||||
</pre>
|
||||
</span>
|
||||
@@ -1727,16 +1761,18 @@ fetch(url, {
|
||||
</blockquote>
|
||||
<pre>
|
||||
|
||||
<code class="language-json" style="max-height: 300px;">[
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440001",
|
||||
"team_member_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"start_date": "2026-02-10",
|
||||
"end_date": "2026-02-12",
|
||||
"status": "pending",
|
||||
"reason": "Family travel"
|
||||
}
|
||||
]</code>
|
||||
<code class="language-json" style="max-height: 300px;">{
|
||||
"data": [
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440001",
|
||||
"team_member_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"start_date": "2026-02-10",
|
||||
"end_date": "2026-02-12",
|
||||
"status": "pending",
|
||||
"reason": "Family travel"
|
||||
}
|
||||
]
|
||||
}</code>
|
||||
</pre>
|
||||
</span>
|
||||
<span id="execution-results-GETapi-ptos" hidden>
|
||||
@@ -1919,12 +1955,14 @@ fetch(url, {
|
||||
<pre>
|
||||
|
||||
<code class="language-json" style="max-height: 300px;">{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440001",
|
||||
"team_member_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"start_date": "2026-02-10",
|
||||
"end_date": "2026-02-12",
|
||||
"status": "pending",
|
||||
"reason": "Family travel"
|
||||
"data": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440001",
|
||||
"team_member_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"start_date": "2026-02-10",
|
||||
"end_date": "2026-02-12",
|
||||
"status": "pending",
|
||||
"reason": "Family travel"
|
||||
}
|
||||
}</code>
|
||||
</pre>
|
||||
</span>
|
||||
@@ -2092,8 +2130,10 @@ fetch(url, {
|
||||
<pre>
|
||||
|
||||
<code class="language-json" style="max-height: 300px;">{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440001",
|
||||
"status": "approved"
|
||||
"data": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440001",
|
||||
"status": "approved"
|
||||
}
|
||||
}</code>
|
||||
</pre>
|
||||
</span>
|
||||
@@ -2229,20 +2269,22 @@ fetch(url, {
|
||||
</blockquote>
|
||||
<pre>
|
||||
|
||||
<code class="language-json" style="max-height: 300px;">[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Project"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Support"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Engagement"
|
||||
}
|
||||
]</code>
|
||||
<code class="language-json" style="max-height: 300px;">{
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Project"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Support"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Engagement"
|
||||
}
|
||||
]
|
||||
}</code>
|
||||
</pre>
|
||||
</span>
|
||||
<span id="execution-results-GETapi-projects-types" hidden>
|
||||
@@ -2360,23 +2402,25 @@ fetch(url, {
|
||||
</blockquote>
|
||||
<pre>
|
||||
|
||||
<code class="language-json" style="max-height: 300px;">[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Pre-sales",
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "SOW Approval",
|
||||
"order": 2
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Gathering Estimates",
|
||||
"order": 3
|
||||
}
|
||||
]</code>
|
||||
<code class="language-json" style="max-height: 300px;">{
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Pre-sales",
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "SOW Approval",
|
||||
"order": 2
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Gathering Estimates",
|
||||
"order": 3
|
||||
}
|
||||
]
|
||||
}</code>
|
||||
</pre>
|
||||
</span>
|
||||
<span id="execution-results-GETapi-projects-statuses" hidden>
|
||||
@@ -2501,31 +2545,31 @@ fetch(url, {
|
||||
</blockquote>
|
||||
<pre>
|
||||
|
||||
<code class="language-json" style="max-height: 300px;">[
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"code": "PROJ-001",
|
||||
"title": "Client Dashboard Redesign",
|
||||
"status_id": 1,
|
||||
"status": {
|
||||
"id": 1,
|
||||
"name": "Pre-sales"
|
||||
},
|
||||
"type_id": 2,
|
||||
"type": {
|
||||
"id": 2,
|
||||
"name": "Support"
|
||||
},
|
||||
"approved_estimate": "120.00",
|
||||
"forecasted_effort": {
|
||||
"2024-02": 40,
|
||||
"2024-03": 60,
|
||||
"2024-04": 20
|
||||
},
|
||||
"created_at": "2024-01-15T10:00:00.000000Z",
|
||||
"updated_at": "2024-01-15T10:00:00.000000Z"
|
||||
}
|
||||
]</code>
|
||||
<code class="language-json" style="max-height: 300px;">{
|
||||
"data": [
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"code": "PROJ-001",
|
||||
"title": "Client Dashboard Redesign",
|
||||
"status": {
|
||||
"id": 1,
|
||||
"name": "Pre-sales"
|
||||
},
|
||||
"type": {
|
||||
"id": 2,
|
||||
"name": "Support"
|
||||
},
|
||||
"approved_estimate": "120.00",
|
||||
"forecasted_effort": {
|
||||
"2024-02": 40,
|
||||
"2024-03": 60,
|
||||
"2024-04": 20
|
||||
},
|
||||
"created_at": "2024-01-15T10:00:00.000000Z",
|
||||
"updated_at": "2024-01-15T10:00:00.000000Z"
|
||||
}
|
||||
]
|
||||
}</code>
|
||||
</pre>
|
||||
</span>
|
||||
<span id="execution-results-GETapi-projects" hidden>
|
||||
@@ -2682,18 +2726,18 @@ fetch(url, {
|
||||
<pre>
|
||||
|
||||
<code class="language-json" style="max-height: 300px;">{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"code": "PROJ-001",
|
||||
"title": "Client Dashboard Redesign",
|
||||
"status_id": 1,
|
||||
"status": {
|
||||
"id": 1,
|
||||
"name": "Pre-sales"
|
||||
},
|
||||
"type_id": 1,
|
||||
"type": {
|
||||
"id": 1,
|
||||
"name": "Project"
|
||||
"data": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"code": "PROJ-001",
|
||||
"title": "Client Dashboard Redesign",
|
||||
"status": {
|
||||
"id": 1,
|
||||
"name": "Pre-sales"
|
||||
},
|
||||
"type": {
|
||||
"id": 1,
|
||||
"name": "Project"
|
||||
}
|
||||
}
|
||||
}</code>
|
||||
</pre>
|
||||
@@ -2868,21 +2912,23 @@ fetch(url, {
|
||||
<pre>
|
||||
|
||||
<code class="language-json" style="max-height: 300px;">{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"code": "PROJ-001",
|
||||
"title": "Client Dashboard Redesign",
|
||||
"status": {
|
||||
"id": 1,
|
||||
"name": "Pre-sales"
|
||||
},
|
||||
"type": {
|
||||
"id": 1,
|
||||
"name": "Project"
|
||||
},
|
||||
"approved_estimate": "120.00",
|
||||
"forecasted_effort": {
|
||||
"2024-02": 40,
|
||||
"2024-03": 60
|
||||
"data": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"code": "PROJ-001",
|
||||
"title": "Client Dashboard Redesign",
|
||||
"status": {
|
||||
"id": 1,
|
||||
"name": "Pre-sales"
|
||||
},
|
||||
"type": {
|
||||
"id": 1,
|
||||
"name": "Project"
|
||||
},
|
||||
"approved_estimate": "120.00",
|
||||
"forecasted_effort": {
|
||||
"2024-02": 40,
|
||||
"2024-03": 60
|
||||
}
|
||||
}
|
||||
}</code>
|
||||
</pre>
|
||||
@@ -3038,10 +3084,15 @@ fetch(url, {
|
||||
<pre>
|
||||
|
||||
<code class="language-json" style="max-height: 300px;">{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"code": "PROJ-002",
|
||||
"title": "Updated Title",
|
||||
"type_id": 2
|
||||
"data": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"code": "PROJ-002",
|
||||
"title": "Updated Title",
|
||||
"type": {
|
||||
"id": 2,
|
||||
"name": "Support"
|
||||
}
|
||||
}
|
||||
}</code>
|
||||
</pre>
|
||||
<blockquote>
|
||||
@@ -3398,10 +3449,12 @@ fetch(url, {
|
||||
<pre>
|
||||
|
||||
<code class="language-json" style="max-height: 300px;">{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"status": {
|
||||
"id": 2,
|
||||
"name": "SOW Approval"
|
||||
"data": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"status": {
|
||||
"id": 2,
|
||||
"name": "SOW Approval"
|
||||
}
|
||||
}
|
||||
}</code>
|
||||
</pre>
|
||||
@@ -3587,8 +3640,10 @@ fetch(url, {
|
||||
<pre>
|
||||
|
||||
<code class="language-json" style="max-height: 300px;">{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"approved_estimate": "120.00"
|
||||
"data": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"approved_estimate": "120.00"
|
||||
}
|
||||
}</code>
|
||||
</pre>
|
||||
<blockquote>
|
||||
@@ -3779,10 +3834,12 @@ fetch(url, {
|
||||
<pre>
|
||||
|
||||
<code class="language-json" style="max-height: 300px;">{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"forecasted_effort": {
|
||||
"2024-02": 40,
|
||||
"2024-03": 60
|
||||
"data": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"forecasted_effort": {
|
||||
"2024-02": 40,
|
||||
"2024-03": 60
|
||||
}
|
||||
}
|
||||
}</code>
|
||||
</pre>
|
||||
@@ -3968,21 +4025,22 @@ fetch(url, {
|
||||
</blockquote>
|
||||
<pre>
|
||||
|
||||
<code class="language-json" style="max-height: 300px;">[
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "John Doe",
|
||||
"role_id": 1,
|
||||
"role": {
|
||||
"id": 1,
|
||||
"name": "Backend Developer"
|
||||
},
|
||||
"hourly_rate": "150.00",
|
||||
"active": true,
|
||||
"created_at": "2024-01-15T10:00:00.000000Z",
|
||||
"updated_at": "2024-01-15T10:00:00.000000Z"
|
||||
}
|
||||
]</code>
|
||||
<code class="language-json" style="max-height: 300px;">{
|
||||
"data": [
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "John Doe",
|
||||
"role": {
|
||||
"id": 1,
|
||||
"name": "Backend Developer"
|
||||
},
|
||||
"hourly_rate": "150.00",
|
||||
"active": true,
|
||||
"created_at": "2024-01-15T10:00:00.000000Z",
|
||||
"updated_at": "2024-01-15T10:00:00.000000Z"
|
||||
}
|
||||
]
|
||||
}</code>
|
||||
</pre>
|
||||
</span>
|
||||
<span id="execution-results-GETapi-team-members" hidden>
|
||||
@@ -4139,17 +4197,18 @@ fetch(url, {
|
||||
<pre>
|
||||
|
||||
<code class="language-json" style="max-height: 300px;">{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "John Doe",
|
||||
"role_id": 1,
|
||||
"role": {
|
||||
"id": 1,
|
||||
"name": "Backend Developer"
|
||||
},
|
||||
"hourly_rate": "150.00",
|
||||
"active": true,
|
||||
"created_at": "2024-01-15T10:00:00.000000Z",
|
||||
"updated_at": "2024-01-15T10:00:00.000000Z"
|
||||
"data": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "John Doe",
|
||||
"role": {
|
||||
"id": 1,
|
||||
"name": "Backend Developer"
|
||||
},
|
||||
"hourly_rate": "150.00",
|
||||
"active": true,
|
||||
"created_at": "2024-01-15T10:00:00.000000Z",
|
||||
"updated_at": "2024-01-15T10:00:00.000000Z"
|
||||
}
|
||||
}</code>
|
||||
</pre>
|
||||
<blockquote>
|
||||
@@ -4345,17 +4404,18 @@ fetch(url, {
|
||||
<pre>
|
||||
|
||||
<code class="language-json" style="max-height: 300px;">{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "John Doe",
|
||||
"role_id": 1,
|
||||
"role": {
|
||||
"id": 1,
|
||||
"name": "Backend Developer"
|
||||
},
|
||||
"hourly_rate": "150.00",
|
||||
"active": true,
|
||||
"created_at": "2024-01-15T10:00:00.000000Z",
|
||||
"updated_at": "2024-01-15T10:00:00.000000Z"
|
||||
"data": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "John Doe",
|
||||
"role": {
|
||||
"id": 1,
|
||||
"name": "Backend Developer"
|
||||
},
|
||||
"hourly_rate": "150.00",
|
||||
"active": true,
|
||||
"created_at": "2024-01-15T10:00:00.000000Z",
|
||||
"updated_at": "2024-01-15T10:00:00.000000Z"
|
||||
}
|
||||
}</code>
|
||||
</pre>
|
||||
<blockquote>
|
||||
@@ -4512,17 +4572,18 @@ fetch(url, {
|
||||
<pre>
|
||||
|
||||
<code class="language-json" style="max-height: 300px;">{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "John Doe",
|
||||
"role_id": 1,
|
||||
"role": {
|
||||
"id": 1,
|
||||
"name": "Backend Developer"
|
||||
},
|
||||
"hourly_rate": "175.00",
|
||||
"active": false,
|
||||
"created_at": "2024-01-15T10:00:00.000000Z",
|
||||
"updated_at": "2024-01-15T11:00:00.000000Z"
|
||||
"data": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "John Doe",
|
||||
"role": {
|
||||
"id": 1,
|
||||
"name": "Backend Developer"
|
||||
},
|
||||
"hourly_rate": "175.00",
|
||||
"active": false,
|
||||
"created_at": "2024-01-15T10:00:00.000000Z",
|
||||
"updated_at": "2024-01-15T11:00:00.000000Z"
|
||||
}
|
||||
}</code>
|
||||
</pre>
|
||||
<blockquote>
|
||||
|
||||
Reference in New Issue
Block a user