Skip to content
Merged
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
134 changes: 134 additions & 0 deletions OPENAPI_DOC.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12619,6 +12619,90 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS__Api__Application__ContentError'
/api/engine/v2/metadata/{name}/bulk:
get:
summary: Fetch metadata with a specific name for multiple resources
description: 'Fetch metadata with a specific name for multiple resources


Allows bulk retrieval of metadata across multiple parent entities'
tags:
- Metadata
operationId: PlaceOS::Api::Metadata_bulk_fetch
parameters:
- name: name
in: path
description: the name of the metadata key to fetch
example: settings
required: true
schema:
type: string
- name: parent_ids
in: query
description: comma-separated list of parent IDs (zones, systems, users, etc.)
example: zone-1,zone-2,zone-3
required: true
schema:
type: array
items:
type: string
responses:
200:
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Hash_String__PlaceOS__Model__Metadata__Interface___Nil_'
409:
description: Conflict
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS__Api__Application__CommonError'
401:
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS__Api__Application__CommonError'
403:
description: Forbidden
404:
description: Not Found
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS__Api__Application__CommonError'
408:
description: Request Timeout
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS__Api__Application__CommonError'
400:
description: Bad Request
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError'
422:
description: Unprocessable Entity
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError'
406:
description: Not Acceptable
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS__Api__Application__ContentError'
415:
description: Unsupported Media Type
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS__Api__Application__ContentError'
/api/engine/v2/metadata/{id}:
get:
summary: Fetch metadata for a model
Expand Down Expand Up @@ -25362,6 +25446,9 @@ components:
type: integer
format: Int64
nullable: true
launch_on_execute:
type: boolean
nullable: true
control_system_id:
type: string
nullable: true
Expand Down Expand Up @@ -26263,6 +26350,9 @@ components:
type: integer
format: Int64
nullable: true
launch_on_execute:
type: boolean
nullable: true
control_system_id:
type: string
nullable: true
Expand Down Expand Up @@ -27525,6 +27615,44 @@ components:
type: object
additionalProperties:
type: object
Hash_String__PlaceOS__Model__Metadata__Interface___Nil_:
type: object
additionalProperties:
type: object
properties:
name:
type: string
description:
type: string
details:
type: object
parent_id:
type: string
nullable: true
schema_id:
type: string
nullable: true
editors:
type: array
items:
type: string
modified_by_id:
type: string
nullable: true
updated_at:
type: integer
format: Int64
created_at:
type: integer
format: Int64
required:
- name
- description
- details
- editors
- updated_at
- created_at
nullable: true
PlaceOS__Api__Metadata__Children:
type: object
properties:
Expand Down Expand Up @@ -28987,6 +29115,9 @@ components:
type: integer
format: Int64
nullable: true
launch_on_execute:
type: boolean
nullable: true
control_system_id:
type: string
nullable: true
Expand Down Expand Up @@ -31722,6 +31853,9 @@ components:
type: integer
format: Int64
nullable: true
launch_on_execute:
type: boolean
nullable: true
control_system_id:
type: string
nullable: true
Expand Down
2 changes: 1 addition & 1 deletion shard.lock
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ shards:

placeos-models:
git: https://github.com/placeos/models.git
version: 9.82.0
version: 9.82.1

placeos-resource:
git: https://github.com/place-labs/resource.git
Expand Down
78 changes: 78 additions & 0 deletions spec/controllers/metadata_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,84 @@ module PlaceOS::Api
end
end

describe "GET /metadata/:name/bulk" do
it "fetches metadata with specific name for multiple resources" do
# Create multiple zones with the same metadata name
zone1 = Model::Generator.zone.save!
zone2 = Model::Generator.zone.save!
zone3 = Model::Generator.zone.save!

zone1_id = zone1.id.as(String)
zone2_id = zone2.id.as(String)
zone3_id = zone3.id.as(String)

# Create metadata with name "settings" for each zone
meta1 = Model::Generator.metadata(name: "settings", parent: zone1_id).save!
meta2 = Model::Generator.metadata(name: "settings", parent: zone2_id).save!
# zone3 intentionally has no metadata

