diff --git a/go.mod b/go.mod
index 8cd52d3b3533422183a392bd755566f2090087d5..c1c371718e1900db44be42b040fd4d19424ce424 100644
--- a/go.mod
+++ b/go.mod
@@ -3,9 +3,9 @@ module git.perx.ru/perxis/perxis-go
 go 1.21
 
 require (
-	github.com/antonmedv/expr v1.9.0
 	github.com/avast/retry-go/v4 v4.5.1
 	github.com/bep/gowebp v0.2.0
+	github.com/expr-lang/expr v1.15.8
 	github.com/go-kit/kit v0.13.0
 	github.com/gosimple/slug v1.13.1
 	github.com/hashicorp/go-multierror v1.1.1
diff --git a/go.sum b/go.sum
index fcb538d7446ef905cc38573b2c88d2496b342ac9..4fa995ca097f0a961d0c4fc9d10cbe640e972c00 100644
--- a/go.sum
+++ b/go.sum
@@ -2,9 +2,6 @@ cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiV
 cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
 cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
 cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
-github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
-github.com/antonmedv/expr v1.9.0 h1:j4HI3NHEdgDnN9p6oI6Ndr0G5QryMY0FNxT4ONrFDGU=
-github.com/antonmedv/expr v1.9.0/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmHhwGEk8=
 github.com/avast/retry-go/v4 v4.5.1 h1:AxIx0HGi4VZ3I02jr78j5lZ3M6x1E0Ivxa6b0pUUh7o=
 github.com/avast/retry-go/v4 v4.5.1/go.mod h1:/sipNsvNB3RRuT5iNcb6h73nw3IBmXJ/H3XrCQYSOpc=
 github.com/bep/gowebp v0.2.0 h1:ZVfK8i9PpZqKHEmthQSt3qCnnHycbLzBPEsVtk2ch2Q=
@@ -12,12 +9,11 @@ github.com/bep/gowebp v0.2.0/go.mod h1:ZhFodwdiFp8ehGJpF4LdPl6unxZm9lLFjxD3z2h2A
 github.com/brianvoe/gofakeit/v6 v6.26.3 h1:3ljYrjPwsUNAUFdUIr2jVg5EhKdcke/ZLop7uVg1Er8=
 github.com/brianvoe/gofakeit/v6 v6.26.3/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
-github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
-github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
+github.com/expr-lang/expr v1.15.8 h1:FL8+d3rSSP4tmK9o+vKfSMqqpGL8n15pEPiHcnBpxoI=
+github.com/expr-lang/expr v1.15.8/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ=
 github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU=
 github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg=
 github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
@@ -64,10 +60,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
-github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
-github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
-github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
 github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -87,25 +79,19 @@ github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OS
 github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/rivo/tview v0.0.0-20200219210816-cd38d7432498/go.mod h1:6lkG1x+13OShEf0EaOCaTQYyB7d5nSbb181KtjlS+84=
-github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
 github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
 github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
 github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
 github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
-github.com/sanity-io/litter v1.2.0/go.mod h1:JF6pZUFgu2Q0sBZ+HSV35P8TVPI1TTzEwyu9FXAw2W4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0=
 github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0=
-github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
@@ -160,8 +146,6 @@ golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
 golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -172,7 +156,6 @@ golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@@ -198,7 +181,6 @@ google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/id/bson.go b/id/bson.go
new file mode 100644
index 0000000000000000000000000000000000000000..9d44c5a8074361b381524dd7c99ad77906cfc68c
--- /dev/null
+++ b/id/bson.go
@@ -0,0 +1,32 @@
+package id
+
+import (
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"go.mongodb.org/mongo-driver/bson"
+	"go.mongodb.org/mongo-driver/bson/bsonrw"
+	"go.mongodb.org/mongo-driver/bson/bsontype"
+)
+
+func (id *ID) MarshalBSONValue() (bsontype.Type, []byte, error) {
+	return bson.MarshalValue(id.String())
+}
+
+func (id *ID) UnmarshalBSONValue(btype bsontype.Type, data []byte) error {
+	if btype != bson.TypeString {
+		return errors.New("cannot unmarshal non-string bson value to ID")
+	}
+	dec, err := bson.NewDecoder(bsonrw.NewBSONValueReader(btype, data))
+	if err != nil {
+		return err
+	}
+	var str string
+	if err = dec.Decode(&str); err != nil {
+		return err
+	}
+	t, err := Parse(str)
+	if err != nil {
+		return err
+	}
+	*id = *t
+	return nil
+}
diff --git a/id/bson_test.go b/id/bson_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..c8080b5d7ae12181343a8f6502e76484c414e578
--- /dev/null
+++ b/id/bson_test.go
@@ -0,0 +1,107 @@
+package id
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"go.mongodb.org/mongo-driver/bson"
+)
+
+func TestID_MarshalUnmarshalBSON(t *testing.T) {
+	tests := []struct {
+		name string
+		id   *ID
+	}{
+		{
+			name: "OrganizationID",
+			id:   &ID{Descriptor: &OrganizationID{OrganizationID: "1"}},
+		},
+		{
+			name: "UserID",
+			id:   &ID{Descriptor: &UserID{UserID: "1"}},
+		},
+		{
+			name: "ServiceID",
+			id:   &ID{Descriptor: &ServiceID{ServiceID: "1"}},
+		},
+		{
+			name: "SpaceID",
+			id:   &ID{Descriptor: &SpaceID{SpaceID: "1"}},
+		},
+		{
+			name: "EnvironmentID",
+			id:   &ID{Descriptor: &EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}},
+		},
+		{
+			name: "ClientID",
+			id:   &ID{Descriptor: &ClientID{ClientID: "1", SpaceID: SpaceID{SpaceID: "1"}}},
+		},
+		{
+			name: "RoleID",
+			id:   &ID{Descriptor: &RoleID{RoleID: "1", SpaceID: SpaceID{SpaceID: "1"}}},
+		},
+		{
+			name: "CollectionID",
+			id:   &ID{Descriptor: &CollectionID{CollectionID: "1", EnvironmentID: EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}}},
+		},
+		{
+			name: "SchemaID",
+			id:   &ID{Descriptor: &SchemaID{CollectionID: "1", EnvironmentID: EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}}},
+		},
+		{
+			name: "ItemID",
+			id:   &ID{Descriptor: &ItemID{ItemID: "1", CollectionID: CollectionID{CollectionID: "1", EnvironmentID: EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}}}},
+		},
+		{
+			name: "RevisionID",
+			id:   &ID{Descriptor: &RevisionID{RevisionID: "1", ItemID: ItemID{ItemID: "1", CollectionID: CollectionID{CollectionID: "1", EnvironmentID: EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}}}}},
+		},
+		{
+			name: "FieldID",
+			id:   &ID{Descriptor: &FieldID{FieldName: "1", ItemID: ItemID{ItemID: "1", CollectionID: CollectionID{CollectionID: "1", EnvironmentID: EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}}}}},
+		},
+		{
+			name: "SystemID",
+			id:   &ID{Descriptor: &SystemID{}},
+		},
+	}
+	type test struct {
+		Text string
+		ID   *ID
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			s := &test{Text: tt.name, ID: tt.id}
+
+			b, err := bson.Marshal(s)
+			require.NoError(t, err)
+
+			var v *test
+			require.NoError(t, bson.Unmarshal(b, &v))
+			assert.Equal(t, s, v, "после Unmarshal объект должен быть идентичен исходному")
+		})
+	}
+}
+
+func TestID_ExampleBSON(t *testing.T) {
+	type data struct {
+		ID     *ID
+		Text   string
+		Number int
+	}
+
+	test := &data{
+		ID:     &ID{Descriptor: &SpaceID{SpaceID: Space}},
+		Text:   "text",
+		Number: 1,
+	}
+
+	b, err := bson.Marshal(test)
+	require.NoError(t, err)
+
+	buf := new(data)
+	err = bson.Unmarshal(b, &buf)
+	require.NoError(t, err)
+	assert.Equal(t, test, buf, "после Unmarshal объект должен совпадать с исходным")
+}
diff --git a/id/client.go b/id/client.go
new file mode 100644
index 0000000000000000000000000000000000000000..db941c5ad54b539551193004674ac15f15d314be
--- /dev/null
+++ b/id/client.go
@@ -0,0 +1,61 @@
+package id
+
+const (
+	Client        = "client"
+	ClientsPrefix = "clients"
+)
+
+type ClientID struct {
+	SpaceID
+	ClientID string `json:"client_id,omitempty" bson:"client_id,omitempty"`
+}
+
+func (t *ClientID) Type() string { return Client }
+
+func (t *ClientID) String() string {
+	return Join(t.SpaceID.String(), ClientsPrefix, t.ClientID)
+
+}
+
+func (t *ClientID) ToMap() map[string]any {
+	m := t.SpaceID.ToMap()
+	m["client_id"] = t.ClientID
+	m["type"] = Client
+	return m
+}
+
+func (t *ClientID) FromMap(m map[string]any) error {
+	if err := t.SpaceID.FromMap(m); err != nil {
+		return err
+	}
+	t.ClientID = m["client_id"].(string)
+	return nil
+}
+
+func (t *ClientID) Validate() error {
+	if t.ClientID == "" {
+		return ErrInvalidID
+	}
+
+	return t.SpaceID.Validate()
+}
+
+func parseClientID(parts []string) (*ClientID, error) {
+	if len(parts) != 4 || parts[2] != ClientsPrefix {
+		return nil, ErrInvalidID
+	}
+
+	spaceID, err := parseSpaceID(parts[:2])
+	if err != nil {
+		return nil, err
+	}
+
+	var id ClientID
+	id.SpaceID = *spaceID
+	id.ClientID = parts[3]
+	return &id, nil
+}
+
+func NewClientID(spaceID, id string) *ID {
+	return &ID{Descriptor: &ClientID{SpaceID: SpaceID{SpaceID: spaceID}, ClientID: id}}
+}
diff --git a/id/collection.go b/id/collection.go
new file mode 100644
index 0000000000000000000000000000000000000000..e0f37585b41e77b58530cc50f249283beb6d8eca
--- /dev/null
+++ b/id/collection.go
@@ -0,0 +1,60 @@
+package id
+
+const (
+	Collection        = "collection"
+	CollectionsPrefix = "cols"
+)
+
+type CollectionID struct {
+	EnvironmentID
+	CollectionID string `json:"col_id,omitempty" bson:"col_id, omitempty"`
+}
+
+func (t *CollectionID) Type() string { return Collection }
+
+func (t *CollectionID) String() string {
+	return Join(t.EnvironmentID.String(), CollectionsPrefix, t.CollectionID)
+}
+
+func (t *CollectionID) ToMap() map[string]any {
+	m := t.EnvironmentID.ToMap()
+	m["col_id"] = t.CollectionID
+	m["type"] = Collection
+	return m
+}
+
+func (t *CollectionID) FromMap(m map[string]any) error {
+	if err := t.EnvironmentID.FromMap(m); err != nil {
+		return err
+	}
+	t.CollectionID = m["col_id"].(string)
+	return nil
+}
+
+func (t *CollectionID) Validate() error {
+	if t.CollectionID == "" {
+		return ErrInvalidID
+	}
+
+	return t.EnvironmentID.Validate()
+}
+
+func parseCollectionID(parts []string) (*CollectionID, error) {
+	if len(parts) != 6 || parts[4] != CollectionsPrefix {
+		return nil, ErrInvalidID
+	}
+
+	envID, err := parseEnvironmentID(parts[:4])
+	if err != nil {
+		return nil, err
+	}
+
+	var id CollectionID
+	id.CollectionID = parts[5]
+	id.EnvironmentID = *envID
+	return &id, nil
+}
+
+func NewCollectionID(spaceID, envID, id string) *ID {
+	return &ID{Descriptor: &CollectionID{EnvironmentID: EnvironmentID{SpaceID: SpaceID{SpaceID: spaceID}, EnvironmentID: envID}, CollectionID: id}}
+}
diff --git a/id/environment.go b/id/environment.go
new file mode 100644
index 0000000000000000000000000000000000000000..d42df3e658bc04ae70989d199257a7836bd30f00
--- /dev/null
+++ b/id/environment.go
@@ -0,0 +1,61 @@
+package id
+
+const (
+	Environment        = "environment"
+	EnvironmentsPrefix = "envs"
+)
+
+type EnvironmentID struct {
+	SpaceID
+	EnvironmentID string `json:"env_id,omitempty" bson:"env_id,omitempty"`
+}
+
+func (t *EnvironmentID) Type() string { return Environment }
+
+func (t *EnvironmentID) String() string {
+	return Join(t.SpaceID.String(), EnvironmentsPrefix, t.EnvironmentID)
+
+}
+
+func (t *EnvironmentID) ToMap() map[string]any {
+	m := t.SpaceID.ToMap()
+	m["env_id"] = t.EnvironmentID
+	m["type"] = Environment
+	return m
+}
+
+func (t *EnvironmentID) FromMap(m map[string]any) error {
+	if err := t.SpaceID.FromMap(m); err != nil {
+		return err
+	}
+	t.EnvironmentID = m["env_id"].(string)
+	return nil
+}
+
+func (t *EnvironmentID) Validate() error {
+	if t.EnvironmentID == "" {
+		return ErrInvalidID
+	}
+
+	return t.SpaceID.Validate()
+}
+
+func parseEnvironmentID(parts []string) (*EnvironmentID, error) {
+	if len(parts) != 4 || parts[2] != EnvironmentsPrefix {
+		return nil, ErrInvalidID
+	}
+
+	spaceID, err := parseSpaceID(parts[:2])
+	if err != nil {
+		return nil, err
+	}
+
+	var id EnvironmentID
+	id.EnvironmentID = parts[3]
+	id.SpaceID = *spaceID
+	return &id, nil
+}
+
+func NewEnvironmentID(spaceID, id string) *ID {
+	return &ID{Descriptor: &EnvironmentID{SpaceID: SpaceID{SpaceID: spaceID}, EnvironmentID: id}}
+}
diff --git a/id/field.go b/id/field.go
new file mode 100644
index 0000000000000000000000000000000000000000..ca1577552dc7df38d555549ddfc53a51e70e63f1
--- /dev/null
+++ b/id/field.go
@@ -0,0 +1,60 @@
+package id
+
+const (
+	Field        = "field"
+	FieldsPrefix = "fields"
+)
+
+type FieldID struct {
+	ItemID
+	FieldName string `json:"field_name,omitempty" bson:"field_name,omitempty"`
+}
+
+func (t *FieldID) Type() string { return Field }
+
+func (t *FieldID) String() string {
+	return Join(t.ItemID.String(), FieldsPrefix, t.FieldName)
+
+}
+
+func (t *FieldID) ToMap() map[string]any {
+	m := t.ItemID.ToMap()
+	m["field_name"] = t.FieldName
+	m["type"] = Field
+	return m
+}
+
+func (t *FieldID) FromMap(m map[string]any) error {
+	if err := t.ItemID.FromMap(m); err != nil {
+		return err
+	}
+	t.FieldName = m["field_name"].(string)
+	return nil
+}
+
+func (t *FieldID) Validate() error {
+	if t.FieldName == "" {
+		return ErrInvalidID
+	}
+
+	return t.ItemID.Validate()
+}
+
+func parseFieldID(parts []string) (*FieldID, error) {
+	if len(parts) != 10 || parts[8] != FieldsPrefix {
+		return nil, ErrInvalidID
+	}
+
+	itemID, err := parseItemID(parts[:8])
+	if err != nil {
+		return nil, err
+	}
+
+	var id FieldID
+	id.ItemID = *itemID
+	id.FieldName = parts[9]
+	return &id, nil
+}
+func NewFieldID(spaceID, envID, collID, itemID, id string) *ID {
+	return &ID{Descriptor: &FieldID{ItemID: ItemID{CollectionID: CollectionID{EnvironmentID: EnvironmentID{SpaceID: SpaceID{SpaceID: spaceID}, EnvironmentID: envID}, CollectionID: collID}, ItemID: itemID}, FieldName: id}}
+}
diff --git a/id/id.go b/id/id.go
new file mode 100644
index 0000000000000000000000000000000000000000..ca75c29f94b6fe9100b435e8f546835991bc9fa3
--- /dev/null
+++ b/id/id.go
@@ -0,0 +1,143 @@
+package id
+
+import (
+	"strings"
+
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+)
+
+const Separator = '/'
+
+var (
+	ErrInvalidID = errors.New("invalid id")
+)
+
+type Descriptor interface {
+	String() string
+	Type() string
+	ToMap() map[string]any
+	FromMap(map[string]any) error
+	Validate() error
+}
+
+type ID struct {
+	Descriptor
+}
+
+func Parse(s string) (*ID, error) {
+	parts := Split(s)
+
+	if id, _ := parseServiceID(parts); id != nil {
+		return &ID{Descriptor: id}, nil
+	}
+
+	if id, _ := parseUserID(parts); id != nil {
+		return &ID{Descriptor: id}, nil
+	}
+
+	if id, _ := parseOrganizationID(parts); id != nil {
+		return &ID{Descriptor: id}, nil
+	}
+
+	if id, _ := parseSpaceID(parts); id != nil {
+		return &ID{Descriptor: id}, nil
+	}
+
+	if id, _ := parseEnvironmentID(parts); id != nil {
+		return &ID{Descriptor: id}, nil
+	}
+
+	if id, _ := parseClientID(parts); id != nil {
+		return &ID{Descriptor: id}, nil
+	}
+
+	if id, _ := parseRoleID(parts); id != nil {
+		return &ID{Descriptor: id}, nil
+	}
+
+	if id, _ := parseCollectionID(parts); id != nil {
+		return &ID{Descriptor: id}, nil
+	}
+
+	if id, _ := parseSchemaID(parts); id != nil {
+		return &ID{Descriptor: id}, nil
+	}
+
+	if id, _ := parseItemID(parts); id != nil {
+		return &ID{Descriptor: id}, nil
+	}
+
+	if id, _ := parseRevisionID(parts); id != nil {
+		return &ID{Descriptor: id}, nil
+	}
+
+	if id, _ := parseFieldID(parts); id != nil {
+		return &ID{Descriptor: id}, nil
+	}
+
+	if id, _ := parseSystemID(parts); id != nil {
+		return &ID{Descriptor: id}, nil
+	}
+
+	return nil, ErrInvalidID
+}
+
+func Split(id string) []string {
+	if id[0] != Separator {
+		return nil
+	}
+	return strings.Split(id[1:], string(Separator))
+}
+
+func Join(parts ...string) string {
+	s := strings.Join(parts, string(Separator))
+	if s[0] != Separator {
+		s = string(Separator) + s
+	}
+	return s
+}
+
+func FromMap(m map[string]any) (*ID, error) {
+	if m == nil {
+		return nil, errors.New("nil map")
+	}
+
+	v := new(ID)
+
+	switch m["type"] {
+	case Organization:
+		v.Descriptor = new(OrganizationID)
+	case Service:
+		v.Descriptor = new(ServiceID)
+	case User:
+		v.Descriptor = new(UserID)
+	case Space:
+		v.Descriptor = new(SpaceID)
+	case Environment:
+		v.Descriptor = new(EnvironmentID)
+	case Client:
+		v.Descriptor = new(ClientID)
+	case Role:
+		v.Descriptor = new(RoleID)
+	case Collection:
+		v.Descriptor = new(CollectionID)
+	case Schema:
+		v.Descriptor = new(SchemaID)
+	case Item:
+		v.Descriptor = new(ItemID)
+	case Revision:
+		v.Descriptor = new(RevisionID)
+	case Field:
+		v.Descriptor = new(FieldID)
+	case System:
+		v.Descriptor = new(SystemID)
+	default:
+		return nil, errors.New("unknown type")
+	}
+
+	if err := v.Descriptor.FromMap(m); err != nil {
+		return nil, err
+	}
+
+	return v, nil
+}
diff --git a/id/id_test.go b/id/id_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..041f1cdc6018e6c84c0c4c6a4be4b3944ef34082
--- /dev/null
+++ b/id/id_test.go
@@ -0,0 +1,301 @@
+package id
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func Test_ParseID(t *testing.T) {
+	tests := []struct {
+		name      string
+		id        string
+		result    *ID
+		wantError bool
+	}{
+		{
+			name:   "ServiceID",
+			id:     "/services/<service_id>",
+			result: &ID{Descriptor: &ServiceID{ServiceID: "<service_id>"}},
+		},
+		{
+			name:   "UserID",
+			id:     "/users/<user_id>",
+			result: &ID{Descriptor: &UserID{UserID: "<user_id>"}},
+		},
+		{
+			name:   "OrganizationID",
+			id:     "/orgs/<org_id>",
+			result: &ID{Descriptor: &OrganizationID{OrganizationID: "<org_id>"}},
+		},
+		{
+			name:   "SpaceID",
+			id:     "/spaces/<space_id>",
+			result: &ID{Descriptor: &SpaceID{SpaceID: "<space_id>"}},
+		},
+		{
+			name: "ClientID",
+			id:   "/spaces/<space_id>/clients/<client_id>",
+			result: &ID{Descriptor: &ClientID{
+				SpaceID:  SpaceID{SpaceID: "<space_id>"},
+				ClientID: "<client_id>",
+			}},
+		},
+		{
+			name: "RoleID",
+			id:   "/spaces/<space_id>/roles/<role_id>",
+			result: &ID{Descriptor: &RoleID{
+				SpaceID: SpaceID{SpaceID: "<space_id>"},
+				RoleID:  "<role_id>",
+			}},
+		},
+		{
+			name: "EnvironmentID",
+			id:   "/spaces/<space_id>/envs/<env_id>",
+			result: &ID{Descriptor: &EnvironmentID{
+				SpaceID:       SpaceID{SpaceID: "<space_id>"},
+				EnvironmentID: "<env_id>",
+			}},
+		},
+		{
+			name: "CollectionID",
+			id:   "/spaces/<space_id>/envs/<env_id>/cols/<collection_id>",
+			result: &ID{Descriptor: &CollectionID{
+				EnvironmentID: EnvironmentID{
+					SpaceID:       SpaceID{SpaceID: "<space_id>"},
+					EnvironmentID: "<env_id>",
+				},
+				CollectionID: "<collection_id>",
+			}},
+		},
+		{
+			name: "SchemaID",
+			id:   "/spaces/<space_id>/envs/<env_id>/schema/<collection_id>",
+			result: &ID{Descriptor: &SchemaID{
+				EnvironmentID: EnvironmentID{
+					SpaceID:       SpaceID{SpaceID: "<space_id>"},
+					EnvironmentID: "<env_id>",
+				},
+				CollectionID: "<collection_id>",
+			}},
+		},
+		{
+			name: "ItemID",
+			id:   "/spaces/<space_id>/envs/<env_id>/cols/<collection_id>/items/<item_id>",
+			result: &ID{Descriptor: &ItemID{
+				CollectionID: CollectionID{
+					EnvironmentID: EnvironmentID{
+						SpaceID:       SpaceID{SpaceID: "<space_id>"},
+						EnvironmentID: "<env_id>",
+					},
+					CollectionID: "<collection_id>",
+				},
+				ItemID: "<item_id>",
+			}},
+		},
+		{
+			name: "RevisionID",
+			id:   "/spaces/<space_id>/envs/<env_id>/cols/<collection_id>/items/<item_id>/revs/<rev_id>",
+			result: &ID{Descriptor: &RevisionID{
+				ItemID: ItemID{
+					CollectionID: CollectionID{
+						EnvironmentID: EnvironmentID{
+							SpaceID:       SpaceID{SpaceID: "<space_id>"},
+							EnvironmentID: "<env_id>",
+						},
+						CollectionID: "<collection_id>",
+					},
+					ItemID: "<item_id>",
+				},
+				RevisionID: "<rev_id>",
+			}},
+		},
+		{
+			name: "FieldID",
+			id:   "/spaces/<space_id>/envs/<env_id>/cols/<collection_id>/items/<item_id>/fields/<field_name>",
+			result: &ID{Descriptor: &FieldID{
+				ItemID: ItemID{
+					CollectionID: CollectionID{
+						EnvironmentID: EnvironmentID{
+							SpaceID:       SpaceID{SpaceID: "<space_id>"},
+							EnvironmentID: "<env_id>",
+						},
+						CollectionID: "<collection_id>",
+					},
+					ItemID: "<item_id>",
+				},
+				FieldName: "<field_name>",
+			}},
+		},
+		{
+			name:   "SystemID",
+			id:     "/system",
+			result: &ID{Descriptor: &SystemID{}},
+		},
+		{
+			name:      "With error #1: no backslash in the beginning of id",
+			id:        "spaces/<space_id>",
+			result:    nil,
+			wantError: true,
+		},
+		{
+			name:      "With error #2: backslash in the end of id",
+			id:        "/spaces/<space_id>/",
+			result:    nil,
+			wantError: true,
+		},
+		{
+			name:      "With error #3: typo in 'spaces'",
+			id:        "/space/<space_id>",
+			result:    nil,
+			wantError: true,
+		},
+		{
+			name:      "With error #4: no space_id in id",
+			id:        "/spaces",
+			result:    nil,
+			wantError: true,
+		},
+		{
+			name:      "With error #5: multiple backslashes in the end of id",
+			id:        "/spaces/<space_id>///",
+			result:    nil,
+			wantError: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			id, err := Parse(tt.id)
+			if tt.wantError {
+				require.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+			require.Equal(t, tt.result, id)
+			require.Equal(t, tt.id, id.String(), "проверяем корректность работы метода String, полученное ID должно совпадать с исходным")
+		})
+	}
+}
+
+func Test_Map(t *testing.T) {
+	tests := []struct {
+		name string
+		id   *ID
+	}{
+		{
+			name: "ServiceID",
+			id:   &ID{Descriptor: &ServiceID{ServiceID: "<service_id>"}},
+		},
+		{
+			name: "UserID",
+			id:   &ID{Descriptor: &UserID{UserID: "<user_id>"}},
+		},
+		{
+			name: "OrganizationID",
+			id:   &ID{Descriptor: &OrganizationID{OrganizationID: "<org_id>"}},
+		},
+		{
+			name: "SpaceID",
+			id:   &ID{Descriptor: &SpaceID{SpaceID: "<space_id>"}},
+		},
+		{
+			name: "ClientID",
+			id: &ID{Descriptor: &ClientID{
+				SpaceID:  SpaceID{SpaceID: "<space_id>"},
+				ClientID: "<client_id>",
+			}},
+		},
+		{
+			name: "RoleID",
+			id: &ID{Descriptor: &RoleID{
+				SpaceID: SpaceID{SpaceID: "<space_id>"},
+				RoleID:  "<role_id>",
+			}},
+		},
+		{
+			name: "EnvironmentID",
+			id: &ID{Descriptor: &EnvironmentID{
+				SpaceID:       SpaceID{SpaceID: "<space_id>"},
+				EnvironmentID: "<env_id>",
+			}},
+		},
+		{
+			name: "CollectionID",
+			id: &ID{Descriptor: &CollectionID{
+				EnvironmentID: EnvironmentID{
+					SpaceID:       SpaceID{SpaceID: "<space_id>"},
+					EnvironmentID: "<env_id>",
+				},
+				CollectionID: "<collection_id>",
+			}},
+		},
+		{
+			name: "Schema ID",
+			id: &ID{Descriptor: &SchemaID{
+				EnvironmentID: EnvironmentID{
+					SpaceID:       SpaceID{SpaceID: "<space_id>"},
+					EnvironmentID: "<env_id>",
+				},
+				CollectionID: "<collection_id>",
+			}},
+		},
+		{
+			name: "ItemID",
+			id: &ID{Descriptor: &ItemID{
+				CollectionID: CollectionID{
+					EnvironmentID: EnvironmentID{
+						SpaceID:       SpaceID{SpaceID: "<space_id>"},
+						EnvironmentID: "<env_id>",
+					},
+					CollectionID: "<collection_id>",
+				},
+				ItemID: "<item_id>",
+			}},
+		},
+		{
+			name: "RevisionID",
+			id: &ID{Descriptor: &RevisionID{
+				ItemID: ItemID{
+					CollectionID: CollectionID{
+						EnvironmentID: EnvironmentID{
+							SpaceID:       SpaceID{SpaceID: "<space_id>"},
+							EnvironmentID: "<env_id>",
+						},
+						CollectionID: "<collection_id>",
+					},
+					ItemID: "<item_id>",
+				},
+				RevisionID: "<rev_id>",
+			}},
+		},
+		{
+			name: "FieldID",
+			id: &ID{Descriptor: &FieldID{
+				ItemID: ItemID{
+					CollectionID: CollectionID{
+						EnvironmentID: EnvironmentID{
+							SpaceID:       SpaceID{SpaceID: "<space_id>"},
+							EnvironmentID: "<env_id>",
+						},
+						CollectionID: "<collection_id>",
+					},
+					ItemID: "<item_id>",
+				},
+				FieldName: "<field_name>",
+			}},
+		},
+		{
+			name: "SystemID",
+			id:   &ID{Descriptor: &SystemID{}},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			v, err := FromMap(tt.id.ToMap())
+			require.NoError(t, err)
+			assert.Equal(t, tt.id, v, "проверка FromMap для типа ID, должен быть равен исходному значению")
+			assert.Equal(t, v.ToMap(), tt.id.ToMap())
+		})
+	}
+}
diff --git a/id/item.go b/id/item.go
new file mode 100644
index 0000000000000000000000000000000000000000..70c3e7be95b517a85e07e487eb00f94f9f73e14f
--- /dev/null
+++ b/id/item.go
@@ -0,0 +1,61 @@
+package id
+
+const (
+	Item        = "item"
+	ItemsPrefix = "items"
+)
+
+type ItemID struct {
+	CollectionID
+	ItemID string `json:"item_id,omitempty" bson:"item_id,omitempty"`
+}
+
+func (t *ItemID) Type() string { return Item }
+
+func (t *ItemID) String() string {
+	return Join(t.CollectionID.String(), ItemsPrefix, t.ItemID)
+
+}
+
+func (t *ItemID) ToMap() map[string]any {
+	m := t.CollectionID.ToMap()
+	m["item_id"] = t.ItemID
+	m["type"] = Item
+	return m
+}
+
+func (t *ItemID) FromMap(m map[string]any) error {
+	if err := t.CollectionID.FromMap(m); err != nil {
+		return err
+	}
+	t.ItemID = m["item_id"].(string)
+	return nil
+}
+
+func (t *ItemID) Validate() error {
+	if t.ItemID == "" {
+		return ErrInvalidID
+	}
+
+	return t.CollectionID.Validate()
+}
+
+func parseItemID(parts []string) (*ItemID, error) {
+	if len(parts) != 8 || parts[6] != ItemsPrefix {
+		return nil, ErrInvalidID
+	}
+
+	collID, err := parseCollectionID(parts[:6])
+	if err != nil {
+		return nil, err
+	}
+
+	var id ItemID
+	id.CollectionID = *collID
+	id.ItemID = parts[7]
+	return &id, nil
+}
+
+func NewItemID(spaceID, envID, collID, id string) *ID {
+	return &ID{Descriptor: &ItemID{CollectionID: CollectionID{EnvironmentID: EnvironmentID{SpaceID: SpaceID{SpaceID: spaceID}, EnvironmentID: envID}, CollectionID: collID}, ItemID: id}}
+}
diff --git a/id/json.go b/id/json.go
new file mode 100644
index 0000000000000000000000000000000000000000..34b87589dfd81e6b793c5812fb2b90f43fb724f1
--- /dev/null
+++ b/id/json.go
@@ -0,0 +1,23 @@
+package id
+
+import (
+	jsoniter "github.com/json-iterator/go"
+)
+
+func (id *ID) MarshalJSON() ([]byte, error) {
+	return jsoniter.Marshal(id.String())
+}
+
+func (id *ID) UnmarshalJSON(b []byte) error {
+	var s string
+	var err error
+	if err = jsoniter.Unmarshal(b, &s); err != nil {
+		return err
+	}
+	t, err := Parse(s)
+	if err != nil {
+		return err
+	}
+	*id = *t
+	return nil
+}
diff --git a/id/json_test.go b/id/json_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..afe831d4ef6038eaa6bc8a565b0623334cdf6bc8
--- /dev/null
+++ b/id/json_test.go
@@ -0,0 +1,101 @@
+package id
+
+import (
+	"testing"
+
+	jsoniter "github.com/json-iterator/go"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestID_MarshalUnmarshalJSON(t *testing.T) {
+	tests := []struct {
+		name string
+		id   *ID
+	}{
+		{
+			name: "OrganizationID",
+			id:   &ID{Descriptor: &OrganizationID{OrganizationID: "1"}},
+		},
+		{
+			name: "UserID",
+			id:   &ID{Descriptor: &UserID{UserID: "1"}},
+		},
+		{
+			name: "ServiceID",
+			id:   &ID{Descriptor: &ServiceID{ServiceID: "1"}},
+		},
+		{
+			name: "SpaceID",
+			id:   &ID{Descriptor: &SpaceID{SpaceID: "1"}},
+		},
+		{
+			name: "EnvironmentID",
+			id:   &ID{Descriptor: &EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}},
+		},
+		{
+			name: "ClientID",
+			id:   &ID{Descriptor: &ClientID{ClientID: "1", SpaceID: SpaceID{SpaceID: "1"}}},
+		},
+		{
+			name: "RoleID",
+			id:   &ID{Descriptor: &RoleID{RoleID: "1", SpaceID: SpaceID{SpaceID: "1"}}},
+		},
+		{
+			name: "CollectionID",
+			id:   &ID{Descriptor: &CollectionID{CollectionID: "1", EnvironmentID: EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}}},
+		},
+		{
+			name: "SchemaID",
+			id:   &ID{Descriptor: &SchemaID{CollectionID: "1", EnvironmentID: EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}}},
+		},
+		{
+			name: "ItemID",
+			id:   &ID{Descriptor: &ItemID{ItemID: "1", CollectionID: CollectionID{CollectionID: "1", EnvironmentID: EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}}}},
+		},
+		{
+			name: "RevisionID",
+			id:   &ID{Descriptor: &RevisionID{RevisionID: "1", ItemID: ItemID{ItemID: "1", CollectionID: CollectionID{CollectionID: "1", EnvironmentID: EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}}}}},
+		},
+		{
+			name: "FieldID",
+			id:   &ID{Descriptor: &FieldID{FieldName: "1", ItemID: ItemID{ItemID: "1", CollectionID: CollectionID{CollectionID: "1", EnvironmentID: EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}}}}},
+		},
+		{
+			name: "SystemID",
+			id:   &ID{Descriptor: &SystemID{}},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			b, err := jsoniter.Marshal(&tt.id)
+			require.NoError(t, err)
+
+			var i ID
+			require.NoError(t, jsoniter.Unmarshal(b, &i))
+			assert.Equal(t, tt.id, &i, "после Unmarshal объект должен быть идентичен исходному")
+		})
+	}
+}
+
+func TestID_ExampleJSON(t *testing.T) {
+	type data struct {
+		ID     *ID
+		Text   string
+		Number int
+	}
+
+	test := &data{
+		ID:     &ID{Descriptor: &SpaceID{SpaceID: Space}},
+		Text:   "text",
+		Number: 1,
+	}
+
+	b, err := jsoniter.Marshal(test)
+	require.NoError(t, err)
+
+	buf := new(data)
+	err = jsoniter.Unmarshal(b, &buf)
+	require.NoError(t, err)
+	assert.Equal(t, test, buf, "после Unmarshal объект должен совпадать с исходным")
+}
diff --git a/id/organization.go b/id/organization.go
new file mode 100644
index 0000000000000000000000000000000000000000..fe9d22837977b486e78689ebee5f426bdee80fcf
--- /dev/null
+++ b/id/organization.go
@@ -0,0 +1,49 @@
+package id
+
+const (
+	Organization        = "organization"
+	OrganizationsPrefix = "orgs"
+)
+
+type OrganizationID struct {
+	OrganizationID string `json:"organization_id,omitempty" bson:"organization_id,omitempty"`
+}
+
+func (t *OrganizationID) Type() string { return Organization }
+
+func (t *OrganizationID) String() string {
+	return Join(OrganizationsPrefix, t.OrganizationID)
+}
+
+func (t *OrganizationID) ToMap() map[string]any {
+	return map[string]any{
+		"organization_id": t.OrganizationID,
+		"type":            Organization,
+	}
+}
+
+func (t *OrganizationID) FromMap(m map[string]any) error {
+	t.OrganizationID = m["organization_id"].(string)
+	return nil
+}
+
+func (t *OrganizationID) Validate() error {
+	if t.OrganizationID == "" {
+		return ErrInvalidID
+	}
+	return nil
+}
+
+func parseOrganizationID(parts []string) (*OrganizationID, error) {
+	var id OrganizationID
+	if len(parts) != 2 || parts[0] != OrganizationsPrefix {
+		return nil, ErrInvalidID
+	}
+
+	id.OrganizationID = parts[1]
+	return &id, nil
+}
+
+func NewOrganizationID(id string) *ID {
+	return &ID{Descriptor: &OrganizationID{OrganizationID: id}}
+}
diff --git a/id/revision.go b/id/revision.go
new file mode 100644
index 0000000000000000000000000000000000000000..0cb417e132fe1683f1e53a081e7c6244820478df
--- /dev/null
+++ b/id/revision.go
@@ -0,0 +1,61 @@
+package id
+
+const (
+	Revision        = "revision"
+	RevisionsPrefix = "revs"
+)
+
+type RevisionID struct {
+	ItemID
+	RevisionID string `json:"rev_id" bson:"rev_id,omitempty"`
+}
+
+func (t *RevisionID) Type() string { return Revision }
+
+func (t *RevisionID) String() string {
+	return Join(t.ItemID.String(), RevisionsPrefix, t.RevisionID)
+
+}
+
+func (t *RevisionID) ToMap() map[string]any {
+	m := t.ItemID.ToMap()
+	m["rev_id"] = t.RevisionID
+	m["type"] = Revision
+	return m
+}
+
+func (t *RevisionID) FromMap(m map[string]any) error {
+	if err := t.ItemID.FromMap(m); err != nil {
+		return err
+	}
+	t.RevisionID = m["rev_id"].(string)
+	return nil
+}
+
+func (t *RevisionID) Validate() error {
+	if t.RevisionID == "" {
+		return ErrInvalidID
+	}
+
+	return t.ItemID.Validate()
+}
+
+func parseRevisionID(parts []string) (*RevisionID, error) {
+	if len(parts) != 10 || parts[8] != RevisionsPrefix {
+		return nil, ErrInvalidID
+	}
+
+	itemID, err := parseItemID(parts[:8])
+	if err != nil {
+		return nil, err
+	}
+
+	var id RevisionID
+	id.ItemID = *itemID
+	id.RevisionID = parts[9]
+	return &id, nil
+}
+
+func NewRevisionID(spaceID, envID, collID, itemID, id string) *ID {
+	return &ID{Descriptor: &RevisionID{ItemID: ItemID{CollectionID: CollectionID{EnvironmentID: EnvironmentID{SpaceID: SpaceID{SpaceID: spaceID}, EnvironmentID: envID}, CollectionID: collID}, ItemID: itemID}, RevisionID: id}}
+}
diff --git a/id/role.go b/id/role.go
new file mode 100644
index 0000000000000000000000000000000000000000..abb6537fc605d3f01f644a67a724cc81e3a6b54b
--- /dev/null
+++ b/id/role.go
@@ -0,0 +1,61 @@
+package id
+
+const (
+	Role        = "role"
+	RolesPrefix = "roles"
+)
+
+type RoleID struct {
+	SpaceID
+	RoleID string `json:"role_id,omitempty" bson:"role_id,omitempty"`
+}
+
+func (t *RoleID) Type() string { return Role }
+
+func (t *RoleID) String() string {
+	return Join(t.SpaceID.String(), RolesPrefix, t.RoleID)
+
+}
+
+func (t *RoleID) ToMap() map[string]any {
+	m := t.SpaceID.ToMap()
+	m["role_id"] = t.RoleID
+	m["type"] = Role
+	return m
+}
+
+func (t *RoleID) FromMap(m map[string]any) error {
+	if err := t.SpaceID.FromMap(m); err != nil {
+		return err
+	}
+	t.RoleID = m["role_id"].(string)
+	return nil
+}
+
+func (t *RoleID) Validate() error {
+	if t.RoleID == "" {
+		return ErrInvalidID
+	}
+
+	return t.SpaceID.Validate()
+}
+
+func parseRoleID(parts []string) (*RoleID, error) {
+	if len(parts) != 4 || parts[2] != RolesPrefix {
+		return nil, ErrInvalidID
+	}
+
+	spaceID, err := parseSpaceID(parts[:2])
+	if err != nil {
+		return nil, err
+	}
+
+	var id RoleID
+	id.SpaceID = *spaceID
+	id.RoleID = parts[3]
+	return &id, nil
+}
+
+func NewRoleID(spaceID, id string) *ID {
+	return &ID{Descriptor: &RoleID{SpaceID: SpaceID{SpaceID: spaceID}, RoleID: id}}
+}
diff --git a/id/schema.go b/id/schema.go
new file mode 100644
index 0000000000000000000000000000000000000000..e3afee8c6cf29bca6cae1f1eedd72253c47362a5
--- /dev/null
+++ b/id/schema.go
@@ -0,0 +1,60 @@
+package id
+
+const (
+	Schema       = "schema"
+	SchemaPrefix = "schema"
+)
+
+type SchemaID struct {
+	EnvironmentID
+	CollectionID string `json:"col_id" bson:"col_id,omitempty"`
+}
+
+func (t *SchemaID) Type() string { return Schema }
+
+func (t *SchemaID) String() string {
+	return Join(t.EnvironmentID.String(), SchemaPrefix, t.CollectionID)
+}
+
+func (t *SchemaID) ToMap() map[string]any {
+	m := t.EnvironmentID.ToMap()
+	m["col_id"] = t.CollectionID
+	m["type"] = Schema
+	return m
+}
+
+func (t *SchemaID) FromMap(m map[string]any) error {
+	if err := t.EnvironmentID.FromMap(m); err != nil {
+		return err
+	}
+	t.CollectionID = m["col_id"].(string)
+	return nil
+}
+
+func (t *SchemaID) Validate() error {
+	if t.CollectionID == "" {
+		return ErrInvalidID
+	}
+
+	return t.EnvironmentID.Validate()
+}
+
+func parseSchemaID(parts []string) (*SchemaID, error) {
+	if len(parts) != 6 || parts[4] != SchemaPrefix {
+		return nil, ErrInvalidID
+	}
+
+	envID, err := parseEnvironmentID(parts[:4])
+	if err != nil {
+		return nil, err
+	}
+
+	var id SchemaID
+	id.EnvironmentID = *envID
+	id.CollectionID = parts[5]
+	return &id, nil
+}
+
+func NewSchemaID(spaceID, envID, id string) *ID {
+	return &ID{Descriptor: &SchemaID{EnvironmentID: EnvironmentID{SpaceID: SpaceID{SpaceID: spaceID}, EnvironmentID: envID}, CollectionID: id}}
+}
diff --git a/id/service.go b/id/service.go
new file mode 100644
index 0000000000000000000000000000000000000000..23bb23aa500fbba082e1aae578eae6da5624a5b9
--- /dev/null
+++ b/id/service.go
@@ -0,0 +1,49 @@
+package id
+
+const (
+	Service        = "service"
+	ServicesPrefix = "services"
+)
+
+type ServiceID struct {
+	ServiceID string `json:"service_id,omitempty" bson:"service_id,omitempty"`
+}
+
+func (t *ServiceID) Type() string { return Service }
+
+func (t *ServiceID) String() string {
+	return Join(ServicesPrefix, t.ServiceID)
+}
+
+func (t *ServiceID) ToMap() map[string]any {
+	return map[string]any{
+		"service_id": t.ServiceID,
+		"type":       Service,
+	}
+}
+
+func (t *ServiceID) FromMap(m map[string]any) error {
+	t.ServiceID = m["service_id"].(string)
+	return nil
+}
+
+func (t *ServiceID) Validate() error {
+	if t.ServiceID == "" {
+		return ErrInvalidID
+	}
+	return nil
+}
+
+func parseServiceID(parts []string) (*ServiceID, error) {
+	var id ServiceID
+	if len(parts) != 2 || parts[0] != ServicesPrefix {
+		return nil, ErrInvalidID
+	}
+
+	id.ServiceID = parts[1]
+	return &id, nil
+}
+
+func NewServiceID(id string) *ID {
+	return &ID{Descriptor: &ServiceID{ServiceID: id}}
+}
diff --git a/id/space.go b/id/space.go
new file mode 100644
index 0000000000000000000000000000000000000000..39096673456d74462ef203b10814396fe48b2bcf
--- /dev/null
+++ b/id/space.go
@@ -0,0 +1,48 @@
+package id
+
+const (
+	Space        = "space"
+	SpacesPrefix = "spaces"
+)
+
+type SpaceID struct {
+	SpaceID string `json:"space_id,omitempty" bson:"space_id,omitempty"`
+}
+
+func (t *SpaceID) Type() string { return Space }
+
+func (t *SpaceID) String() string {
+	return Join(SpacesPrefix, t.SpaceID)
+}
+
+func (t *SpaceID) ToMap() map[string]any {
+	return map[string]any{
+		"space_id": t.SpaceID,
+		"type":     Space,
+	}
+}
+
+func (t *SpaceID) FromMap(m map[string]any) error {
+	t.SpaceID = m["space_id"].(string)
+	return nil
+}
+
+func (t *SpaceID) Validate() error {
+	if t.SpaceID == "" {
+		return ErrInvalidID
+	}
+	return nil
+}
+
+func parseSpaceID(parts []string) (*SpaceID, error) {
+	var id SpaceID
+	if len(parts) != 2 || parts[0] != SpacesPrefix {
+		return nil, ErrInvalidID
+	}
+
+	id.SpaceID = parts[1]
+	return &id, nil
+}
+func NewSpaceID(id string) *ID {
+	return &ID{Descriptor: &SpaceID{SpaceID: id}}
+}
diff --git a/id/system.go b/id/system.go
new file mode 100644
index 0000000000000000000000000000000000000000..de2f3c2571896e448f8aad17be32df58b73a6ea4
--- /dev/null
+++ b/id/system.go
@@ -0,0 +1,22 @@
+package id
+
+const System = "system"
+
+type SystemID struct{}
+
+func (t *SystemID) Type() string                   { return Space }
+func (t *SystemID) String() string                 { return string(Separator) + System }
+func (t *SystemID) ToMap() map[string]any          { return map[string]any{"type": System} }
+func (t *SystemID) FromMap(m map[string]any) error { return nil }
+func (t *SystemID) Validate() error                { return nil }
+
+func parseSystemID(parts []string) (*SystemID, error) {
+	var id SystemID
+	if len(parts) != 1 || parts[0] != System {
+		return nil, ErrInvalidID
+	}
+	return &id, nil
+}
+func NewSystemID() *ID {
+	return &ID{Descriptor: &SystemID{}}
+}
diff --git a/id/user.go b/id/user.go
new file mode 100644
index 0000000000000000000000000000000000000000..c76f6c9fa0e8ce440a1ef15e36bbb82a73301be5
--- /dev/null
+++ b/id/user.go
@@ -0,0 +1,49 @@
+package id
+
+const (
+	User        = "user"
+	UsersPrefix = "users"
+)
+
+type UserID struct {
+	UserID string `json:"user_id,omitempty" bson:"user_id,omitempty"`
+}
+
+func (t *UserID) Type() string { return User }
+
+func (t *UserID) String() string {
+	return Join(UsersPrefix, t.UserID)
+}
+
+func (t *UserID) ToMap() map[string]any {
+	return map[string]any{
+		"user_id": t.UserID,
+		"type":    User,
+	}
+}
+
+func (t *UserID) FromMap(m map[string]any) error {
+	t.UserID = m["user_id"].(string)
+	return nil
+}
+
+func (t *UserID) Validate() error {
+	if t.UserID == "" {
+		return ErrInvalidID
+	}
+	return nil
+}
+
+func parseUserID(parts []string) (*UserID, error) {
+	var id UserID
+	if len(parts) != 2 || parts[0] != UsersPrefix {
+		return nil, ErrInvalidID
+	}
+
+	id.UserID = parts[1]
+	return &id, nil
+}
+
+func NewUserID(id string) *ID {
+	return &ID{Descriptor: &UserID{UserID: id}}
+}
diff --git a/pkg/environments/environment.go b/pkg/environments/environment.go
index cbd468dc361951e048a0840f4158a42c962e7af1..e9d2b96e8337ac2ad94315b671aeea42e54c47cb 100644
--- a/pkg/environments/environment.go
+++ b/pkg/environments/environment.go
@@ -91,24 +91,4 @@ func (e Environment) Clone() *Environment {
 	}
 
 	return clone
