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  }