diff --git a/Makefile b/Makefile index d9c0842..113136b 100644 --- a/Makefile +++ b/Makefile @@ -4,8 +4,11 @@ up: down: docker compose down -migrate: +migrate: # e. g. make revision name="add auth" docker compose exec backend alembic upgrade head +revision: + docker compose exec backend alembic revision -m "$(name)" + dev: docker compose -f docker-compose.dev.yaml up --build -d \ No newline at end of file diff --git a/backend/app/api/v1/routes/events.py b/backend/app/api/v1/routes/events.py index a11cfa0..622bf0c 100644 --- a/backend/app/api/v1/routes/events.py +++ b/backend/app/api/v1/routes/events.py @@ -1,9 +1,19 @@ -from fastapi import APIRouter, Depends, HTTPException, status +import yaml +from fastapi import ( + APIRouter, + Depends, + HTTPException, + Query, + Response, + status, +) +from fastapi.responses import JSONResponse from sqlalchemy.orm import Session from app.core.database import get_db from app.modules.events import crud as event_crud from app.modules.events.schemas import EventCreate, EventOut +from app.modules.events.service import generate_json_schema_for_event from app.modules.fields.crud import get_fields_by_ids from app.modules.tags.crud import get_or_create_tags @@ -111,3 +121,48 @@ def delete_event_route(event_id: int, db: Session = Depends(get_db)): if db_event is None: raise HTTPException(status_code=404, detail="Event not found") return db_event + + +@router.get("/{event_id}/schema.json", response_class=JSONResponse) +def get_event_json_schema( + event_id: int, + include_descriptions: bool = Query(True), + include_examples: bool = Query(True), + additional_properties: bool = Query(True), + db: Session = Depends(get_db), +): + db_event = event_crud.get_event(db=db, event_id=event_id) + if not db_event: + raise HTTPException(status_code=404, detail="Event not found") + + event = EventOut.model_validate(db_event) + schema = generate_json_schema_for_event( + event, + include_descriptions=include_descriptions, + include_examples=include_examples, + additional_properties=additional_properties, + ) + return schema + + +@router.get("/{event_id}/schema.yaml") +def get_event_yaml_schema( + event_id: int, + include_descriptions: bool = Query(True), + include_examples: bool = Query(True), + additional_properties: bool = Query(True), + db: Session = Depends(get_db), +): + db_event = event_crud.get_event(db=db, event_id=event_id) + if not db_event: + raise HTTPException(status_code=404, detail="Event not found") + + event = EventOut.model_validate(db_event) + schema = generate_json_schema_for_event( + event, + include_descriptions=include_descriptions, + include_examples=include_examples, + additional_properties=additional_properties, + ) + yaml_data = yaml.dump(schema, sort_keys=False) + return Response(content=yaml_data, media_type="application/x-yaml") diff --git a/backend/app/modules/events/service.py b/backend/app/modules/events/service.py index e69de29..324a379 100644 --- a/backend/app/modules/events/service.py +++ b/backend/app/modules/events/service.py @@ -0,0 +1,51 @@ +from app.modules.events.schemas import EventOut + + +def map_field_type_to_openapi(field_type: str) -> dict: + return { + "string": {"type": "string"}, + "integer": {"type": "integer", "format": "int32"}, + "number": {"type": "number", "format": "float"}, + "boolean": {"type": "boolean"}, + "object": {"type": "object"}, + "array": {"type": "array"}, + }.get(field_type, {"type": "string"}) + + +def generate_json_schema_for_event( + event: EventOut, + *, + additional_properties: bool = True, + include_descriptions: bool = True, + include_examples: bool = True, +) -> dict: + schema = { + "type": "object", + "additionalProperties": additional_properties, + "properties": {}, + "required": [], + } + + for field in event.fields: + field_type_info = map_field_type_to_openapi(field.field_type) + + field_schema = {"type": field_type_info["type"]} + if "format" in field_type_info: + field_schema["format"] = field_type_info["format"] + + if include_descriptions and field.description: + field_schema["description"] = field.description + + if include_examples and field.example is not None: + field_schema["example"] = field.example + + schema["properties"][field.name] = field_schema + + # if not field.optional: + if True: + schema["required"].append(field.name) + + if not schema["required"]: + del schema["required"] + + return schema diff --git a/frontend/src/index.css b/frontend/src/index.css index 9c13443..ec41b7f 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -108,8 +108,8 @@ --muted-foreground: oklch(0.708 0 0); --accent: oklch(0.269 0 0); --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.396 0.141 25.723); - --destructive-foreground: oklch(0.637 0.237 25.331); + --destructive: oklch(0.48 0.18 25.723); + --destructive-foreground: oklch(0.72 0.24 25.331); --border: oklch(0.269 0 0); --input: oklch(0.269 0 0); --ring: oklch(0.439 0 0); diff --git a/frontend/src/modules/events/components/EventDetailsCard.vue b/frontend/src/modules/events/components/EventDetailsCard.vue index 7b0606f..6a6dbeb 100644 --- a/frontend/src/modules/events/components/EventDetailsCard.vue +++ b/frontend/src/modules/events/components/EventDetailsCard.vue @@ -23,6 +23,7 @@ const eventExample = useEventExample(props.event.fields) const emit = defineEmits<{ (e: 'edit'): void (e: 'delete'): void + (e: 'export'): void }>() const { showCopied, showCopyError } = useEnhancedToast() @@ -50,6 +51,9 @@ const columns = getEventFieldsColumns() diff --git a/frontend/src/modules/fields/components/FieldEditModal.vue b/frontend/src/modules/fields/components/FieldEditModal.vue index 63c68f5..e9566e1 100644 --- a/frontend/src/modules/fields/components/FieldEditModal.vue +++ b/frontend/src/modules/fields/components/FieldEditModal.vue @@ -25,7 +25,7 @@ defineProps<{ Edit Field - + {{ description }} diff --git a/frontend/src/shared/components/layout/DetailsCardLayout.vue b/frontend/src/shared/components/layout/DetailsCardLayout.vue index 0d68348..69e13ee 100644 --- a/frontend/src/shared/components/layout/DetailsCardLayout.vue +++ b/frontend/src/shared/components/layout/DetailsCardLayout.vue @@ -87,7 +87,7 @@ const handleCopyTitle = async () => { - + diff --git a/frontend/src/shared/components/modals/DeleteModal.vue b/frontend/src/shared/components/modals/DeleteModal.vue index 127b58a..27db1b6 100644 --- a/frontend/src/shared/components/modals/DeleteModal.vue +++ b/frontend/src/shared/components/modals/DeleteModal.vue @@ -23,7 +23,7 @@ defineProps<{ Are you sure? - + {{ description }} diff --git a/frontend/src/shared/ui/button/index.ts b/frontend/src/shared/ui/button/index.ts index 6a9e0aa..ea9226a 100644 --- a/frontend/src/shared/ui/button/index.ts +++ b/frontend/src/shared/ui/button/index.ts @@ -9,7 +9,7 @@ export const buttonVariants = cva( variant: { default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90', destructive: - 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 dark:hover:bg-destructive/50', outline: 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50', secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',