diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index 515b66da..52fda46c 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -27,21 +27,21 @@ const clientAtom: jotai.Atom = jotai.atom((get) => { if (clientId == null) { return null; } - return WOS.getStaticObjectValue(WOS.makeORef("client", clientId), get); + return WOS.getObjectValue(WOS.makeORef("client", clientId), get); }); const windowDataAtom: jotai.Atom = jotai.atom((get) => { const windowId = get(windowIdAtom); if (windowId == null) { return null; } - return WOS.getStaticObjectValue(WOS.makeORef("window", windowId), get); + return WOS.getObjectValue(WOS.makeORef("window", windowId), get); }); const workspaceAtom: jotai.Atom = jotai.atom((get) => { const windowData = get(windowDataAtom); if (windowData == null) { return null; } - return WOS.getStaticObjectValue(WOS.makeORef("workspace", windowData.workspaceid), get); + return WOS.getObjectValue(WOS.makeORef("workspace", windowData.workspaceid), get); }); const atoms = { diff --git a/frontend/app/store/wos.ts b/frontend/app/store/wos.ts index 86771adf..66f78eb5 100644 --- a/frontend/app/store/wos.ts +++ b/frontend/app/store/wos.ts @@ -147,7 +147,7 @@ function useWaveObjectValue(oref: string): [T, boolean] { return [atomVal.value, atomVal.loading]; } -function useWaveObject(oref: string): [T, boolean, (T) => void] { +function useWaveObject(oref: string): [T, boolean, (T) => void] { let wov = waveObjectValueCache.get(oref); if (wov == null) { wov = createWaveValueObject(oref, true); @@ -162,6 +162,7 @@ function useWaveObject(oref: string): [T, boolean, (T) => void] { const [atomVal, setAtomVal] = jotai.useAtom(wov.dataAtom); const simpleSet = (val: T) => { setAtomVal({ value: val, loading: false }); + UpdateObject(val, false); }; return [atomVal.value, atomVal.loading, simpleSet]; } @@ -243,15 +244,39 @@ function wrapObjectServiceCall(fnName: string, ...args: any[]): Promise { return prtn; } -function getStaticObjectValue(oref: string, getFn: jotai.Getter): T { +// gets the value of a WaveObject from the cache. +// should provide getFn if it is available (e.g. inside of a jotai atom) +// otherwise it will use the globalStore.get function +function getObjectValue(oref: string, getFn?: jotai.Getter): T { let wov = waveObjectValueCache.get(oref); if (wov == null) { return null; } + if (getFn == null) { + getFn = globalStore.get; + } const atomVal = getFn(wov.dataAtom); return atomVal.value; } +// sets the value of a WaveObject in the cache. +// should provide setFn if it is available (e.g. inside of a jotai atom) +// otherwise it will use the globalStore.set function +function setObjectValue(value: WaveObj, setFn?: jotai.Setter, pushToServer?: boolean) { + const oref = makeORef(value.otype, value.oid); + let wov = waveObjectValueCache.get(oref); + if (wov == null) { + return; + } + if (setFn == null) { + setFn = globalStore.set; + } + setFn(wov.dataAtom, { value: value, loading: false }); + if (pushToServer) { + UpdateObject(value, false); + } +} + export function AddTabToWorkspace(tabName: string, activateTab: boolean): Promise<{ tabId: string }> { return wrapObjectServiceCall("AddTabToWorkspace", tabName, activateTab); } @@ -272,16 +297,21 @@ export function CloseTab(tabId: string): Promise { return wrapObjectServiceCall("CloseTab", tabId); } -export function UpdateBlockMeta(blockId: string, meta: MetadataType): Promise { - return wrapObjectServiceCall("UpdateBlockMeta", blockId, meta); +export function UpdateObjectMeta(blockId: string, meta: MetadataType): Promise { + return wrapObjectServiceCall("UpdateObjectMeta", blockId, meta); +} + +export function UpdateObject(waveObj: WaveObj, returnUpdates: boolean): Promise { + return wrapObjectServiceCall("UpdateObject", waveObj, returnUpdates); } export { cleanWaveObjectCache, clearWaveObjectCache, - getStaticObjectValue, + getObjectValue, loadAndPinWaveObject, makeORef, + setObjectValue, updateWaveObject, updateWaveObjects, useWaveObject, diff --git a/pkg/service/objectservice/objectservice.go b/pkg/service/objectservice/objectservice.go index ed378bbf..372ec1b7 100644 --- a/pkg/service/objectservice/objectservice.go +++ b/pkg/service/objectservice/objectservice.go @@ -180,13 +180,47 @@ func (svc *ObjectService) CloseTab(uiContext wstore.UIContext, tabId string) (an return updatesRtn(ctx, nil) } -func (svc *ObjectService) UpdateBlockMeta(uiContext wstore.UIContext, blockId string, meta map[string]any) (any, error) { +func (svc *ObjectService) UpdateObjectMeta(uiContext wstore.UIContext, orefStr string, meta map[string]any) (any, error) { ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) defer cancelFn() ctx = wstore.ContextWithUpdates(ctx) - err := wstore.UpdateBlockMeta(ctx, blockId, meta) + oref, err := parseORef(orefStr) if err != nil { - return nil, fmt.Errorf("error merging block meta: %w", err) + return nil, fmt.Errorf("error parsing object reference: %w", err) + } + err = wstore.UpdateObjectMeta(ctx, *oref, meta) + if err != nil { + return nil, fmt.Errorf("error updateing %q meta: %w", orefStr, err) } return updatesRtn(ctx, nil) } + +func (svc *ObjectService) UpdateObject(uiContext wstore.UIContext, objData map[string]any, returnUpdates bool) (any, error) { + ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) + defer cancelFn() + ctx = wstore.ContextWithUpdates(ctx) + + oref, err := waveobj.ORefFromMap(objData) + if err != nil { + return nil, fmt.Errorf("objData is not a valid object, requires otype and oid: %w", err) + } + found, err := wstore.DBExistsORef(ctx, *oref) + if err != nil { + return nil, fmt.Errorf("error getting object: %w", err) + } + if !found { + return nil, fmt.Errorf("object not found: %s", oref) + } + newObj, err := waveobj.FromJsonMap(objData) + if err != nil { + return nil, fmt.Errorf("error converting data to valid wave object: %w", err) + } + err = wstore.DBUpdate(ctx, newObj) + if err != nil { + return nil, fmt.Errorf("error updating object: %w", err) + } + if returnUpdates { + return updatesRtn(ctx, nil) + } + return nil, nil +} diff --git a/pkg/waveobj/waveobj.go b/pkg/waveobj/waveobj.go index 3473b122..da3c5971 100644 --- a/pkg/waveobj/waveobj.go +++ b/pkg/waveobj/waveobj.go @@ -4,11 +4,9 @@ package waveobj import ( - "bytes" "encoding/json" "fmt" "reflect" - "strings" "sync" "github.com/mitchellh/mapstructure" @@ -18,14 +16,20 @@ const ( OTypeKeyName = "otype" OIDKeyName = "oid" VersionKeyName = "version" + MetaKeyName = "meta" OIDGoFieldName = "OID" VersionGoFieldName = "Version" + MetaGoFieldName = "Meta" ) type ORef struct { - OType string `json:"otype"` - OID string `json:"oid"` + OType string `json:"otype" mapstructure:"otype"` + OID string `json:"oid" mapstructure:"oid"` +} + +func (oref ORef) String() string { + return fmt.Sprintf("%s:%s", oref.OType, oref.OID) } type WaveObj interface { @@ -36,6 +40,7 @@ type waveObjDesc struct { RType reflect.Type OIDField reflect.StructField VersionField reflect.StructField + MetaField reflect.StructField } var waveObjMap = sync.Map{} @@ -73,6 +78,15 @@ func RegisterType(rtype reflect.Type) { if versionField.Tag.Get("json") != VersionKeyName { panic(fmt.Sprintf("Version field json tag must be %q for %v", VersionKeyName, rtype)) } + metaField, found := rtype.Elem().FieldByName(MetaGoFieldName) + if !found { + panic(fmt.Sprintf("missing Meta field for %v", rtype)) + } + if metaField.Type.Kind() != reflect.Map || + metaField.Type.Elem().Kind() != reflect.Interface || + metaField.Type.Key().Kind() != reflect.String { + panic(fmt.Sprintf("Meta field must be map[string]any for %v", rtype)) + } _, found = waveObjMap.Load(otype) if found { panic(fmt.Sprintf("otype %q already registered", otype)) @@ -81,6 +95,7 @@ func RegisterType(rtype reflect.Type) { RType: rtype, OIDField: oidField, VersionField: versionField, + MetaField: metaField, }) } @@ -124,6 +139,22 @@ func SetVersion(waveObj WaveObj, version int) { reflect.ValueOf(waveObj).Elem().FieldByIndex(desc.VersionField.Index).SetInt(int64(version)) } +func GetMeta(waveObj WaveObj) map[string]any { + desc := getWaveObjDesc(waveObj.GetOType()) + if desc == nil { + return nil + } + return reflect.ValueOf(waveObj).Elem().FieldByIndex(desc.MetaField.Index).Interface().(map[string]any) +} + +func SetMeta(waveObj WaveObj, meta map[string]any) { + desc := getWaveObjDesc(waveObj.GetOType()) + if desc == nil { + return + } + reflect.ValueOf(waveObj).Elem().FieldByIndex(desc.MetaField.Index).Set(reflect.ValueOf(meta)) +} + func ToJsonMap(w WaveObj) (map[string]any, error) { m := make(map[string]any) dconfig := &mapstructure.DecoderConfig{ @@ -158,6 +189,10 @@ func FromJson(data []byte) (WaveObj, error) { if err != nil { return nil, err } + return FromJsonMap(m) +} + +func FromJsonMap(m map[string]any) (WaveObj, error) { otype, ok := m[OTypeKeyName].(string) if !ok { return nil, fmt.Errorf("missing otype") @@ -182,6 +217,16 @@ func FromJson(data []byte) (WaveObj, error) { return wobj, nil } +func ORefFromMap(m map[string]any) (*ORef, error) { + oref := ORef{} + err := mapstructure.Decode(m, &oref) + if err != nil { + return nil, err + } + return &oref, nil + +} + func FromJsonGen[T WaveObj](data []byte) (T, error) { obj, err := FromJson(data) if err != nil { @@ -195,152 +240,3 @@ func FromJsonGen[T WaveObj](data []byte) (T, error) { } return rtn, nil } - -func getTSFieldName(field reflect.StructField) string { - jsonTag := field.Tag.Get("json") - if jsonTag != "" { - parts := strings.Split(jsonTag, ",") - namePart := parts[0] - if namePart != "" { - if namePart == "-" { - return "" - } - return namePart - } - // if namePart is empty, still uses default - } - return field.Name -} - -func isFieldOmitEmpty(field reflect.StructField) bool { - jsonTag := field.Tag.Get("json") - if jsonTag != "" { - parts := strings.Split(jsonTag, ",") - if len(parts) > 1 { - for _, part := range parts[1:] { - if part == "omitempty" { - return true - } - } - } - } - return false -} - -func typeToTSType(t reflect.Type) (string, []reflect.Type) { - switch t.Kind() { - case reflect.String: - return "string", nil - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, - reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, - reflect.Float32, reflect.Float64: - return "number", nil - case reflect.Bool: - return "boolean", nil - case reflect.Slice, reflect.Array: - elemType, subTypes := typeToTSType(t.Elem()) - if elemType == "" { - return "", nil - } - return fmt.Sprintf("%s[]", elemType), subTypes - case reflect.Map: - if t.Key().Kind() != reflect.String { - return "", nil - } - elemType, subTypes := typeToTSType(t.Elem()) - if elemType == "" { - return "", nil - } - return fmt.Sprintf("{[key: string]: %s}", elemType), subTypes - case reflect.Struct: - return t.Name(), []reflect.Type{t} - case reflect.Ptr: - return typeToTSType(t.Elem()) - case reflect.Interface: - return "any", nil - default: - return "", nil - } -} - -var tsRenameMap = map[string]string{ - "Window": "WaveWindow", -} - -func generateTSTypeInternal(rtype reflect.Type) (string, []reflect.Type) { - var buf bytes.Buffer - waveObjType := reflect.TypeOf((*WaveObj)(nil)).Elem() - tsTypeName := rtype.Name() - if tsRename, ok := tsRenameMap[tsTypeName]; ok { - tsTypeName = tsRename - } - var isWaveObj bool - if rtype.Implements(waveObjType) || reflect.PointerTo(rtype).Implements(waveObjType) { - isWaveObj = true - buf.WriteString(fmt.Sprintf("type %s = WaveObj & {\n", tsTypeName)) - } else { - buf.WriteString(fmt.Sprintf("type %s = {\n", tsTypeName)) - } - var subTypes []reflect.Type - for i := 0; i < rtype.NumField(); i++ { - field := rtype.Field(i) - if field.PkgPath != "" { - continue - } - fieldName := getTSFieldName(field) - if fieldName == "" { - continue - } - if isWaveObj && (fieldName == OTypeKeyName || fieldName == OIDKeyName || fieldName == VersionKeyName) { - continue - } - optMarker := "" - if isFieldOmitEmpty(field) { - optMarker = "?" - } - tsTypeTag := field.Tag.Get("tstype") - if tsTypeTag != "" { - buf.WriteString(fmt.Sprintf(" %s%s: %s;\n", fieldName, optMarker, tsTypeTag)) - continue - } - tsType, fieldSubTypes := typeToTSType(field.Type) - if tsType == "" { - continue - } - subTypes = append(subTypes, fieldSubTypes...) - buf.WriteString(fmt.Sprintf(" %s%s: %s;\n", fieldName, optMarker, tsType)) - } - buf.WriteString("};\n") - return buf.String(), subTypes -} - -func GenerateWaveObjTSType() string { - var buf bytes.Buffer - buf.WriteString("type WaveObj = {\n") - buf.WriteString(" otype: string;\n") - buf.WriteString(" oid: string;\n") - buf.WriteString(" version: number;\n") - buf.WriteString("};\n") - return buf.String() -} - -func GenerateTSType(rtype reflect.Type, tsTypesMap map[reflect.Type]string) { - if rtype == nil { - return - } - if rtype.Kind() == reflect.Ptr { - rtype = rtype.Elem() - } - if _, ok := tsTypesMap[rtype]; ok { - return - } - if rtype == waveObjRType { - tsTypesMap[rtype] = GenerateWaveObjTSType() - return - } - tsType, subTypes := generateTSTypeInternal(rtype) - tsTypesMap[rtype] = tsType - for _, subType := range subTypes { - GenerateTSType(subType, tsTypesMap) - } -} diff --git a/pkg/waveobj/waveobj_gen.go b/pkg/waveobj/waveobj_gen.go new file mode 100644 index 00000000..a27bf37c --- /dev/null +++ b/pkg/waveobj/waveobj_gen.go @@ -0,0 +1,160 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package waveobj + +import ( + "bytes" + "fmt" + "reflect" + "strings" +) + +func getTSFieldName(field reflect.StructField) string { + jsonTag := field.Tag.Get("json") + if jsonTag != "" { + parts := strings.Split(jsonTag, ",") + namePart := parts[0] + if namePart != "" { + if namePart == "-" { + return "" + } + return namePart + } + // if namePart is empty, still uses default + } + return field.Name +} + +func isFieldOmitEmpty(field reflect.StructField) bool { + jsonTag := field.Tag.Get("json") + if jsonTag != "" { + parts := strings.Split(jsonTag, ",") + if len(parts) > 1 { + for _, part := range parts[1:] { + if part == "omitempty" { + return true + } + } + } + } + return false +} + +func typeToTSType(t reflect.Type) (string, []reflect.Type) { + switch t.Kind() { + case reflect.String: + return "string", nil + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64: + return "number", nil + case reflect.Bool: + return "boolean", nil + case reflect.Slice, reflect.Array: + elemType, subTypes := typeToTSType(t.Elem()) + if elemType == "" { + return "", nil + } + return fmt.Sprintf("%s[]", elemType), subTypes + case reflect.Map: + if t.Key().Kind() != reflect.String { + return "", nil + } + elemType, subTypes := typeToTSType(t.Elem()) + if elemType == "" { + return "", nil + } + return fmt.Sprintf("{[key: string]: %s}", elemType), subTypes + case reflect.Struct: + return t.Name(), []reflect.Type{t} + case reflect.Ptr: + return typeToTSType(t.Elem()) + case reflect.Interface: + return "any", nil + default: + return "", nil + } +} + +var tsRenameMap = map[string]string{ + "Window": "WaveWindow", +} + +func generateTSTypeInternal(rtype reflect.Type) (string, []reflect.Type) { + var buf bytes.Buffer + waveObjType := reflect.TypeOf((*WaveObj)(nil)).Elem() + tsTypeName := rtype.Name() + if tsRename, ok := tsRenameMap[tsTypeName]; ok { + tsTypeName = tsRename + } + var isWaveObj bool + if rtype.Implements(waveObjType) || reflect.PointerTo(rtype).Implements(waveObjType) { + isWaveObj = true + buf.WriteString(fmt.Sprintf("type %s = WaveObj & {\n", tsTypeName)) + } else { + buf.WriteString(fmt.Sprintf("type %s = {\n", tsTypeName)) + } + var subTypes []reflect.Type + for i := 0; i < rtype.NumField(); i++ { + field := rtype.Field(i) + if field.PkgPath != "" { + continue + } + fieldName := getTSFieldName(field) + if fieldName == "" { + continue + } + if isWaveObj && (fieldName == OTypeKeyName || fieldName == OIDKeyName || fieldName == VersionKeyName) { + continue + } + optMarker := "" + if isFieldOmitEmpty(field) { + optMarker = "?" + } + tsTypeTag := field.Tag.Get("tstype") + if tsTypeTag != "" { + buf.WriteString(fmt.Sprintf(" %s%s: %s;\n", fieldName, optMarker, tsTypeTag)) + continue + } + tsType, fieldSubTypes := typeToTSType(field.Type) + if tsType == "" { + continue + } + subTypes = append(subTypes, fieldSubTypes...) + buf.WriteString(fmt.Sprintf(" %s%s: %s;\n", fieldName, optMarker, tsType)) + } + buf.WriteString("};\n") + return buf.String(), subTypes +} + +func GenerateWaveObjTSType() string { + var buf bytes.Buffer + buf.WriteString("type WaveObj = {\n") + buf.WriteString(" otype: string;\n") + buf.WriteString(" oid: string;\n") + buf.WriteString(" version: number;\n") + buf.WriteString("};\n") + return buf.String() +} + +func GenerateTSType(rtype reflect.Type, tsTypesMap map[reflect.Type]string) { + if rtype == nil { + return + } + if rtype.Kind() == reflect.Ptr { + rtype = rtype.Elem() + } + if _, ok := tsTypesMap[rtype]; ok { + return + } + if rtype == waveObjRType { + tsTypesMap[rtype] = GenerateWaveObjTSType() + return + } + tsType, subTypes := generateTSTypeInternal(rtype) + tsTypesMap[rtype] = tsType + for _, subType := range subTypes { + GenerateTSType(subType, tsTypesMap) + } +} diff --git a/pkg/wstore/wstore.go b/pkg/wstore/wstore.go index 38a56211..62c38a42 100644 --- a/pkg/wstore/wstore.go +++ b/pkg/wstore/wstore.go @@ -6,14 +6,11 @@ package wstore import ( "bytes" "context" - "encoding/json" "fmt" "log" - "reflect" "time" "github.com/google/uuid" - "github.com/wavetermdev/thenextwave/pkg/shellexec" "github.com/wavetermdev/thenextwave/pkg/waveobj" ) @@ -137,146 +134,6 @@ func ContextUpdatesRollbackTx(ctx context.Context) { updates.UpdatesStack = updates.UpdatesStack[:len(updates.UpdatesStack)-1] } -type WaveObjTombstone struct { - OType string `json:"otype"` - OID string `json:"oid"` -} - -const ( - UpdateType_Update = "update" - UpdateType_Delete = "delete" -) - -type WaveObjUpdate struct { - UpdateType string `json:"updatetype"` - OType string `json:"otype"` - OID string `json:"oid"` - Obj waveobj.WaveObj `json:"obj,omitempty"` -} - -func (update WaveObjUpdate) MarshalJSON() ([]byte, error) { - rtn := make(map[string]any) - rtn["updatetype"] = update.UpdateType - rtn["otype"] = update.OType - rtn["oid"] = update.OID - if update.Obj != nil { - var err error - rtn["obj"], err = waveobj.ToJsonMap(update.Obj) - if err != nil { - return nil, err - } - } - return json.Marshal(rtn) -} - -type UIContext struct { - WindowId string `json:"windowid"` - ActiveTabId string `json:"activetabid"` -} - -type Client struct { - OID string `json:"oid"` - Version int `json:"version"` - MainWindowId string `json:"mainwindowid"` -} - -func (*Client) GetOType() string { - return "client" -} - -func AllWaveObjTypes() []reflect.Type { - return []reflect.Type{ - reflect.TypeOf(&Client{}), - reflect.TypeOf(&Window{}), - reflect.TypeOf(&Workspace{}), - reflect.TypeOf(&Tab{}), - reflect.TypeOf(&Block{}), - } -} - -// stores the ui-context of the window -// workspaceid, active tab, active block within each tab, window size, etc. -type Window struct { - OID string `json:"oid"` - Version int `json:"version"` - WorkspaceId string `json:"workspaceid"` - ActiveTabId string `json:"activetabid"` - ActiveBlockMap map[string]string `json:"activeblockmap"` // map from tabid to blockid - Pos Point `json:"pos"` - WinSize WinSize `json:"winsize"` - LastFocusTs int64 `json:"lastfocusts"` -} - -func (*Window) GetOType() string { - return "window" -} - -type Workspace struct { - OID string `json:"oid"` - Version int `json:"version"` - Name string `json:"name"` - TabIds []string `json:"tabids"` -} - -func (*Workspace) GetOType() string { - return "workspace" -} - -type Tab struct { - OID string `json:"oid"` - Version int `json:"version"` - Name string `json:"name"` - BlockIds []string `json:"blockids"` -} - -func (*Tab) GetOType() string { - return "tab" -} - -type FileDef struct { - FileType string `json:"filetype,omitempty"` - Path string `json:"path,omitempty"` - Url string `json:"url,omitempty"` - Content string `json:"content,omitempty"` - Meta map[string]any `json:"meta,omitempty"` -} - -type BlockDef struct { - Controller string `json:"controller,omitempty"` - View string `json:"view,omitempty"` - Files map[string]*FileDef `json:"files,omitempty"` - Meta map[string]any `json:"meta,omitempty"` -} - -type RuntimeOpts struct { - TermSize shellexec.TermSize `json:"termsize,omitempty"` - WinSize WinSize `json:"winsize,omitempty"` -} - -type Point struct { - X int `json:"x"` - Y int `json:"y"` -} - -type WinSize struct { - Width int `json:"width"` - Height int `json:"height"` -} - -type Block struct { - OID string `json:"oid"` - Version int `json:"version"` - BlockDef *BlockDef `json:"blockdef"` - Controller string `json:"controller"` - View string `json:"view"` - Meta map[string]any `json:"meta,omitempty"` - RuntimeOpts *RuntimeOpts `json:"runtimeopts,omitempty"` -} - -func (*Block) GetOType() string { - return "block" -} - func CreateTab(ctx context.Context, workspaceId string, name string) (*Tab, error) { return WithTxRtn(ctx, func(tx *TxWrap) (*Tab, error) { ws, _ := DBGet[*Workspace](tx.Context(), workspaceId) @@ -395,20 +252,37 @@ func CloseTab(ctx context.Context, workspaceId string, tabId string) error { }) } -func UpdateBlockMeta(ctx context.Context, blockId string, meta map[string]any) error { +func UpdateMeta(ctx context.Context, oref waveobj.ORef, meta map[string]any) error { return WithTx(ctx, func(tx *TxWrap) error { - block, _ := DBGet[*Block](tx.Context(), blockId) - if block == nil { - return fmt.Errorf("block not found: %q", blockId) + obj, _ := DBGetORef(tx.Context(), oref) + if obj == nil { + return fmt.Errorf("object not found: %q", oref) + } + // obj.SetMeta(meta) + DBUpdate(tx.Context(), obj) + return nil + }) +} + +func UpdateObjectMeta(ctx context.Context, oref waveobj.ORef, meta map[string]any) error { + return WithTx(ctx, func(tx *TxWrap) error { + obj, _ := DBGetORef(tx.Context(), oref) + if obj == nil { + return fmt.Errorf("object not found: %q", oref) + } + objMeta := waveobj.GetMeta(obj) + if objMeta == nil { + objMeta = make(map[string]any) } for k, v := range meta { if v == nil { - delete(block.Meta, k) + delete(objMeta, k) continue } - block.Meta[k] = v + objMeta[k] = v } - DBUpdate(tx.Context(), block) + waveobj.SetMeta(obj, objMeta) + DBUpdate(tx.Context(), obj) return nil }) } diff --git a/pkg/wstore/wstore_dbops.go b/pkg/wstore/wstore_dbops.go index e912fe8d..5a33ce80 100644 --- a/pkg/wstore/wstore_dbops.go +++ b/pkg/wstore/wstore_dbops.go @@ -72,6 +72,14 @@ func DBGetSingletonByType(ctx context.Context, otype string) (waveobj.WaveObj, e }) } +func DBExistsORef(ctx context.Context, oref waveobj.ORef) (bool, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (bool, error) { + table := tableNameFromOType(oref.OType) + query := fmt.Sprintf("SELECT oid FROM %s WHERE oid = ?", table) + return tx.Exists(query, oref.OID), nil + }) +} + func DBGet[T waveobj.WaveObj](ctx context.Context, id string) (T, error) { rtn, err := DBGetORef(ctx, waveobj.ORef{OType: getOTypeGen[T](), OID: id}) return genericCastWithErr[T](rtn, err) diff --git a/pkg/wstore/wstore_types.go b/pkg/wstore/wstore_types.go new file mode 100644 index 00000000..18b4f1f8 --- /dev/null +++ b/pkg/wstore/wstore_types.go @@ -0,0 +1,151 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package wstore + +import ( + "encoding/json" + "reflect" + + "github.com/wavetermdev/thenextwave/pkg/shellexec" + "github.com/wavetermdev/thenextwave/pkg/waveobj" +) + +type UIContext struct { + WindowId string `json:"windowid"` + ActiveTabId string `json:"activetabid"` +} + +const ( + UpdateType_Update = "update" + UpdateType_Delete = "delete" +) + +type WaveObjUpdate struct { + UpdateType string `json:"updatetype"` + OType string `json:"otype"` + OID string `json:"oid"` + Obj waveobj.WaveObj `json:"obj,omitempty"` +} + +func (update WaveObjUpdate) MarshalJSON() ([]byte, error) { + rtn := make(map[string]any) + rtn["updatetype"] = update.UpdateType + rtn["otype"] = update.OType + rtn["oid"] = update.OID + if update.Obj != nil { + var err error + rtn["obj"], err = waveobj.ToJsonMap(update.Obj) + if err != nil { + return nil, err + } + } + return json.Marshal(rtn) +} + +type Client struct { + OID string `json:"oid"` + Version int `json:"version"` + MainWindowId string `json:"mainwindowid"` + Meta map[string]any `json:"meta"` +} + +func (*Client) GetOType() string { + return "client" +} + +// stores the ui-context of the window +// workspaceid, active tab, active block within each tab, window size, etc. +type Window struct { + OID string `json:"oid"` + Version int `json:"version"` + WorkspaceId string `json:"workspaceid"` + ActiveTabId string `json:"activetabid"` + ActiveBlockMap map[string]string `json:"activeblockmap"` // map from tabid to blockid + Pos Point `json:"pos"` + WinSize WinSize `json:"winsize"` + LastFocusTs int64 `json:"lastfocusts"` + Meta map[string]any `json:"meta"` +} + +func (*Window) GetOType() string { + return "window" +} + +type Workspace struct { + OID string `json:"oid"` + Version int `json:"version"` + Name string `json:"name"` + TabIds []string `json:"tabids"` + Meta map[string]any `json:"meta"` +} + +func (*Workspace) GetOType() string { + return "workspace" +} + +type Tab struct { + OID string `json:"oid"` + Version int `json:"version"` + Name string `json:"name"` + BlockIds []string `json:"blockids"` + Meta map[string]any `json:"meta"` +} + +func (*Tab) GetOType() string { + return "tab" +} + +type FileDef struct { + FileType string `json:"filetype,omitempty"` + Path string `json:"path,omitempty"` + Url string `json:"url,omitempty"` + Content string `json:"content,omitempty"` + Meta map[string]any `json:"meta,omitempty"` +} + +type BlockDef struct { + Controller string `json:"controller,omitempty"` + View string `json:"view,omitempty"` + Files map[string]*FileDef `json:"files,omitempty"` + Meta map[string]any `json:"meta,omitempty"` +} + +type RuntimeOpts struct { + TermSize shellexec.TermSize `json:"termsize,omitempty"` + WinSize WinSize `json:"winsize,omitempty"` +} + +type Point struct { + X int `json:"x"` + Y int `json:"y"` +} + +type WinSize struct { + Width int `json:"width"` + Height int `json:"height"` +} + +type Block struct { + OID string `json:"oid"` + Version int `json:"version"` + BlockDef *BlockDef `json:"blockdef"` + Controller string `json:"controller"` + View string `json:"view"` + RuntimeOpts *RuntimeOpts `json:"runtimeopts,omitempty"` + Meta map[string]any `json:"meta"` +} + +func (*Block) GetOType() string { + return "block" +} + +func AllWaveObjTypes() []reflect.Type { + return []reflect.Type{ + reflect.TypeOf(&Client{}), + reflect.TypeOf(&Window{}), + reflect.TypeOf(&Workspace{}), + reflect.TypeOf(&Tab{}), + reflect.TypeOf(&Block{}), + } +}