feat(catalog): expose entityRef on Location type and add PUT /locations/:id

- Add `entityRef` field to all Location API responses, carrying the stable
  entity ref (e.g. `location:default/generated-<sha1hex>`) that was
  already persisted to the `location_entity_ref` column.
- Make `entityRef` filterable via `POST /locations/by-query`.
- Add `PUT /locations/:id` endpoint that updates the `type`/`target` of
  an existing location and issues the corresponding delta mutation so the
  catalog entity is updated in-place without changing its entity ref.
- Wire `updateLocation` through `CatalogApi`, `CatalogService`,
  `CatalogClient`, `LocationService`, `LocationStore`, and their
  implementations and mocks.

Signed-off-by: Fredrik Adelöw <freben@spotify.com>
Made-with: Cursor
This commit is contained in:
Fredrik Adelöw
2026-04-07 16:36:06 +02:00
parent 587981973c
commit c384fff709
34 changed files with 615 additions and 48 deletions
@@ -84,6 +84,14 @@ export class InMemoryCatalogClient implements CatalogApi {
_request?: QueryLocationsInitialRequest,
): AsyncIterable<Location_2[]>;
// (undocumented)
updateLocation(
_id: string,
_location: {
type?: string;
target: string;
},
): Promise<Location_2>;
// (undocumented)
validateEntity(
_entity: Entity,
_locationRef: string,
+17
View File
@@ -102,6 +102,14 @@ export interface CatalogApi {
request?: QueryLocationsInitialRequest,
options?: CatalogRequestOptions,
): AsyncIterable<Location_2[]>;
updateLocation(
id: string,
location: {
type?: string;
target: string;
},
options?: CatalogRequestOptions,
): Promise<Location_2>;
validateEntity(
entity: Entity,
locationRef: string,
@@ -196,6 +204,14 @@ export class CatalogClient implements CatalogApi {
request?: QueryLocationsInitialRequest,
options?: CatalogRequestOptions,
): AsyncIterable<Location_2[]>;
updateLocation(
id: string,
location: {
type?: string;
target: string;
},
options?: CatalogRequestOptions,
): Promise<Location_2>;
validateEntity(
entity: Entity,
locationRef: string,
@@ -307,6 +323,7 @@ type Location_2 = {
id: string;
type: string;
target: string;
entityRef: string;
};
export { Location_2 as Location };
@@ -1107,6 +1107,7 @@ describe('CatalogClient', () => {
id: '42',
type: 'url',
target: 'https://example.com',
entityRef: 'location:default/generated-42',
},
},
{
@@ -1114,6 +1115,7 @@ describe('CatalogClient', () => {
id: '43',
type: 'url',
target: 'https://example.com',
entityRef: 'location:default/generated-43',
},
},
] satisfies GetLocations200ResponseInner[];
@@ -612,6 +612,27 @@ export class CatalogClient implements CatalogApi {
.find(l => locationRef === stringifyLocationRef(l));
}
/**
* {@inheritdoc CatalogApi.updateLocation}
*/
async updateLocation(
id: string,
location: { type?: string; target: string },
options?: CatalogRequestOptions,
): Promise<Location> {
const { type = 'url', target } = location;
const response = await this.apiClient.updateLocation(
{ path: { id }, body: { type, target } },
options,
);
if (response.status !== 200) {
throw await ResponseError.fromResponse(response);
}
return response.json();
}
/**
* {@inheritdoc CatalogApi.removeLocationById}
*/
@@ -40,6 +40,7 @@ import { CreateLocationRequest } from '../models/CreateLocationRequest.model';
import { GetLocations200ResponseInner } from '../models/GetLocations200ResponseInner.model';
import { GetLocationsByQueryRequest } from '../models/GetLocationsByQueryRequest.model';
import { Location } from '../models/Location.model';
import { LocationInput } from '../models/LocationInput.model';
import { LocationsQueryResponse } from '../models/LocationsQueryResponse.model';
/**
@@ -217,6 +218,15 @@ export type GetLocations = {};
export type GetLocationsByQuery = {
body: GetLocationsByQueryRequest;
};
/**
* @public
*/
export type UpdateLocation = {
path: {
id: string;
};
body: LocationInput;
};
/**
* @public
@@ -747,4 +757,32 @@ export class DefaultApiClient {
body: JSON.stringify(request.body),
});
}
/**
* Update the type and target of an existing location by id.
* @param id -
* @param locationInput -
*/
public async updateLocation(
// @ts-ignore
request: UpdateLocation,
options?: RequestOptions,
): Promise<TypedResponse<Location>> {
const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);
const uriTemplate = `/locations/{id}`;
const uri = parser.parse(uriTemplate).expand({
id: request.path.id,
});
return await this.fetchApi.fetch(`${baseUrl}${uri}`, {
headers: {
'Content-Type': 'application/json',
...(options?.token && { Authorization: `Bearer ${options?.token}` }),
},
method: 'PUT',
body: JSON.stringify(request.body),
});
}
}
@@ -26,4 +26,8 @@ export interface Location {
target: string;
type: string;
id: string;
/**
* The entity ref of the corresponding Location kind entity, e.g. location:default/generated-<sha1hex>.
*/
entityRef: string;
}
@@ -584,6 +584,13 @@ export class InMemoryCatalogClient implements CatalogApi {
throw new NotImplementedError('Method not implemented.');
}
async updateLocation(
_id: string,
_location: { type?: string; target: string },
): Promise<Location> {
throw new NotImplementedError('Method not implemented.');
}
async getLocationByEntity(
_entityRef: string | CompoundEntityRef,
): Promise<Location | undefined> {
+15
View File
@@ -372,6 +372,8 @@ export type Location = {
id: string;
type: string;
target: string;
/** The entity ref of the corresponding Location kind entity, e.g. `location:default/generated-<sha1hex>`. */
entityRef: string;
};
/**
@@ -829,6 +831,19 @@ export interface CatalogApi {
options?: CatalogRequestOptions,
): Promise<void>;
/**
* Updates the type and target of an existing registered location.
*
* @param id - The location ID to update
* @param location - The new type and target for the location
* @param options - Additional options
*/
updateLocation(
id: string,
location: { type?: string; target: string },
options?: CatalogRequestOptions,
): Promise<Location>;
/**
* Gets a location associated with an entity.
*