go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/projects/nodes/pkg/controller/api.go (about) 1 /* 2 3 Copyright (c) 2023 - Present. Will Charczuk. All rights reserved. 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository. 5 6 */ 7 8 package controller 9 10 import ( 11 "bytes" 12 "compress/gzip" 13 "context" 14 "encoding/csv" 15 "encoding/json" 16 "fmt" 17 "io" 18 "net/http" 19 "strconv" 20 "strings" 21 "time" 22 23 "github.com/wcharczuk/go-incr" 24 "go.temporal.io/api/enums/v1" 25 "go.temporal.io/api/failure/v1" 26 "go.temporal.io/sdk/client" 27 "go.temporal.io/sdk/temporal" 28 29 "go.charczuk.com/projects/nodes/pkg/dbmodel" 30 "go.charczuk.com/projects/nodes/pkg/incrutil" 31 "go.charczuk.com/projects/nodes/pkg/types" 32 "go.charczuk.com/projects/nodes/worker/workflows" 33 "go.charczuk.com/sdk/apputil" 34 "go.charczuk.com/sdk/iter" 35 "go.charczuk.com/sdk/uuid" 36 "go.charczuk.com/sdk/web" 37 ) 38 39 type API struct { 40 apputil.BaseController 41 Temporal client.Client 42 DB *dbmodel.Manager 43 } 44 45 func (a API) Register(app *web.App) { 46 app.Get("/api/v1/session", web.SessionAware(a.getSession)) 47 48 app.Get("/api/v1/graphs", web.SessionRequired(a.getGraphs)) 49 app.Get("/api/v1/graph/:graph_id", web.SessionRequired(a.getGraph)) 50 51 app.Get("/api/v1/workflow/:workflow_id/status/:run_id", web.SessionRequired(a.getWorkflowRunStatus)) 52 app.Post("/api/v1/workflow/:workflow_id/cancel/:run_id", web.SessionRequired(a.postWorkflowRunCancel)) 53 54 app.Get("/api/v1/graph.active", web.SessionRequired(a.getGraphActive)) 55 app.Post("/api/v1/graph", web.SessionRequired(a.postGraph)) 56 app.Patch("/api/v1/graph/:graph_id", web.SessionRequired(a.patchGraph)) 57 app.Delete("/api/v1/graph/:graph_id", web.SessionRequired(a.deleteGraph)) 58 59 app.Post("/api/v1/graph.load", web.SessionRequired(a.postGraphLoad)) 60 61 app.Get("/api/v1/graph/:graph_id/save", web.SessionRequired(a.getGraphSave)) 62 app.Get("/api/v1/graph/:graph_id/logs", web.SessionRequired(a.getGraphLogs)) 63 64 app.Get("/api/v1/graph/:graph_id/viewport", web.SessionRequired(a.getViewport)) 65 app.Put("/api/v1/graph/:graph_id/viewport", web.SessionRequired(a.putViewport)) 66 67 app.Post("/api/v1/graph/:graph_id/stabilize", web.SessionRequired(a.postStabilize)) 68 69 app.Post("/api/v1/graph/:graph_id/node", web.SessionRequired(a.postNode)) 70 app.Get("/api/v1/graph/:graph_id/node/:id", web.SessionRequired(a.getNode)) 71 app.Get("/api/v1/graph/:graph_id/nodes", web.SessionRequired(a.getNodes)) 72 app.Delete("/api/v1/graph/:graph_id/node/:id", web.SessionRequired(a.deleteNode)) 73 74 app.Post("/api/v1/graph/:graph_id/node.stale/:id", web.SessionRequired(a.postNodeStale)) 75 76 app.Get("/api/v1/graph/:graph_id/node.values", web.SessionRequired(a.getNodeValues)) 77 app.Get("/api/v1/graph/:graph_id/node.value/:id", web.SessionRequired(a.getNodeValue)) 78 app.Put("/api/v1/graph/:graph_id/node.value/:id", web.SessionRequired(a.putNodeValue)) 79 80 app.Get("/api/v1/graph/:graph_id/node.value.table/:id/info", web.SessionRequired(a.getNodeValueTableInfo)) 81 app.Get("/api/v1/graph/:graph_id/node.value.table/:id/range", web.SessionRequired(a.getNodeValueTableRange)) 82 app.Put("/api/v1/graph/:graph_id/node.value.table/:id", web.SessionRequired(a.putNodeValueTableOps)) 83 84 app.Get("/api/v1/graph/:graph_id/node.value.table/:id/csv", web.SessionRequired(a.getNodeValueTableExportCSV)) 85 app.Post("/api/v1/graph/:graph_id/node.value.table/:id/csv", web.SessionRequired(a.postNodeValueTableImportCSV)) 86 87 app.Patch("/api/v1/graph/:graph_id/nodes", web.SessionRequired(a.patchNodes)) 88 app.Patch("/api/v1/graph/:graph_id/node/:id", web.SessionRequired(a.patchNode)) 89 90 app.Get("/api/v1/graph/:graph_id/edges", web.SessionRequired(a.getEdges)) 91 app.Post("/api/v1/graph/:graph_id/edge", web.SessionRequired(a.postEdge)) 92 app.Delete("/api/v1/graph/:graph_id/edge", web.SessionRequired(a.deleteEdge)) 93 } 94 95 func (a API) getStore(r web.Context) (*dbmodel.Store, *dbmodel.Graph, error) { 96 graphID, err := getGraphID(r) 97 if err != nil { 98 return nil, nil, err 99 } 100 graph, found, err := a.DB.Graph(r, graphID) 101 if err != nil { 102 return nil, nil, err 103 } 104 if !found { 105 return nil, nil, nil 106 } 107 if graph.UserID != a.GetUserID(r) { 108 return nil, nil, nil 109 } 110 return &dbmodel.Store{ 111 GraphID: graph.ID, 112 UserID: graph.UserID, 113 Model: a.DB, 114 }, &graph, nil 115 } 116 117 // getSession gets the session. 118 func (a API) getSession(r web.Context) web.Result { 119 session := r.Session() 120 if session == nil { 121 return web.JSON().NotAuthorized() 122 } 123 return web.JSON().Result(http.StatusOK, session) 124 } 125 126 type workflowRunInfo struct { 127 WorkflowID string `json:"workflow_id"` 128 RunID string `json:"run_id"` 129 } 130 131 func (a API) postStabilize(r web.Context) web.Result { 132 store, _, err := a.getStore(r) 133 if err != nil { 134 return web.JSON().InternalError(err) 135 } 136 if store == nil { 137 return web.JSON().NotFound() 138 } 139 140 workflowRun, err := a.Temporal.ExecuteWorkflow(r, client.StartWorkflowOptions{ 141 TaskQueue: "default", 142 WorkflowExecutionTimeout: 120 * time.Second, // eat a worker restart potentially. 143 RetryPolicy: &temporal.RetryPolicy{ 144 MaximumAttempts: 1, // only attempt the _workflow_ once! 145 }, 146 }, workflows.Stabilize, workflows.StabilizeArgs{ 147 UserID: store.UserID, 148 GraphID: store.GraphID, 149 }) 150 if err != nil { 151 return web.JSON().InternalError(err) 152 } 153 return web.JSON().Result(http.StatusOK, workflowRunInfo{ 154 WorkflowID: workflowRun.GetID(), 155 RunID: workflowRun.GetRunID(), 156 }) 157 } 158 159 type workflowRunStatus struct { 160 WorkflowID string `json:"workflow_id"` 161 RunID string `json:"run_id"` 162 TaskQueue string `json:"task_queue"` 163 Status string `json:"status"` 164 StartTime time.Time `json:"start_time"` 165 CloseTime time.Time `json:"close_time"` 166 Failure *failure.Failure `json:"failure"` 167 } 168 169 func (a API) workflowStatusIsFailure(status enums.WorkflowExecutionStatus) bool { 170 switch status { 171 case enums.WORKFLOW_EXECUTION_STATUS_FAILED, enums.WORKFLOW_EXECUTION_STATUS_TIMED_OUT: 172 return true 173 default: 174 return false 175 } 176 } 177 178 func (a API) getWorkflowRunCloseEvent(ctx context.Context, workflowID, runID string) (*failure.Failure, error) { 179 historyIterator := a.Temporal.GetWorkflowHistory(ctx, workflowID, runID, false /*isLongPoll*/, enums.HISTORY_EVENT_FILTER_TYPE_CLOSE_EVENT) 180 if !historyIterator.HasNext() { 181 return nil, nil 182 } 183 history, err := historyIterator.Next() 184 if err != nil { 185 return nil, err 186 } 187 switch history.EventType { 188 case enums.EVENT_TYPE_WORKFLOW_EXECUTION_FAILED: 189 failedEventAttributes := history.GetWorkflowExecutionFailedEventAttributes() 190 if failedEventAttributes == nil { 191 return nil, nil 192 } 193 return failedEventAttributes.Failure, nil 194 case enums.EVENT_TYPE_ACTIVITY_TASK_FAILED: 195 failedEventAttributes := history.GetActivityTaskFailedEventAttributes() 196 if failedEventAttributes == nil { 197 return nil, nil 198 } 199 return failedEventAttributes.Failure, nil 200 case enums.EVENT_TYPE_ACTIVITY_TASK_TIMED_OUT: 201 timedOutEventAttributes := history.GetActivityTaskTimedOutEventAttributes() 202 if timedOutEventAttributes == nil { 203 return nil, nil 204 } 205 return timedOutEventAttributes.Failure, nil 206 default: 207 return nil, nil 208 } 209 } 210 211 func (a API) getWorkflowRunStatus(r web.Context) web.Result { 212 workflowID, err := web.RouteValue[string](r, "workflow_id") 213 if err != nil { 214 return web.JSON().BadRequest(err) 215 } 216 runID, err := web.RouteValue[string](r, "run_id") 217 if err != nil { 218 return web.JSON().BadRequest(err) 219 } 220 res, err := a.Temporal.DescribeWorkflowExecution(r, workflowID, runID) 221 if err != nil { 222 return web.JSON().InternalError(err) 223 } 224 var failure *failure.Failure 225 if a.workflowStatusIsFailure(res.WorkflowExecutionInfo.GetStatus()) { 226 failure, err = a.getWorkflowRunCloseEvent(r, workflowID, runID) 227 if err != nil { 228 return web.JSON().InternalError(err) 229 } 230 } 231 return web.JSON().Result(http.StatusOK, workflowRunStatus{ 232 WorkflowID: workflowID, 233 RunID: runID, 234 TaskQueue: res.WorkflowExecutionInfo.GetTaskQueue(), 235 Status: res.WorkflowExecutionInfo.GetStatus().String(), 236 StartTime: res.WorkflowExecutionInfo.GetStartTime().AsTime(), 237 CloseTime: res.WorkflowExecutionInfo.GetCloseTime().AsTime(), 238 Failure: failure, 239 }) 240 } 241 242 func (a API) postWorkflowRunCancel(r web.Context) web.Result { 243 workflowID, err := web.RouteValue[string](r, "workflow_id") 244 if err != nil { 245 return web.JSON().BadRequest(err) 246 } 247 runID, err := web.RouteValue[string](r, "run_id") 248 if err != nil { 249 return web.JSON().BadRequest(err) 250 } 251 err = a.Temporal.CancelWorkflow(r, workflowID, runID) 252 if err != nil { 253 return web.JSON().InternalError(err) 254 } 255 return web.JSON().OK() 256 } 257 258 func (a API) getGraphs(r web.Context) web.Result { 259 graphs, err := a.DB.GraphsForUser(r, a.GetUserID(r)) 260 if err != nil { 261 return web.JSON().InternalError(err) 262 } 263 apiGraphs := iter.Apply(graphs, dbmodel.TypeGraphFromGraph) 264 return web.JSON().Result(http.StatusOK, apiGraphs) 265 } 266 267 func (a API) getGraph(r web.Context) web.Result { 268 graphID, err := getGraphID(r) 269 if err != nil { 270 return web.JSON().BadRequest(err) 271 } 272 graph, found, err := a.DB.Graph(r, graphID) 273 if err != nil { 274 return web.JSON().InternalError(err) 275 } 276 if !found || graph.UserID != a.GetUserID(r) { 277 return web.JSON().NotFound() 278 } 279 return web.JSON().Result(http.StatusOK, dbmodel.TypeGraphFromGraph(graph)) 280 } 281 282 func (a API) getGraphActive(r web.Context) web.Result { 283 graph, found, err := a.DB.GraphActiveForUser(r, a.GetUserID(r)) 284 if err != nil { 285 return web.JSON().InternalError(err) 286 } 287 if !found || graph.UserID != a.GetUserID(r) { 288 return web.JSON().NotFound() 289 } 290 return web.JSON().Result(http.StatusOK, dbmodel.TypeGraphFromGraph(graph)) 291 } 292 293 func (a API) postGraph(r web.Context) web.Result { 294 var graph types.Graph 295 if err := web.BodyAsJSON(r, &graph); err != nil { 296 return web.JSON().BadRequest(err) 297 } 298 dbgraph := dbmodel.GraphFromType(&graph) 299 dbgraph.ID = uuid.V4() 300 dbgraph.UserID = a.GetUserID(r) 301 dbgraph.CreatedUTC = time.Now().UTC() 302 if err := a.DB.Invoke(r).Create(&dbgraph); err != nil { 303 return web.JSON().InternalError(err) 304 } 305 return web.JSON().Result(http.StatusOK, dbgraph.ID) 306 } 307 308 func (a API) patchGraph(r web.Context) web.Result { 309 graphID, err := getGraphID(r) 310 if err != nil { 311 return web.JSON().BadRequest(err) 312 } 313 graph, found, err := a.DB.Graph(r, graphID) 314 if err != nil { 315 return web.JSON().InternalError(err) 316 } 317 if !found || graph.UserID != a.GetUserID(r) { 318 return web.JSON().NotFound() 319 } 320 ps := make(types.PatchSet) 321 if err := web.BodyAsJSON(r, &ps); err != nil { 322 return web.JSON().BadRequest(err) 323 } 324 if _, err := a.DB.PatchGraph(r, graphID, ps); err != nil { 325 return web.JSON().InternalError(err) 326 } 327 return web.JSON().OK() 328 } 329 330 func (a API) deleteGraph(r web.Context) web.Result { 331 graphID, err := getGraphID(r) 332 if err != nil { 333 return web.JSON().BadRequest(err) 334 } 335 graph, found, err := a.DB.Graph(r, graphID) 336 if err != nil { 337 return web.JSON().InternalError(err) 338 } 339 if !found || graph.UserID != a.GetUserID(r) { 340 return web.JSON().NotFound() 341 } 342 if _, err := a.DB.DeleteGraph(r, graphID); err != nil { 343 return web.JSON().InternalError(err) 344 } 345 return web.JSON().OK() 346 } 347 348 func (a API) getGraphSave(r web.Context) web.Result { 349 store, _, err := a.getStore(r) 350 if err != nil { 351 return web.JSON().InternalError(err) 352 } 353 if store == nil { 354 return web.JSON().NotFound() 355 } 356 saveData, err := store.Save(r) 357 if err != nil { 358 return web.JSON().InternalError(err) 359 } 360 if ok := r.Request().URL.Query().Has("raw"); ok { 361 r.Response().Header().Set(`Content-Disposition`, `inline; filename="graph.json"`) 362 return web.JSON().Result(http.StatusOK, saveData) 363 } 364 365 buf := new(bytes.Buffer) 366 gzw := gzip.NewWriter(buf) 367 if err := json.NewEncoder(gzw).Encode(saveData); err != nil { 368 return web.JSON().InternalError(err) 369 } 370 _ = gzw.Flush() 371 _ = gzw.Close() 372 373 r.Response().Header().Set(web.HeaderContentLength, strconv.Itoa(buf.Len())) 374 r.Response().Header().Set(`Content-Disposition`, `inline; filename="graph.json.gz"`) 375 _, _ = io.Copy(r.Response(), bytes.NewReader(buf.Bytes())) 376 if typed, ok := r.Response().(http.Flusher); ok { 377 typed.Flush() 378 } 379 return nil 380 } 381 382 func (a API) postGraphLoad(r web.Context) web.Result { 383 store := &dbmodel.Store{ 384 UserID: a.GetUserID(r), 385 Model: a.DB, 386 } 387 body, err := r.Request().MultipartReader() 388 if err != nil { 389 return web.JSON().BadRequest(err) 390 } 391 bodyPart, err := body.NextPart() 392 if err != nil { 393 return web.JSON().BadRequest(err) 394 } 395 396 var reader io.Reader 397 if strings.HasSuffix(strings.ToLower(bodyPart.FileName()), ".gz") { 398 reader, err = gzip.NewReader(bodyPart) 399 if err != nil { 400 return web.JSON().BadRequest(err) 401 } 402 } else { 403 reader = bodyPart 404 } 405 406 var data types.GraphFull 407 if err = json.NewDecoder(reader).Decode(&data); err != nil { 408 return web.JSON().BadRequest(err) 409 } 410 id, err := store.Load(r, &data) 411 if err != nil { 412 return web.JSON().BadRequest(err) 413 } 414 415 return web.JSON().Result(http.StatusOK, id) 416 } 417 418 func (a API) getViewport(r web.Context) web.Result { 419 _, graph, err := a.getStore(r) 420 if err != nil { 421 return web.JSON().InternalError(err) 422 } 423 if graph == nil { 424 return web.JSON().NotFound() 425 } 426 return web.JSON().Result(http.StatusOK, types.Viewport{ 427 X: graph.ViewportX, 428 Y: graph.ViewportY, 429 Zoom: graph.ViewportZoom, 430 }) 431 } 432 433 func (a API) putViewport(r web.Context) web.Result { 434 store, _, err := a.getStore(r) 435 if err != nil { 436 return web.JSON().InternalError(err) 437 } 438 if store == nil { 439 return web.JSON().NotFound() 440 } 441 442 var viewport types.Viewport 443 if err := web.BodyAsJSON(r, &viewport); err != nil { 444 return web.JSON().BadRequest(err) 445 } 446 if err := store.SetViewport(r, viewport); err != nil { 447 return web.JSON().BadRequest(err) 448 } 449 return web.JSON().OK() 450 } 451 452 func (a API) getGraphLogs(r web.Context) web.Result { 453 store, _, err := a.getStore(r) 454 if err != nil { 455 return web.JSON().InternalError(err) 456 } 457 if store == nil { 458 return web.JSON().NotFound() 459 } 460 461 logs, err := store.Logs(r) 462 if err != nil { 463 return web.JSON().InternalError(err) 464 } 465 return web.JSON().Result(http.StatusOK, logs) 466 } 467 468 func (a API) postNode(r web.Context) web.Result { 469 store, _, err := a.getStore(r) 470 if err != nil { 471 return web.JSON().InternalError(err) 472 } 473 if store == nil { 474 return web.JSON().NotFound() 475 } 476 477 var node types.Node 478 err = web.BodyAsJSON(r, &node) 479 if err != nil { 480 return web.JSON().BadRequest(err) 481 } 482 var in incr.Identifier 483 in, err = store.AddNode(r, &node) 484 if err != nil { 485 return web.JSON().BadRequest(err) 486 } 487 return web.JSON().Result(http.StatusOK, in.String()) 488 } 489 490 func (a API) getNodes(r web.Context) web.Result { 491 store, _, err := a.getStore(r) 492 if err != nil { 493 return web.JSON().InternalError(err) 494 } 495 if store == nil { 496 return web.JSON().NotFound() 497 } 498 499 nodes, err := store.Nodes(r) 500 if err != nil { 501 return web.JSON().InternalError(err) 502 } 503 return web.JSON().Result(http.StatusOK, nodes) 504 } 505 506 func (a API) getNode(r web.Context) web.Result { 507 store, _, err := a.getStore(r) 508 if err != nil { 509 return web.JSON().InternalError(err) 510 } 511 if store == nil { 512 return web.JSON().NotFound() 513 } 514 515 nodeID, err := getNodeID(r) 516 if err != nil { 517 return web.JSON().BadRequest(err) 518 } 519 node, ok, err := store.Node(r, nodeID) 520 if err != nil { 521 return web.JSON().InternalError(err) 522 } 523 if !ok { 524 return web.JSON().NotFound() 525 } 526 return web.JSON().Result(http.StatusOK, node) 527 } 528 529 func (a API) deleteNode(r web.Context) web.Result { 530 store, _, err := a.getStore(r) 531 if err != nil { 532 return web.JSON().InternalError(err) 533 } 534 if store == nil { 535 return web.JSON().NotFound() 536 } 537 538 nodeID, err := getNodeID(r) 539 if err != nil { 540 return web.JSON().BadRequest(err) 541 } 542 var ok bool 543 ok, err = store.RemoveNode(r, nodeID) 544 if err != nil { 545 return web.JSON().BadRequest(err) 546 } 547 if !ok { 548 return web.JSON().NotFound() 549 } 550 return web.JSON().OK() 551 } 552 553 func (a API) postNodeStale(r web.Context) web.Result { 554 store, _, err := a.getStore(r) 555 if err != nil { 556 return web.JSON().InternalError(err) 557 } 558 if store == nil { 559 return web.JSON().NotFound() 560 } 561 562 rawNodeID, err := web.RouteValue[string](r, "id") 563 if err != nil { 564 return web.JSON().BadRequest(err) 565 } 566 567 parsedNodeID, err := incr.ParseIdentifier(rawNodeID) 568 if err != nil { 569 return web.JSON().BadRequest(err) 570 } 571 572 var ok bool 573 ok, err = store.MarkNodeStale(r, parsedNodeID) 574 if err != nil { 575 return web.JSON().BadRequest(err) 576 } 577 if !ok { 578 return web.JSON().NotFound() 579 } 580 return web.JSON().OK() 581 } 582 583 func (a API) getNodeValue(r web.Context) web.Result { 584 store, _, err := a.getStore(r) 585 if err != nil { 586 return web.JSON().InternalError(err) 587 } 588 if store == nil { 589 return web.JSON().NotFound() 590 } 591 592 nodeID, err := getNodeID(r) 593 if err != nil { 594 return web.JSON().BadRequest(err) 595 } 596 value, ok, err := store.NodeValue(r, nodeID) 597 if err != nil { 598 return web.JSON().InternalError(err) 599 } 600 if !ok { 601 return web.JSON().NotFound() 602 } 603 return web.JSON().Result(http.StatusOK, value) 604 } 605 606 func (a API) getNodeValues(r web.Context) web.Result { 607 store, _, err := a.getStore(r) 608 if err != nil { 609 return web.JSON().InternalError(err) 610 } 611 if store == nil { 612 return web.JSON().NotFound() 613 } 614 615 rawNodeIDs, err := web.QueryValue[string](r, "ids") 616 if err != nil { 617 return web.JSON().BadRequest(err) 618 } 619 var ids []incr.Identifier 620 for _, v := range strings.Split(rawNodeIDs, ",") { 621 parsed, err := incr.ParseIdentifier(v) 622 if err != nil { 623 return web.JSON().BadRequest(err) 624 } 625 ids = append(ids, parsed) 626 } 627 values, err := store.NodeValues(r, ids...) 628 if err != nil { 629 return web.JSON().InternalError(err) 630 } 631 return web.JSON().Result(http.StatusOK, values) 632 } 633 634 func (a API) putNodeValue(r web.Context) web.Result { 635 store, _, err := a.getStore(r) 636 if err != nil { 637 return web.JSON().InternalError(err) 638 } 639 if store == nil { 640 return web.JSON().NotFound() 641 } 642 643 nodeID, err := getNodeID(r) 644 if err != nil { 645 return web.JSON().BadRequest(err) 646 } 647 648 // get the node type for the node id 649 node, ok, err := store.Node(r, nodeID) 650 if err != nil { 651 return web.JSON().InternalError(err) 652 } 653 if !ok { 654 return web.JSON().NotFound() 655 } 656 var newElementValue any 657 if node.Metadata.NodeType == incrutil.NodeTypeTable { 658 bodyValue := new(types.Table) 659 if err = web.BodyAsJSON(r, bodyValue); err != nil { 660 return web.JSON().BadRequest(err) 661 } 662 newElementValue = bodyValue 663 } else { 664 if err = web.BodyAsJSON(r, &newElementValue); err != nil { 665 return web.JSON().BadRequest(err) 666 } 667 } 668 669 ok, err = store.SetNodeValue(r, nodeID, newElementValue) 670 if err != nil { 671 return web.JSON().BadRequest(err) 672 } 673 if !ok { 674 return web.JSON().NotFound() 675 } 676 return web.JSON().OK() 677 } 678 679 func (a API) getNodeValueTableInfo(r web.Context) web.Result { 680 store, _, err := a.getStore(r) 681 if err != nil { 682 return web.JSON().InternalError(err) 683 } 684 if store == nil { 685 return web.JSON().NotFound() 686 } 687 688 nodeID, err := getNodeID(r) 689 if err != nil { 690 return web.JSON().BadRequest(err) 691 } 692 693 value, found, err := store.NodeValue(r, nodeID) 694 if err != nil { 695 return web.JSON().InternalError(err) 696 } 697 if !found { 698 return web.JSON().NotFound() 699 } 700 valueTyped, ok := value.(*types.Table) 701 if !ok { 702 return web.JSON().NotFound() 703 } 704 return web.JSON().Result(http.StatusOK, types.TableInfo{ 705 Rows: valueTyped.RowCount(), 706 Columns: iter.Apply(valueTyped.Columns, func(tc types.TableColumn) string { return tc.Name }), 707 }) 708 } 709 710 func (a API) getNodeValueTableRange(r web.Context) web.Result { 711 store, _, err := a.getStore(r) 712 if err != nil { 713 return web.JSON().InternalError(err) 714 } 715 if store == nil { 716 return web.JSON().NotFound() 717 } 718 719 nodeID, err := getNodeID(r) 720 if err != nil { 721 return web.JSON().BadRequest(err) 722 } 723 724 rawRange, err := web.QueryValue[string](r, "range") 725 if err != nil { 726 return web.JSON().BadRequest(err) 727 } 728 rawRangeComponents := strings.Split(rawRange, ",") 729 if len(rawRangeComponents) != 4 { 730 return web.JSON().BadRequest(fmt.Errorf("invalid table range; expect 4 integers in csv form")) 731 } 732 visibleRange, err := iter.ApplyError(rawRangeComponents, strconv.Atoi) 733 if err != nil { 734 return web.JSON().BadRequest(fmt.Errorf("invalid table range; expect 4 integers; %w", err)) 735 } 736 737 value, found, err := store.NodeValue(r, nodeID) 738 if err != nil { 739 return web.JSON().InternalError(err) 740 } 741 if !found { 742 return web.JSON().NotFound() 743 } 744 valueTyped, ok := value.(*types.Table) 745 if !ok { 746 return web.JSON().NotFound() 747 } 748 values := valueTyped.Range(types.TableRange{ 749 Top: visibleRange[0], 750 Left: visibleRange[1], 751 Bottom: visibleRange[2], 752 Right: visibleRange[3], 753 }) 754 return web.JSON().Result(http.StatusOK, values) 755 } 756 757 func (a API) putNodeValueTableOps(r web.Context) web.Result { 758 store, _, err := a.getStore(r) 759 if err != nil { 760 return web.JSON().InternalError(err) 761 } 762 if store == nil { 763 return web.JSON().NotFound() 764 } 765 766 nodeID, err := getNodeID(r) 767 if err != nil { 768 return web.JSON().BadRequest(err) 769 } 770 var ops []types.TableOp 771 if err := web.BodyAsJSON(r, &ops); err != nil { 772 return web.JSON().BadRequest(err) 773 } 774 ok, err := store.PatchNodeTable(r, nodeID, ops...) 775 if err != nil { 776 return web.JSON().BadRequest(err) 777 } 778 if !ok { 779 return web.JSON().NotFound() 780 } 781 return web.JSON().OK() 782 } 783 784 func (a API) getNodeValueTableExportCSV(r web.Context) web.Result { 785 store, _, err := a.getStore(r) 786 if err != nil { 787 return web.JSON().InternalError(err) 788 } 789 if store == nil { 790 return web.JSON().NotFound() 791 } 792 nodeID, err := getNodeID(r) 793 if err != nil { 794 return web.JSON().BadRequest(err) 795 } 796 797 value, found, err := store.NodeValue(r, nodeID) 798 if err != nil { 799 return web.JSON().InternalError(err) 800 } 801 if !found { 802 return web.JSON().NotFound() 803 } 804 typed, ok := value.(*types.Table) 805 if !ok { 806 return web.JSON().NotFound() 807 } 808 809 buf := new(bytes.Buffer) 810 csvw := csv.NewWriter(buf) 811 _ = csvw.Write(iter.Apply(typed.Columns, func(c types.TableColumn) string { return c.Name })) 812 rows := typed.Rows() 813 for _, row := range rows { 814 rowStrings := make([]string, 0, len(row)) 815 for _, v := range row { 816 if v == nil { 817 rowStrings = append(rowStrings, "") 818 continue 819 } 820 rowStrings = append(rowStrings, fmt.Sprint(v)) 821 } 822 csvw.Write(rowStrings) 823 } 824 csvw.Flush() 825 826 r.Response().Header().Set(web.HeaderContentLength, strconv.Itoa(buf.Len())) 827 r.Response().Header().Set(web.HeaderContentType, "text/csv") 828 r.Response().Header().Set(`Content-Disposition`, `inline; filename="table.csv"`) 829 _, _ = io.Copy(r.Response(), bytes.NewReader(buf.Bytes())) 830 if typed, ok := r.Response().(http.Flusher); ok { 831 typed.Flush() 832 } 833 return nil 834 } 835 836 func (a API) postNodeValueTableImportCSV(r web.Context) web.Result { 837 store, _, err := a.getStore(r) 838 if err != nil { 839 return web.JSON().InternalError(err) 840 } 841 if store == nil { 842 return web.JSON().NotFound() 843 } 844 845 nodeID, err := getNodeID(r) 846 if err != nil { 847 return web.JSON().BadRequest(err) 848 } 849 850 body, err := r.Request().MultipartReader() 851 if err != nil { 852 return web.JSON().BadRequest(err) 853 } 854 bodyPart, err := body.NextPart() 855 if err != nil { 856 return web.JSON().BadRequest(err) 857 } 858 859 csvr := csv.NewReader(bodyPart) 860 csvData, err := csvr.ReadAll() 861 if err != nil { 862 return web.JSON().BadRequest(err) 863 } 864 table, err := types.TableFromCSV(csvData) 865 if err != nil { 866 return web.JSON().BadRequest(err) 867 } 868 ok, err := store.SetNodeValue(r, nodeID, table) 869 if err != nil { 870 return web.JSON().BadRequest(err) 871 } 872 if !ok { 873 return web.JSON().NotFound() 874 } 875 return web.JSON().OK() 876 } 877 878 func (a API) patchNodes(r web.Context) web.Result { 879 store, _, err := a.getStore(r) 880 if err != nil { 881 return web.JSON().InternalError(err) 882 } 883 if store == nil { 884 return web.JSON().NotFound() 885 } 886 887 var patchSet types.PatchSet 888 if err := web.BodyAsJSON(r, &patchSet); err != nil { 889 return web.JSON().BadRequest(err) 890 } 891 if err := store.PatchNodes(r, patchSet); err != nil { 892 return web.JSON().BadRequest(err) 893 } 894 return web.JSON().OK() 895 } 896 897 func (a API) patchNode(r web.Context) web.Result { 898 store, _, err := a.getStore(r) 899 if err != nil { 900 return web.JSON().InternalError(err) 901 } 902 if store == nil { 903 return web.JSON().NotFound() 904 } 905 906 id, err := getNodeID(r) 907 if err != nil { 908 return web.JSON().BadRequest(err) 909 } 910 911 var patchSet types.PatchSet 912 if err := web.BodyAsJSON(r, &patchSet); err != nil { 913 return web.JSON().BadRequest(err) 914 } 915 ok, err := store.PatchNode(r, id, patchSet) 916 if err != nil { 917 return web.JSON().BadRequest(err) 918 } 919 if !ok { 920 return web.JSON().NotFound() 921 } 922 return web.JSON().OK() 923 } 924 925 func (a API) getEdges(r web.Context) web.Result { 926 store, _, err := a.getStore(r) 927 if err != nil { 928 return web.JSON().InternalError(err) 929 } 930 if store == nil { 931 return web.JSON().NotFound() 932 } 933 934 edges, err := store.Edges(r) 935 if err != nil { 936 return web.JSON().InternalError(err) 937 } 938 return web.JSON().Result(http.StatusOK, edges) 939 } 940 941 func (a API) postEdge(r web.Context) web.Result { 942 store, _, err := a.getStore(r) 943 if err != nil { 944 return web.JSON().InternalError(err) 945 } 946 if store == nil { 947 return web.JSON().NotFound() 948 } 949 950 var args types.Edge 951 if err := web.BodyAsJSON(r, &args); err != nil { 952 return web.JSON().BadRequest(err) 953 } 954 err = store.LinkInput(r, args) 955 if err != nil { 956 return web.JSON().BadRequest(err) 957 } 958 return web.JSON().OK() 959 } 960 961 func (a API) deleteEdge(r web.Context) web.Result { 962 store, _, err := a.getStore(r) 963 if err != nil { 964 return web.JSON().InternalError(err) 965 } 966 if store == nil { 967 return web.JSON().NotFound() 968 } 969 970 var args types.Edge 971 if err := web.BodyAsJSON(r, &args); err != nil { 972 return web.JSON().BadRequest(err) 973 } 974 err = store.UnlinkInput(r, args) 975 if err != nil { 976 return web.JSON().BadRequest(err) 977 } 978 return web.JSON().OK() 979 }