mirror of
https://github.com/Sea-Haven-Industries/afterhours-shift-manager.git
synced 2026-10-07 11:38:59 +00:00
feat(api): add OpenAPI 3.1 and Redocly lint in CI (DEV-289)
Same extends: recommended ruleset and @redocly/cli 2.52.1 as internal-portal. Covers health, roster, and portal /api/shifts. Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
This commit is contained in:
parent
da616eebfa
commit
70f66708a1
8 changed files with 871 additions and 2 deletions
24
.github/workflows/ci.yaml
vendored
24
.github/workflows/ci.yaml
vendored
|
|
@ -62,9 +62,29 @@ jobs:
|
||||||
- name: Terraform validate
|
- name: Terraform validate
|
||||||
run: terraform validate
|
run: terraform validate
|
||||||
|
|
||||||
|
openapi:
|
||||||
|
name: OpenAPI
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 5
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
|
with:
|
||||||
|
node-version: "24.19.0"
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install JavaScript tooling
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Lint OpenAPI
|
||||||
|
run: npm run openapi:lint
|
||||||
|
|
||||||
ci:
|
ci:
|
||||||
name: ci / ci
|
name: ci / ci
|
||||||
needs: [pytest, terraform]
|
needs: [pytest, terraform, openapi]
|
||||||
if: ${{ always() && !cancelled() }}
|
if: ${{ always() && !cancelled() }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
|
|
@ -73,6 +93,7 @@ jobs:
|
||||||
env:
|
env:
|
||||||
PYTEST_RESULT: ${{ needs.pytest.result }}
|
PYTEST_RESULT: ${{ needs.pytest.result }}
|
||||||
TERRAFORM_RESULT: ${{ needs.terraform.result }}
|
TERRAFORM_RESULT: ${{ needs.terraform.result }}
|
||||||
|
OPENAPI_RESULT: ${{ needs.openapi.result }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
fail=0
|
fail=0
|
||||||
|
|
@ -91,4 +112,5 @@ jobs:
|
||||||
}
|
}
|
||||||
check pytest "${PYTEST_RESULT}"
|
check pytest "${PYTEST_RESULT}"
|
||||||
check terraform "${TERRAFORM_RESULT}"
|
check terraform "${TERRAFORM_RESULT}"
|
||||||
|
check openapi "${OPENAPI_RESULT}"
|
||||||
exit "${fail}"
|
exit "${fail}"
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
||||||
|
node_modules/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
.aws-sam/
|
.aws-sam/
|
||||||
|
|
|
||||||
7
.redocly.yaml
Normal file
7
.redocly.yaml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# Same Redocly recommended ruleset as internal-portal (DEV-223 / DEV-289).
|
||||||
|
extends:
|
||||||
|
- recommended
|
||||||
|
|
||||||
|
apis:
|
||||||
|
afterhours@v1:
|
||||||
|
root: openapi.yaml
|
||||||
|
|
@ -59,6 +59,8 @@ Example: `/oncall admin holiday add 2026-07-04 2 x2 Independence Day` schedules
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
- **Runtime**: Python 3.12 on ECS Fargate (arm64) plus dual-run Lambdas until cutover. seahaven-prod `011934824531`, seahaven-dev `710827005802`
|
- **Runtime**: Python 3.12 on ECS Fargate (arm64) plus dual-run Lambdas until cutover. seahaven-prod `011934824531`, seahaven-dev `710827005802`
|
||||||
|
- **VPC**: This stack owns `10.70.0.0/16`. Meals and portal Fargate attach with HCP `existing_vpc_id` / `existing_public_subnet_ids` from outputs `vpc_id` and `public_subnet_ids`.
|
||||||
|
- **HTTP contract**: `openapi.yaml`, linted in CI with `npm run openapi:lint` (Redocly `extends: recommended`, same as internal-portal and meal-order-manager).
|
||||||
- **Data**: DynamoDB single-table (`afterhours-shifts`)
|
- **Data**: DynamoDB single-table (`afterhours-shifts`)
|
||||||
- **IaC**: HCP Terraform workspaces tagged `app:afterhours-shift-manager` (`afterhours-shift-manager-dev` / `-prod`) plus GitHub Actions `deploy-api.yaml` (image). Terraform does not package `src/`.
|
- **IaC**: HCP Terraform workspaces tagged `app:afterhours-shift-manager` (`afterhours-shift-manager-dev` / `-prod`) plus GitHub Actions `deploy-api.yaml` (image). Terraform does not package `src/`.
|
||||||
- **Slack**: Slack Bolt on `POST /slack/events`
|
- **Slack**: Slack Bolt on `POST /slack/events`
|
||||||
|
|
@ -299,7 +301,8 @@ pytest
|
||||||
|
|
||||||
Each Lambda has its own `app.py`, so the per-package `conftest.py` loads each one
|
Each Lambda has its own `app.py`, so the per-package `conftest.py` loads each one
|
||||||
under a unique module name (importlib mode) to avoid collisions. CI runs the same
|
under a unique module name (importlib mode) to avoid collisions. CI runs the same
|
||||||
suite on every PR via pytest plus `terraform fmt` / `init -backend=false` / `validate`.
|
suite on every PR via pytest plus `terraform fmt` / `init -backend=false` /
|
||||||
|
`validate` and `npm run openapi:lint`.
|
||||||
|
|
||||||
Local Fargate process (needs the same DynamoDB table and Secrets Manager names
|
Local Fargate process (needs the same DynamoDB table and Secrets Manager names
|
||||||
the Lambdas use, plus `JOBS_QUEUE_URL` to consume jobs):
|
the Lambdas use, plus `JOBS_QUEUE_URL` to consume jobs):
|
||||||
|
|
|
||||||
777
openapi.yaml
Normal file
777
openapi.yaml
Normal file
|
|
@ -0,0 +1,777 @@
|
||||||
|
openapi: 3.1.0
|
||||||
|
info:
|
||||||
|
title: After Hours Shift Manager
|
||||||
|
version: 0.1.0
|
||||||
|
description: >
|
||||||
|
Flask HTTP API on ECS Fargate. Slack `/oncall` stays on POST /slack/events
|
||||||
|
and is not part of this contract. The internal portal SPA calls /api/shifts
|
||||||
|
with a Cognito ID token from GET /api/auth/meals-token. Paychex calls
|
||||||
|
PUT/DELETE /roster with a shared bearer token. Error shape for portal
|
||||||
|
routes is `{ error: { code, message } }`. Health matches the portal BFF
|
||||||
|
`{ stage, sha }`. Lint with the same Redocly `extends: recommended` config
|
||||||
|
as internal-portal and meal-order-manager.
|
||||||
|
contact:
|
||||||
|
name: Sea Haven Engineering
|
||||||
|
license:
|
||||||
|
name: Proprietary
|
||||||
|
identifier: LicenseRef-SeaHaven
|
||||||
|
|
||||||
|
servers:
|
||||||
|
- url: /
|
||||||
|
description: After Hours ALB origin (VITE_SHIFTS_API_BASE)
|
||||||
|
|
||||||
|
tags:
|
||||||
|
- name: Runtime
|
||||||
|
description: Unauthenticated health
|
||||||
|
- name: Roster
|
||||||
|
description: Paychex hire and offboard
|
||||||
|
- name: Shifts
|
||||||
|
description: Employee and admin After Hours for the portal SPA
|
||||||
|
|
||||||
|
security: []
|
||||||
|
|
||||||
|
paths:
|
||||||
|
/api/health:
|
||||||
|
get:
|
||||||
|
operationId: getHealth
|
||||||
|
tags: [Runtime]
|
||||||
|
summary: Runtime health
|
||||||
|
security: []
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Process is up
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Health"
|
||||||
|
|
||||||
|
/roster:
|
||||||
|
put:
|
||||||
|
operationId: putRoster
|
||||||
|
tags: [Roster]
|
||||||
|
summary: Upsert a roster row
|
||||||
|
security:
|
||||||
|
- rosterBearer: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/RosterUpsert"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Row written
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/RosterOk"
|
||||||
|
"400":
|
||||||
|
$ref: "#/components/responses/RosterError"
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/RosterError"
|
||||||
|
"503":
|
||||||
|
$ref: "#/components/responses/RosterError"
|
||||||
|
|
||||||
|
/roster/{extension}:
|
||||||
|
delete:
|
||||||
|
operationId: deleteRoster
|
||||||
|
tags: [Roster]
|
||||||
|
summary: Remove a roster row
|
||||||
|
security:
|
||||||
|
- rosterBearer: []
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/Extension"
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: Gone, including when the row was already missing
|
||||||
|
"400":
|
||||||
|
$ref: "#/components/responses/RosterError"
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/RosterError"
|
||||||
|
"503":
|
||||||
|
$ref: "#/components/responses/RosterError"
|
||||||
|
|
||||||
|
/api/shifts:
|
||||||
|
get:
|
||||||
|
operationId: getShifts
|
||||||
|
tags: [Shifts]
|
||||||
|
summary: Week snapshot for the signed-in employee
|
||||||
|
security:
|
||||||
|
- portalCognito: []
|
||||||
|
parameters:
|
||||||
|
- name: week
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
enum: [this, next]
|
||||||
|
default: this
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Linked snapshot or unlinked Google account
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ShiftsSnapshot"
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
"503":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
options:
|
||||||
|
operationId: optionsShifts
|
||||||
|
tags: [Shifts]
|
||||||
|
summary: CORS preflight
|
||||||
|
security: []
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: Empty preflight
|
||||||
|
|
||||||
|
/api/shifts/pick:
|
||||||
|
post:
|
||||||
|
operationId: pickShift
|
||||||
|
tags: [Shifts]
|
||||||
|
summary: Pick up a shift or request late pickup
|
||||||
|
security:
|
||||||
|
- portalCognito: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ShiftDateBody"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
$ref: "#/components/responses/MutationOk"
|
||||||
|
"400":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
"404":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
"409":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
"503":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
|
||||||
|
/api/shifts/drop:
|
||||||
|
post:
|
||||||
|
operationId: dropShift
|
||||||
|
tags: [Shifts]
|
||||||
|
summary: Drop a held shift
|
||||||
|
security:
|
||||||
|
- portalCognito: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ShiftDateBody"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
$ref: "#/components/responses/MutationOk"
|
||||||
|
"400":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
"404":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
"409":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
|
||||||
|
/api/shifts/swap:
|
||||||
|
post:
|
||||||
|
operationId: requestSwap
|
||||||
|
tags: [Shifts]
|
||||||
|
summary: Request a swap
|
||||||
|
security:
|
||||||
|
- portalCognito: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/SwapBody"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
$ref: "#/components/responses/MutationOk"
|
||||||
|
"400":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
"404":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
"409":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
|
||||||
|
/api/shifts/swaps/{date}/{shiftType}/accept:
|
||||||
|
post:
|
||||||
|
operationId: acceptSwap
|
||||||
|
tags: [Shifts]
|
||||||
|
summary: Accept a pending swap
|
||||||
|
security:
|
||||||
|
- portalCognito: []
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/ShiftDate"
|
||||||
|
- $ref: "#/components/parameters/ShiftType"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
$ref: "#/components/responses/MutationOk"
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
"404":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
"409":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
|
||||||
|
/api/shifts/swaps/{date}/{shiftType}/decline:
|
||||||
|
post:
|
||||||
|
operationId: declineSwap
|
||||||
|
tags: [Shifts]
|
||||||
|
summary: Decline a pending swap
|
||||||
|
security:
|
||||||
|
- portalCognito: []
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/ShiftDate"
|
||||||
|
- $ref: "#/components/parameters/ShiftType"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
$ref: "#/components/responses/MutationOk"
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
"404":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
|
||||||
|
/api/shifts/admin/override:
|
||||||
|
post:
|
||||||
|
operationId: adminOverride
|
||||||
|
tags: [Shifts]
|
||||||
|
summary: Assign a shift
|
||||||
|
security:
|
||||||
|
- portalCognito: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/AdminOverrideBody"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
$ref: "#/components/responses/MutationOk"
|
||||||
|
"400":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
"403":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
"404":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
|
||||||
|
/api/shifts/admin/open:
|
||||||
|
post:
|
||||||
|
operationId: adminOpen
|
||||||
|
tags: [Shifts]
|
||||||
|
summary: Mark a shift open
|
||||||
|
security:
|
||||||
|
- portalCognito: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ShiftDateBody"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
$ref: "#/components/responses/MutationOk"
|
||||||
|
"400":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
"403":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
|
||||||
|
/api/shifts/admin/clear:
|
||||||
|
post:
|
||||||
|
operationId: adminClear
|
||||||
|
tags: [Shifts]
|
||||||
|
summary: Clear an override
|
||||||
|
security:
|
||||||
|
- portalCognito: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ShiftDateBody"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
$ref: "#/components/responses/MutationOk"
|
||||||
|
"400":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
"403":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
|
||||||
|
/api/shifts/admin/holidays:
|
||||||
|
post:
|
||||||
|
operationId: adminHolidayAdd
|
||||||
|
tags: [Shifts]
|
||||||
|
summary: Schedule a holiday day shift
|
||||||
|
security:
|
||||||
|
- portalCognito: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/HolidayBody"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
$ref: "#/components/responses/MutationOk"
|
||||||
|
"400":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
"403":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
"409":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
|
||||||
|
/api/shifts/admin/holidays/{date}:
|
||||||
|
delete:
|
||||||
|
operationId: adminHolidayRemove
|
||||||
|
tags: [Shifts]
|
||||||
|
summary: Remove a holiday
|
||||||
|
security:
|
||||||
|
- portalCognito: []
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/ShiftDate"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
$ref: "#/components/responses/MutationOk"
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
"403":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
"404":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
|
||||||
|
/api/shifts/admin/pickups/{date}/{shiftType}/{extension}/approve:
|
||||||
|
post:
|
||||||
|
operationId: adminPickupApprove
|
||||||
|
tags: [Shifts]
|
||||||
|
summary: Approve a late pickup
|
||||||
|
security:
|
||||||
|
- portalCognito: []
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/ShiftDate"
|
||||||
|
- $ref: "#/components/parameters/ShiftType"
|
||||||
|
- $ref: "#/components/parameters/Extension"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
$ref: "#/components/responses/MutationOk"
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
"403":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
"404":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
"409":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
|
||||||
|
/api/shifts/admin/pickups/{date}/{shiftType}/{extension}/deny:
|
||||||
|
post:
|
||||||
|
operationId: adminPickupDeny
|
||||||
|
tags: [Shifts]
|
||||||
|
summary: Deny a late pickup
|
||||||
|
security:
|
||||||
|
- portalCognito: []
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/ShiftDate"
|
||||||
|
- $ref: "#/components/parameters/ShiftType"
|
||||||
|
- $ref: "#/components/parameters/Extension"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
$ref: "#/components/responses/MutationOk"
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
"403":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
"404":
|
||||||
|
$ref: "#/components/responses/PortalError"
|
||||||
|
|
||||||
|
components:
|
||||||
|
securitySchemes:
|
||||||
|
portalCognito:
|
||||||
|
type: http
|
||||||
|
scheme: bearer
|
||||||
|
bearerFormat: JWT
|
||||||
|
description: Portal Cognito ID token
|
||||||
|
rosterBearer:
|
||||||
|
type: http
|
||||||
|
scheme: bearer
|
||||||
|
description: Shared Paychex roster token
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
ShiftDate:
|
||||||
|
name: date
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: date
|
||||||
|
ShiftType:
|
||||||
|
name: shiftType
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
enum: [day, night]
|
||||||
|
Extension:
|
||||||
|
name: extension
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
|
||||||
|
responses:
|
||||||
|
MutationOk:
|
||||||
|
description: Mutation applied
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/MutationResult"
|
||||||
|
PortalError:
|
||||||
|
description: Portal JSON error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/PortalErrorEnvelope"
|
||||||
|
RosterError:
|
||||||
|
description: Roster string error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/RosterErrorBody"
|
||||||
|
|
||||||
|
schemas:
|
||||||
|
Health:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [stage, sha]
|
||||||
|
properties:
|
||||||
|
stage:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
description: Workspace stage (`dev`, `prod`, or `local`)
|
||||||
|
sha:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
description: Git SHA or `unknown` locally
|
||||||
|
|
||||||
|
PortalErrorEnvelope:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [error]
|
||||||
|
properties:
|
||||||
|
error:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [code, message]
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
|
||||||
|
RosterErrorBody:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [error]
|
||||||
|
properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
|
||||||
|
RosterUpsert:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [name, extension, slack_user_id]
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
extension:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
slack_user_id:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
format: email
|
||||||
|
|
||||||
|
RosterOk:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [ok]
|
||||||
|
properties:
|
||||||
|
ok:
|
||||||
|
type: boolean
|
||||||
|
const: true
|
||||||
|
|
||||||
|
ShiftDateBody:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [date]
|
||||||
|
properties:
|
||||||
|
date:
|
||||||
|
type: string
|
||||||
|
format: date
|
||||||
|
shiftType:
|
||||||
|
type: string
|
||||||
|
enum: [day, night, holiday]
|
||||||
|
|
||||||
|
SwapBody:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [date, targetExtension]
|
||||||
|
properties:
|
||||||
|
date:
|
||||||
|
type: string
|
||||||
|
format: date
|
||||||
|
shiftType:
|
||||||
|
type: string
|
||||||
|
enum: [day, night]
|
||||||
|
targetExtension:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
|
||||||
|
AdminOverrideBody:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [date, extension]
|
||||||
|
properties:
|
||||||
|
date:
|
||||||
|
type: string
|
||||||
|
format: date
|
||||||
|
extension:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
shiftType:
|
||||||
|
type: string
|
||||||
|
enum: [day, night]
|
||||||
|
|
||||||
|
HolidayBody:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [date, slots, label]
|
||||||
|
properties:
|
||||||
|
date:
|
||||||
|
type: string
|
||||||
|
format: date
|
||||||
|
slots:
|
||||||
|
type: integer
|
||||||
|
minimum: 1
|
||||||
|
label:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
multiplier:
|
||||||
|
type: [number, string, "null"]
|
||||||
|
|
||||||
|
MutationResult:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [ok]
|
||||||
|
properties:
|
||||||
|
ok:
|
||||||
|
type: boolean
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
latePickup:
|
||||||
|
type: boolean
|
||||||
|
repointed:
|
||||||
|
type: boolean
|
||||||
|
|
||||||
|
RosterPerson:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [extension, name, email]
|
||||||
|
properties:
|
||||||
|
extension:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
slackUserId:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
ShiftAssignee:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [extension, name]
|
||||||
|
properties:
|
||||||
|
extension:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
ShiftSlot:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required:
|
||||||
|
- kind
|
||||||
|
- shiftType
|
||||||
|
- label
|
||||||
|
- slots
|
||||||
|
- openSlots
|
||||||
|
- multiplier
|
||||||
|
- assignees
|
||||||
|
- mine
|
||||||
|
- canPick
|
||||||
|
- canDrop
|
||||||
|
- latePickup
|
||||||
|
properties:
|
||||||
|
kind:
|
||||||
|
type: string
|
||||||
|
enum: [holiday, override, available, weekly]
|
||||||
|
shiftType:
|
||||||
|
type: string
|
||||||
|
enum: [day, night]
|
||||||
|
label:
|
||||||
|
type: string
|
||||||
|
slots:
|
||||||
|
type: integer
|
||||||
|
openSlots:
|
||||||
|
type: integer
|
||||||
|
multiplier:
|
||||||
|
type: number
|
||||||
|
assignees:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/ShiftAssignee"
|
||||||
|
mine:
|
||||||
|
type: boolean
|
||||||
|
canPick:
|
||||||
|
type: boolean
|
||||||
|
canDrop:
|
||||||
|
type: boolean
|
||||||
|
latePickup:
|
||||||
|
type: boolean
|
||||||
|
|
||||||
|
ShiftDay:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [date, dayName, slots]
|
||||||
|
properties:
|
||||||
|
date:
|
||||||
|
type: string
|
||||||
|
format: date
|
||||||
|
dayName:
|
||||||
|
type: string
|
||||||
|
slots:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/ShiftSlot"
|
||||||
|
|
||||||
|
PendingSwap:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required:
|
||||||
|
- date
|
||||||
|
- shiftType
|
||||||
|
- requesterExt
|
||||||
|
- requesterName
|
||||||
|
- targetExt
|
||||||
|
- targetName
|
||||||
|
- incoming
|
||||||
|
properties:
|
||||||
|
date:
|
||||||
|
type: string
|
||||||
|
shiftType:
|
||||||
|
type: string
|
||||||
|
enum: [day, night]
|
||||||
|
requesterExt:
|
||||||
|
type: string
|
||||||
|
requesterName:
|
||||||
|
type: string
|
||||||
|
targetExt:
|
||||||
|
type: string
|
||||||
|
targetName:
|
||||||
|
type: string
|
||||||
|
incoming:
|
||||||
|
type: boolean
|
||||||
|
|
||||||
|
PendingPickup:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [date, shiftType, requesterExt, requesterName, isHoliday]
|
||||||
|
properties:
|
||||||
|
date:
|
||||||
|
type: string
|
||||||
|
shiftType:
|
||||||
|
type: string
|
||||||
|
enum: [day, night]
|
||||||
|
requesterExt:
|
||||||
|
type: string
|
||||||
|
requesterName:
|
||||||
|
type: string
|
||||||
|
isHoliday:
|
||||||
|
type: boolean
|
||||||
|
|
||||||
|
HolidaySummary:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [date, label, slots, multiplier]
|
||||||
|
properties:
|
||||||
|
date:
|
||||||
|
type: string
|
||||||
|
label:
|
||||||
|
type: string
|
||||||
|
slots:
|
||||||
|
type: integer
|
||||||
|
multiplier:
|
||||||
|
type: number
|
||||||
|
|
||||||
|
ShiftsSnapshot:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [linked, isAdmin]
|
||||||
|
properties:
|
||||||
|
linked:
|
||||||
|
type: boolean
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
week:
|
||||||
|
type: string
|
||||||
|
enum: [this, next]
|
||||||
|
weekStart:
|
||||||
|
type: string
|
||||||
|
format: date
|
||||||
|
me:
|
||||||
|
$ref: "#/components/schemas/RosterPerson"
|
||||||
|
isAdmin:
|
||||||
|
type: boolean
|
||||||
|
days:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/ShiftDay"
|
||||||
|
pendingSwaps:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/PendingSwap"
|
||||||
|
pendingPickups:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/PendingPickup"
|
||||||
|
upcomingHolidays:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/HolidaySummary"
|
||||||
|
roster:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/RosterPerson"
|
||||||
30
package-lock.json
generated
Normal file
30
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"name": "afterhours-shift-manager",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "afterhours-shift-manager",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"@redocly/cli": "2.52.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@redocly/cli": {
|
||||||
|
"version": "2.52.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@redocly/cli/-/cli-2.52.1.tgz",
|
||||||
|
"integrity": "sha512-gwfx1WelVDHU/cw+GkpLV9+xksOI3obJGgL1nUVrAFUyXNDxM+aifXDUZk7BfbOIpF69Sea2ant0cOqbQfg/KQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"openapi": "bin/cli.js",
|
||||||
|
"redocly": "bin/cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.12.0 || >=20.19.0 <21.0.0",
|
||||||
|
"npm": ">=10"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
package.json
Normal file
11
package.json
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"name": "afterhours-shift-manager",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"openapi:lint": "redocly lint --config .redocly.yaml openapi.yaml"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@redocly/cli": "2.52.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -116,6 +116,21 @@ def test_ci_runs_pytest_and_terraform_validate():
|
||||||
assert "terraform fmt -check" in CI
|
assert "terraform fmt -check" in CI
|
||||||
assert "terraform init -backend=false" in CI
|
assert "terraform init -backend=false" in CI
|
||||||
assert "terraform validate" in CI
|
assert "terraform validate" in CI
|
||||||
|
assert "openapi:lint" in CI
|
||||||
|
assert "npm ci" in CI
|
||||||
|
|
||||||
|
|
||||||
|
def test_openapi_uses_redocly_recommended():
|
||||||
|
redocly = (ROOT / ".redocly.yaml").read_text()
|
||||||
|
package = (ROOT / "package.json").read_text()
|
||||||
|
spec = (ROOT / "openapi.yaml").read_text()
|
||||||
|
assert "extends:" in redocly
|
||||||
|
assert "- recommended" in redocly
|
||||||
|
assert 'root: openapi.yaml' in redocly
|
||||||
|
assert '"openapi:lint"' in package
|
||||||
|
assert '"@redocly/cli": "2.52.1"' in package
|
||||||
|
assert "openapi: 3.1.0" in spec
|
||||||
|
assert "required: [stage, sha]" in spec
|
||||||
|
|
||||||
|
|
||||||
def test_checkcomponents_queue_arn_variable_matches_iam_references():
|
def test_checkcomponents_queue_arn_variable_matches_iam_references():
|
||||||
|
|
@ -167,6 +182,9 @@ def test_origins_use_fargate_url():
|
||||||
assert "aws_apigatewayv2_api.http" not in outputs
|
assert "aws_apigatewayv2_api.http" not in outputs
|
||||||
assert '${local.api_url}/slack/events' in outputs
|
assert '${local.api_url}/slack/events' in outputs
|
||||||
assert "value = local.api_url" in outputs
|
assert "value = local.api_url" in outputs
|
||||||
|
assert "output \"vpc_id\"" in outputs
|
||||||
|
assert "output \"public_subnet_ids\"" in outputs
|
||||||
|
assert "aws_vpc.this.id" in outputs
|
||||||
|
|
||||||
|
|
||||||
def test_alb_alarms_remain():
|
def test_alb_alarms_remain():
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue