Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion OPENAPI_DOC.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10757,7 +10757,9 @@ paths:

Filter for a specific metadata by name via `name` param.

Includes the parent metadata by default via `include_parent` param.'
Includes the parent metadata by default via `include_parent` param.

Filter zones by tags via `tags` param.'
tags:
- Metadata
operationId: PlaceOS::Api::Metadata_children_metadata
Expand All @@ -10781,6 +10783,14 @@ paths:
schema:
type: string
nullable: true
- name: tags
in: query
description: return zones with particular tags
example: building,level
schema:
type: array
items:
type: string
responses:
200:
description: OK
Expand Down
170 changes: 170 additions & 0 deletions spec/controllers/metadata_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,176 @@ module PlaceOS::Api

parent.destroy
end

it "filters zone children by single tag" do
parent = Model::Generator.zone.save!
parent_id = parent.id.as(String)

# Create children with different tags
child1 = Model::Generator.zone
child1.parent_id = parent_id
child1.tags = Set{"building", "level1"}
child1.save!
Model::Generator.metadata(parent: child1.id).save!

child2 = Model::Generator.zone
child2.parent_id = parent_id
child2.tags = Set{"room", "level2"}
child2.save!
Model::Generator.metadata(parent: child2.id).save!

child3 = Model::Generator.zone
child3.parent_id = parent_id
child3.tags = Set{"building", "lobby"}
child3.save!
Model::Generator.metadata(parent: child3.id).save!

# Test filtering by "building" tag - should return child1 and child3
result = client.get(
path: "#{Metadata.base_route}/#{parent_id}/children?tags=building",
headers: Spec::Authentication.headers,
)

children_result = Array(NamedTuple(zone: JSON::Any, metadata: Hash(String, Model::Metadata::Interface)))
.from_json(result.body)
children_result.size.should eq 2

# Verify the correct zones are returned
zone_ids = children_result.map(&.[:zone]["id"].as_s)
zone_ids.should contain(child1.id)
zone_ids.should contain(child3.id)

parent.destroy
end

it "filters zone children by multiple tags" do
parent = Model::Generator.zone.save!
parent_id = parent.id.as(String)

# Create children with different tags
child1 = Model::Generator.zone
child1.parent_id = parent_id
child1.tags = Set{"building", "level1"}
child1.save!
Model::Generator.metadata(parent: child1.id).save!

child2 = Model::Generator.zone
child2.parent_id = parent_id
child2.tags = Set{"room", "level2"}
child2.save!
Model::Generator.metadata(parent: child2.id).save!

child3 = Model::Generator.zone
child3.parent_id = parent_id
child3.tags = Set{"building", "lobby"}
child3.save!
Model::Generator.metadata(parent: child3.id).save!

# Test filtering by multiple tags - should return zones with either tag
result = client.get(
path: "#{Metadata.base_route}/#{parent_id}/children?tags=building,room",
headers: Spec::Authentication.headers,
)

children_result = Array(NamedTuple(zone: JSON::Any, metadata: Hash(String, Model::Metadata::Interface)))
.from_json(result.body)
children_result.size.should eq 3

parent.destroy
end

it "returns empty result when no zones match tag filter" do
parent = Model::Generator.zone.save!
parent_id = parent.id.as(String)

# Create children with tags that don't match the filter
child1 = Model::Generator.zone
child1.parent_id = parent_id
child1.tags = Set{"building", "level1"}
child1.save!
Model::Generator.metadata(parent: child1.id).save!

child2 = Model::Generator.zone
child2.parent_id = parent_id
child2.tags = Set{"room", "level2"}
child2.save!
Model::Generator.metadata(parent: child2.id).save!

# Test filtering by non-existent tag
result = client.get(
path: "#{Metadata.base_route}/#{parent_id}/children?tags=nonexistent",
headers: Spec::Authentication.headers,
)

children_result = Array(NamedTuple(zone: JSON::Any, metadata: Hash(String, Model::Metadata::Interface)))
.from_json(result.body)
children_result.size.should eq 0

parent.destroy
end

it "returns all zones when no tag filter is provided" do
parent = Model::Generator.zone.save!
parent_id = parent.id.as(String)

# Create children with tags
3.times do |i|
child = Model::Generator.zone
child.parent_id = parent_id
child.tags = Set{"tag#{i}"}
child.save!
Model::Generator.metadata(parent: child.id).save!
end

# Test without tag filter - should return all zones
result = client.get(
path: "#{Metadata.base_route}/#{parent_id}/children",
headers: Spec::Authentication.headers,
)

children_result = Array(NamedTuple(zone: JSON::Any, metadata: Hash(String, Model::Metadata::Interface)))
.from_json(result.body)
children_result.size.should eq 3

parent.destroy
end

it "combines tag filter with name filter" do
parent = Model::Generator.zone.save!
parent_id = parent.id.as(String)

# Create children with tags
child1 = Model::Generator.zone
child1.parent_id = parent_id
child1.tags = Set{"building"}
child1.save!
Model::Generator.metadata(name: "special", parent: child1.id).save!

child2 = Model::Generator.zone
child2.parent_id = parent_id
child2.tags = Set{"building"}
child2.save!
Model::Generator.metadata(name: "regular", parent: child2.id).save!

child3 = Model::Generator.zone
child3.parent_id = parent_id
child3.tags = Set{"room"}
child3.save!
Model::Generator.metadata(name: "special", parent: child3.id).save!

# Test combining tag and name filters - should return only child1
result = client.get(
path: "#{Metadata.base_route}/#{parent_id}/children?tags=building&name=special",
headers: Spec::Authentication.headers,
)

children_result = Array(NamedTuple(zone: JSON::Any, metadata: Hash(String, Model::Metadata::Interface)))
.from_json(result.body)
children_result.size.should eq 1
children_result.first[:zone]["id"].as_s.should eq child1.id

parent.destroy
end
end

describe "PUT /metadata" do
Expand Down
34 changes: 31 additions & 3 deletions src/placeos-rest-api/controllers/metadata.cr
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,17 @@ module PlaceOS::Api
#
# Filter for a specific metadata by name via `name` param.
# Includes the parent metadata by default via `include_parent` param.
@[AC::Route::GET("/:id/children")]
# Filter zones by tags via `tags` param.
@[AC::Route::GET("/:id/children", converters: {tags: ConvertStringArray})]
def children_metadata(
@[AC::Param::Info(name: "id", description: "the parent id of the metadata to be returned")]
parent_id : String,
@[AC::Param::Info(description: "the parent metadata is included in the results by default", example: "false")]
include_parent : Bool = true,
@[AC::Param::Info(description: "filter for a particular metadata key", example: "config")]
name : String? = nil,
@[AC::Param::Info(description: "return zones with particular tags", example: "building,level")]
tags : Array(String)? = nil,
) : Array(Children)
# Guest JWTs include the control system id that they have access to
if user_token.guest_scope?
Expand All @@ -76,8 +79,33 @@ module PlaceOS::Api

Log.context.set(zone_id: parent_id)
current_zone = ::PlaceOS::Model::Zone.find!(parent_id)
current_zone.children.all.compact_map do |zone|
Children.new(zone, name) if include_parent || zone.id != parent_id

# Get children zones with optional tag filtering
children_zones = if (filter_tags = tags) && !filter_tags.empty?
# Start with all children zones
zones = current_zone.children.to_a

# Filter zones that contain any of the filter tags
zones.select do |zone|
zone_tags = zone.tags || Set(String).new
filter_tags.any? { |tag| zone_tags.includes?(tag) }
end
else
current_zone.children.all.to_a
end

children_zones.compact_map do |zone|
next unless include_parent || zone.id != parent_id

children_obj = Children.new(zone, name)

# If name filter is provided and tags filter is also provided,
# only include zones that have metadata matching the name filter
if name && tags && !tags.empty?
next if children_obj.metadata.empty?
end

children_obj
end
end

Expand Down
Loading