result = client.get(
path: "#{Metadata.base_route}/settings/bulk?parent_ids=#{zone1_id},#{zone2_id},#{zone3_id}",
headers: Spec::Authentication.headers,
)

result.success?.should be_true
metadata = Hash(String, Model::Metadata::Interface?).from_json(result.body)

metadata.size.should eq 3
metadata[zone1_id].should_not be_nil
metadata[zone1_id].not_nil!.name.should eq "settings"
metadata[zone2_id].should_not be_nil
metadata[zone2_id].not_nil!.name.should eq "settings"
metadata[zone3_id].should be_nil
end

it "returns empty metadata for resources without the specified metadata" do
zone1 = Model::Generator.zone.save!
zone2 = Model::Generator.zone.save!

zone1_id = zone1.id.as(String)
zone2_id = zone2.id.as(String)

# Only zone1 has the metadata
Model::Generator.metadata(name: "config", parent: zone1_id).save!

result = client.get(
path: "#{Metadata.base_route}/config/bulk?parent_ids=#{zone1_id},#{zone2_id}",
headers: Spec::Authentication.headers,
)

result.success?.should be_true
metadata = Hash(String, Model::Metadata::Interface?).from_json(result.body)

metadata.size.should eq 2
metadata[zone1_id].should_not be_nil
metadata[zone2_id].should be_nil
end

it "allows guests to fetch bulk metadata" do
_, scoped_headers = Spec::Authentication.authentication(scope: [PlaceOS::Model::UserJWT::Scope.new("guest", PlaceOS::Model::UserJWT::Scope::Access::Read)])

zone1 = Model::Generator.zone.save!
zone2 = Model::Generator.zone.save!

zone1_id = zone1.id.as(String)
zone2_id = zone2.id.as(String)

Model::Generator.metadata(name: "settings", parent: zone1_id).save!
Model::Generator.metadata(name: "settings", parent: zone2_id).save!

result = client.get(
path: "#{Metadata.base_route}/settings/bulk?parent_ids=#{zone1_id},#{zone2_id}",
headers: scoped_headers,
)

result.success?.should be_true
metadata = Hash(String, Model::Metadata::Interface?).from_json(result.body)
metadata.size.should eq 2
end
end

describe "GET /metadata/:id" do
it "shows control_system metadata" do
control_system = Model::Generator.control_system.save!
Expand Down
32 changes: 31 additions & 1 deletion src/placeos-rest-api/controllers/metadata.cr
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ module PlaceOS::Api
###############################################################################################

before_action :can_read, only: [:history]
before_action :can_read_guest, only: [:show, :children_metadata]
before_action :can_read_guest, only: [:show, :children_metadata, :bulk_fetch]
before_action :can_write, only: [:update, :destroy]

# Callbacks
Expand All @@ -30,6 +30,36 @@ module PlaceOS::Api

###############################################################################################

# Fetch metadata with a specific name for multiple resources
#
# Allows bulk retrieval of metadata across multiple parent entities
@[AC::Route::GET("/:name/bulk", converters: {parent_ids: ConvertStringArray})]
def bulk_fetch(
@[AC::Param::Info(name: "name", description: "the name of the metadata key to fetch", example: "settings")]
metadata_name : String,
@[AC::Param::Info(description: "comma-separated list of parent IDs (zones, systems, users, etc.)", example: "zone-1,zone-2,zone-3")]
parent_ids : Array(String),
) : Hash(String, ::PlaceOS::Model::Metadata::Interface?)
# Guest JWTs have restricted access
if user_token.guest_scope?
allowed_ids = guest_ids
# Ensure all requested IDs are in the guest's allowed list
unless parent_ids.all? { |id| allowed_ids.includes?(id) }
raise Error::Forbidden.new
end
end

# Build result hash with parent_id as key and metadata as value
result = {} of String => ::PlaceOS::Model::Metadata::Interface?

parent_ids.each do |parent_id|
metadata = ::PlaceOS::Model::Metadata.for(parent_id, metadata_name).first?
result[parent_id] = metadata ? metadata.interface : nil
end

result
end

# Fetch metadata for a model
#
# Filter for a specific metadata by name via `name` param
Expand Down
Loading