-}
-
-func (e Environment) Fetch(i interface{}) interface{} {
-	p, _ := i.(string)
-	switch p {
-	case "ID":
-		return e.ID
-	case "SpaceID":
-		return e.SpaceID
-	case "Description":
-		return e.Description
-	case "StateInfo":
-		return e.StateInfo
-	case "Aliases":
-		return e.Aliases
-	case "Config":
-		return e.Config
-	default:
-		panic("unknown parameter")
-	}
-}
+}
\ No newline at end of file
diff --git a/pkg/expr/config.go b/pkg/expr/config.go
index 628111173d92f81ccdcc5e4c40b1aecc2ff27626..e6ba9d90b4f4e04c56c17b8591d4a92e30984e6c 100644
--- a/pkg/expr/config.go
+++ b/pkg/expr/config.go
@@ -1,8 +1,8 @@
 package expr
 
 import (
-	"github.com/antonmedv/expr"
-	"github.com/antonmedv/expr/conf"
+	"github.com/expr-lang/expr"
+	"github.com/expr-lang/expr/conf"
 )
 
 type ExprConfig struct {
diff --git a/pkg/expr/expr.go b/pkg/expr/expr.go
index 93d6ee612968ac0a7e1c963a05cda4955532f110..1969b58c6bec892b05f8eff38eec86f3d4099e01 100644
--- a/pkg/expr/expr.go
+++ b/pkg/expr/expr.go
@@ -5,9 +5,9 @@ import (
 	"strings"
 
 	"git.perx.ru/perxis/perxis-go/pkg/data"
-	compiler2 "github.com/antonmedv/expr/compiler"
-	"github.com/antonmedv/expr/parser"
-	"github.com/antonmedv/expr/vm"
+	exprcompiler "github.com/expr-lang/expr/compiler"
+	"github.com/expr-lang/expr/parser"
+	"github.com/expr-lang/expr/vm"
 	"golang.org/x/net/context"
 )
 
@@ -39,7 +39,7 @@ func Eval(ctx context.Context, input string, env map[string]interface{}) (interf
 
 	env, _ = cfg.Env.(map[string]interface{})
 
-	program, err := compiler2.Compile(tree, nil)
+	program, err := exprcompiler.Compile(tree, cfg)
 	if err != nil {
 		return nil, err
 	}
@@ -78,4 +78,4 @@ func IsExpression(input string) bool {
 	}
 
 	return false
-}
+}
\ No newline at end of file
diff --git a/pkg/expr/expr_test.go b/pkg/expr/expr_test.go
index 5eafc368bed934e0bd0805f4818442a75256931c..35153da271daace9f9acb6c7a02411f948f47054 100644
--- a/pkg/expr/expr_test.go
+++ b/pkg/expr/expr_test.go
@@ -1,11 +1,13 @@
 package expr
 
 import (
+	"context"
 	"fmt"
 	"testing"
 	"time"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestIsExpression(t *testing.T) {
@@ -49,3 +51,75 @@ func TestIsExpression(t *testing.T) {
 		})
 	}
 }
