From bc77487d4f34a7af34f4cd7ff70dd228f8710851 Mon Sep 17 00:00:00 2001 From: devsjc <47188100+devsjc@users.noreply.github.com> Date: Thu, 11 Jun 2026 15:27:16 +0100 Subject: [PATCH 1/2] feat(backend): Simplify location ownership --- README.md | 24 +- cmd/main.go | 8 +- internal/server/dummy/adminserverimpl.go | 175 ----- internal/server/dummy/dataserverimpl.go | 11 + internal/server/postgres/adminserverimpl.go | 572 ---------------- .../server/postgres/adminserverimpl_test.go | 631 ------------------ internal/server/postgres/dataserverimpl.go | 83 ++- .../server/postgres/dataserverimpl_test.go | 93 +++ internal/server/postgres/package_test.go | 4 - .../sql/migrations/00008_simple_iam.sql | 93 +++ internal/server/postgres/sql/queries/iam.sql | 196 ------ .../server/postgres/sql/queries/locations.sql | 207 +++--- proto/ocf/dp/dp-admin.messages.proto | 213 ------ proto/ocf/dp/dp-admin.service.proto | 28 - proto/ocf/dp/dp-data.messages.proto | 23 +- proto/ocf/dp/dp-data.service.proto | 3 + proto/ocf/dp/dp.rules.proto | 4 +- 17 files changed, 422 insertions(+), 1946 deletions(-) delete mode 100644 internal/server/dummy/adminserverimpl.go delete mode 100644 internal/server/postgres/adminserverimpl.go delete mode 100644 internal/server/postgres/adminserverimpl_test.go create mode 100644 internal/server/postgres/sql/migrations/00008_simple_iam.sql delete mode 100644 internal/server/postgres/sql/queries/iam.sql delete mode 100644 proto/ocf/dp/dp-admin.messages.proto delete mode 100644 proto/ocf/dp/dp-admin.service.proto diff --git a/README.md b/README.md index db73286..9e5a699 100644 --- a/README.md +++ b/README.md @@ -574,7 +574,7 @@ Forecaster represents a generative source of predicted values. | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | -| energy_source_filter | [EnergySource](#ocf-dp-EnergySource) | optional | Optional filter to only return locations of a specific energy source. || location_type_filter | [LocationType](#ocf-dp-LocationType) | optional | Optional filter to only return locations of a specific location type. || location_uuids_filter | [string](#string) | repeated | Optional filter to only return locations from a given set. || user_oauth_id_filter | [string](#string) | optional | Optional filter to only return locations belonging to a specific user. || permission_filter | [Permission](#ocf-dp-Permission) | optional | Optional filter to only return locations for which the user has a specific permission. || enclosing_location_uuid_filter | [string](#string) | optional | Optional filter to only return locations enclosed within a specific location. || enclosed_location_uuid_filter | [string](#string) | optional | Optional filter to only return locations that enclose a specific location. || location_names_filter | [string](#string) | repeated | Optional filter to only return locations with a specific name. | +| energy_source_filter | [EnergySource](#ocf-dp-EnergySource) | optional | Optional filter to only return locations of a specific energy source. || location_type_filter | [LocationType](#ocf-dp-LocationType) | optional | Optional filter to only return locations of a specific location type. || location_uuids_filter | [string](#string) | repeated | Optional filter to only return locations from a given set. || organisation_id_filter | [string](#string) | optional | Optional filter to only return locations belonging to a specific organisation. || enclosing_location_uuid_filter | [string](#string) | optional | Optional filter to only return locations enclosed within a specific location. || enclosed_location_uuid_filter | [string](#string) | optional | Optional filter to only return locations that enclose a specific location. || location_names_filter | [string](#string) | repeated | Optional filter to only return locations with a specific name. |
ListLocationsResponse @@ -645,6 +645,20 @@ Forecaster represents a generative source of predicted values. | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | forecaster | [Forecaster](#ocf-dp-Forecaster) | | | +
+
UpdateLocationOwnerRequest + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| location_uuid | [string](#string) | | || new_organisation_id | [string](#string) | | | +
+
UpdateLocationOwnerResponse + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| location_uuid | [string](#string) | | || organisation_id | [string](#string) | | |
UpdateLocationRequest @@ -740,6 +754,14 @@ UpdateLocation modifies various attributes associated with a given location. _[UpdateLocationRequest](#ocf-dp-UpdateLocationRequest) / [UpdateLocationResponse](#ocf-dp-UpdateLocationResponse)_ + + +#### UpdateLocationOwner + +UpdateLocationOwner changes the ownership of a location. + +_[UpdateLocationOwnerRequest](#ocf-dp-UpdateLocationOwnerRequest) / [UpdateLocationOwnerResponse](#ocf-dp-UpdateLocationOwnerResponse)_ + #### ListLocations diff --git a/cmd/main.go b/cmd/main.go index 2ef043e..1e2f916 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -56,9 +56,8 @@ func main() { databaseUrl := strings.Trim(conf.GetString("dburl"), "\"") var ( - dataServerImpl pb.DataPlatformDataServiceServer - adminServerImpl pb.DataPlatformAdministrationServiceServer - s *grpc.Server + dataServerImpl pb.DataPlatformDataServiceServer + s *grpc.Server ) // Create a validator to use with protovalidate interceptor @@ -71,7 +70,6 @@ func main() { log.Warn().Msg("running in test mode with fake data. Not for production use") dataServerImpl = dbdy.NewDataPlatformDataServerImpl() - adminServerImpl = dbdy.NewDataPlatformAdministrationServiceServerImpl() // For a dummy-backed server, just validate requests s = grpc.NewServer( @@ -85,7 +83,6 @@ func main() { logInterceptor := ix.NewLoggingInterceptor() txInterceptor := ix.NewTransactionInterceptor(databaseUrl, dbpg.Migrations) dataServerImpl = dbpg.NewDataPlatformDataServiceServerImpl() - adminServerImpl = dbpg.NewDataPlatformAdministrationServiceServerImpl() // For a postgres-backed server, validate requests and manage database transactions s = grpc.NewServer( @@ -109,7 +106,6 @@ func main() { // Create the GRPC server // * Add an interceptor for request validation pb.RegisterDataPlatformDataServiceServer(s, dataServerImpl) - pb.RegisterDataPlatformAdministrationServiceServer(s, adminServerImpl) grpc_health_v1.RegisterHealthServer(s, health.NewServer()) reflection.Register(s) diff --git a/internal/server/dummy/adminserverimpl.go b/internal/server/dummy/adminserverimpl.go deleted file mode 100644 index a2e605a..0000000 --- a/internal/server/dummy/adminserverimpl.go +++ /dev/null @@ -1,175 +0,0 @@ -// Package postgres defines a server implementation for the DataPlatformServiceServer. -// This implementation is backed by a PostgreSQL database. -// -// Functions and structs for connecting to the database are generated from SQL using -// the sqlc library, whilst the Server interface that is being implemented comes from -// the top-level proto definitions. -package dummy - -import ( - "context" - "time" - - "github.com/google/uuid" - "google.golang.org/protobuf/types/known/structpb" - "google.golang.org/protobuf/types/known/timestamppb" - - pb "github.com/openclimatefix/data-platform/internal/gen/ocf/dp" -) - -// --- Server Implementation ---------------------------------------------------------------------- - -func NewDataPlatformAdministrationServiceServerImpl() *DataPlatformAdministrationServiceServerImpl { - return &DataPlatformAdministrationServiceServerImpl{} -} - -type DataPlatformAdministrationServiceServerImpl struct{} - -func (d *DataPlatformAdministrationServiceServerImpl) DeleteOrganisation( - ctx context.Context, - req *pb.DeleteOrganisationRequest, -) (*pb.DeleteOrganisationResponse, error) { - return &pb.DeleteOrganisationResponse{}, nil -} - -// CreateLocationPolicyGroup implements dp.DataPlatformAdministrationServiceServer. -func (d *DataPlatformAdministrationServiceServerImpl) CreateLocationPolicyGroup( - ctx context.Context, - req *pb.CreateLocationPolicyGroupRequest, -) (*pb.CreateLocationPolicyGroupResponse, error) { - return &pb.CreateLocationPolicyGroupResponse{ - LocationPolicyGroupId: uuid.New().String(), - Name: req.Name, - }, nil -} - -// GetLocationPolicyGroup implements dp.DataPlatformAdministrationServiceServer. -func (d *DataPlatformAdministrationServiceServerImpl) GetLocationPolicyGroup( - ctx context.Context, - req *pb.GetLocationPolicyGroupRequest, -) (*pb.GetLocationPolicyGroupResponse, error) { - return &pb.GetLocationPolicyGroupResponse{ - LocationPolicyGroupId: uuid.New().String(), - Name: "Dummy Location Policy Group", - LocationPolicies: []*pb.LocationPolicy{ - { - LocationId: uuid.New().String(), - EnergySource: pb.EnergySource_ENERGY_SOURCE_SOLAR, - Permission: pb.Permission_PERMISSION_READ, - }, - }, - }, nil -} - -// AddLocationPoliciesToGroup implements dp.DataPlatformAdministrationServiceServer. -func (d *DataPlatformAdministrationServiceServerImpl) AddLocationPoliciesToGroup( - context.Context, - *pb.AddLocationPoliciesToGroupRequest, -) (*pb.AddLocationPoliciesToGroupResponse, error) { - return &pb.AddLocationPoliciesToGroupResponse{}, nil -} - -// RemoveLocationPoliciesFromGroup implements dp.DataPlatformAdministrationServiceServer. -func (d *DataPlatformAdministrationServiceServerImpl) RemoveLocationPoliciesFromGroup( - context.Context, - *pb.RemoveLocationPoliciesFromGroupRequest, -) (*pb.RemoveLocationPoliciesFromGroupResponse, error) { - return &pb.RemoveLocationPoliciesFromGroupResponse{}, nil -} - -// GetUser implements dp.DataPlatformAdministrationServiceServer. -func (d *DataPlatformAdministrationServiceServerImpl) GetUser( - ctx context.Context, - req *pb.GetUserRequest, -) (*pb.GetUserResponse, error) { - return &pb.GetUserResponse{ - UserId: uuid.New().String(), - OauthId: req.OauthId, - Organisation: "Dummy Organisation", - LocationPolicyGroups: []string{"DUMMY_POLICY_GROUP_ID_1", "DUMMY_POLICY_GROUP_ID_2"}, - CreatedAt: timestamppb.New(time.Now().UTC()), - Metadata: &structpb.Struct{}, - }, nil -} - -// CreateUser implements dp.DataPlatformAdministrationServiceServer. -func (d *DataPlatformAdministrationServiceServerImpl) CreateUser( - ctx context.Context, - req *pb.CreateUserRequest, -) (*pb.CreateUserResponse, error) { - return &pb.CreateUserResponse{ - UserId: uuid.New().String(), - OauthId: req.OauthId, - }, nil -} - -// DeleteUser implements dp.DataPlatformAdministrationServiceServer. -func (d *DataPlatformAdministrationServiceServerImpl) DeleteUser( - context.Context, - *pb.DeleteUserRequest, -) (*pb.DeleteUserResponse, error) { - return &pb.DeleteUserResponse{}, nil -} - -// CreateOrganisation implements dp.DataPlatformAdministrationServiceServer. -func (d *DataPlatformAdministrationServiceServerImpl) CreateOrganisation( - ctx context.Context, - req *pb.CreateOrganisationRequest, -) (*pb.CreateOrganisationResponse, error) { - return &pb.CreateOrganisationResponse{ - OrgId: uuid.New().String(), - OrgName: req.OrgName, - }, nil -} - -// GetOrganisation implements dp.DataPlatformAdministrationServiceServer. -func (d *DataPlatformAdministrationServiceServerImpl) GetOrganisation( - ctx context.Context, - req *pb.GetOrganisationRequest, -) (*pb.GetOrganisationResponse, error) { - return &pb.GetOrganisationResponse{ - OrgId: uuid.New().String(), - OrgName: req.OrgName, - Metadata: &structpb.Struct{}, - CreatedAt: timestamppb.New(time.Now().UTC()), - LocationPolicyGroups: []string{"DUMMY_POLICY_GROUP_ID_1", "DUMMY_POLICY_GROUP_ID_2"}, - UserOauthIds: []string{uuid.New().String()}, - }, nil -} - -// AddLocationPolicyGroupToOrganisation implements dp.DataPlatformAdministrationServiceServer. -func (d *DataPlatformAdministrationServiceServerImpl) AddLocationPolicyGroupToOrganisation( - context.Context, - *pb.AddLocationPolicyGroupToOrganisationRequest, -) (*pb.AddLocationPolicyGroupToOrganisationResponse, error) { - return &pb.AddLocationPolicyGroupToOrganisationResponse{}, nil -} - -// AddUserToOrganisation implements dp.DataPlatformAdministrationServiceServer. -func (d *DataPlatformAdministrationServiceServerImpl) AddUserToOrganisation( - context.Context, - *pb.AddUserToOrganisationRequest, -) (*pb.AddUserToOrganisationResponse, error) { - return &pb.AddUserToOrganisationResponse{}, nil -} - -// RemoveLocationPolicyGroupFromOrganisation implements dp.DataPlatformAdministrationServiceServer. -func (d *DataPlatformAdministrationServiceServerImpl) RemoveLocationPolicyGroupFromOrganisation( - context.Context, - *pb.RemoveLocationPolicyGroupFromOrganisationRequest, -) (*pb.RemoveLocationPolicyGroupFromOrganisationResponse, error) { - return &pb.RemoveLocationPolicyGroupFromOrganisationResponse{}, nil -} - -// RemoveUserFromOrganisation implements dp.DataPlatformAdministrationServiceServer. -func (d *DataPlatformAdministrationServiceServerImpl) RemoveUserFromOrganisation( - context.Context, - *pb.RemoveUserFromOrganisationRequest, -) (*pb.RemoveUserFromOrganisationResponse, error) { - return &pb.RemoveUserFromOrganisationResponse{}, nil -} - -// Compile-time check to ensure the interface is implemented fully. -var _ pb.DataPlatformAdministrationServiceServer = (*DataPlatformAdministrationServiceServerImpl)( - nil, -) diff --git a/internal/server/dummy/dataserverimpl.go b/internal/server/dummy/dataserverimpl.go index 646271a..c93146e 100644 --- a/internal/server/dummy/dataserverimpl.go +++ b/internal/server/dummy/dataserverimpl.go @@ -314,6 +314,17 @@ func (d *DataPlatformDataServiceServerImpl) UpdateLocation( }, nil } +// UpdateLocationOwner implements [dp.DataPlatformDataServiceServer]. +func (d *DataPlatformDataServiceServerImpl) UpdateLocationOwner( + ctx context.Context, + req *pb.UpdateLocationOwnerRequest, +) (*pb.UpdateLocationOwnerResponse, error) { + return &pb.UpdateLocationOwnerResponse{ + LocationUuid: uuid.New().String(), + OrganisationId: req.NewOrganisationId, + }, nil +} + // CreateObservations implements dp.DataPlatformDataServiceServer. func (d *DataPlatformDataServiceServerImpl) CreateObservations( ctx context.Context, diff --git a/internal/server/postgres/adminserverimpl.go b/internal/server/postgres/adminserverimpl.go deleted file mode 100644 index 97fa5eb..0000000 --- a/internal/server/postgres/adminserverimpl.go +++ /dev/null @@ -1,572 +0,0 @@ -// Package postgres defines server implementation for the DataPlatformServiceServer. -// This implementation is backed by a PostgreSQL database. -// -// Functions and structs for connecting to the database are generated from SQL using -// the sqlc library, whilst the Server interface that is being implemented comes from -// the top-level proto definitions. -// -// NOTE: I am happy to use MustParse for the UUID handling here as the validation middleware -// is handling uuid checks upstream. -package postgres - -import ( - "context" - - "github.com/google/uuid" - "github.com/rs/zerolog" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/types/known/timestamppb" - - pb "github.com/openclimatefix/data-platform/internal/gen/ocf/dp" - ix "github.com/openclimatefix/data-platform/internal/interceptors" - db "github.com/openclimatefix/data-platform/internal/server/postgres/gen" -) - -// --- Server Implementation ---------------------------------------------------------------------- - -func NewDataPlatformAdministrationServiceServerImpl() *DataPlatformAdministrationServiceServerImpl { - return &DataPlatformAdministrationServiceServerImpl{} -} - -// DataPlatformAdministrationServiceServerImpl implements the pb.DataPlatformDataServiceServer interface. -// It requires the database transaction for the request to be set in the context. -// It also expects a zerolog logger to be set in the context. -type DataPlatformAdministrationServiceServerImpl struct{} - -func (d *DataPlatformAdministrationServiceServerImpl) DeleteOrganisation( - ctx context.Context, - req *pb.DeleteOrganisationRequest, -) (*pb.DeleteOrganisationResponse, error) { - l := zerolog.Ctx(ctx) - querier := db.New(ix.GetTxFromContext(ctx)) - - dobnParams := db.DeleteOrgByNameParams{ - OrgName: req.OrgName, - } - - err := querier.DeleteOrgByName(ctx, dobnParams) - if err != nil { - l.Error().Err(err).Msgf("querier.DeleteOrgByName(%+v)", dobnParams) - - return nil, status.Error( - codes.Internal, - "Encountered database error. Check application logs.", - ) - } - - return &pb.DeleteOrganisationResponse{}, nil -} - -func (d *DataPlatformAdministrationServiceServerImpl) CreateLocationPolicyGroup( - ctx context.Context, - req *pb.CreateLocationPolicyGroupRequest, -) (*pb.CreateLocationPolicyGroupResponse, error) { - l := zerolog.Ctx(ctx) - querier := db.New(ix.GetTxFromContext(ctx)) - - clpgParams := db.CreateLocationPolicyGroupParams{ - LocationPolicyGroupName: req.Name, - } - - dbGroup, err := querier.CreateLocationPolicyGroup(ctx, clpgParams) - if err != nil { - l.Error().Err(err).Msg("Error creating location policy group") - - return nil, status.Error( - codes.Internal, - "Encountered database error", - ) - } - - l.Debug(). - Str("dp.loc_policygroup.name", dbGroup.LocationPolicyGroupName). - Str("dp.loc_policygroup.uuid", dbGroup.LocationPolicyGroupUuid.String()). - Msg("created location policy group") - - return &pb.CreateLocationPolicyGroupResponse{ - LocationPolicyGroupId: dbGroup.LocationPolicyGroupUuid.String(), - Name: dbGroup.LocationPolicyGroupName, - }, nil -} - -// CreateOrganisation implements dp.DataPlatformAdministrationServiceServer. -func (d *DataPlatformAdministrationServiceServerImpl) CreateOrganisation( - ctx context.Context, - req *pb.CreateOrganisationRequest, -) (*pb.CreateOrganisationResponse, error) { - l := zerolog.Ctx(ctx) - querier := db.New(ix.GetTxFromContext(ctx)) - - coprms := db.CreateOrgParams{ - OrgName: req.OrgName, - Metadata: req.Metadata, - } - - dbOrg, err := querier.CreateOrg(ctx, coprms) - if err != nil { - l.Error().Err(err).Msgf("querier.CreateOrg(%+v)", coprms) - - return nil, status.Error( - codes.InvalidArgument, - "Invalid Organisation request. Ensure name is unique and metadata is valid JSON.", - ) - } - - l.Debug(). - Str("dp.organisation.name", dbOrg.OrgName). - Str("dp.organisation.uuid", dbOrg.OrgUuid.String()). - Msg("created organisation") - - return &pb.CreateOrganisationResponse{ - OrgId: dbOrg.OrgUuid.String(), - OrgName: dbOrg.OrgName, - }, nil -} - -// CreateUser implements dp.DataPlatformAdministrationServiceServer. -func (d *DataPlatformAdministrationServiceServerImpl) CreateUser( - ctx context.Context, - req *pb.CreateUserRequest, -) (*pb.CreateUserResponse, error) { - l := zerolog.Ctx(ctx) - - querier := db.New(ix.GetTxFromContext(ctx)) - - goParams := db.GetOrgByNameParams{ - OrgName: req.Organisation, - } - - dbOrg, err := querier.GetOrgByName(ctx, goParams) - if err != nil { - l.Error().Err(err).Msgf("querier.GetOrgByName(%+v)", goParams) - - return nil, status.Errorf( - codes.NotFound, - "Organisation with name '%s' not found. Choose an existing organisation, "+ - "or create a new organisation before adding users to it.", - req.Organisation, - ) - } - - cuParams := db.CreateUserParams{ - OrgUuid: dbOrg.OrgUuid, - OauthID: req.OauthId, - Metadata: req.Metadata, - } - - dbUser, err := querier.CreateUser(ctx, cuParams) - if err != nil { - l.Error().Err(err).Msgf("querier.CreateUser(%+v)", cuParams) - - return nil, status.Error( - codes.InvalidArgument, - "Invalid User request. Ensure OAuth ID is of the correct format and metadata is valid JSON.", - ) - } - - l.Debug(). - Str("dp.user.oauthid", dbUser.OauthID). - Str("dp.user.uuid", dbUser.UserUuid.String()). - Str("dp.user.org_name", dbOrg.OrgName). - Msg("created user") - - return &pb.CreateUserResponse{ - UserId: dbUser.UserUuid.String(), - OauthId: dbUser.OauthID, - }, nil -} - -// DeleteUser implements dp.DataPlatformAdministrationServiceServer. -func (d *DataPlatformAdministrationServiceServerImpl) DeleteUser( - ctx context.Context, - req *pb.DeleteUserRequest, -) (*pb.DeleteUserResponse, error) { - l := zerolog.Ctx(ctx) - querier := db.New(ix.GetTxFromContext(ctx)) - - userUuid := uuid.MustParse(req.UserId) - duParams := db.DeleteUserParams{ - UserUuid: userUuid, - } - - err := querier.DeleteUser(ctx, duParams) - if err != nil { - l.Error().Err(err).Msgf("querier.DeleteUser(%+v)", duParams) - - return nil, status.Errorf( - codes.NotFound, - "User with ID '%s' not found", - req.UserId, - ) - } - - return &pb.DeleteUserResponse{}, nil -} - -// GetLocationPolicyGroup implements dp.DataPlatformAdministrationServiceServer. -func (d *DataPlatformAdministrationServiceServerImpl) GetLocationPolicyGroup( - ctx context.Context, - req *pb.GetLocationPolicyGroupRequest, -) (*pb.GetLocationPolicyGroupResponse, error) { - l := zerolog.Ctx(ctx) - querier := db.New(ix.GetTxFromContext(ctx)) - - gprms := db.GetLocationPolicyGroupByNameParams{ - LocationPolicyGroupName: req.LocationPolicyGroupName, - } - - dbGroup, err := querier.GetLocationPolicyGroupByName(ctx, gprms) - if err != nil { - l.Error().Err(err).Msgf("querier.GetLocationPolicyGroupByName(%+v)", gprms) - - return nil, status.Errorf( - codes.NotFound, - "No location policy group found with name '%s'", - req.LocationPolicyGroupName, - ) - } - - llpParams := db.ListLocationPoliciesByGroupParams{ - LocationPolicyGroupUuid: dbGroup.LocationPolicyGroupUuid, - } - - dbPolicies, err := querier.ListLocationPoliciesByGroup(ctx, llpParams) - if err != nil { - l.Error().Err(err).Msgf("querier.ListLocationPoliciesByGroup(%+v)", llpParams) - - return nil, status.Errorf( - codes.NotFound, - "No location policy group found with ID '%s'", - dbGroup.LocationPolicyGroupUuid, - ) - } - - policies := make([]*pb.LocationPolicy, len(dbPolicies)) - for i, p := range dbPolicies { - policies[i] = &pb.LocationPolicy{ - LocationId: p.GeometryUuid.String(), - EnergySource: pb.EnergySource(p.SourceTypeID), - Permission: pb.Permission(p.PermissionID), - } - } - - l.Debug(). - Str("dp.loc_policygroup.name", dbGroup.LocationPolicyGroupName). - Str("dp.loc_policygroup.uuid", dbGroup.LocationPolicyGroupUuid.String()). - Int("dp.loc_policygroup.count", len(policies)). - Msg("found location policy group") - - return &pb.GetLocationPolicyGroupResponse{ - LocationPolicyGroupId: dbGroup.LocationPolicyGroupUuid.String(), - Name: dbGroup.LocationPolicyGroupName, - LocationPolicies: policies, - }, nil -} - -// AddLocationPoliciesToGroup implements dp.DataPlatformAdministrationServiceServer. -func (d *DataPlatformAdministrationServiceServerImpl) AddLocationPoliciesToGroup( - ctx context.Context, - req *pb.AddLocationPoliciesToGroupRequest, -) (*pb.AddLocationPoliciesToGroupResponse, error) { - l := zerolog.Ctx(ctx) - querier := db.New(ix.GetTxFromContext(ctx)) - - for _, p := range req.LocationPolicies { - locUuid := uuid.MustParse(p.LocationId) - apParams := db.AddLocationPolicesToGroupParams{ - PermissionID: int16(p.Permission), - SourceTypeID: int16(p.EnergySource), - LocationPolicyGroupName: req.LocationPolicyGroupName, - GeometryUuids: []uuid.UUID{locUuid}, - } - - err := querier.AddLocationPolicesToGroup(ctx, apParams) - if err != nil { - l.Error().Err(err).Msgf("querier.AddLocationPolicesToGroup(%+v)", apParams) - - return nil, status.Errorf( - codes.Internal, - "No location policy group found with name '%s'", - req.LocationPolicyGroupName, - ) - } - } - - l.Debug(). - Str("dp.loc_policygroup.name", req.LocationPolicyGroupName). - Int("dp.loc_policygroup.added_count", len(req.LocationPolicies)). - Msg("added location policies to group") - - return &pb.AddLocationPoliciesToGroupResponse{}, nil -} - -// RemoveLocationPoliciesFromGroup implements dp.DataPlatformAdministrationServiceServer. -func (d *DataPlatformAdministrationServiceServerImpl) RemoveLocationPoliciesFromGroup( - ctx context.Context, - req *pb.RemoveLocationPoliciesFromGroupRequest, -) (*pb.RemoveLocationPoliciesFromGroupResponse, error) { - l := zerolog.Ctx(ctx) - querier := db.New(ix.GetTxFromContext(ctx)) - - for _, p := range req.LocationPolicies { - locUuid := uuid.MustParse(p.LocationId) - rpParams := db.RemoveLocationPoliciesFromGroupParams{ - PermissionID: int16(p.Permission), - SourceTypeID: int16(p.EnergySource), - LocationPolicyGroupName: req.LocationPolicyGroupName, - GeometryUuid: locUuid, - } - - err := querier.RemoveLocationPoliciesFromGroup(ctx, rpParams) - if err != nil { - l.Error().Err(err).Msgf("querier.RemoveLocationPoliciesFromGroup(%+v)", rpParams) - - return nil, status.Errorf( - codes.Internal, - "No location policy group found with name '%s'", - req.LocationPolicyGroupName, - ) - } - } - - l.Debug(). - Str("dp.loc_policygroup.name", req.LocationPolicyGroupName). - Int("dp.loc_policygroup.removed_count", len(req.LocationPolicies)). - Msg("removed location policies from group") - - return &pb.RemoveLocationPoliciesFromGroupResponse{}, nil -} - -// GetOrganisation implements dp.DataPlatformAdministrationServiceServer. -func (d *DataPlatformAdministrationServiceServerImpl) GetOrganisation( - ctx context.Context, - req *pb.GetOrganisationRequest, -) (*pb.GetOrganisationResponse, error) { - l := zerolog.Ctx(ctx) - querier := db.New(ix.GetTxFromContext(ctx)) - - goParams := db.GetOrgByNameParams{ - OrgName: req.OrgName, - } - - dbOrg, err := querier.GetOrgByName(ctx, goParams) - if err != nil { - l.Error().Err(err).Msgf("querier.GetOrgByName(%+v)", goParams) - - return nil, status.Errorf( - codes.NotFound, - "Organisation with name '%s' not found", - req.OrgName, - ) - } - - l.Debug(). - Str("dp.organisation.name", dbOrg.OrgName). - Str("dp.organisation.uuid", dbOrg.OrgUuid.String()). - Msg("found organisation") - - return &pb.GetOrganisationResponse{ - OrgId: dbOrg.OrgUuid.String(), - OrgName: dbOrg.OrgName, - Metadata: dbOrg.Metadata, - CreatedAt: timestamppb.New(dbOrg.CreatedAtUtc.Time), - LocationPolicyGroups: dbOrg.LocationPolicyGroupNames, - UserOauthIds: dbOrg.OauthIds, - }, nil -} - -// GetUser implements dp.DataPlatformAdministrationServiceServer. -func (d *DataPlatformAdministrationServiceServerImpl) GetUser( - ctx context.Context, - req *pb.GetUserRequest, -) (*pb.GetUserResponse, error) { - l := zerolog.Ctx(ctx) - querier := db.New(ix.GetTxFromContext(ctx)) - - guParams := db.GetUserByOAuthIDParams{ - OauthID: req.OauthId, - } - - dbUser, err := querier.GetUserByOAuthID(ctx, guParams) - if err != nil { - l.Error().Err(err).Msgf("querier.GetUserByOAuthID(%+v)", guParams) - - return nil, status.Errorf( - codes.NotFound, - "User with OAuth ID '%s' not found", - req.OauthId, - ) - } - - goParams := db.GetOrgByNameParams{ - OrgName: dbUser.OrgName, - } - - dbOrg, err := querier.GetOrgByName(ctx, goParams) - if err != nil { - l.Error().Err(err).Msgf("querier.GetOrgByName(%+v)", goParams) - - return nil, status.Errorf( - codes.Internal, - "Error fetching organisation for user with OAuth ID '%s'", - req.OauthId, - ) - } - - l.Debug(). - Str("dp.user.oauthid", dbUser.OauthID). - Str("dp.user.uuid", dbUser.UserUuid.String()). - Str("dp.user.org_name", dbUser.OrgName). - Msg("found user") - - return &pb.GetUserResponse{ - UserId: dbUser.UserUuid.String(), - OauthId: dbUser.OauthID, - Organisation: dbUser.OrgName, - LocationPolicyGroups: dbOrg.LocationPolicyGroupNames, - CreatedAt: ×tamppb.Timestamp{}, - Metadata: dbUser.Metadata, - }, nil -} - -// AddLocationPolicyGroupToOrganisation implements dp.DataPlatformAdministrationServiceServer. -func (d *DataPlatformAdministrationServiceServerImpl) AddLocationPolicyGroupToOrganisation( - ctx context.Context, - req *pb.AddLocationPolicyGroupToOrganisationRequest, -) (*pb.AddLocationPolicyGroupToOrganisationResponse, error) { - l := zerolog.Ctx(ctx) - querier := db.New(ix.GetTxFromContext(ctx)) - - agprms := db.AddLocationPolicyGroupToOrgByNamesParams{ - OrgName: req.OrgName, - LocationPolicyGroupName: req.LocationPolicyGroupName, - } - - err := querier.AddLocationPolicyGroupToOrgByNames(ctx, agprms) - if err != nil { - l.Error().Err(err).Msgf("querier.AddLocationPolicyGroupToOrgByNames(%+v)", agprms) - - return nil, status.Errorf( - codes.Internal, - "Error adding location policy group '%s' to organisation '%s'. "+ - "Ensure organisation and location policy group exist.", - req.LocationPolicyGroupName, - req.OrgName, - ) - } - - l.Debug(). - Str("dp.organisation.name", req.OrgName). - Str("dp.loc_policygroup.name", req.LocationPolicyGroupName). - Msg("added location policy group to organisation") - - return &pb.AddLocationPolicyGroupToOrganisationResponse{}, nil -} - -// RemoveLocationPolicyGroupFromOrganisation implements dp.DataPlatformAdministrationServiceServer. -func (d *DataPlatformAdministrationServiceServerImpl) RemoveLocationPolicyGroupFromOrganisation( - ctx context.Context, - req *pb.RemoveLocationPolicyGroupFromOrganisationRequest, -) (*pb.RemoveLocationPolicyGroupFromOrganisationResponse, error) { - l := zerolog.Ctx(ctx) - querier := db.New(ix.GetTxFromContext(ctx)) - - rgprms := db.RemoveLocationPolicyGroupFromOrgByNamesParams{ - OrgName: req.OrgName, - LocationPolicyGroupName: req.LocationPolicyGroupName, - } - - err := querier.RemoveLocationPolicyGroupFromOrgByNames(ctx, rgprms) - if err != nil { - l.Error().Err(err).Msgf("querier.RemoveLocationPolicyGroupFromOrgByNames(%+v)", rgprms) - - return nil, status.Errorf( - codes.Internal, - "Error removing location policy group '%s' from organisation '%s'. "+ - "Ensure organisation and location policy group exist.", - req.LocationPolicyGroupName, - req.OrgName, - ) - } - - l.Debug(). - Str("dp.organisation.name", req.OrgName). - Str("dp.loc_policygroup.name", req.LocationPolicyGroupName). - Msg("removed location policy group from organisation") - - return &pb.RemoveLocationPolicyGroupFromOrganisationResponse{}, nil -} - -// AddUserToOrganisation implements dp.DataPlatformAdministrationServiceServer. -func (d *DataPlatformAdministrationServiceServerImpl) AddUserToOrganisation( - ctx context.Context, - req *pb.AddUserToOrganisationRequest, -) (*pb.AddUserToOrganisationResponse, error) { - l := zerolog.Ctx(ctx) - querier := db.New(ix.GetTxFromContext(ctx)) - - auprms := db.AddUserToOrgByOAuthIDAndNameParams{ - OrgName: req.OrgName, - OauthID: req.UserOauthId, - } - - err := querier.AddUserToOrgByOAuthIDAndName(ctx, auprms) - if err != nil { - l.Error().Err(err).Msgf("querier.AddUserToOrgByOAuthIDAndName(%+v)", auprms) - - return nil, status.Errorf( - codes.Internal, - "Error adding user with OAuth ID '%s' to organisation '%s'. "+ - "Ensure organisation and user exist.", - req.UserOauthId, - req.OrgName, - ) - } - - l.Debug(). - Str("dp.organisation.name", req.OrgName). - Str("dp.user.oauthid", req.UserOauthId). - Msg("added user to organisation") - - return &pb.AddUserToOrganisationResponse{}, nil -} - -// RemoveUserFromOrganisation implements dp.DataPlatformAdministrationServiceServer. -func (d *DataPlatformAdministrationServiceServerImpl) RemoveUserFromOrganisation( - ctx context.Context, - req *pb.RemoveUserFromOrganisationRequest, -) (*pb.RemoveUserFromOrganisationResponse, error) { - l := zerolog.Ctx(ctx) - querier := db.New(ix.GetTxFromContext(ctx)) - - ruprms := db.RemoveUserFromOrgByOAuthIDAndNameParams{ - OrgName: req.OrgName, - OauthID: req.UserOauthId, - } - - err := querier.RemoveUserFromOrgByOAuthIDAndName(ctx, ruprms) - if err != nil { - l.Error().Err(err).Msgf("querier.RemoveUserFromOrgByOAuthIDAndName(%+v)", ruprms) - - return nil, status.Errorf( - codes.Internal, - "Error removing user with OAuth ID '%s' from organisation '%s'. "+ - "Ensure organisation and user exist.", - req.UserOauthId, - req.OrgName, - ) - } - - l.Debug(). - Str("dp.organisation.name", req.OrgName). - Str("dp.user.oauthid", req.UserOauthId). - Msg("removed user from organisation") - - return &pb.RemoveUserFromOrganisationResponse{}, nil -} - -// Compile-time check to ensure the interface is implemented fully. -var _ pb.DataPlatformAdministrationServiceServer = (*DataPlatformAdministrationServiceServerImpl)( - nil, -) diff --git a/internal/server/postgres/adminserverimpl_test.go b/internal/server/postgres/adminserverimpl_test.go deleted file mode 100644 index 9cff6b9..0000000 --- a/internal/server/postgres/adminserverimpl_test.go +++ /dev/null @@ -1,631 +0,0 @@ -package postgres - -import ( - "fmt" - "strings" - "testing" - "time" - - "github.com/google/uuid" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/types/known/structpb" - "google.golang.org/protobuf/types/known/timestamppb" - - pb "github.com/openclimatefix/data-platform/internal/gen/ocf/dp" -) - -func TestCreateOrganisation(t *testing.T) { - metadata, err := structpb.NewStruct(map[string]any{"source": "test"}) - require.NoError(t, err) - - testCases := []struct { - name string - createReq *pb.CreateOrganisationRequest - }{ - { - name: "Should create organisation", - createReq: &pb.CreateOrganisationRequest{ - OrgName: "test_create_organisation_1", - Metadata: metadata, - }, - }, - { - name: "Shouldn't create organisation with duplicate name", - createReq: &pb.CreateOrganisationRequest{ - OrgName: "test_create_organisation_1", - Metadata: metadata, - }, - }, - { - name: "Shouldn't create organisation with empty name", - createReq: &pb.CreateOrganisationRequest{ - OrgName: "", - Metadata: metadata, - }, - }, - { - name: "Should create another organisation", - createReq: &pb.CreateOrganisationRequest{ - OrgName: "test_create_organisation_2", - Metadata: metadata, - }, - }, - { - name: "Should create organisation with empty metadata", - createReq: &pb.CreateOrganisationRequest{ - OrgName: "test_create_organisation_3", - Metadata: nil, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - resp, err := ac.CreateOrganisation(t.Context(), tc.createReq) - if strings.Split(tc.name, " ")[0] == "Shouldn't" { - require.Error(t, err) - } else { - require.NoError(t, err) - - // Read back the organisation - dbOrg, err := ac.GetOrganisation(t.Context(), &pb.GetOrganisationRequest{ - OrgName: resp.OrgName, - }) - require.NoError(t, err) - - require.Equal(t, tc.createReq.OrgName, dbOrg.OrgName) - require.Equal(t, tc.createReq.Metadata.AsMap(), dbOrg.Metadata.AsMap()) - } - }) - } -} - -func TestDeleteOrganisation(t *testing.T) { - metadata, err := structpb.NewStruct(map[string]any{"source": "test"}) - require.NoError(t, err) - - orgResp, err := ac.CreateOrganisation(t.Context(), &pb.CreateOrganisationRequest{ - OrgName: "test_delete_organisation_1", - Metadata: metadata, - }) - - require.NoError(t, err) - - testCases := []struct { - name string - deleteReq *pb.DeleteOrganisationRequest - }{ - { - name: "Should delete existing organisation", - deleteReq: &pb.DeleteOrganisationRequest{ - OrgName: orgResp.OrgName, - }, - }, - { - name: "Should handle deleting a non-existent organisation", - deleteReq: &pb.DeleteOrganisationRequest{ - OrgName: "non_existent_delete_organisation", - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - _, err := ac.DeleteOrganisation(t.Context(), tc.deleteReq) - if strings.Contains(tc.name, "Shouldn't") { - require.Error(t, err) - } else { - require.NoError(t, err) - _, err := ac.GetOrganisation(t.Context(), &pb.GetOrganisationRequest{ - OrgName: tc.deleteReq.OrgName, - }) - require.Error(t, err) - } - }) - } -} - -func TestAddRemoveLocationPolicyGroupToOrganisation(t *testing.T) { - orgResp, err := ac.CreateOrganisation(t.Context(), &pb.CreateOrganisationRequest{ - OrgName: "test_add_remove_location_policy_group_organisation", - }) - require.NoError(t, err) - lpResp, err := ac.CreateLocationPolicyGroup(t.Context(), &pb.CreateLocationPolicyGroupRequest{ - Name: "test_add_remove_location_policy_group_policy_group", - }) - require.NoError(t, err) - - testCases := []struct { - name string - addRequest *pb.AddLocationPolicyGroupToOrganisationRequest - removeRequest *pb.RemoveLocationPolicyGroupFromOrganisationRequest - expectedPolicyGroupCount int - }{ - { - name: "Should add location policy group to organisation", - addRequest: &pb.AddLocationPolicyGroupToOrganisationRequest{ - OrgName: orgResp.OrgName, - LocationPolicyGroupName: lpResp.Name, - }, - expectedPolicyGroupCount: 1, - }, - { - name: "Should handle adding duplicate location policy groups", - addRequest: &pb.AddLocationPolicyGroupToOrganisationRequest{ - OrgName: orgResp.OrgName, - LocationPolicyGroupName: lpResp.Name, - }, - expectedPolicyGroupCount: 1, - }, - { - name: "Should remove location policy group from organisation", - removeRequest: &pb.RemoveLocationPolicyGroupFromOrganisationRequest{ - OrgName: orgResp.OrgName, - LocationPolicyGroupName: lpResp.Name, - }, - expectedPolicyGroupCount: 0, - }, - { - name: "Should handle non-existent location policy group removal", - removeRequest: &pb.RemoveLocationPolicyGroupFromOrganisationRequest{ - OrgName: orgResp.OrgName, - LocationPolicyGroupName: "non_existent_policy_group", - }, - expectedPolicyGroupCount: 0, - }, - { - name: "Shouldn't add non-existent location policy group to organisation", - addRequest: &pb.AddLocationPolicyGroupToOrganisationRequest{ - OrgName: orgResp.OrgName, - LocationPolicyGroupName: "non_existent_policy_group", - }, - expectedPolicyGroupCount: 0, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - if tc.addRequest != nil { - _, err = ac.AddLocationPolicyGroupToOrganisation(t.Context(), tc.addRequest) - } - - if tc.removeRequest != nil { - _, err = ac.RemoveLocationPolicyGroupFromOrganisation(t.Context(), tc.removeRequest) - } - - if strings.Contains(tc.name, "Shouldn't") { - require.Error(t, err) - } else { - require.NoError(t, err) - // Read back the organisation - dbOrg, err := ac.GetOrganisation(t.Context(), &pb.GetOrganisationRequest{ - OrgName: orgResp.OrgName, - }) - require.NoError(t, err) - require.Equal(t, tc.expectedPolicyGroupCount, len(dbOrg.LocationPolicyGroups)) - } - }) - } -} - -func TestAddRemoveLocationPoliciesFromGroup(t *testing.T) { - lResp, err := dc.CreateLocation(t.Context(), &pb.CreateLocationRequest{ - LocationName: "test_add_remove_location_policies_location", - EnergySource: pb.EnergySource_ENERGY_SOURCE_SOLAR, - GeometryWkt: "POINT(8.3 33.44)", - EffectiveCapacityWatts: 5000, - LocationType: pb.LocationType_LOCATION_TYPE_SITE, - Metadata: &structpb.Struct{}, - ValidFromUtc: timestamppb.New(time.Now().UTC().Add(-1 * time.Hour)), - }) - require.NoError(t, err) - lpResp, err := ac.CreateLocationPolicyGroup(t.Context(), &pb.CreateLocationPolicyGroupRequest{ - Name: "test_add_remove_location_policies_policy_group", - }) - require.NoError(t, err) - - testCases := []struct { - name string - addRequest *pb.AddLocationPoliciesToGroupRequest - removeRequest *pb.RemoveLocationPoliciesFromGroupRequest - expectedPolicyCount int - }{ - { - name: "Should add location policies to group", - addRequest: &pb.AddLocationPoliciesToGroupRequest{ - LocationPolicyGroupName: lpResp.Name, - LocationPolicies: []*pb.LocationPolicy{ - { - LocationId: lResp.LocationUuid, - EnergySource: pb.EnergySource_ENERGY_SOURCE_SOLAR, - Permission: pb.Permission_PERMISSION_READ, - }, - }, - }, - expectedPolicyCount: 1, - }, - { - name: "Should handle adding duplicate location policies", - addRequest: &pb.AddLocationPoliciesToGroupRequest{ - LocationPolicyGroupName: lpResp.Name, - LocationPolicies: []*pb.LocationPolicy{ - { - LocationId: lResp.LocationUuid, - EnergySource: pb.EnergySource_ENERGY_SOURCE_SOLAR, - Permission: pb.Permission_PERMISSION_READ, - }, - }, - }, - expectedPolicyCount: 1, - }, - // { - // name: "Shouldn't add location policy referencing non-existent source", - // addRequest: &pb.AddLocationPoliciesToGroupRequest{ - // LocationPolicyGroupName: lpResp.Name, - // LocationPolicies: []*pb.LocationPolicy{ - // { - // LocationId: lResp.LocationUuid, - // EnergySource: pb.EnergySource_ENERGY_SOURCE_WIND, - // Permission: pb.Permission_PERMISSION_READ, - // }, - // }, - // }, - // expectedPolicyCount: 1, - // }, - { - name: "Shouldn't add location policy referencing non-existent location", - addRequest: &pb.AddLocationPoliciesToGroupRequest{ - LocationPolicyGroupName: lpResp.Name, - LocationPolicies: []*pb.LocationPolicy{ - { - LocationId: uuid.New().String(), - EnergySource: pb.EnergySource_ENERGY_SOURCE_SOLAR, - Permission: pb.Permission_PERMISSION_READ, - }, - }, - }, - expectedPolicyCount: 1, - }, - { - name: "Should remove location policies from group", - removeRequest: &pb.RemoveLocationPoliciesFromGroupRequest{ - LocationPolicyGroupName: lpResp.Name, - LocationPolicies: []*pb.LocationPolicy{ - { - LocationId: lResp.LocationUuid, - EnergySource: pb.EnergySource_ENERGY_SOURCE_SOLAR, - Permission: pb.Permission_PERMISSION_READ, - }, - }, - }, - expectedPolicyCount: 0, - }, - { - name: "Should handle non-existent location policy removal", - removeRequest: &pb.RemoveLocationPoliciesFromGroupRequest{ - LocationPolicyGroupName: lpResp.Name, - LocationPolicies: []*pb.LocationPolicy{ - { - LocationId: uuid.New().String(), - EnergySource: pb.EnergySource_ENERGY_SOURCE_SOLAR, - Permission: pb.Permission_PERMISSION_READ, - }, - }, - }, - expectedPolicyCount: 0, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - if tc.addRequest != nil { - _, err = ac.AddLocationPoliciesToGroup(t.Context(), tc.addRequest) - } - - if tc.removeRequest != nil { - _, err = ac.RemoveLocationPoliciesFromGroup(t.Context(), tc.removeRequest) - } - - if strings.Contains(tc.name, "Shouldn't") { - require.Error(t, err) - } else { - require.NoError(t, err) - // Read back the location policy group - dbLPG, err := ac.GetLocationPolicyGroup( - t.Context(), - &pb.GetLocationPolicyGroupRequest{ - LocationPolicyGroupName: lpResp.Name, - }, - ) - require.NoError(t, err) - require.Equal(t, tc.expectedPolicyCount, len(dbLPG.LocationPolicies)) - } - }) - } -} - -func TestCreateUser(t *testing.T) { - metadata, err := structpb.NewStruct(map[string]any{"source": "test"}) - require.NoError(t, err) - - orgResp, err := ac.CreateOrganisation(t.Context(), &pb.CreateOrganisationRequest{ - OrgName: "test_create_user_organisation", - Metadata: metadata, - }) - require.NoError(t, err) - - testCases := []struct { - name string - createReq *pb.CreateUserRequest - }{ - { - name: "Should create user", - createReq: &pb.CreateUserRequest{ - OauthId: "TEST_CREATE_USER_1", - Organisation: orgResp.OrgName, - Metadata: metadata, - }, - }, - { - name: "Shouldn't create user with duplicate oauth ID", - createReq: &pb.CreateUserRequest{ - OauthId: "TEST_CREATE_USER_1", - Organisation: orgResp.OrgName, - Metadata: metadata, - }, - }, - { - name: "Shouldn't create user with empty oauth ID", - createReq: &pb.CreateUserRequest{ - OauthId: "", - Organisation: orgResp.OrgName, - Metadata: metadata, - }, - }, - { - name: "Should create another user", - createReq: &pb.CreateUserRequest{ - OauthId: "TEST_CREATE_USER_2", - Organisation: orgResp.OrgName, - Metadata: metadata, - }, - }, - { - name: "Should create user with empty metadata", - createReq: &pb.CreateUserRequest{ - OauthId: "TEST_CREATE_USER_3", - Organisation: orgResp.OrgName, - Metadata: nil, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - _, err := ac.CreateUser(t.Context(), tc.createReq) - if strings.Split(tc.name, " ")[0] == "Shouldn't" { - require.Error(t, err) - } else { - require.NoError(t, err) - - // Read back the user - dbUser, err := ac.GetUser(t.Context(), &pb.GetUserRequest{ - OauthId: tc.createReq.OauthId, - }) - require.NoError(t, err) - - require.Equal(t, tc.createReq.OauthId, dbUser.OauthId) - require.Equal(t, tc.createReq.Organisation, dbUser.Organisation) - require.Equal(t, tc.createReq.Metadata.AsMap(), dbUser.Metadata.AsMap()) - } - }) - } -} - -func DeleteUser(t *testing.T) { - metadata, err := structpb.NewStruct(map[string]any{"source": "test"}) - require.NoError(t, err) - - orgResp, err := ac.CreateOrganisation(t.Context(), &pb.CreateOrganisationRequest{ - OrgName: "test_delete_user_organisation", - Metadata: metadata, - }) - require.NoError(t, err) - - createResp, err := ac.CreateUser(t.Context(), &pb.CreateUserRequest{ - OauthId: "TEST_DELETE_USER", - Organisation: orgResp.OrgName, - Metadata: metadata, - }) - require.NoError(t, err) - - testCases := []struct { - name string - deleteReq *pb.DeleteUserRequest - }{ - { - name: "Should delete existing user", - deleteReq: &pb.DeleteUserRequest{ - UserId: createResp.UserId, - }, - }, - { - name: "Shouldn't delete non-existent user", - deleteReq: &pb.DeleteUserRequest{ - UserId: "non_existent_user_id", - }, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - _, err := ac.DeleteUser(t.Context(), tc.deleteReq) - if strings.Split(tc.name, " ")[0] == "Shouldn't" { - require.Error(t, err) - } else { - require.NoError(t, err) - - // Try to read back the user - _, err := ac.GetUser(t.Context(), &pb.GetUserRequest{ - OauthId: "TEST_DELETE_USER", - }) - // Obviously should error here - require.Error(t, err) - } - }) - } -} - -func TestListLocationsIamFilters(t *testing.T) { - pivotTime := time.Now().Truncate(time.Minute) - - // Create a bunch of locations - var ( - locationUuids []string - locationPolicies []*pb.LocationPolicy - ) - - for i := range 5 { - resp, err := dc.CreateLocation(t.Context(), &pb.CreateLocationRequest{ - LocationName: fmt.Sprintf( - "test_list_locations_site_%02d", - i, - ), - GeometryWkt: fmt.Sprintf("POINT(-5.%d 51.%d)", i, i), - EffectiveCapacityWatts: uint64(1000000 + i*100), - EnergySource: pb.EnergySource_ENERGY_SOURCE_SOLAR, - LocationType: pb.LocationType_LOCATION_TYPE_GSP, - ValidFromUtc: timestamppb.New(pivotTime.Add(-time.Hour * 4)), - Metadata: &structpb.Struct{}, - }) - require.NoError(t, err) - - locationUuids = append(locationUuids, resp.LocationUuid) - locationPolicies = append(locationPolicies, &pb.LocationPolicy{ - LocationId: resp.LocationUuid, - EnergySource: pb.EnergySource_ENERGY_SOURCE_SOLAR, - Permission: pb.Permission_PERMISSION_WRITE, - }) - } - - // Create an organisation with a user and location policies that have write access on the locations - orgResp, err := ac.CreateOrganisation(t.Context(), &pb.CreateOrganisationRequest{ - OrgName: "test_list_locations_organisation", - }) - require.NoError(t, err) - user1Resp, err := ac.CreateUser(t.Context(), &pb.CreateUserRequest{ - OauthId: "TEST_LIST_LOCATIONS_USER", - Organisation: orgResp.OrgName, - }) - require.NoError(t, err) - lpResp, err := ac.CreateLocationPolicyGroup(t.Context(), &pb.CreateLocationPolicyGroupRequest{ - Name: "test_list_locations_policy_group_1", - }) - require.NoError(t, err) - _, err = ac.AddLocationPolicyGroupToOrganisation( - t.Context(), - &pb.AddLocationPolicyGroupToOrganisationRequest{ - OrgName: orgResp.OrgName, - LocationPolicyGroupName: lpResp.Name, - }, - ) - require.NoError(t, err) - _, err = ac.AddLocationPoliciesToGroup(t.Context(), &pb.AddLocationPoliciesToGroupRequest{ - LocationPolicyGroupName: lpResp.Name, - LocationPolicies: locationPolicies, - }) - require.NoError(t, err) - // Create an organisation with a user and one read policy - orgResp, err = ac.CreateOrganisation(t.Context(), &pb.CreateOrganisationRequest{ - OrgName: "test_list_locations_organisation_2", - }) - require.NoError(t, err) - user2Resp, err := ac.CreateUser(t.Context(), &pb.CreateUserRequest{ - OauthId: "TEST_LIST_LOCATIONS_USER_2", - Organisation: orgResp.OrgName, - }) - require.NoError(t, err) - lpResp, err = ac.CreateLocationPolicyGroup(t.Context(), &pb.CreateLocationPolicyGroupRequest{ - Name: "test_list_locations_policy_group_2", - }) - require.NoError(t, err) - _, err = ac.AddLocationPolicyGroupToOrganisation( - t.Context(), - &pb.AddLocationPolicyGroupToOrganisationRequest{ - OrgName: orgResp.OrgName, - LocationPolicyGroupName: lpResp.Name, - }, - ) - require.NoError(t, err) - _, err = ac.AddLocationPoliciesToGroup(t.Context(), &pb.AddLocationPoliciesToGroupRequest{ - LocationPolicyGroupName: lpResp.Name, - LocationPolicies: []*pb.LocationPolicy{ - { - LocationId: locationUuids[0], - EnergySource: pb.EnergySource_ENERGY_SOURCE_SOLAR, - Permission: pb.Permission_PERMISSION_READ, - }, - }, - }) - require.NoError(t, err) - - permissionFilter := new(pb.Permission) - *permissionFilter = pb.Permission_PERMISSION_READ - - // All tests in the table need to filter by the location uuids just created as the postgres - // container the tests are run against is reused across the tests for speed of unit testing. - // As such, it may contain more than just the locations created here, depending on the number of - // tests being run. - // TODO: This is a fairly minimal test suite, and I imagine there are plenty of edge cases that - // are not covered here. This purely covers the basic filtering functionality, and should by - // improved upon in future. - tests := []struct { - name string - req *pb.ListLocationsRequest - expectedCount int - }{ - { - name: "Should filter locations by user 1", - req: &pb.ListLocationsRequest{ - UserOauthIdFilter: &user1Resp.OauthId, - LocationUuidsFilter: locationUuids, - }, - expectedCount: 5, - }, - { - name: "Should filter locations by user 2", - req: &pb.ListLocationsRequest{ - UserOauthIdFilter: &user2Resp.OauthId, - LocationUuidsFilter: locationUuids, - }, - expectedCount: 1, - }, - { - name: "Should filter locations by permission", - req: &pb.ListLocationsRequest{ - PermissionFilter: permissionFilter, - LocationUuidsFilter: locationUuids, - }, - expectedCount: 1, - }, - { - name: "Should filter locations by user and permission", - req: &pb.ListLocationsRequest{ - UserOauthIdFilter: &user2Resp.OauthId, - PermissionFilter: permissionFilter, - LocationUuidsFilter: locationUuids, - }, - expectedCount: 1, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - resp, err := dc.ListLocations(t.Context(), tt.req) - if strings.Contains(tt.name, "Shouldn't") { - require.Error(t, err) - } else { - require.NoError(t, err) - require.Equal(t, tt.expectedCount, len(resp.Locations)) - } - }) - } -} diff --git a/internal/server/postgres/dataserverimpl.go b/internal/server/postgres/dataserverimpl.go index 7bb89b8..e8544fb 100644 --- a/internal/server/postgres/dataserverimpl.go +++ b/internal/server/postgres/dataserverimpl.go @@ -1505,6 +1505,44 @@ func (s *DataPlatformDataServiceServerImpl) UpdateLocation( }, nil } +func (s *DataPlatformDataServiceServerImpl) UpdateLocationOwner( + ctx context.Context, + req *pb.UpdateLocationOwnerRequest, +) (resp *pb.UpdateLocationOwnerResponse, err error) { + l := zerolog.Ctx(ctx) + + querier := db.New(ix.GetTxFromContext(ctx)) + + params := db.ReownGeometryParams{ + GeometryUuid: uuid.MustParse(req.LocationUuid), + NewOwningEntityExternalID: req.NewOrganisationId, + } + + dbGeom, err := querier.ReownGeometry(ctx, params) + if err != nil { + l.Err(err).Msgf("querier.ReownGeometry(%+v)", params) + return nil, status.Errorf(codes.InvalidArgument, "Invalid location UUID: %v", err) + } + + l.Debug(). + Str("dp.geometry.uuid", dbGeom.GeometryUuid.String()). + Str("dp.geometry.new_owner_org_id", req.NewOrganisationId). + Msg("updated location owner") + + err = querier.RefreshSourcesMaterializedView(ctx) + if err != nil { + l.Err(err).Msg("querier.RefreshSourcesMaterializedView()") + return nil, status.Error(codes.Internal, "Failed to update sources materialised view") + } + + l.Debug().Msg("refreshed sources materialised view") + + return &pb.UpdateLocationOwnerResponse{ + LocationUuid: dbGeom.GeometryUuid.String(), + OrganisationId: req.NewOrganisationId, + }, nil +} + func (s *DataPlatformDataServiceServerImpl) GetLocationsAsGeoJSON( ctx context.Context, req *pb.GetLocationsAsGeoJSONRequest, @@ -1744,12 +1782,6 @@ func (s *DataPlatformDataServiceServerImpl) ListLocations( parsedUuids[i] = uuid.MustParse(id) } - var permissionId *int16 - if req.PermissionFilter != nil { - pid := int16(req.PermissionFilter.Number()) - permissionId = &pid - } - var sourceTypeId *int16 if req.EnergySourceFilter != nil { stid := int16(req.EnergySourceFilter.Number()) @@ -1766,13 +1798,12 @@ func (s *DataPlatformDataServiceServerImpl) ListLocations( if req.EnclosingLocationUuidFilter != nil { llprms := db.ListSourcesAtTimestampWithinParams{ - OuterGeometryUuid: uuid.MustParse(*req.EnclosingLocationUuidFilter), - AtTimestampUtc: pgtype.Timestamp{Time: time.Now().UTC(), Valid: true}, - OauthID: req.UserOauthIdFilter, - GeometryUuids: parsedUuids, - PermissionID: permissionId, - SourceTypeID: sourceTypeId, - GeometryTypeID: locationTypeId, + OuterGeometryUuid: uuid.MustParse(*req.EnclosingLocationUuidFilter), + AtTimestampUtc: pgtype.Timestamp{Time: time.Now().UTC(), Valid: true}, + OwningEntityExternalID: req.OrganisationIdFilter, + GeometryUuids: parsedUuids, + SourceTypeID: sourceTypeId, + GeometryTypeID: locationTypeId, } glResp, err := querier.ListSourcesAtTimestampWithin(ctx, llprms) @@ -1796,13 +1827,12 @@ func (s *DataPlatformDataServiceServerImpl) ListLocations( } } else if req.EnclosedLocationUuidFilter != nil { llprms := db.ListSourcesAtTimestampWithoutParams{ - InnerGeometryUuid: uuid.MustParse(*req.EnclosedLocationUuidFilter), - AtTimestampUtc: pgtype.Timestamp{Time: time.Now().UTC(), Valid: true}, - OauthID: req.UserOauthIdFilter, - GeometryUuids: parsedUuids, - PermissionID: permissionId, - SourceTypeID: sourceTypeId, - GeometryTypeID: locationTypeId, + InnerGeometryUuid: uuid.MustParse(*req.EnclosedLocationUuidFilter), + AtTimestampUtc: pgtype.Timestamp{Time: time.Now().UTC(), Valid: true}, + OwningEntityExternalID: req.OrganisationIdFilter, + GeometryUuids: parsedUuids, + SourceTypeID: sourceTypeId, + GeometryTypeID: locationTypeId, } glResp, err := querier.ListSourcesAtTimestampWithout(ctx, llprms) @@ -1826,13 +1856,12 @@ func (s *DataPlatformDataServiceServerImpl) ListLocations( } } else { lsprms := db.ListSourcesAtTimestampParams{ - OauthID: req.UserOauthIdFilter, - GeometryUuids: parsedUuids, - AtTimestampUtc: pgtype.Timestamp{Time: time.Now().UTC(), Valid: true}, - PermissionID: permissionId, - SourceTypeID: sourceTypeId, - GeometryTypeID: locationTypeId, - GeometryNames: req.LocationNamesFilter, + OwningEntityExternalID: req.OrganisationIdFilter, + GeometryUuids: parsedUuids, + AtTimestampUtc: pgtype.Timestamp{Time: time.Now().UTC(), Valid: true}, + SourceTypeID: sourceTypeId, + GeometryTypeID: locationTypeId, + GeometryNames: req.LocationNamesFilter, } glResp, err := querier.ListSourcesAtTimestamp(ctx, lsprms) diff --git a/internal/server/postgres/dataserverimpl_test.go b/internal/server/postgres/dataserverimpl_test.go index 29c4f7c..98ac857 100644 --- a/internal/server/postgres/dataserverimpl_test.go +++ b/internal/server/postgres/dataserverimpl_test.go @@ -91,6 +91,21 @@ func TestCreateLocation(t *testing.T) { Metadata: metadata, }, }, + { + name: "Should create location with pipe in the name", + req: &pb.CreateLocationRequest{ + LocationName: "location|with|pipe", + EnergySource: pb.EnergySource_ENERGY_SOURCE_SOLAR, + GeometryWkt: "POINT(0.0 51.5)", + EffectiveCapacityWatts: 1230, + LocationType: pb.LocationType_LOCATION_TYPE_SITE, + Metadata: metadata, + }, + expectedLatLng: &pb.LatLng{ + Latitude: 51.5, + Longitude: 0.0, + }, + }, { name: "Should create location with large capacity", req: &pb.CreateLocationRequest{ @@ -317,6 +332,84 @@ func TestUpdateLocation(t *testing.T) { } } +func TestUpdateLocationOwner(t *testing.T) { + metadata, err := structpb.NewStruct(map[string]any{"source": "test"}) + require.NoError(t, err) + createResp, err := dc.CreateLocation(t.Context(), &pb.CreateLocationRequest{ + LocationName: "test_update_location_owner_site", + GeometryWkt: "POINT(-0.1 51.5)", + EffectiveCapacityWatts: 1234e6, + Metadata: metadata, + EnergySource: pb.EnergySource_ENERGY_SOURCE_SOLAR, + LocationType: pb.LocationType_LOCATION_TYPE_SITE, + ValidFromUtc: timestamppb.New(time.Date(2019, 5, 6, 6, 0, 0, 0, time.UTC)), + }) + require.NoError(t, err) + + createResp, err = dc.CreateLocation(t.Context(), &pb.CreateLocationRequest{ + LocationName: "test_update_location_owner_site_2", + GeometryWkt: "POINT(-0.2 51.6)", + EffectiveCapacityWatts: 1000e6, + Metadata: metadata, + EnergySource: pb.EnergySource_ENERGY_SOURCE_SOLAR, + LocationType: pb.LocationType_LOCATION_TYPE_SITE, + ValidFromUtc: timestamppb.New(time.Date(2019, 5, 6, 6, 0, 0, 0, time.UTC)), + }) + require.NoError(t, err) + + testcases := []struct { + name string + newOwner string + shouldError bool + }{ + { + name: "Should update owner to a new value", + newOwner: "first_owner", + shouldError: false, + }, + { + name: "Should update owner to the same value", + newOwner: "first_owner", + shouldError: false, + }, + { + name: "Should update owner to a different value", + newOwner: "second_owner", + shouldError: false, + }, + { + name: "Should remove owner with empty string", + newOwner: "", + shouldError: false, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + _, err := dc.UpdateLocationOwner(t.Context(), &pb.UpdateLocationOwnerRequest{ + LocationUuid: createResp.LocationUuid, + NewOrganisationId: tc.newOwner, + }) + if tc.shouldError { + require.Error(t, err) + } else { + require.NoError(t, err) + listResp, err := dc.ListLocations(t.Context(), &pb.ListLocationsRequest{ + LocationUuidsFilter: []string{createResp.LocationUuid}, + OrganisationIdFilter: &tc.newOwner, + }) + require.NoError(t, err) + + if tc.newOwner == "" { + require.Len(t, listResp.Locations, 0) + } else { + require.Len(t, listResp.Locations, 1) + } + } + }) + } +} + func TestCreateUpdateForecaster(t *testing.T) { testcases := []struct { name string diff --git a/internal/server/postgres/package_test.go b/internal/server/postgres/package_test.go index 295877f..067798a 100644 --- a/internal/server/postgres/package_test.go +++ b/internal/server/postgres/package_test.go @@ -26,7 +26,6 @@ import ( var ( // Global gRPC clients for use in tests. - ac pb.DataPlatformAdministrationServiceClient dc pb.DataPlatformDataServiceClient pgConnString string ) @@ -131,10 +130,8 @@ func setupTestMain(ctx context.Context, m *testing.M) (code int, err error) { defer lis.Close() dataServerImpl := NewDataPlatformDataServiceServerImpl() - adminServerImpl := NewDataPlatformAdministrationServiceServerImpl() pb.RegisterDataPlatformDataServiceServer(s, dataServerImpl) - pb.RegisterDataPlatformAdministrationServiceServer(s, adminServerImpl) go func() { if err := s.Serve(lis); err != nil { @@ -160,7 +157,6 @@ func setupTestMain(ctx context.Context, m *testing.M) (code int, err error) { defer cc.Close() dc = pb.NewDataPlatformDataServiceClient(cc) - ac = pb.NewDataPlatformAdministrationServiceClient(cc) // m.Run() executes the regular, user-defined test functions. // Any defer statements that have been made will be run after m.Run() diff --git a/internal/server/postgres/sql/migrations/00008_simple_iam.sql b/internal/server/postgres/sql/migrations/00008_simple_iam.sql new file mode 100644 index 0000000..65da778 --- /dev/null +++ b/internal/server/postgres/sql/migrations/00008_simple_iam.sql @@ -0,0 +1,93 @@ +-- +goose Up + +/* + * Removes most of the functionality of the IAM setup. + * + * This is due to a new relience on an external system for user management. The only remaining + * element relevant to the data platform is the organistion object. + * + * Note that the dropping of the IAM schema is permanent and not restored by the migration down. + * It is empty in all deployed version of this application and so can and should be safely removed. + */ + +DROP SCHEMA iam CASCADE; + +CREATE TABLE IF NOT EXISTS loc.entities ( + entity_id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + external_id TEXT NOT NULL + CONSTRAINT external_id_format_check CHECK ( + external_id IS NOT NULL + AND LENGTH(external_id) > 0 + AND LENGTH(external_id) <= 128 + ), + UNIQUE (external_id) +); + +ALTER TABLE loc.geometries + ADD COLUMN owning_entity_id INTEGER DEFAULT NULL + REFERENCES loc.entities(entity_id) + ON UPDATE CASCADE + ON DELETE SET NULL; + +CREATE INDEX idx_owning_entity_id ON loc.geometries (owning_entity_id); + +DROP MATERIALIZED VIEW IF EXISTS loc.sources_mv; +CREATE MATERIALIZED VIEW loc.sources_mv AS +SELECT + sh.geometry_uuid, + sh.source_type_id, + sh.capacity_watts, + sh.capacity_limit_sip, + sh.metadata, + COALESCE(sh.metadata || g.metadata, sh.metadata, g.metadata)::JSONB AS metadata_jsonb, + g.geometry_name, + g.geometry_type_id, + g.owning_entity_id, + ST_X(g.associated_point)::REAL AS longitude, + ST_Y(g.associated_point)::REAL AS latitude, + TSRANGE( + sh.valid_from_utc, + LEAD(sh.valid_from_utc, 1) OVER ( + PARTITION BY sh.geometry_uuid, sh.source_type_id + ORDER BY sh.valid_from_utc + ) + ) AS sys_period +FROM loc.sources_history AS sh +INNER JOIN loc.geometries AS g USING (geometry_uuid); +-- Prevent overlapping records. Required for concurrent refreshes. +CREATE UNIQUE INDEX ON loc.sources_mv (geometry_uuid, source_type_id, sys_period); +CREATE INDEX idx_sources_mv_gist_sys_period ON loc.sources_mv USING gist (sys_period); +CREATE INDEX idx_sources_mv_owning_entity_id ON loc.sources_mv (owning_entity_id); + + +-- +goose Down +DROP MATERIALIZED VIEW IF EXISTS loc.sources_mv; + +ALTER TABLE loc.geometries + DROP COLUMN owning_entity_id; + +DROP TABLE IF EXISTS loc.entities; + +CREATE MATERIALIZED VIEW loc.sources_mv AS +SELECT + sh.geometry_uuid, + sh.source_type_id, + sh.capacity_watts, + sh.capacity_limit_sip, + sh.metadata, + g.geometry_name, + g.geometry_type_id, + ST_X(g.associated_point)::REAL AS longitude, + ST_Y(g.associated_point)::REAL AS latitude, + TSRANGE( + sh.valid_from_utc, + LEAD(sh.valid_from_utc, 1) OVER ( + PARTITION BY sh.geometry_uuid, sh.source_type_id + ORDER BY sh.valid_from_utc + ) + ) AS sys_period +FROM loc.sources_history AS sh +INNER JOIN loc.geometries AS g USING (geometry_uuid); +CREATE UNIQUE INDEX ON loc.sources_mv (geometry_uuid, source_type_id, sys_period); +CREATE INDEX ON loc.sources_mv USING gist (sys_period); + diff --git a/internal/server/postgres/sql/queries/iam.sql b/internal/server/postgres/sql/queries/iam.sql deleted file mode 100644 index 3d56a1f..0000000 --- a/internal/server/postgres/sql/queries/iam.sql +++ /dev/null @@ -1,196 +0,0 @@ -/*= Queries for the IAM schema ================================================================= */ - -/*- Org Table -----------------------------------------------------------------------------------*/ - --- name: CreateOrg :one -INSERT INTO iam.orgs (org_name, metadata) -VALUES ( - LOWER(sqlc.arg(org_name)::TEXT), - CASE WHEN sqlc.arg(metadata)::JSONB = '{}'::JSONB THEN NULL ELSE sqlc.arg(metadata)::JSONB END -) -RETURNING org_uuid, org_name, metadata; - --- name: UpdateOrg :one -UPDATE iam.orgs -SET - org_name = LOWER(sqlc.arg(new_org_name)::TEXT), - metadata = CASE WHEN sqlc.arg(metadata)::JSONB = '{}'::JSONB THEN NULL ELSE sqlc.arg(metadata)::JSONB END -WHERE org_uuid = $1 -RETURNING org_uuid, org_name, metadata; - --- name: GetOrgByName :one -SELECT - od.org_uuid, - od.org_name, - od.created_at_utc, - od.user_uuids, - od.oauth_ids, - od.location_policy_group_uuids, - od.location_policy_group_names, - od.metadata -FROM iam.org_details_v AS od -WHERE org_name = LOWER(sqlc.arg(org_name)::TEXT); - --- name: ListOrgs :many -SELECT - org_uuid, - org_name, - metadata, - UUIDV7_EXTRACT_TIMESTAMP(org_uuid)::TIMESTAMP AS created_at_utc -FROM iam.orgs -ORDER BY org_name; - --- name: DeleteOrgByName :exec -DELETE FROM iam.orgs -WHERE org_name = LOWER(sqlc.arg(org_name)::TEXT); - --- name: AddUserToOrgByOAuthIDAndName :exec -INSERT INTO iam.users (org_uuid, oauth_id) -VALUES ( - ( - SELECT o.org_uuid FROM iam.orgs AS o - WHERE o.org_name = LOWER(sqlc.arg(org_name)::TEXT) - ), - sqlc.arg(oauth_id)::TEXT -) -ON CONFLICT DO NOTHING; - --- name: RemoveUserFromOrgByOAuthIDAndName :exec -DELETE FROM iam.users -WHERE org_uuid = ( - SELECT o.org_uuid FROM iam.orgs AS o - WHERE o.org_name = LOWER(sqlc.arg(org_name)::TEXT) - ) - AND oauth_id = sqlc.arg(oauth_id)::TEXT; - -/*- Users Table ---------------------------------------------------------------------------------*/ - --- name: CreateUser :one -INSERT INTO iam.users (org_uuid, oauth_id, metadata) -VALUES ( - $1, - $2, - CASE WHEN sqlc.arg(metadata)::JSONB = '{}'::JSONB THEN NULL ELSE sqlc.arg(metadata)::JSONB END -) -RETURNING user_uuid, org_uuid, oauth_id, metadata; - --- name: GetUserByOAuthID :one -SELECT - u.user_uuid, - u.org_uuid, - o.org_name, - u.oauth_id, - u.metadata, - UUIDV7_EXTRACT_TIMESTAMP(u.user_uuid)::TIMESTAMP AS created_at_utc -FROM iam.orgs AS o - INNER JOIN iam.users AS u USING (org_uuid) -WHERE u.oauth_id = $1; - --- name: DeleteUser :exec -DELETE FROM iam.users -WHERE user_uuid = $1; - -/*- Location Policy Groups ----------------------------------------------------------------------*/ - --- name: CreateLocationPolicyGroup :one -INSERT INTO iam.location_policy_groups (location_policy_group_name) -VALUES ($1) -RETURNING location_policy_group_uuid, location_policy_group_name; - --- name: UpdateLocationPolicyGroup :one -UPDATE iam.location_policy_groups -SET location_policy_group_name = $2 -WHERE location_policy_group_uuid = $1 -RETURNING location_policy_group_uuid, location_policy_group_name; - --- name: GetLocationPolicyGroupByUUID :one -SELECT - location_policy_group_uuid, - location_policy_group_name -FROM iam.location_policy_groups -WHERE location_policy_group_uuid = $1; - --- name: GetLocationPolicyGroupByName :one -SELECT - location_policy_group_uuid, - location_policy_group_name -FROM iam.location_policy_groups -WHERE location_policy_group_name = LOWER(sqlc.arg(location_policy_group_name)::TEXT); - --- name: ListLocationPolicyGroups :many -SELECT - location_policy_group_uuid, - location_policy_group_name -FROM iam.location_policy_groups -ORDER BY location_policy_group_name; - --- name: DeleteLocationPolicyGroup :exec -DELETE FROM iam.location_policy_groups -WHERE location_policy_group_uuid = $1; - --- name: AddLocationPolicyGroupToOrgByNames :exec -INSERT INTO iam.org_location_policy_groups (org_uuid, location_policy_group_uuid) -VALUES ( - ( - SELECT o.org_uuid FROM iam.orgs AS o - WHERE o.org_name = LOWER(sqlc.arg(org_name)::TEXT) - ), - ( - SELECT lpg.location_policy_group_uuid FROM iam.location_policy_groups AS lpg - WHERE lpg.location_policy_group_name = LOWER(sqlc.arg(location_policy_group_name)::TEXT) - ) -) -ON CONFLICT DO NOTHING; - --- name: RemoveLocationPolicyGroupFromOrgByNames :exec -DELETE FROM iam.org_location_policy_groups -WHERE org_uuid = ( - SELECT o.org_uuid FROM iam.orgs AS o - WHERE o.org_name = LOWER(sqlc.arg(org_name)::TEXT) - ) - AND location_policy_group_uuid = ( - SELECT lpg.location_policy_group_uuid FROM iam.location_policy_groups AS lpg - WHERE lpg.location_policy_group_name = LOWER(sqlc.arg(location_policy_group_name)::TEXT) - ); - -/*- Location Policies ---------------------------------------------------------------------------*/ - --- name: ListLocationPoliciesByGroup :many -SELECT - lp.permission_id, - r.permission_name, - lp.source_type_id, - st.source_type_name, - lp.geometry_uuid, - lp.location_policy_group_uuid -FROM iam.location_policies AS lp - INNER JOIN iam.permissions AS r USING (permission_id) - INNER JOIN loc.source_types AS st USING (source_type_id) -WHERE lp.location_policy_group_uuid = $1; - --- name: AddLocationPolicesToGroup :exec -INSERT INTO iam.location_policies ( - permission_id, - source_type_id, - geometry_uuid, - location_policy_group_uuid -) SELECT - $1, - $2, - loc_uuid, - ( - SELECT lpg.location_policy_group_uuid FROM iam.location_policy_groups AS lpg - WHERE lpg.location_policy_group_name = sqlc.arg(location_policy_group_name)::TEXT - ) -FROM UNNEST(ARRAY[sqlc.arg(geometry_uuids)::UUID []]) AS t (loc_uuid) -ON CONFLICT DO NOTHING; - --- name: RemoveLocationPoliciesFromGroup :exec -DELETE FROM iam.location_policies -WHERE location_policy_group_uuid = ( - SELECT lpg.location_policy_group_uuid FROM iam.location_policy_groups AS lpg - WHERE lpg.location_policy_group_name = sqlc.arg(location_policy_group_name)::TEXT - ) - AND geometry_uuid = $1 - AND source_type_id = $2 - AND permission_id = $3; diff --git a/internal/server/postgres/sql/queries/locations.sql b/internal/server/postgres/sql/queries/locations.sql index 7dd9c47..41e34a4 100644 --- a/internal/server/postgres/sql/queries/locations.sql +++ b/internal/server/postgres/sql/queries/locations.sql @@ -21,6 +21,36 @@ WHERE l.geometry_uuid = $1 RETURNING l.geometry_uuid, l.geometry_name, ST_X(l.associated_point)::REAL AS longitude, ST_Y(l.associated_point)::REAL AS latitude; +-- name: ReownGeometry :one +/* ReownGeometry assigns a new owning_entity_id to a geometry. + * To keep maintenance down to a minimum, it creates any missing entities on the fly. +* If the input external_id is an empty string, the owning_entity_id is set to NULL. + */ +WITH input AS ( + SELECT NULLIF(sqlc.arg(new_owning_entity_external_id)::TEXT, '') AS ext_id +), +target_entity AS ( + INSERT INTO loc.entities (external_id) + SELECT ext_id FROM input + WHERE ext_id IS NOT NULL + ON CONFLICT (external_id) DO NOTHING + RETURNING entity_id +), +selected_entity AS ( + SELECT entity_id FROM target_entity + UNION ALL + SELECT e.entity_id + FROM loc.entities AS e + INNER JOIN input AS i ON e.external_id = i.ext_id + LIMIT 1 +) +UPDATE loc.geometries AS l +SET owning_entity_id = (SELECT entity_id FROM selected_entity) +WHERE l.geometry_uuid = $1 +RETURNING + l.geometry_uuid, + l.owning_entity_id; + -- name: GetGeometryWKB :one /* GetGeometryWKB returns the geometries in WKB format for the given geometry UUIDs. */ SELECT @@ -63,15 +93,15 @@ SELECT s.capacity_limit_sip, s.source_type_id, s.geometry_uuid, - l.geometry_name, + s.geometry_name, s.sys_period, - ST_X(l.associated_point)::REAL AS longitude, - ST_Y(l.associated_point)::REAL AS latitude, - COALESCE(s.metadata || l.metadata, s.metadata, l.metadata)::JSONB AS metadata_jsonb + s.longitude, + s.latitude, + s.owning_entity_id, + s.metadata_jsonb FROM loc.sources_mv AS s - INNER JOIN loc.geometries AS l USING (geometry_uuid) WHERE - l.geometry_uuid = $1 + s.geometry_uuid = $1 AND s.source_type_id = $2 AND s.sys_period @> sqlc.arg(at_timestamp_utc)::TIMESTAMP; @@ -161,33 +191,29 @@ ORDER BY valid_from_utc ASC; -- name: ListSourcesAtTimestamp :many /* ListSourcesAtTimestamp returns all sources that match the given filters. - * It uses left joins to include geometries even if there are no associated policies, to allow the - * caller to not include permission-based filtering. */ WITH unfiltered_sources AS ( SELECT - u.oauth_id, - lp.permission_id, ls.source_type_id, ls.geometry_uuid, ls.capacity_watts, ls.capacity_limit_sip, - l.geometry_name, - l.geometry_type_id, - ST_X(l.associated_point)::REAL AS longitude, - ST_Y(l.associated_point)::REAL AS latitude, - COALESCE(l.metadata || ls.metadata, l.metadata, ls.metadata)::JSONB AS metadata_jsonb + ls.geometry_name, + ls.geometry_type_id, + ls.owning_entity_id, + ls.longitude, + ls.latitude, + ls.metadata_jsonb FROM loc.sources_mv AS ls - INNER JOIN loc.geometries AS l USING (geometry_uuid) - LEFT OUTER JOIN iam.location_policies AS lp USING (geometry_uuid, source_type_id) - LEFT OUTER JOIN iam.org_location_policy_groups USING (location_policy_group_uuid) - LEFT OUTER JOIN iam.users AS u USING (org_uuid) WHERE ls.sys_period @> sqlc.arg(at_timestamp_utc)::TIMESTAMP ) SELECT * FROM unfiltered_sources AS us WHERE - (sqlc.narg(source_type_id)::SMALLINT IS NULL OR us.source_type_id = sqlc.narg(source_type_id)::SMALLINT) + ( + sqlc.narg(source_type_id)::SMALLINT IS NULL + OR us.source_type_id = sqlc.narg(source_type_id)::SMALLINT + ) AND ( ARRAY_LENGTH(sqlc.arg(geometry_uuids)::UUID [], 1) IS NULL OR us.geometry_uuid = ANY(sqlc.arg(geometry_uuids)::UUID []) @@ -196,112 +222,121 @@ WHERE ARRAY_LENGTH(sqlc.arg(geometry_names)::TEXT [], 1) IS NULL OR us.geometry_name = ANY(sqlc.arg(geometry_names)::TEXT []) ) - AND (sqlc.narg(geometry_type_id)::SMALLINT IS NULL OR us.geometry_type_id = sqlc.narg(geometry_type_id)::SMALLINT) - AND (sqlc.narg(oauth_id)::TEXT IS NULL OR us.oauth_id = sqlc.arg(oauth_id)::TEXT) - AND (sqlc.narg(permission_id)::SMALLINT IS NULL OR us.permission_id = sqlc.narg(permission_id)::SMALLINT); + AND ( + sqlc.narg(geometry_type_id)::SMALLINT IS NULL + OR us.geometry_type_id = sqlc.narg(geometry_type_id)::SMALLINT + ) + AND ( + sqlc.narg(owning_entity_external_id)::TEXT IS NULL + OR us.owning_entity_id = ( + SELECT entity_id + FROM loc.entities + WHERE external_id = sqlc.narg(owning_entity_external_id)::TEXT + ) + ); -- name: ListSourcesAtTimestampWithin :many /* ListSourcesAtTimestampWithin returns all sources that match the given filters * and are within a given geometry. - * This has to be seperated from the ListSourcesAtTimestamp query due to the spatial join. */ -WITH contained_geometries AS ( - SELECT - l.geometry_uuid, - l.geometry_name, - l.geometry_type_id, - l.geom, - l.associated_point, - l.metadata - FROM loc.geometries AS l - INNER JOIN - loc.geometries AS l_outer ON ST_WITHIN( - l.geom, - l_outer.geom - ) AND l_outer.geometry_uuid = sqlc.arg(outer_geometry_uuid)::UUID - AND l.geometry_uuid <> sqlc.arg(outer_geometry_uuid)::UUID +WITH target_outer AS ( + SELECT geom + FROM loc.geometries + WHERE geometry_uuid = sqlc.arg(outer_geometry_uuid)::UUID ), unfiltered_sources AS ( SELECT - u.oauth_id, - lp.permission_id, ls.source_type_id, ls.geometry_uuid, ls.capacity_watts, ls.capacity_limit_sip, - l.geometry_name, - l.geometry_type_id, - ST_X(l.associated_point)::REAL AS longitude, - ST_Y(l.associated_point)::REAL AS latitude, - COALESCE(l.metadata || ls.metadata, l.metadata, ls.metadata)::JSONB AS metadata_jsonb + ls.geometry_name, + ls.geometry_type_id, + ls.longitude, + ls.latitude, + ls.owning_entity_id, + ls.metadata_jsonb FROM loc.sources_mv AS ls - INNER JOIN contained_geometries AS l USING (geometry_uuid) - LEFT OUTER JOIN iam.location_policies AS lp USING (geometry_uuid, source_type_id) - LEFT OUTER JOIN iam.org_location_policy_groups USING (location_policy_group_uuid) - LEFT OUTER JOIN iam.users AS u USING (org_uuid) + INNER JOIN loc.geometries AS l USING (geometry_uuid) WHERE ls.sys_period @> sqlc.arg(at_timestamp_utc)::TIMESTAMP + AND ls.geometry_uuid <> sqlc.arg(outer_geometry_uuid)::UUID + AND ST_WITHIN(l.geom, (SELECT geom FROM target_outer)) ) SELECT * FROM unfiltered_sources AS us WHERE - (sqlc.narg(source_type_id)::SMALLINT IS NULL OR us.source_type_id = sqlc.narg(source_type_id)::SMALLINT) + ( + sqlc.narg(source_type_id)::SMALLINT IS NULL + OR us.source_type_id = sqlc.narg(source_type_id)::SMALLINT + ) AND ( ARRAY_LENGTH(sqlc.arg(geometry_uuids)::UUID [], 1) IS NULL OR us.geometry_uuid = ANY(sqlc.arg(geometry_uuids)::UUID []) ) - AND (sqlc.narg(geometry_type_id)::SMALLINT IS NULL OR us.geometry_type_id = sqlc.narg(geometry_type_id)::SMALLINT) - AND (sqlc.narg(oauth_id)::TEXT IS NULL OR us.oauth_id = sqlc.arg(oauth_id)::TEXT) - AND (sqlc.narg(permission_id)::SMALLINT IS NULL OR us.permission_id = sqlc.narg(permission_id)::SMALLINT); + AND ( + ARRAY_LENGTH(sqlc.arg(geometry_names)::TEXT [], 1) IS NULL + OR us.geometry_name = ANY(sqlc.arg(geometry_names)::TEXT []) + ) + AND ( + sqlc.narg(geometry_type_id)::SMALLINT IS NULL + OR us.geometry_type_id = sqlc.narg(geometry_type_id)::SMALLINT + ) + AND ( + sqlc.narg(owning_entity_external_id)::TEXT IS NULL + OR us.owning_entity_id = ( + SELECT entity_id + FROM loc.entities + WHERE external_id = sqlc.narg(owning_entity_external_id)::TEXT + ) + ); -- name: ListSourcesAtTimestampWithout :many /* ListSourcesAtTimestampWithout returns all sources that match the given filters - * and contain a given geometry. - * This has to be seperated from the ListSourcesAtTimestamp query due to the spatial join. + * and contain a given geometry's associated point. */ -WITH containing_geometries AS ( - SELECT - l.geometry_uuid, - l.geometry_name, - l.geometry_type_id, - l.geom, - l.associated_point, - l.metadata - FROM loc.geometries AS l - INNER JOIN - loc.geometries AS l_inner ON ST_WITHIN( - l_inner.associated_point, - l.geom - ) AND l_inner.geometry_uuid = sqlc.arg(inner_geometry_uuid)::UUID - AND l.geometry_uuid <> sqlc.arg(inner_geometry_uuid)::UUID +WITH target_inner AS ( + SELECT associated_point + FROM loc.geometries + WHERE geometry_uuid = sqlc.arg(inner_geometry_uuid)::UUID ), unfiltered_sources AS ( SELECT - u.oauth_id, - lp.permission_id, ls.source_type_id, ls.geometry_uuid, ls.capacity_watts, ls.capacity_limit_sip, - l.geometry_name, - l.geometry_type_id, - ST_X(l.associated_point)::REAL AS longitude, - ST_Y(l.associated_point)::REAL AS latitude, - COALESCE(l.metadata || ls.metadata, l.metadata, ls.metadata)::JSONB AS metadata_jsonb + ls.geometry_name, + ls.geometry_type_id, + ls.longitude, + ls.latitude, + ls.owning_entity_id, + ls.metadata_jsonb FROM loc.sources_mv AS ls - INNER JOIN containing_geometries AS l USING (geometry_uuid) - LEFT OUTER JOIN iam.location_policies AS lp USING (geometry_uuid, source_type_id) - LEFT OUTER JOIN iam.org_location_policy_groups USING (location_policy_group_uuid) - LEFT OUTER JOIN iam.users AS u USING (org_uuid) + INNER JOIN loc.geometries AS l USING (geometry_uuid) WHERE ls.sys_period @> sqlc.arg(at_timestamp_utc)::TIMESTAMP + AND ls.geometry_uuid <> sqlc.arg(inner_geometry_uuid)::UUID + AND ST_CONTAINS(l.geom, (SELECT associated_point FROM target_inner)) ) SELECT * FROM unfiltered_sources AS us WHERE - (sqlc.narg(source_type_id)::SMALLINT IS NULL OR us.source_type_id = sqlc.narg(source_type_id)::SMALLINT) + ( + sqlc.narg(source_type_id)::SMALLINT IS NULL + OR us.source_type_id = sqlc.narg(source_type_id)::SMALLINT + ) AND ( ARRAY_LENGTH(sqlc.arg(geometry_uuids)::UUID [], 1) IS NULL OR us.geometry_uuid = ANY(sqlc.arg(geometry_uuids)::UUID []) ) - AND (sqlc.narg(geometry_type_id)::SMALLINT IS NULL OR us.geometry_type_id = sqlc.narg(geometry_type_id)::SMALLINT) - AND (sqlc.narg(oauth_id)::TEXT IS NULL OR us.oauth_id = sqlc.arg(oauth_id)::TEXT) - AND (sqlc.narg(permission_id)::SMALLINT IS NULL OR us.permission_id = sqlc.narg(permission_id)::SMALLINT); + AND ( + sqlc.narg(geometry_type_id)::SMALLINT IS NULL + OR us.geometry_type_id = sqlc.narg(geometry_type_id)::SMALLINT + ) + AND ( + sqlc.narg(owning_entity_external_id)::TEXT IS NULL + OR us.owning_entity_id = ( + SELECT entity_id + FROM loc.entities + WHERE external_id = sqlc.narg(owning_entity_external_id)::TEXT + ) + ); diff --git a/proto/ocf/dp/dp-admin.messages.proto b/proto/ocf/dp/dp-admin.messages.proto deleted file mode 100644 index 286bc46..0000000 --- a/proto/ocf/dp/dp-admin.messages.proto +++ /dev/null @@ -1,213 +0,0 @@ -syntax = "proto3"; - -package ocf.dp; - -import "google/protobuf/timestamp.proto"; -import "google/protobuf/struct.proto"; -import "buf/validate/validate.proto"; -import "ocf/dp/dp.common.proto"; - -option go_package = "github.com/openclimatefix/data-platform/internal/protogen/ocf/dp;dp"; - - -message LocationPolicy { - string location_id = 1 [ - (buf.validate.field).string.uuid = true - ]; - EnergySource energy_source = 2; - Permission permission = 3; -} - -message CreateUserRequest { - string oauth_id = 1 [ - (buf.validate.field).string.min_len = 5, - (buf.validate.field).string.max_len = 128 - ]; - string organisation = 2 [ - (buf.validate.field).string.min_len = 2, - (buf.validate.field).string.max_len = 128 - ]; - google.protobuf.Struct metadata = 3; -} - -message CreateUserResponse { - string user_id = 1 [ - (buf.validate.field).string.uuid = true - ]; - string oauth_id = 2; -} - - -message GetUserRequest { - string oauth_id = 1 [ - (buf.validate.field).string.min_len = 5, - (buf.validate.field).string.max_len = 128 - ]; -} - -message GetUserResponse { - string user_id = 1 [ - (buf.validate.field).string.uuid = true - ]; - string oauth_id = 2; - string organisation = 3; - repeated string location_policy_groups = 4; - google.protobuf.Timestamp created_at = 5; - google.protobuf.Struct metadata = 6; -} - - -message DeleteUserRequest { - string user_id = 1 [ - (buf.validate.field).string.uuid = true - ]; -} - -message DeleteUserResponse {} - -message CreateOrganisationRequest { - string org_name = 1 [ - (buf.validate.field).string.min_len = 2, - (buf.validate.field).string.max_len = 128 - ]; - google.protobuf.Struct metadata = 2; -} - -message CreateOrganisationResponse { - string org_id = 1 [ - (buf.validate.field).string.uuid = true - ]; - string org_name = 2; -} - - -message GetOrganisationRequest { - string org_name = 1 [ - (buf.validate.field).string.min_len = 2, - (buf.validate.field).string.max_len = 128 - ]; -} - -message GetOrganisationResponse { - string org_id = 1 [ - (buf.validate.field).string.uuid = true - ]; - string org_name = 2; - google.protobuf.Struct metadata = 3; - google.protobuf.Timestamp created_at = 4; - repeated string location_policy_groups = 5; - repeated string user_oauth_ids = 6; -} - -message DeleteOrganisationRequest { - string org_name = 1 [ - (buf.validate.field).string.min_len = 2, - (buf.validate.field).string.max_len = 128 - ]; -} - -message DeleteOrganisationResponse {} - -message AddLocationPolicyGroupToOrganisationRequest { - string org_name = 1 [ - (buf.validate.field).string.min_len = 2, - (buf.validate.field).string.max_len = 128 - ]; - string location_policy_group_name = 2 [ - (buf.validate.field).string.min_len = 2, - (buf.validate.field).string.max_len = 128 - ]; -} - -message AddLocationPolicyGroupToOrganisationResponse {} - - -message RemoveLocationPolicyGroupFromOrganisationRequest { - string org_name = 1 [ - (buf.validate.field).string.min_len = 2, - (buf.validate.field).string.max_len = 128 - ]; - string location_policy_group_name = 2 [ - (buf.validate.field).string.min_len = 2, - (buf.validate.field).string.max_len = 128 - ]; -} - -message RemoveLocationPolicyGroupFromOrganisationResponse {} - - -message AddUserToOrganisationRequest { - string org_name = 1 [ - (buf.validate.field).string.min_len = 2, - (buf.validate.field).string.max_len = 128 - ]; - string user_oauth_id = 2 [ - (buf.validate.field).string.min_len = 5, - (buf.validate.field).string.max_len = 128 - ]; -} - -message AddUserToOrganisationResponse {} - -message RemoveUserFromOrganisationRequest { - string org_name = 1 [ - (buf.validate.field).string.min_len = 2, - (buf.validate.field).string.max_len = 128 - ]; - string user_oauth_id = 2 [ - (buf.validate.field).string.min_len = 5, - (buf.validate.field).string.max_len = 128 - ]; -} - -message RemoveUserFromOrganisationResponse {} - - -message CreateLocationPolicyGroupRequest { - string name = 1 [ - (buf.validate.field).string.min_len = 2, - (buf.validate.field).string.max_len = 128 - ]; -} - -message CreateLocationPolicyGroupResponse { - string location_policy_group_id = 1; - string name = 2; -} - -message GetLocationPolicyGroupRequest { - string location_policy_group_name = 1 [ - (buf.validate.field).string.min_len = 2, - (buf.validate.field).string.max_len = 128 - ]; -} - -message GetLocationPolicyGroupResponse { - string location_policy_group_id = 1; - string name = 2; - repeated LocationPolicy location_policies = 3; -} - -message AddLocationPoliciesToGroupRequest { - string location_policy_group_name = 1 [ - (buf.validate.field).string.min_len = 2, - (buf.validate.field).string.max_len = 128 - ]; - repeated LocationPolicy location_policies = 2 [ - (buf.validate.field).repeated.max_items = 10000 - ]; -} - -message AddLocationPoliciesToGroupResponse {} - -message RemoveLocationPoliciesFromGroupRequest { - string location_policy_group_name = 1 [ - (buf.validate.field).string.min_len = 2, - (buf.validate.field).string.max_len = 128 - ]; - repeated LocationPolicy location_policies = 2 [ - (buf.validate.field).repeated.max_items = 10000 - ]; -} - -message RemoveLocationPoliciesFromGroupResponse {} diff --git a/proto/ocf/dp/dp-admin.service.proto b/proto/ocf/dp/dp-admin.service.proto deleted file mode 100644 index 08e126f..0000000 --- a/proto/ocf/dp/dp-admin.service.proto +++ /dev/null @@ -1,28 +0,0 @@ -syntax = "proto3"; - -package ocf.dp; - -import "ocf/dp/dp-admin.messages.proto"; - -option go_package = "github.com/openclimatefix/data-platform/internal/protogen/ocf/dp;dp"; - -service DataPlatformAdministrationService { - - rpc CreateUser(CreateUserRequest) returns (CreateUserResponse) {} - rpc GetUser(GetUserRequest) returns (GetUserResponse) {} - rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse) {} - - rpc CreateOrganisation(CreateOrganisationRequest) returns (CreateOrganisationResponse) {} - rpc GetOrganisation(GetOrganisationRequest) returns (GetOrganisationResponse) {} - rpc DeleteOrganisation(DeleteOrganisationRequest) returns (DeleteOrganisationResponse) {} - rpc AddUserToOrganisation(AddUserToOrganisationRequest) returns (AddUserToOrganisationResponse) {} - rpc RemoveUserFromOrganisation(RemoveUserFromOrganisationRequest) returns (RemoveUserFromOrganisationResponse) {} - rpc AddLocationPolicyGroupToOrganisation(AddLocationPolicyGroupToOrganisationRequest) returns (AddLocationPolicyGroupToOrganisationResponse) {} - rpc RemoveLocationPolicyGroupFromOrganisation(RemoveLocationPolicyGroupFromOrganisationRequest) returns (RemoveLocationPolicyGroupFromOrganisationResponse) {} - - rpc CreateLocationPolicyGroup(CreateLocationPolicyGroupRequest) returns (CreateLocationPolicyGroupResponse) {} - rpc GetLocationPolicyGroup(GetLocationPolicyGroupRequest) returns (GetLocationPolicyGroupResponse) {} - rpc AddLocationPoliciesToGroup(AddLocationPoliciesToGroupRequest) returns (AddLocationPoliciesToGroupResponse) {} - rpc RemoveLocationPoliciesFromGroup(RemoveLocationPoliciesFromGroupRequest) returns (RemoveLocationPoliciesFromGroupResponse) {} - -} diff --git a/proto/ocf/dp/dp-data.messages.proto b/proto/ocf/dp/dp-data.messages.proto index 914b66a..edcf2ba 100644 --- a/proto/ocf/dp/dp-data.messages.proto +++ b/proto/ocf/dp/dp-data.messages.proto @@ -558,6 +558,22 @@ message UpdateLocationResponse { uint64 effective_capacity_watts = 3; } + +message UpdateLocationOwnerRequest { + string location_uuid = 1 [ + (buf.validate.field).required = true, + (buf.validate.field).string.uuid = true + ]; + string new_organisation_id = 3 [ + (buf.validate.field).string.max_len = 128 + ]; +} + +message UpdateLocationOwnerResponse { + string location_uuid = 1; + string organisation_id = 2; +} + message GetLocationsAsGeoJSONRequest { repeated string location_uuids = 1 [ (buf.validate.field).repeated.min_items = 1, @@ -734,12 +750,10 @@ message ListLocationsRequest { string: {uuid: true} } ]; - // Optional filter to only return locations belonging to a specific user. - optional string user_oauth_id_filter = 4 [ + // Optional filter to only return locations belonging to a specific organisation. + optional string organisation_id_filter = 4 [ (buf.validate.field).string.max_len = 128 ]; - // Optional filter to only return locations for which the user has a specific permission. - optional Permission permission_filter = 5; // Optional filter to only return locations enclosed within a specific location. optional string enclosing_location_uuid_filter = 6 [ (buf.validate.field).string.uuid = true @@ -836,4 +850,3 @@ message ForecastDatum { map metadata = 9; google.protobuf.Timestamp target_timestamp_utc = 10; } - diff --git a/proto/ocf/dp/dp-data.service.proto b/proto/ocf/dp/dp-data.service.proto index 244efcf..a3bc40e 100644 --- a/proto/ocf/dp/dp-data.service.proto +++ b/proto/ocf/dp/dp-data.service.proto @@ -36,6 +36,9 @@ service DataPlatformDataService { rpc CreateLocation(CreateLocationRequest) returns (CreateLocationResponse) {} /* UpdateLocation modifies various attributes associated with a given location. */ rpc UpdateLocation(UpdateLocationRequest) returns (UpdateLocationResponse) {} + /* UpdateLocationOwner changes the ownership of a location. */ + rpc UpdateLocationOwner(UpdateLocationOwnerRequest) returns (UpdateLocationOwnerResponse) {} + /* ListLocations fetches a list of registered locations that match the supplied filters. */ rpc ListLocations(ListLocationsRequest) returns (ListLocationsResponse) {} /* CreateForecaster registers a new forecaster. diff --git a/proto/ocf/dp/dp.rules.proto b/proto/ocf/dp/dp.rules.proto index 2bc812f..08f61ba 100644 --- a/proto/ocf/dp/dp.rules.proto +++ b/proto/ocf/dp/dp.rules.proto @@ -9,8 +9,8 @@ option go_package = "github.com/openclimatefix/data-platform/internal/protogen/o extend buf.validate.StringRules { optional bool valid_location_name = 4457818 [(buf.validate.predefined).cel = { id: "string.valid.locationname", - message: "Required field, 2-100 chars long; lowercase, alphanumeric and underscores only", - expression: "this.size() >= 2 && this.size() <= 100 && this.matches(\'^[a-z0-9_]+$\')" + message: "Required field, 2-100 chars long; lowercase, alphanumeric and underscores and pipes only", + expression: "this.size() >= 2 && this.size() <= 100 && this.matches(\'^[a-z0-9_|]+$\')" }]; } From 2e6c4b0d19975036876a992e8835b6d0af073f7c Mon Sep 17 00:00:00 2001 From: devsjc <47188100+devsjc@users.noreply.github.com> Date: Thu, 11 Jun 2026 16:10:19 +0100 Subject: [PATCH 2/2] fix(fox): fix --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 256cef8..a3548a9 100644 --- a/Makefile +++ b/Makefile @@ -283,7 +283,7 @@ gen.proto.docs: ${PROTOC} ${PROTOC_GEN_DOC} -I=proto \ -I=$(PROTOC_INCLUDE) \ --doc_out=gen/docs \ - --doc_opt=gen/docs/markdown.tmpl,docs.md:=buf/*,google/*,ocf/dp/dp.rules.proto,ocf/dp/dp-admin.service.proto,ocf/dp/dp-admin.messages.proto + --doc_opt=gen/docs/markdown.tmpl,docs.md:=buf/*,google/*,ocf/dp/dp.rules.proto @sed -n '1,//p' README.md > README.tmp @echo "" >> README.tmp @cat gen/docs/docs.md >> README.tmp