package share import ( "time" "forge.cadoles.com/arcad/edge/pkg/app" "forge.cadoles.com/arcad/edge/pkg/module/util" "github.com/dop251/goja" "github.com/pkg/errors" ) const ( AnyType ValueType = "*" AnyName string = "*" ) type Module struct { appID app.ID repository Repository } 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", TypeText); err != nil { panic(errors.Wrap(err, "could not set 'TYPE_TEXT' property")) } if err := export.Set("TYPE_NUMBER", TypeNumber); err != nil { panic(errors.Wrap(err, "could not set 'TYPE_NUMBER' property")) } if err := export.Set("TYPE_BOOL", TypeBool); err != nil { panic(errors.Wrap(err, "could not set 'TYPE_BOOL' property")) } if err := export.Set("TYPE_PATH", 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 []Attribute if len(call.Arguments) > 2 { attributes = assertAttributes(call.Arguments[2:], rt) } else { attributes = make([]Attribute, 0) } for _, attr := range attributes { if err := AssertType(attr.Value(), attr.Type()); err != nil { panic(rt.ToValue(errors.WithStack(err))) } } resource, err := m.repository.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.repository.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([]FindResourcesOptionFunc, 0) if len(call.Arguments) > 1 { name := util.AssertString(call.Argument(1), rt) if name != AnyName { funcs = append(funcs, WithName(name)) } } if len(call.Arguments) > 2 { valueType := assertValueType(call.Argument(2), rt) if valueType != AnyType { funcs = append(funcs, WithType(valueType)) } } resources, err := m.repository.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.repository.DeleteResource(ctx, m.appID, resourceID) if err != nil { panic(rt.ToValue(errors.WithStack(err))) } return nil } func ModuleFactory(appID app.ID, repository Repository) app.ServerModuleFactory { return func(server *app.Server) app.ServerModule { return &Module{ appID: appID, repository: repository, } } } func assertResourceID(v goja.Value, r *goja.Runtime) ResourceID { value := v.Export() switch typ := value.(type) { case string: return ResourceID(typ) case 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) []Attribute { attributes := make([]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 ValueType switch typ := rawType.(type) { case ValueType: valueType = typ case string: valueType = 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] = 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) ValueType { value := v.Export() switch typ := value.(type) { case string: return ValueType(typ) case ValueType: return typ default: panic(r.ToValue(errors.Errorf("expected value to be a string or ValueType, got '%T'", value))) } } type gojaResource struct { ID 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 := 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 := 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 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 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 []Resource) []*gojaResource { gojaResources := make([]*gojaResource, len(resources)) for idx, res := range resources { gojaResources[idx] = toGojaResource(res) } return gojaResources } func toResource(res *gojaResource) Resource { return NewBaseResource( res.Origin, res.ID, toAttributes(res.Attributes)..., ) } func toAttributes(gojaAttributes []*gojaAttribute) []Attribute { attributes := make([]Attribute, len(gojaAttributes)) for idx, gojaAttr := range gojaAttributes { attr := NewBaseAttribute( gojaAttr.Name, gojaAttr.Type, gojaAttr.Value, ) attr.SetCreatedAt(gojaAttr.CreatedAt) attr.SetUpdatedAt(gojaAttr.UpdatedAt) attributes[idx] = attr } return attributes }