+
+type testEnvStruct struct {
+	ID   string      `expr:"id"`
+	Size int         `expr:"size"`
+	Data interface{} `expr:"data"`
+}
+
+func (s *testEnvStruct) Equal(other *testEnvStruct) bool {
+	return s.ID == other.ID
+}
+
+func TestExpr_Example(t *testing.T) {
+	ctx := context.Background()
+
+	tests := []struct {
+		name       string
+		exp        string
+		env        map[string]interface{}
+		wantErr    bool
+		wantResult interface{}
+	}{
+		{
+			name:       "get field by expr tag",
+			exp:        "s.id",
+			env:        map[string]interface{}{"s": &testEnvStruct{ID: "id1"}},
+			wantResult: "id1",
+		},
+		{
+			name:       "get field by field name",
+			exp:        "s.ID",
+			env:        map[string]interface{}{"s": &testEnvStruct{ID: "id1"}},
+			wantResult: "id1",
+		},
+		{
+			name:       "get nested field",
+			exp:        "m.s.size",
+			env:        map[string]interface{}{"m": map[string]interface{}{"s": &testEnvStruct{Size: 1}}},
+			wantResult: 1,
+		},
+		{
+			name:       "check field",
+			exp:        "s.data.size < 100",
+			env:        map[string]interface{}{"s": &testEnvStruct{Data: &testEnvStruct{Size: 0}}},
+			wantResult: true,
+		},
+		{
+			name:       "use method",
+			exp:        "s1.Equal(s2)",
+			env:        map[string]interface{}{"s1": &testEnvStruct{ID: "id1"}, "s2": &testEnvStruct{ID: "id2"}},
+			wantResult: false,
+		},
+		{
+			name:    "field not exists",
+			exp:     "s.not_exists",
+			env:     map[string]interface{}{"s": &testEnvStruct{}},
+			wantErr: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result, err := Eval(ctx, tt.exp, tt.env)
+			if tt.wantErr {
+				require.Error(t, err)
+				return
+			}
+
+			require.NoError(t, err)
+			require.Equal(t, tt.wantResult, result)
+		})
+	}
+}
diff --git a/pkg/expr/mongo.go b/pkg/expr/mongo.go
index 989454178f640144791a9e6fd84d39317fd1e283..597180a671e80c2a5bc79bb8efd9bbee940120e4 100644
--- a/pkg/expr/mongo.go
+++ b/pkg/expr/mongo.go
@@ -4,13 +4,14 @@ import (
 	"context"
 	"fmt"
 	"regexp"
+	"strconv"
 	"strings"
 
-	"github.com/antonmedv/expr"
-	"github.com/antonmedv/expr/ast"
-	compiler2 "github.com/antonmedv/expr/compiler"
-	"github.com/antonmedv/expr/conf"
-	"github.com/antonmedv/expr/parser"
+	"github.com/expr-lang/expr"
+	"github.com/expr-lang/expr/ast"
+	exprcompiler "github.com/expr-lang/expr/compiler"
+	"github.com/expr-lang/expr/conf"
+	"github.com/expr-lang/expr/parser"
 	"go.mongodb.org/mongo-driver/bson"
 )
 
