mirror of
https://github.com/Sea-Haven-Industries/afterhours-shift-manager.git
synced 2026-09-30 09:03:11 +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
|
||||
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:
|
||||
name: ci / ci
|
||||
needs: [pytest, terraform]
|
||||
needs: [pytest, terraform, openapi]
|
||||
if: ${{ always() && !cancelled() }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
|
@ -73,6 +93,7 @@ jobs:
|
|||
env:
|
||||
PYTEST_RESULT: ${{ needs.pytest.result }}
|
||||
TERRAFORM_RESULT: ${{ needs.terraform.result }}
|
||||
OPENAPI_RESULT: ${{ needs.openapi.result }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
fail=0
|
||||
|
|
@ -91,4 +112,5 @@ jobs:
|
|||
}
|
||||
check pytest "${PYTEST_RESULT}"
|
||||
check terraform "${TERRAFORM_RESULT}"
|
||||
check openapi "${OPENAPI_RESULT}"
|
||||
exit "${fail}"
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
|||
node_modules/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.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
|
||||
|
||||
- **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`)
|
||||
- **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`
|
||||
|
|
@ -299,7 +301,8 @@ pytest
|
|||
|
||||
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
|
||||
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
|
||||
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 init -backend=false" 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():
|
||||
|
|
@ -167,6 +182,9 @@ def test_origins_use_fargate_url():
|
|||
assert "aws_apigatewayv2_api.http" not in outputs
|
||||
assert '${local.api_url}/slack/events' 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():
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue