package share import ( "time" "forge.cadoles.com/arcad/edge/pkg/app" "forge.cadoles.com/arcad/edge/pkg/module/util" "forge.cadoles.com/arcad/edge/pkg/storage/share" "github.com/dop251/goja" "github.com/pkg/errors" ) const ( AnyType share.ValueType = "*" AnyName string = "*" ) type Module struct { appID app.ID store share.Store } func (m *Module) Name() string { return "share" } func (m *Module) Export(export *goja.Object) { if err := export.Set("upsertResource", m.upsertResource); err != nil { panic(errors.Wrap(err, "could not set 'upsertResource' function")) } if err := export.Set("findResources", m.findResources); err != nil { panic(errors.Wrap(err, "could not set 'findResources' function")) } if err := export.Set("deleteAttributes", m.deleteAttributes); err != nil { panic(errors.Wrap(err, "could not set 'deleteAttributes' function")) } if err := export.Set("deleteResource", m.deleteResource); err != nil { panic(errors.Wrap(err, "could not set 'deleteResource' function")) } if err := export.Set("ANY_TYPE", AnyType); err != nil { panic(errors.Wrap(err, "could not set 'ANY_TYPE' property")) } if err := export.Set("ANY_NAME", AnyName); err != nil { panic(errors.Wrap(err, "could not set 'ANY_NAME' property")) } if err := export.Set("TYPE_TEXT", share.TypeText); err != nil { panic(errors.Wrap(err, "could not set 'TYPE_TEXT' property")) } if err := export.Set("TYPE_NUMBER", share.TypeNumber); err != nil { panic(errors.Wrap(err, "could not set 'TYPE_NUMBER' property")) } if err := export.Set("TYPE_BOOL", share.TypeBool); err != nil { panic(errors.Wrap(err, "could not set 'TYPE_BOOL' property")) } if err := export.Set("TYPE_PATH", share.TypePath); err != nil { panic(errors.Wrap(err, "could not set 'TYPE_PATH' property")) } } func (m *Module) upsertResource(call goja.FunctionCall, rt *goja.Runtime) goja.Value { ctx := util.AssertContext(call.Argument(0), rt) resourceID := assertResourceID(call.Argument(1), rt) var attributes []share.Attribute if len(call.Arguments) > 2 { attributes = assertAttributes(call.Arguments[2:], rt) } else { attributes = make([]share.Attribute, 0) } for _, attr := range attributes { if err := share.AssertType(attr.Value(), attr.Type()); err != nil { panic(rt.ToValue(errors.WithStack(err))) } } resource, err := m.store.UpdateAttributes(ctx, m.appID, resourceID, attributes...) if err != nil { panic(rt.ToValue(errors.WithStack(err))) } return rt.ToValue(toGojaResource(resource)) } func (m *Module) deleteAttributes(call goja.FunctionCall, rt *goja.Runtime) goja.Value { ctx := util.AssertContext(call.Argument(0), rt) resourceID := assertResourceID(call.Argument(1), rt) var names []string if len(call.Arguments) > 2 { names = assertStrings(call.Arguments[2:], rt) } else { names = make([]string, 0) } err := m.store.DeleteAttributes(ctx, m.appID, resourceID, names...) if err != nil { panic(rt.ToValue(errors.WithStack(err))) } return nil } func (m *Module) findResources(call goja.FunctionCall, rt *goja.Runtime) goja.Value { ctx := util.AssertContext(call.Argument(0), rt) funcs := make([]share.FindResourcesOptionFunc, 0) if len(call.Arguments) > 1 { name := util.AssertString(call.Argument(1), rt) if name != AnyName { funcs = append(funcs, share.WithName(name)) } } if len(call.Arguments) > 2 { valueType := assertValueType(call.Argument(2), rt) if valueType != AnyType { funcs = append(funcs, share.WithType(valueType)) } } resources, err := m.store.FindResources(ctx, funcs...) if err != nil { panic(rt.ToValue(errors.WithStack(err))) } return rt.ToValue(toGojaResources(resources)) } func (m *Module) deleteResource(call goja.FunctionCall, rt *goja.Runtime) goja.Value { ctx := util.AssertContext(call.Argument(0), rt) resourceID := assertResourceID(call.Argument(1), rt) err := m.store.DeleteResource(ctx, m.appID, resourceID) if err != nil { panic(rt.ToValue(errors.WithStack(err))) } return nil } func ModuleFactory(appID app.ID, store share.Store) app.ServerModuleFactory { return func(server *app.Server) app.ServerModule { return &Module{ appID: appID, store: store, } } } func assertResourceID(v goja.Value, r *goja.Runtime) share.ResourceID { value := v.Export() switch typ := value.(type) { case string: return share.ResourceID(typ) case share.ResourceID: return typ default: panic(r.ToValue(errors.Errorf("expected value to be a string or ResourceID, got '%T'", value))) } } func assertAttributes(values []goja.Value, r *goja.Runtime) []share.Attribute { attributes := make([]share.Attribute, len(values)) for idx, val := range values { export := val.Export() rawAttr, ok := export.(map[string]any) if !ok { panic(r.ToValue(errors.Errorf("unexpected attribute value, got '%v'", export))) } rawName, exists := rawAttr["name"] if !exists { panic(r.ToValue(errors.Errorf("could not find 'name' property on attribute '%v'", export))) } name, ok := rawName.(string) if !ok { panic(r.ToValue(errors.Errorf("unexpected value for attribute property 'name': expected 'string', got '%T'", rawName))) } rawType, exists := rawAttr["type"] if !exists { panic(r.ToValue(errors.Errorf("could not find 'type' property on attribute '%v'", export))) } var valueType share.ValueType switch typ := rawType.(type) { case share.ValueType: valueType = typ case string: valueType = share.ValueType(typ) default: panic(r.ToValue(errors.Errorf("unexpected value for attribute property 'type': expected 'string' or 'ValueType', got '%T'", rawType))) } value, exists := rawAttr["value"] if !exists { panic(r.ToValue(errors.Errorf("could not find 'value' property on attribute '%v'", export))) } attributes[idx] = share.NewBaseAttribute( name, valueType, value, ) } return attributes } func assertStrings(values []goja.Value, r *goja.Runtime) []string { strings := make([]string, len(values)) for idx, v := range values { strings[idx] = util.AssertString(v, r) } return strings } func assertValueType(v goja.Value, r *goja.Runtime) share.ValueType { value := v.Export() switch typ := value.(type) { case string: return share.ValueType(typ) case share.ValueType: return typ default: panic(r.ToValue(errors.Errorf("expected value to be a string or ValueType, got '%T'", value))) } } type gojaResource struct { ID share.ResourceID `goja:"id" json:"id"` Origin app.ID `goja:"origin" json:"origin"` Attributes []*gojaAttribute `goja:"attributes" json:"attributes"` } func (r *gojaResource) Has(call goja.FunctionCall, rt *goja.Runtime) goja.Value { name := util.AssertString(call.Argument(0), rt) valueType := assertValueType(call.Argument(1), rt) hasAttr := share.HasAttribute(toResource(r), name, valueType) return rt.ToValue(hasAttr) } func (r *gojaResource) Get(call goja.FunctionCall, rt *goja.Runtime) goja.Value { name := util.AssertString(call.Argument(0), rt) valueType := assertValueType(call.Argument(1), rt) var defaultValue any if len(call.Arguments) > 2 { defaultValue = call.Argument(2).Export() } attr := share.GetAttribute(toResource(r), name, valueType) if attr == nil { return rt.ToValue(defaultValue) } return rt.ToValue(attr.Value()) } type gojaAttribute struct { Name string `goja:"name" json:"name"` Type share.ValueType `goja:"type" json:"type"` Value any `goja:"value" json:"value"` CreatedAt time.Time `goja:"createdAt" json:"createdAt"` UpdatedAt time.Time `goja:"updatedAt" json:"updatedAt"` } func toGojaResource(res share.Resource) *gojaResource { attributes := make([]*gojaAttribute, len(res.Attributes())) for idx, attr := range res.Attributes() { attributes[idx] = &gojaAttribute{ Name: attr.Name(), Type: attr.Type(), Value: attr.Value(), CreatedAt: attr.CreatedAt(), UpdatedAt: attr.UpdatedAt(), } } return &gojaResource{ ID: res.ID(), Origin: res.Origin(), Attributes: attributes, } } func toGojaResources(resources []share.Resource) []*gojaResource { gojaResources := make([]*gojaResource, len(resources)) for idx, res := range resources { gojaResources[idx] = toGojaResource(res) } return gojaResources } func toResource(res *gojaResource) share.Resource { return share.NewBaseResource( res.Origin, res.ID, toAttributes(res.Attributes)..., ) } func toAttributes(gojaAttributes []*gojaAttribute) []share.Attribute { attributes := make([]share.Attribute, len(gojaAttributes)) for idx, gojaAttr := range gojaAttributes { attr := share.NewBaseAttribute( gojaAttr.Name, gojaAttr.Type, gojaAttr.Value, ) attr.SetCreatedAt(gojaAttr.CreatedAt) attr.SetUpdatedAt(gojaAttr.UpdatedAt) attributes[idx] = attr } return attributes }