@@ -76,7 +77,7 @@ func (c *compiler) eval(node ast.Node) interface{} {
 		Node:   node,
 		Source: c.tree.Source,
 	}
-	prg, err := compiler2.Compile(t, c.config)
+	prg, err := exprcompiler.Compile(t, c.config)
 	if err != nil {
 		panic(fmt.Sprintf("compile error %s", err.Error()))
 	}
@@ -107,18 +108,14 @@ func (c *compiler) compile(node ast.Node) interface{} {
 		return c.UnaryNode(n)
 	case *ast.BinaryNode:
 		return c.BinaryNode(n)
-	case *ast.MatchesNode:
-		return c.MatchesNode(n)
-	case *ast.PropertyNode:
-		return c.PropertyNode(n)
-	case *ast.IndexNode:
-		return c.IndexNode(n)
+	case *ast.MemberNode:
+		return c.MemberNode(n)
+	case *ast.ChainNode:
+		return c.ChainNode(n)
 	case *ast.SliceNode:
 		return c.SliceNode(n)
-	case *ast.MethodNode:
-		return c.MethodNode(n)
-	case *ast.FunctionNode:
-		return c.FunctionNode(n)
+	case *ast.CallNode:
+		return c.CallNode(n)
 	case *ast.BuiltinNode:
 		return c.BuiltinNode(n)
 	case *ast.ClosureNode:
@@ -127,6 +124,8 @@ func (c *compiler) compile(node ast.Node) interface{} {
 		return c.PointerNode(n)
 	case *ast.ConditionalNode:
 		return c.ConditionalNode(n)
+	case *ast.VariableDeclaratorNode:
+		return c.VariableDeclaratorNode(n)
 	case *ast.ArrayNode:
 		return c.ArrayNode(n)
 	case *ast.MapNode:
@@ -208,12 +207,14 @@ func (c *compiler) ConstantNode(node *ast.ConstantNode) interface{} {
 }
 
 func (c *compiler) UnaryNode(node *ast.UnaryNode) interface{} {
-	op := c.compile(node.Node)
-
 	switch node.Operator {
-
 	case "!", "not":
-		return bson.M{"$not": op}
+		nodeIn, ok := node.Node.(*ast.BinaryNode)
+		if ok && nodeIn.Operator == "in" {
+			return bson.M{c.identifier(nodeIn.Left): bson.M{"$nin": c.eval(nodeIn.Right)}}
+		}
+
+		return bson.M{"$nor": bson.A{c.compile(node.Node)}}
 	default:
 		panic(fmt.Sprintf("unknown operator (%v)", node.Operator))
 	}
@@ -221,8 +222,8 @@ func (c *compiler) UnaryNode(node *ast.UnaryNode) interface{} {
 
 func (c *compiler) identifier(node ast.Node) string {
 	switch l := node.(type) {
-	case *ast.PropertyNode:
-		return c.PropertyNode(l)
+	case *ast.MemberNode:
+		return c.MemberNode(l)
 	case *ast.IdentifierNode:
 		return c.IdentifierNode(l)
 	}
@@ -230,6 +231,10 @@ func (c *compiler) identifier(node ast.Node) string {
 }
 
 func (c *compiler) BinaryNode(node *ast.BinaryNode) interface{} {
+	if result := c.handleLenNode(node); result != nil {
+		return result
+	}
+
 	switch node.Operator {
 	case "==":
 		return bson.M{c.identifier(node.Left): c.eval(node.Right)}
@@ -324,45 +329,24 @@ func (c *compiler) BinaryNode(node *ast.BinaryNode) interface{} {
 	}
 }
 
-func (c *compiler) MatchesNode(node *ast.MatchesNode) interface{} {
-	panic("unsupported match node")
-	//if node.Regexp != nil {
-	//	c.compile(node.Left)
-	//	c.emit(OpMatchesConst, c.makeConstant(node.Regexp)...)
-	//	return
-	//}
-	//c.compile(node.Left)
-	//c.compile(node.Right)
-	//c.emit(OpMatches)
+func (c *compiler) ChainNode(node *ast.ChainNode) string {
+	panic("unsupported chain node")
 }
 
-func (c *compiler) PropertyNode(node *ast.PropertyNode) string {
+func (c *compiler) MemberNode(node *ast.MemberNode) string {
 	v := c.compile(node.Node)
 	if val, ok := v.(string); ok {
-		return fmt.Sprintf("%s.%s", val, node.Property)
+		return fmt.Sprintf("%s.%s", val, c.compile(node.Property))
 	}
 	panic(fmt.Sprintf("unsupported property for %v", ast.Dump(node.Node)))
 }
 
-func (c *compiler) IndexNode(node *ast.IndexNode) string {
-	return fmt.Sprintf("{index-%v}", c.compile(node.Index))
-}
-
 func (c *compiler) SliceNode(node *ast.SliceNode) interface{} {
 	panic("unsupported slice node")
 }
 
-func (c *compiler) MethodNode(node *ast.MethodNode) interface{} {
-	panic("unsupported method node")
-	//c.compile(node.Node)
-	//for _, arg := range node.Arguments {
-	//	c.compile(arg)
-	//}
-	//c.emit(OpMethod, c.makeConstant(Call{Name: node.Method, Size: len(node.Arguments)})...)
-}
-
-func (c *compiler) FunctionNode(node *ast.FunctionNode) interface{} {
-	switch node.Name {
+func (c *compiler) CallNode(node *ast.CallNode) interface{} {
+	switch node.Callee.String() {
 	case "search", "q":
 		val := c.compile(node.Arguments[0])
 		return bson.M{"$text": bson.M{"$search": val}}
@@ -425,6 +409,12 @@ func (c *compiler) FunctionNode(node *ast.FunctionNode) interface{} {
 		}
 
 		return bson.M{fields: bson.M{"$in": array}}
+	case "exists":
+		if len(node.Arguments) != 1 {
+			panic("exists() expects exactly 1 argument")
+		}
+		field := c.identifier(node.Arguments[0])
+		return bson.M{field: bson.M{"$exists": true}}
 
 	case "icontains":
 		v := c.identifier(node.Arguments[0])
@@ -633,6 +623,10 @@ func (c *compiler) ConditionalNode(node *ast.ConditionalNode) interface{} {
 	//c.patchJump(end)
 }
 
+func (c *compiler) VariableDeclaratorNode(node *ast.VariableDeclaratorNode) int {
+	panic("unsupported variable declarator node ")
+}
+
 func (c *compiler) ArrayNode(node *ast.ArrayNode) interface{} {
 	panic("unsupported array node")
 	//for _, node := range node.Nodes {
@@ -658,3 +652,50 @@ func (c *compiler) PairNode(node *ast.PairNode) interface{} {
 	//c.compile(node.Key)
 	//c.compile(node.Value)
 }
+
+// handleLenNode получает узел AST и возвращает запрос для mongo,
+// если узел представляет вызов функции len, и nil в противном случае.
+func (c *compiler) handleLenNode(node *ast.BinaryNode) bson.M {
+	lenNode, ok := node.Left.(*ast.BuiltinNode)
+	if !ok || lenNode.Name != "len" {
+		return nil
+	}
+
+	if len(lenNode.Arguments) != 1 {
+		panic("len() expects exactly 1 argument")
+	}
+
+	length, ok := c.eval(node.Right).(int)
+	if !ok {
+		panic("len() can only be compared with number value")
+	}
+	if length < 0 {
+		panic("len() can only be compared with non-negative number")
+	}
+
+	field := c.identifier(lenNode.Arguments[0])
+	switch op := node.Operator; {
+	case (op == "==" || op == "<=") && length == 0:
+		return bson.M{field: bson.M{"$eq": bson.A{}}}
+	case (op == "!=" || op == ">") && length == 0:
+		return bson.M{field: bson.M{"$exists": true, "$type": "array", "$ne": bson.A{}}}
+	case op == ">=" && length == 0:
+		return bson.M{field: bson.M{"$exists": true, "$type": "array"}}
+	case op == "<" && length == 0:
+		panic("invalid comparison: len() cannot be less than 0")
+	case op == "==":
+		return bson.M{field: bson.M{"$size": length}}
+	case op == "!=":
+		return bson.M{field: bson.M{"$not": bson.M{"$size": length}, "$type": "array"}}
+	case op == ">":
+		return bson.M{field + "." + strconv.Itoa(length): bson.M{"$exists": true}}
+	case op == ">=":
+		return bson.M{field + "." + strconv.Itoa(length-1): bson.M{"$exists": true}}
+	case op == "<":
+		return bson.M{field + "." + strconv.Itoa(length-1): bson.M{"$exists": false}, field: bson.M{"$type": "array"}}
+	case op == "<=":
+		return bson.M{field + "." + strconv.Itoa(length): bson.M{"$exists": false}, field: bson.M{"$type": "array"}}
+	default:
+		panic("invalid comparison operator with len()")
+	}
+}
diff --git a/pkg/expr/mongo_test.go b/pkg/expr/mongo_test.go
index 75ec627c124cc0a24768d3cec30a4eac0ae34c15..dee6e668c490a6d375a77c532862f5389b1bd5f4 100644
--- a/pkg/expr/mongo_test.go
+++ b/pkg/expr/mongo_test.go
@@ -6,8 +6,8 @@ import (
 	"time"
 
 	"git.perx.ru/perxis/perxis-go/pkg/id"
-	"github.com/antonmedv/expr"
-	"github.com/antonmedv/expr/ast"
+	"github.com/expr-lang/expr"
+	"github.com/expr-lang/expr/ast"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"go.mongodb.org/mongo-driver/bson"
@@ -29,6 +29,29 @@ func TestConvertToMongo(t *testing.T) {
 	}{
 		{"equal", "s == 3", nil, bson.M{"s": 3}, false},
 		{"in array", "s in [1,2,3]", nil, bson.M{"s": bson.M{"$in": []interface{}{1, 2, 3}}}, false},
+		{"not in array", "s not in [1,2,3]", nil, bson.M{"s": bson.M{"$nin": []interface{}{1, 2, 3}}}, false},
+		{"exists#1", "exists(s)", nil, bson.M{"s": bson.M{"$exists": true}}, false},
+		{"exists#2", "exists(s, s)", nil, nil, true},
+		{"len#1", "len(s)", nil, nil, true},
+		{"len#2", "len(s) <> 1", nil, nil, true},
+		{"len#3", "len(s) == -1", nil, nil, true},
+		{"len#4", "len(s, s) == -1", nil, nil, true},
+		{"len#5", "len(s) == s", nil, nil, true},
+		{"len eq", "len(s) == 1", nil, bson.M{"s": bson.M{"$size": 1}}, false},
+		{"len eq zero", "len(s) == 0", nil, bson.M{"s": bson.M{"$eq": bson.A{}}}, false},
+		{"len ne", "len(s) != 1", nil, bson.M{"s": bson.M{"$not": bson.M{"$size": 1}, "$type": "array"}}, false},
+		{"len ne zero", "len(s) != 0", nil, bson.M{"s": bson.M{"$exists": true, "$ne": bson.A{}, "$type": "array"}}, false},
+		{"len gt", "len(s) > 1", nil, bson.M{"s.1": bson.M{"$exists": true}}, false},
+		{"len gt zero", "len(s) > 0", nil, bson.M{"s": bson.M{"$exists": true, "$type": "array", "$ne": bson.A{}}}, false},
+		{"len gte", "len(s) >= 1", nil, bson.M{"s.0": bson.M{"$exists": true}}, false},
+		{"len gte zero", "len(s) >= 0", nil, bson.M{"s": bson.M{"$exists": true, "$type": "array"}}, false},
+		{"len lt", "len(s) < 1", nil, bson.M{"s.0": bson.M{"$exists": false}, "s": bson.M{"$type": "array"}}, false},
+		{"len lt zero", "len(s) < 0", nil, nil, true},
+		{"len lte", "len(s) <= 1", nil, bson.M{"s.1": bson.M{"$exists": false}, "s": bson.M{"$type": "array"}}, false},
+		{"len lte zero", "len(s) <= 0", nil, bson.M{"s": bson.M{"$eq": bson.A{}}}, false},
+		{"field#1", "s.test > 3", nil, bson.M{"s.test": bson.M{"$gt": 3}}, false},
+		{"field#2", "s['test'] > 3", nil, bson.M{"s.test": bson.M{"$gt": 3}}, false},
+		{"field#3", "s[test] > 3", nil, bson.M{"s.test": bson.M{"$gt": 3}}, false},
 		{"contains", "s contains 'some'", nil, bson.M{"s": bson.M{"$regex": "some"}}, false},
 		{"contains with . + () $ {} ^", "value contains 'something with . + () $ {} ^'", nil, bson.M{"value": bson.M{"$regex": "something with \\. \\+ \\(\\) \\$ \\{\\} \\^"}}, false},
 		{"startsWith", "s startsWith 'some'", nil, bson.M{"s": bson.M{"$regex": "^some.*"}}, false},
@@ -42,6 +65,8 @@ func TestConvertToMongo(t *testing.T) {
 		{"iendsWith", "iendsWith(s, 'some')", nil, bson.M{"s": bson.M{"$regex": ".*some$", "$options": "i"}}, false},
 		{"iendsWith . + () $ {} ^", "iendsWith(s,'. + () $ {} ^')", nil, bson.M{"s": bson.M{"$regex": ".*\\. \\+ \\(\\) \\$ \\{\\} \\^$", "$options": "i"}}, false},
 		{"or", "s==2 || s > 10", nil, bson.M{"$or": bson.A{bson.M{"s": 2}, bson.M{"s": bson.M{"$gt": 10}}}}, false},
+		{"not#1", "not icontains(s, 'some')", nil, bson.M{"$nor": bson.A{bson.M{"s": bson.M{"$options": "i", "$regex": "some"}}}}, false},
+		{"not#2", "not (s.test > 3)", nil, bson.M{"$nor": bson.A{bson.M{"s.test": bson.M{"$gt": 3}}}}, false},
 		{"search", "search('some') || s > 10", nil, bson.M{"$or": bson.A{bson.M{"$text": bson.M{"$search": "some"}}, bson.M{"s": bson.M{"$gt": 10}}}}, false},
 		{"vars:or", "s== a + 2 || s > a + 10", map[string]interface{}{"a": 100}, bson.M{"$or": bson.A{bson.M{"s": 102}, bson.M{"s": bson.M{"$gt": 110}}}}, false},
 		{"near", "near(a, [55.5, 37.5], 1000)", map[string]interface{}{"a": []interface{}{55, 37}}, bson.M{"a.geometry": bson.M{"$near": bson.D{{Key: "$geometry", Value: map[string]interface{}{"coordinates": []interface{}{55.5, 37.5}, "type": "Point"}}, {Key: "$maxDistance", Value: 1000}}}}, false},
@@ -51,6 +76,7 @@ func TestConvertToMongo(t *testing.T) {
 		{"in", "In(s, [1,2,3])", nil, bson.M{"s": bson.M{"$in": []interface{}{1, 2, 3}}}, false},
 		{"in", "In(s, 1)", nil, bson.M{"s": bson.M{"$in": []interface{}{1}}}, false},
 		{"text search or id", "id", nil, nil, true},
+		{"struct env", "db_item.id == env_item.id", map[string]interface{}{"env_item": &testEnvStruct{ID: "id1"}}, bson.M{"db_item.id": "id1"}, false},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
@@ -83,8 +109,7 @@ func BenchmarkConvertToMongo(b *testing.B) {
 
 type testVisitor struct{}
 
-func (v *testVisitor) Enter(node *ast.Node) {}
-func (v *testVisitor) Exit(node *ast.Node) {
+func (v *testVisitor) Visit(node *ast.Node) {
 	if n, ok := (*node).(*ast.IdentifierNode); ok {
 		n.Value = "some" + "." + n.Value
 	}
diff --git a/pkg/expr/time.go b/pkg/expr/time.go
index 740da301a70b65a15f83ac70b1f512c47c171b35..b72292c0c9f4d85bbe42d282f52dcde060cdf8e4 100644
--- a/pkg/expr/time.go
+++ b/pkg/expr/time.go
@@ -3,7 +3,7 @@ package expr
 import (
 	"time"
 
-	"github.com/antonmedv/expr"
+	"github.com/expr-lang/expr"
 )
 
 const DefaultTimeLayout = time.RFC3339
@@ -18,7 +18,6 @@ func init() {
 		expr.Operator("<", "Time.Before"),
 		expr.Operator(">", "Time.After"),
 		expr.Operator("<=", "Time.BeforeOrEqual"),
-		expr.Operator(">", "Time.After"),
 		expr.Operator(">=", "Time.AfterOrEqual"),
 
 		// Time and duration manipulation.
diff --git a/pkg/extension/schema.go b/pkg/extension/schema.go
index 501de9a247c3ba4240dc50ba641d7e2c56a1d2fc..521ebad6428dbd308228090469f369367e65068d 100644
--- a/pkg/extension/schema.go
+++ b/pkg/extension/schema.go
@@ -72,7 +72,7 @@ func NewActionsCollection(spaceID, envID string) *collections.Collection {
 
 	// UI
 	sch.Field.UI.ListView = &field.View{Options: map[string]interface{}{
-		"fields":    []interface{}{"name", "action", "kind", "updated_at", "updated_by", "state"},
+		"fields":    []interface{}{"icon", "name", "action", "kind", "updated_at", "updated_by", "state"},
 		"sort":      []interface{}{"name"},
 		"page_size": float64(50),
 	}}
diff --git a/pkg/files/file.go b/pkg/files/file.go
index 0e4d89f7004413b2c2a0ae9eff663cdcfc0ef3a1..d2236b83368e3d5efc763fd6f7e53664e15cc827 100644
--- a/pkg/files/file.go
+++ b/pkg/files/file.go
@@ -16,13 +16,13 @@ const (
 
 // File - описание файла в системе хранения perxis
 type File struct {
-	ID       string  `mapstructure:"id,omitempty" json:"id"`                                       // Уникальный идентификатор файла в хранилище
-	Name     string  `mapstructure:"name,omitempty" json:"name" bson:"name,omitempty"`             // Имя файла
-	Size     int     `mapstructure:"size,omitempty" json:"size" bson:"size,omitempty"`             // Размер файла
-	MimeType string  `mapstructure:"mimeType,omitempty" json:"mimeType" bson:"mimeType,omitempty"` // Mime-type файла
-	URL      string  `mapstructure:"url,omitempty" json:"url" bson:"url,omitempty"`                // Адрес для загрузки файла
-	Key      string  `mapstructure:"key,omitempty" json:"key" bson:"key,omitempty"`                // Ключ для хранения файла в хранилище
-	File     fs.File `mapstructure:"-" json:"-" bson:"-"`                                          // Файл для загрузки(из файловой системы)
+	ID       string  `mapstructure:"id,omitempty" json:"id" expr:"id"`                                              // Уникальный идентификатор файла в хранилище
+	Name     string  `mapstructure:"name,omitempty" json:"name" bson:"name,omitempty" expr:"name"`                  // Имя файла
+	Size     int     `mapstructure:"size,omitempty" json:"size" bson:"size,omitempty" expr:"size"`                  // Размер файла
+	MimeType string  `mapstructure:"mimeType,omitempty" json:"mimeType" bson:"mimeType,omitempty" expr:"mime_type"` // Mime-type файла
+	URL      string  `mapstructure:"url,omitempty" json:"url" bson:"url,omitempty" expr:"url"`                      // Адрес для загрузки файла
+	Key      string  `mapstructure:"key,omitempty" json:"key" bson:"key,omitempty" expr:"key"`                      // Ключ для хранения файла в хранилище
+	File     fs.File `mapstructure:"-" json:"-" bson:"-"`                                                           // Файл для загрузки(из файловой системы)
 }
 
 func (f File) Clone() *File {
@@ -47,26 +47,6 @@ func (f *File) SetURLWithTemplate(t *template.Template) error {
 	return nil
 }
 
-func (f File) Fetch(i interface{}) interface{} {
-	p, _ := i.(string)
-	switch p {
-	case "id":
-		return f.ID
-	case "name":
-		return f.Name
-	case "size":
-		return f.Size
-	case "mime_type":
-		return f.MimeType
-	case "url":
-		return f.URL
-	case "key":
-		return f.Key
-	default:
-		panic("unknown parameter")
-	}
-}
-
 func NewFile(name, mimeType string, size int, temp bool) *File {
 	i := id.GenerateNewID()
 	if temp {
diff --git a/pkg/files/file_test.go b/pkg/files/file_test.go
index 14fb89ce8c054d57ea708a10eb27318b474ff99c..617bed4c09ef95ab58874d856a36c5584abd4589 100644
--- a/pkg/files/file_test.go
+++ b/pkg/files/file_test.go
@@ -1,9 +1,11 @@
 package files
 
 import (
+	"context"
 	"testing"
 	"text/template"
 
+	"git.perx.ru/perxis/perxis-go/pkg/expr"
 	"github.com/stretchr/testify/require"
 )
 
@@ -55,3 +57,35 @@ func TestFile_SetURLWithTemplate(t *testing.T) {
 		})
 	}
 }
+
+func TestFile_InExpr(t *testing.T) {
+	ctx := context.Background()
+
+	tests := []struct {
+		exp        string
+		env        map[string]interface{}
+		wantResult interface{}
+		wantErr    bool
+	}{
+		{"f.id", map[string]interface{}{"f": &File{ID: "some_id"}}, "some_id", false},
+		{"f.name", map[string]interface{}{"f": &File{Name: "some_name"}}, "some_name", false},
+		{"f.size", map[string]interface{}{"f": &File{Size: 1}}, 1, false},
+		{"f.mime_type", map[string]interface{}{"f": &File{MimeType: "some_mime_type"}}, "some_mime_type", false},
+		{"f.url", map[string]interface{}{"f": &File{URL: "some_url"}}, "some_url", false},
+		{"f.key", map[string]interface{}{"f": &File{Key: "some_key"}}, "some_key", false},
+		{"f.not_exists", map[string]interface{}{"f": &File{}}, "", true},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.exp, func(t *testing.T) {
+			result, err := expr.Eval(ctx, tt.exp, tt.env)
+			if tt.wantErr {
+				require.Error(t, err)
+				return
+			}
+
+			require.NoError(t, err)
+			require.Equal(t, tt.wantResult, result)
+		})
+	}
+}
diff --git a/pkg/items/dummy.go b/pkg/items/dummy.go
new file mode 100644
index 0000000000000000000000000000000000000000..fc1f725abc3e5f9c16164daca30f3334c16f9f86
--- /dev/null
+++ b/pkg/items/dummy.go
@@ -0,0 +1,17 @@
+package items
+
+import "context"
+
+type FindResultDummy struct {
+	Items []*Item
+	Total int
+	Error error
+}
+type Dummy struct {
+	Items
+	FindResult *FindResultDummy
+}
+
+func (d *Dummy) Find(_ context.Context, _, _, _ string, _ *Filter, _ ...*FindOptions) ([]*Item, int, error) {
+	return d.FindResult.Items, d.FindResult.Total, d.FindResult.Error
+}
diff --git a/pkg/items/item.go b/pkg/items/item.go
index 7c34d2bb3563b338d6c3cb0baf97c4e49f58b535..2e76469c7b0a63d4cddf4ad3cd74961f1932a458 100644
--- a/pkg/items/item.go
+++ b/pkg/items/item.go
@@ -398,6 +398,16 @@ func (i *Item) Get(field string) (any, error) {
 	return i.getItemData(field)
 }
 
+// Delete удаляет значение поля Data
+func (i *Item) Delete(field string) error {
+	// Если data == nil, то нет необходимости выполнять удаление
+	if i.Data == nil {
+		return nil
+	}
+
+	return data.Delete(field, i.Data)
+}
+
 // GetSystemField возвращает описание поля для системных аттрибутов Item
 func GetSystemField(fld string) (*field.Field, error) {
 	switch fld {
diff --git a/pkg/items/item_test.go b/pkg/items/item_test.go
index fb54fc501f45281bbafd37983de3b8638d5692d4..dfcc16ee1c7b40441b5df54acc48a12b276669ca 100644
--- a/pkg/items/item_test.go
+++ b/pkg/items/item_test.go
@@ -25,6 +25,42 @@ func TestItem_Set(t *testing.T) {
 
 }
 
+func TestItem_DeleteItemData(t *testing.T) {
+	tests := []struct {
+		name    string
+		item    *Item
+		field   string
+		want    map[string]any
+		wantErr assert.ErrorAssertionFunc
+	}{
+		{
+			name:    "Simple",
+			item:    &Item{Data: map[string]any{"a": "b", "c": "d"}},
+			field:   "a",
+			want:    map[string]any{"c": "d"},
+			wantErr: assert.NoError,
+		},
+		{
+			name:    "Item data is nil",
+			item:    &Item{Data: nil},
+			field:   "a",
+			want:    nil,
+			wantErr: assert.NoError,
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			err := tc.item.Delete(tc.field)
+			assert.NoError(t, err)
+			if !tc.wantErr(t, err) {
+				return
+			}
+			assert.Equal(t, tc.want, tc.item.Data)
+		})
+	}
+}
+
 func TestGetField(t *testing.T) {
 	sch := schema.New(
 		"a", field.String(),
diff --git a/pkg/items/pagination.go b/pkg/items/pagination.go
index 0c07c415ccfd2ad2d005c2c02fae92ad8e01036d..1910a34ba67c21cb1ebf54923ebe750af0fd10d2 100644
--- a/pkg/items/pagination.go
+++ b/pkg/items/pagination.go
@@ -130,6 +130,11 @@ func (b *BatchProcessor) Do(ctx context.Context, f func(batch []*Item) error) (i
 			return 0, err
 		}
 
+		// на случай, когда первый запрос вернул 0 элементов
+		if len(batch) == 0 {
+			break
+		}
+
 		if err = f(batch); err != nil {
 			return 0, err
 		}
diff --git a/pkg/items/pagination_test.go b/pkg/items/pagination_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..bf13af39e00bd19550ddd49bae3e165b5cfa9fff
--- /dev/null
+++ b/pkg/items/pagination_test.go
@@ -0,0 +1,35 @@
+package items
+
+import (
+	"context"
+	"testing"
+
+	"git.perx.ru/perxis/perxis-go/pkg/environments"
+	"git.perx.ru/perxis/perxis-go/pkg/options"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestBatchProcessor(t *testing.T) {
+
+	itemssvc := &Dummy{FindResult: &FindResultDummy{Items: nil, Total: 0, Error: nil}}
+
+	b := &BatchProcessor{
+		Items:        itemssvc,
+		SpaceID:      "sp",
+		EnvID:        environments.DefaultEnvironment,
+		CollectionID: "col",
+		FindOptions: &FindOptions{
+			Regular:     true,
+			Hidden:      true,
+			Templates:   true,
+			FindOptions: *options.NewFindOptions(0, 10),
+		},
+		Filter: NewFilter("a > 5"),
+	}
+
+	var counter int
+	_, err := b.Do(context.Background(), func(batch []*Item) error { counter++; return nil })
+	require.NoError(t, err)
+	assert.Equal(t, 0, counter)
+}
diff --git a/pkg/references/reference.go b/pkg/references/reference.go
index 5740c929bfde2c0c11abcd96d75ce3122448f50e..171ded420c3fd5601cac9ad9f74ad69883f964ea 100644
--- a/pkg/references/reference.go
+++ b/pkg/references/reference.go
@@ -7,9 +7,9 @@ import (
 )
 
 type Reference struct {
-	ID           string `json:"id" bson:"id" mapstructure:"id"`
-	CollectionID string `json:"collection_id" bson:"collection_id" mapstructure:"collection_id"`
-	Disabled     bool   `json:"disabled,omitempty" bson:"disabled,omitempty" mapstructure:"disabled"`
+	ID           string `json:"id" bson:"id" mapstructure:"id" expr:"id"`
+	CollectionID string `json:"collection_id" bson:"collection_id" mapstructure:"collection_id" expr:"collection_id"`
+	Disabled     bool   `json:"disabled,omitempty" bson:"disabled,omitempty" mapstructure:"disabled" expr:"disabled"`
 }
 
 func (r *Reference) MarshalBSON() ([]byte, error) {
@@ -108,17 +108,3 @@ func EqualArrays(sr1, sr2 []*Reference) bool {
 func (r *Reference) IsValid() bool {
 	return r != nil && r.ID != "" && r.CollectionID != "" && !r.Disabled
 }
-
-func (r *Reference) Fetch(i interface{}) interface{} {
-	p, _ := i.(string)
-	switch p {
-	case "id":
-		return r.ID
-	case "collection_id":
-		return r.CollectionID
-	case "disabled":
-		return r.Disabled
-	default:
-		panic("unknown parameter")
-	}
-}
diff --git a/pkg/references/reference_test.go b/pkg/references/reference_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..5e79de47ab96cc9e48b8ba0eb644f9519e9ee914
--- /dev/null
+++ b/pkg/references/reference_test.go
@@ -0,0 +1,41 @@
+package references
+
+import (
+	"context"
+	"testing"
+
+	"git.perx.ru/perxis/perxis-go/pkg/expr"
+	"github.com/stretchr/testify/require"
+)
+
+func TestReference_InExpr(t *testing.T) {
+	ctx := context.Background()
+
+	tests := []struct {
+		exp        string
+		env        map[string]interface{}
+		wantResult interface{}
+		wantErr    bool
+	}{
+		{"r.id", map[string]interface{}{"r": &Reference{ID: "some_id"}}, "some_id", false},
+		{"r.collection_id", map[string]interface{}{"r": &Reference{CollectionID: "some_coll_id"}}, "some_coll_id", false},
+		{"r.disabled", map[string]interface{}{"r": &Reference{Disabled: true}}, true, false},
+		{"r.String()", map[string]interface{}{"r": &Reference{ID: "id", CollectionID: "collID"}}, "collID.id", false},
+		{"r1.Equal(r2)", map[string]interface{}{"r1": &Reference{"id", "collID", false}, "r2": &Reference{"id", "collID", false}}, true, false},
+		{"r.IsValid()", map[string]interface{}{"r": &Reference{}}, false, false},
+		{"r.not_exists", map[string]interface{}{"r": &Reference{}}, false, true},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.exp, func(t *testing.T) {
+			result, err := expr.Eval(ctx, tt.exp, tt.env)
+			if tt.wantErr {
+				require.Error(t, err)
+				return
+			}
+
+			require.NoError(t, err)
+			require.Equal(t, tt.wantResult, result)
+		})
+	}
+}
diff --git a/pkg/spaces/space.go b/pkg/spaces/space.go
index aa039c46f95e6c1ec3b0e49b65e766dc7451cd9f..83edb3f14706793aed6077adcee856598358b044 100644
--- a/pkg/spaces/space.go
+++ b/pkg/spaces/space.go
@@ -42,24 +42,4 @@ type StateInfo struct {
 
 func (s Space) Clone() *Space {
 	return &s
-}
-
-func (s Space) Fetch(i interface{}) interface{} {
-	p, _ := i.(string)
-	switch p {
-	case "ID":
-		return s.ID
-	case "OrgID":
-		return s.OrgID
-	case "Name":
-		return s.Name
-	case "Description":
-		return s.Description
-	case "Config":
-		return s.Config
-	case "StateInfo":
-		return s.StateInfo
-	default:
-		panic("unknown parameter")
-	}
-}
+}
\ No newline at end of file