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:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': minor
|
||||
---
|
||||
|
||||
Location responses now include an `entityRef` field with the stable entity reference for each location. The `entityRef` field is also filterable via `POST /locations/by-query`. Added `PUT /locations/:id` endpoint for updating the type and target of an existing location.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/catalog-client': minor
|
||||
---
|
||||
|
||||
Added `entityRef` field to the `Location` type, exposing the stable entity reference for each registered location. Added `updateLocation` method to `CatalogApi` for updating the type and target of an existing location.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-node': minor
|
||||
---
|
||||
|
||||
Added `updateLocation` method to `CatalogService` for updating the type and target of an existing location.
|
||||
@@ -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,
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -58,7 +58,12 @@ describe('DefaultApiExplorerPage', () => {
|
||||
],
|
||||
}),
|
||||
getLocationByRef: () =>
|
||||
Promise.resolve({ id: 'id', type: 'url', target: 'url' }),
|
||||
Promise.resolve({
|
||||
id: 'id',
|
||||
type: 'url',
|
||||
target: 'url',
|
||||
entityRef: 'location:default/generated-id',
|
||||
}),
|
||||
getEntitiesByRefs: () => Promise.resolve({ items: [] }),
|
||||
getEntityFacets: async () => ({
|
||||
facets: { 'relations.ownedBy': [] },
|
||||
|
||||
@@ -59,6 +59,7 @@ describe('GithubLocationAnalyzer', () => {
|
||||
id: 'test',
|
||||
target: location.target,
|
||||
type: location.type ?? 'url',
|
||||
entityRef: 'location:default/generated-test',
|
||||
},
|
||||
exists: false,
|
||||
entities: [
|
||||
|
||||
@@ -321,6 +321,8 @@ describe('DefaultLocationStore', () => {
|
||||
id: locationId,
|
||||
type: 'url',
|
||||
target: 'https://example.com',
|
||||
entityRef:
|
||||
'location:default/generated-7ade06d301ec98b80352203e9969e7640dc618b8',
|
||||
});
|
||||
|
||||
await expect(
|
||||
@@ -848,23 +850,31 @@ describe('DefaultLocationStore', () => {
|
||||
type: 'url',
|
||||
target:
|
||||
'https://github.com/backstage/backstage/blob/master/packages/catalog-model/catalog-info.yaml',
|
||||
entityRef:
|
||||
'location:default/generated-0ecbc46527aae891650cc1ad4eb17e15391fa96a',
|
||||
};
|
||||
const l2 = {
|
||||
id: '00000000-0000-0000-0000-000000000002',
|
||||
type: 'url',
|
||||
target:
|
||||
'https://github.com/backstage/backstage/blob/master/plugins/catalog/catalog-info.yaml',
|
||||
entityRef:
|
||||
'location:default/generated-888dd2d9775aaf5b722ebdece23c21e2541e90ce',
|
||||
};
|
||||
const l3 = {
|
||||
id: '00000000-0000-0000-0000-000000000003',
|
||||
type: 'url',
|
||||
target:
|
||||
'https://github.com/backstage/backstage/blob/master/plugins/scaffolder/catalog-info.yaml',
|
||||
entityRef:
|
||||
'location:default/generated-d4255ab29a8321cb6eae30cee45969a272e1206e',
|
||||
};
|
||||
const l4 = {
|
||||
id: '00000000-0000-0000-0000-000000000004',
|
||||
type: 'file',
|
||||
target: '/tmp/catalog-info.yaml',
|
||||
entityRef:
|
||||
'location:default/generated-d14ac9f97f7d042d45b2130dcf3d087e000f07f2',
|
||||
};
|
||||
|
||||
it.each(databases.eachSupportedId())(
|
||||
@@ -878,7 +888,9 @@ describe('DefaultLocationStore', () => {
|
||||
await knex<DbLocationsRow>('locations').delete();
|
||||
for (const location of locations) {
|
||||
await knex<DbLocationsRow>('locations').insert({
|
||||
...location,
|
||||
id: location.id,
|
||||
type: location.type,
|
||||
target: location.target,
|
||||
location_entity_ref: computeLocationEntityRef(
|
||||
location.type,
|
||||
location.target,
|
||||
|
||||
@@ -133,15 +133,23 @@ export class DefaultLocationStore implements LocationStore, EntityProvider {
|
||||
});
|
||||
}
|
||||
|
||||
return { id: location.id, type: location.type, target: location.target };
|
||||
return {
|
||||
id: location.id,
|
||||
type: location.type,
|
||||
target: location.target,
|
||||
entityRef: location.location_entity_ref,
|
||||
};
|
||||
}
|
||||
|
||||
async listLocations(): Promise<Location[]> {
|
||||
return (await this.locations()).map(({ id, type, target }) => ({
|
||||
id,
|
||||
type,
|
||||
target,
|
||||
}));
|
||||
return (await this.locations()).map(
|
||||
({ id, type, target, location_entity_ref }) => ({
|
||||
id,
|
||||
type,
|
||||
target,
|
||||
entityRef: location_entity_ref,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async queryLocations(options: {
|
||||
@@ -179,6 +187,7 @@ export class DefaultLocationStore implements LocationStore, EntityProvider {
|
||||
id: item.id,
|
||||
target: item.target,
|
||||
type: item.type,
|
||||
entityRef: item.location_entity_ref,
|
||||
})),
|
||||
totalItems: Number(count),
|
||||
};
|
||||
@@ -192,8 +201,60 @@ export class DefaultLocationStore implements LocationStore, EntityProvider {
|
||||
if (!items.length) {
|
||||
throw new NotFoundError(`Found no location with ID ${id}`);
|
||||
}
|
||||
const { id: rowId, type, target } = items[0];
|
||||
return { id: rowId, type, target };
|
||||
const { id: rowId, type, target, location_entity_ref } = items[0];
|
||||
return { id: rowId, type, target, entityRef: location_entity_ref };
|
||||
}
|
||||
|
||||
async updateLocation(id: string, location: LocationInput): Promise<Location> {
|
||||
if (!this.connection) {
|
||||
throw new Error('location store is not initialized');
|
||||
}
|
||||
|
||||
const updated = await this.db.transaction(async tx => {
|
||||
const [old] = await tx<DbLocationsRow>('locations')
|
||||
.where({ id })
|
||||
.select();
|
||||
|
||||
if (!old) {
|
||||
throw new NotFoundError(`Found no location with ID ${id}`);
|
||||
}
|
||||
|
||||
await tx<DbLocationsRow>('locations').where({ id }).update({
|
||||
type: location.type,
|
||||
target: location.target,
|
||||
});
|
||||
|
||||
const [row] = await tx<DbLocationsRow>('locations')
|
||||
.where({ id })
|
||||
.select();
|
||||
return { old, row };
|
||||
});
|
||||
|
||||
const oldEntity = locationSpecToLocationEntity({
|
||||
location: updated.old,
|
||||
locationEntityRef: updated.old.location_entity_ref,
|
||||
});
|
||||
const newEntity = locationSpecToLocationEntity({
|
||||
location: updated.row,
|
||||
locationEntityRef: updated.row.location_entity_ref,
|
||||
});
|
||||
|
||||
await this.connection.applyMutation({
|
||||
type: 'delta',
|
||||
added: [
|
||||
{ entity: newEntity, locationKey: getEntityLocationRef(newEntity) },
|
||||
],
|
||||
removed: [
|
||||
{ entity: oldEntity, locationKey: getEntityLocationRef(oldEntity) },
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
id: updated.row.id,
|
||||
type: updated.row.type,
|
||||
target: updated.row.target,
|
||||
entityRef: updated.row.location_entity_ref,
|
||||
};
|
||||
}
|
||||
|
||||
async deleteLocation(id: string): Promise<void> {
|
||||
@@ -264,6 +325,7 @@ export class DefaultLocationStore implements LocationStore, EntityProvider {
|
||||
id: locationRow.id,
|
||||
type: locationRow.type,
|
||||
target: locationRow.target,
|
||||
entityRef: locationRow.location_entity_ref,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -692,13 +754,15 @@ function applyLocationFilterToQuery(
|
||||
|
||||
for (const [keyAnyCase, value] of entries) {
|
||||
const key = keyAnyCase.toLocaleLowerCase('en-US');
|
||||
if (!['id', 'type', 'target'].includes(key)) {
|
||||
if (!['id', 'type', 'target', 'entityref'].includes(key)) {
|
||||
throw new InputError(
|
||||
`Invalid filter predicate, expected key to be 'id', 'type', or 'target', got '${keyAnyCase}'`,
|
||||
`Invalid filter predicate, expected key to be 'id', 'type', 'target', or 'entityRef', got '${keyAnyCase}'`,
|
||||
);
|
||||
}
|
||||
|
||||
result = applyFilterValueToQuery(clientType, result, key, value);
|
||||
// Map the API field name to the underlying column name
|
||||
const column = key === 'entityref' ? 'location_entity_ref' : key;
|
||||
result = applyFilterValueToQuery(clientType, result, column, value);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -571,10 +571,14 @@ components:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
entityRef:
|
||||
type: string
|
||||
description: The entity ref of the corresponding Location kind entity, e.g. location:default/generated-<sha1hex>.
|
||||
required:
|
||||
- target
|
||||
- type
|
||||
- id
|
||||
- entityRef
|
||||
description: Entity location for a specific entity.
|
||||
additionalProperties: false
|
||||
LocationSpec:
|
||||
@@ -1377,6 +1381,35 @@ paths:
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
put:
|
||||
operationId: UpdateLocation
|
||||
tags:
|
||||
- Locations
|
||||
description: Update the type and target of an existing location by id.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LocationInput'
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Location'
|
||||
default:
|
||||
$ref: '#/components/responses/ErrorResponse'
|
||||
security:
|
||||
- {}
|
||||
- JWT: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
delete:
|
||||
operationId: DeleteLocation
|
||||
tags:
|
||||
|
||||
@@ -156,6 +156,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';
|
||||
|
||||
/**
|
||||
@@ -218,6 +219,16 @@ export type GetLocationsByQuery = {
|
||||
body: GetLocationsByQueryRequest;
|
||||
response: LocationsQueryResponse | Error;
|
||||
};
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type UpdateLocation = {
|
||||
path: {
|
||||
id: string;
|
||||
};
|
||||
body: LocationInput;
|
||||
response: Location | Error;
|
||||
};
|
||||
|
||||
export type EndpointMap = {
|
||||
'#_delete|/entities/by-uid/{uid}': DeleteEntityByUid;
|
||||
@@ -257,4 +268,6 @@ export type EndpointMap = {
|
||||
'#get|/locations': GetLocations;
|
||||
|
||||
'#post|/locations/by-query': GetLocationsByQuery;
|
||||
|
||||
'#put|/locations/{id}': UpdateLocation;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -529,8 +529,13 @@ export const spec = {
|
||||
id: {
|
||||
type: 'string',
|
||||
},
|
||||
entityRef: {
|
||||
type: 'string',
|
||||
description:
|
||||
'The entity ref of the corresponding Location kind entity, e.g. location:default/generated-<sha1hex>.',
|
||||
},
|
||||
},
|
||||
required: ['target', 'type', 'id'],
|
||||
required: ['target', 'type', 'id', 'entityRef'],
|
||||
description: 'Entity location for a specific entity.',
|
||||
additionalProperties: false,
|
||||
},
|
||||
@@ -1654,6 +1659,53 @@ export const spec = {
|
||||
},
|
||||
],
|
||||
},
|
||||
put: {
|
||||
operationId: 'UpdateLocation',
|
||||
tags: ['Locations'],
|
||||
description:
|
||||
'Update the type and target of an existing location by id.',
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/LocationInput',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Ok',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/Location',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
default: {
|
||||
$ref: '#/components/responses/ErrorResponse',
|
||||
},
|
||||
},
|
||||
security: [
|
||||
{},
|
||||
{
|
||||
JWT: [],
|
||||
},
|
||||
],
|
||||
parameters: [
|
||||
{
|
||||
in: 'path',
|
||||
name: 'id',
|
||||
required: true,
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
delete: {
|
||||
operationId: 'DeleteLocation',
|
||||
tags: ['Locations'],
|
||||
|
||||
@@ -25,6 +25,7 @@ describe('AuthorizedLocationService', () => {
|
||||
listLocations: jest.fn(),
|
||||
queryLocations: jest.fn(),
|
||||
getLocation: jest.fn(),
|
||||
updateLocation: jest.fn(),
|
||||
deleteLocation: jest.fn(),
|
||||
getLocationByEntity: jest.fn(),
|
||||
};
|
||||
|
||||
@@ -123,6 +123,25 @@ export class AuthorizedLocationService implements LocationService {
|
||||
return this.locationService.getLocation(id, options);
|
||||
}
|
||||
|
||||
async updateLocation(
|
||||
id: string,
|
||||
location: LocationInput,
|
||||
options: { credentials: BackstageCredentials },
|
||||
): Promise<Location> {
|
||||
const authorizationResponse = (
|
||||
await this.permissionApi.authorize(
|
||||
[{ permission: catalogLocationCreatePermission }],
|
||||
{ credentials: options.credentials },
|
||||
)
|
||||
)[0];
|
||||
|
||||
if (authorizationResponse.result === AuthorizeResult.DENY) {
|
||||
throw new NotAllowedError();
|
||||
}
|
||||
|
||||
return this.locationService.updateLocation(id, location, options);
|
||||
}
|
||||
|
||||
async deleteLocation(
|
||||
id: string,
|
||||
options: { credentials: BackstageCredentials },
|
||||
|
||||
@@ -29,6 +29,7 @@ describe('DefaultLocationServiceTest', () => {
|
||||
listLocations: jest.fn(),
|
||||
queryLocations: jest.fn(),
|
||||
getLocation: jest.fn(),
|
||||
updateLocation: jest.fn(),
|
||||
getLocationByEntity: jest.fn(),
|
||||
};
|
||||
const locationService = new DefaultLocationService(store, orchestrator);
|
||||
@@ -144,7 +145,11 @@ describe('DefaultLocationServiceTest', () => {
|
||||
});
|
||||
|
||||
store.listLocations.mockResolvedValueOnce([
|
||||
{ id: '137', ...locationSpec },
|
||||
{
|
||||
id: '137',
|
||||
...locationSpec,
|
||||
entityRef: 'location:default/generated-137',
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await locationService.createLocation(
|
||||
@@ -226,7 +231,12 @@ describe('DefaultLocationServiceTest', () => {
|
||||
});
|
||||
|
||||
store.listLocations.mockResolvedValueOnce([
|
||||
{ id: '987', type: 'url', target: 'https://example.com' },
|
||||
{
|
||||
id: '987',
|
||||
type: 'url',
|
||||
target: 'https://example.com',
|
||||
entityRef: 'location:default/generated-987',
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await locationService.createLocation(
|
||||
@@ -245,6 +255,7 @@ describe('DefaultLocationServiceTest', () => {
|
||||
store.createLocation.mockResolvedValue({
|
||||
...locationSpec,
|
||||
id: '123',
|
||||
entityRef: 'location:default/generated-123',
|
||||
});
|
||||
|
||||
await expect(
|
||||
@@ -255,6 +266,7 @@ describe('DefaultLocationServiceTest', () => {
|
||||
id: '123',
|
||||
target: 'https://backstage.io/catalog-info.yaml',
|
||||
type: 'url',
|
||||
entityRef: 'location:default/generated-123',
|
||||
},
|
||||
});
|
||||
expect(store.createLocation).toHaveBeenCalledWith(
|
||||
@@ -275,6 +287,7 @@ describe('DefaultLocationServiceTest', () => {
|
||||
store.createLocation.mockResolvedValue({
|
||||
...locationSpec,
|
||||
id: '123',
|
||||
entityRef: 'location:default/generated-123',
|
||||
});
|
||||
|
||||
const locationServiceAllowingUnknownType = new DefaultLocationService(
|
||||
@@ -293,6 +306,7 @@ describe('DefaultLocationServiceTest', () => {
|
||||
id: '123',
|
||||
target: 'https://backstage.io/catalog-info.yaml',
|
||||
type: 'unknown',
|
||||
entityRef: 'location:default/generated-123',
|
||||
},
|
||||
});
|
||||
expect(store.createLocation).toHaveBeenCalledWith(
|
||||
@@ -325,6 +339,7 @@ describe('DefaultLocationServiceTest', () => {
|
||||
store.createLocation.mockResolvedValueOnce({
|
||||
id: 'existing-id',
|
||||
...locationSpec,
|
||||
entityRef: 'location:default/generated-existing-id',
|
||||
});
|
||||
|
||||
const result = await locationService.createLocation(locationSpec, false, {
|
||||
@@ -333,7 +348,11 @@ describe('DefaultLocationServiceTest', () => {
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
location: { id: 'existing-id', ...locationSpec },
|
||||
location: {
|
||||
id: 'existing-id',
|
||||
...locationSpec,
|
||||
entityRef: 'location:default/generated-existing-id',
|
||||
},
|
||||
entities: [],
|
||||
});
|
||||
expect(store.createLocation).toHaveBeenCalledWith(locationSpec, {
|
||||
|
||||
@@ -25,7 +25,10 @@ import {
|
||||
import { Location } from '@backstage/catalog-client';
|
||||
import { CatalogProcessingOrchestrator } from '../processing/types';
|
||||
import { LocationInput, LocationService, LocationStore } from './types';
|
||||
import { locationSpecToMetadataName } from '../util/conversion';
|
||||
import {
|
||||
computeLocationEntityRef,
|
||||
locationSpecToMetadataName,
|
||||
} from '../util/conversion';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { DeferredEntity } from '@backstage/plugin-catalog-node';
|
||||
import { FilterPredicate } from '@backstage/filter-predicates';
|
||||
@@ -99,6 +102,15 @@ export class DefaultLocationService implements LocationService {
|
||||
getLocation(id: string): Promise<Location> {
|
||||
return this.store.getLocation(id);
|
||||
}
|
||||
|
||||
updateLocation(
|
||||
id: string,
|
||||
location: LocationInput,
|
||||
_options: { credentials: BackstageCredentials },
|
||||
): Promise<Location> {
|
||||
return this.store.updateLocation(id, location);
|
||||
}
|
||||
|
||||
deleteLocation(id: string): Promise<void> {
|
||||
return this.store.deleteLocation(id);
|
||||
}
|
||||
@@ -182,7 +194,11 @@ export class DefaultLocationService implements LocationService {
|
||||
|
||||
return {
|
||||
exists: await existsPromise,
|
||||
location: { ...spec, id: `${spec.type}:${spec.target}` },
|
||||
location: {
|
||||
...spec,
|
||||
id: `${spec.type}:${spec.target}`,
|
||||
entityRef: computeLocationEntityRef(spec.type, spec.target),
|
||||
},
|
||||
entities,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ describe('createRouter readonly disabled', () => {
|
||||
createLocation: jest.fn(),
|
||||
queryLocations: jest.fn(),
|
||||
listLocations: jest.fn(),
|
||||
updateLocation: jest.fn(),
|
||||
deleteLocation: jest.fn(),
|
||||
getLocationByEntity: jest.fn(),
|
||||
};
|
||||
@@ -800,7 +801,12 @@ describe('createRouter readonly disabled', () => {
|
||||
describe('GET /locations', () => {
|
||||
it('happy path: lists locations', async () => {
|
||||
const locations: Location[] = [
|
||||
{ id: 'foo', type: 'url', target: 'example.com' },
|
||||
{
|
||||
id: 'foo',
|
||||
type: 'url',
|
||||
target: 'example.com',
|
||||
entityRef: 'location:default/generated-foo',
|
||||
},
|
||||
];
|
||||
locationService.listLocations.mockResolvedValueOnce(locations);
|
||||
|
||||
@@ -811,7 +817,14 @@ describe('createRouter readonly disabled', () => {
|
||||
});
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual([
|
||||
{ data: { id: 'foo', target: 'example.com', type: 'url' } },
|
||||
{
|
||||
data: {
|
||||
id: 'foo',
|
||||
target: 'example.com',
|
||||
type: 'url',
|
||||
entityRef: 'location:default/generated-foo',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -822,6 +835,7 @@ describe('createRouter readonly disabled', () => {
|
||||
id: 'foo',
|
||||
type: 'url',
|
||||
target: 'example.com',
|
||||
entityRef: 'location:default/generated-foo',
|
||||
};
|
||||
locationService.getLocation.mockResolvedValueOnce(location);
|
||||
|
||||
@@ -836,6 +850,7 @@ describe('createRouter readonly disabled', () => {
|
||||
id: 'foo',
|
||||
target: 'example.com',
|
||||
type: 'url',
|
||||
entityRef: 'location:default/generated-foo',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -863,7 +878,11 @@ describe('createRouter readonly disabled', () => {
|
||||
};
|
||||
|
||||
locationService.createLocation.mockResolvedValue({
|
||||
location: { id: 'a', ...spec },
|
||||
location: {
|
||||
id: 'a',
|
||||
...spec,
|
||||
entityRef: 'location:default/generated-a',
|
||||
},
|
||||
entities: [],
|
||||
});
|
||||
|
||||
@@ -879,7 +898,11 @@ describe('createRouter readonly disabled', () => {
|
||||
expect(response.status).toEqual(201);
|
||||
expect(response.body).toEqual(
|
||||
expect.objectContaining({
|
||||
location: { id: 'a', ...spec },
|
||||
location: {
|
||||
id: 'a',
|
||||
...spec,
|
||||
entityRef: 'location:default/generated-a',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -891,7 +914,11 @@ describe('createRouter readonly disabled', () => {
|
||||
};
|
||||
|
||||
locationService.createLocation.mockResolvedValue({
|
||||
location: { id: 'a', ...spec },
|
||||
location: {
|
||||
id: 'a',
|
||||
...spec,
|
||||
entityRef: 'location:default/generated-a',
|
||||
},
|
||||
entities: [],
|
||||
});
|
||||
|
||||
@@ -907,7 +934,11 @@ describe('createRouter readonly disabled', () => {
|
||||
expect(response.status).toEqual(201);
|
||||
expect(response.body).toEqual(
|
||||
expect.objectContaining({
|
||||
location: { id: 'a', ...spec },
|
||||
location: {
|
||||
id: 'a',
|
||||
...spec,
|
||||
entityRef: 'location:default/generated-a',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -920,6 +951,7 @@ describe('createRouter readonly disabled', () => {
|
||||
createLocation: jest.fn(),
|
||||
queryLocations: jest.fn(),
|
||||
listLocations: jest.fn(),
|
||||
updateLocation: jest.fn(),
|
||||
deleteLocation: jest.fn(),
|
||||
getLocationByEntity: jest.fn(),
|
||||
};
|
||||
@@ -943,8 +975,18 @@ describe('createRouter readonly disabled', () => {
|
||||
|
||||
it('happy path: queries locations without pagination', async () => {
|
||||
const locations: Location[] = [
|
||||
{ id: 'loc1', type: 'url', target: 'https://example.com/a' },
|
||||
{ id: 'loc2', type: 'url', target: 'https://example.com/b' },
|
||||
{
|
||||
id: 'loc1',
|
||||
type: 'url',
|
||||
target: 'https://example.com/a',
|
||||
entityRef: 'location:default/generated-loc1',
|
||||
},
|
||||
{
|
||||
id: 'loc2',
|
||||
type: 'url',
|
||||
target: 'https://example.com/b',
|
||||
entityRef: 'location:default/generated-loc2',
|
||||
},
|
||||
];
|
||||
locationService.queryLocations.mockResolvedValueOnce({
|
||||
items: locations,
|
||||
@@ -970,7 +1012,12 @@ describe('createRouter readonly disabled', () => {
|
||||
|
||||
it('happy path: queries locations with filter', async () => {
|
||||
const locations: Location[] = [
|
||||
{ id: 'loc1', type: 'url', target: 'https://example.com/a' },
|
||||
{
|
||||
id: 'loc1',
|
||||
type: 'url',
|
||||
target: 'https://example.com/a',
|
||||
entityRef: 'location:default/generated-loc1',
|
||||
},
|
||||
];
|
||||
locationService.queryLocations.mockResolvedValueOnce({
|
||||
items: locations,
|
||||
@@ -997,9 +1044,24 @@ describe('createRouter readonly disabled', () => {
|
||||
|
||||
it('returns nextCursor when more results exist', async () => {
|
||||
const locations: Location[] = [
|
||||
{ id: 'loc1', type: 'url', target: 'https://example.com/a' },
|
||||
{ id: 'loc2', type: 'url', target: 'https://example.com/b' },
|
||||
{ id: 'loc3', type: 'url', target: 'https://example.com/c' },
|
||||
{
|
||||
id: 'loc1',
|
||||
type: 'url',
|
||||
target: 'https://example.com/a',
|
||||
entityRef: 'location:default/generated-loc1',
|
||||
},
|
||||
{
|
||||
id: 'loc2',
|
||||
type: 'url',
|
||||
target: 'https://example.com/b',
|
||||
entityRef: 'location:default/generated-loc2',
|
||||
},
|
||||
{
|
||||
id: 'loc3',
|
||||
type: 'url',
|
||||
target: 'https://example.com/c',
|
||||
entityRef: 'location:default/generated-loc3',
|
||||
},
|
||||
];
|
||||
locationService.queryLocations.mockResolvedValueOnce({
|
||||
items: locations,
|
||||
@@ -1032,7 +1094,12 @@ describe('createRouter readonly disabled', () => {
|
||||
|
||||
it('uses cursor for pagination', async () => {
|
||||
const locations: Location[] = [
|
||||
{ id: 'loc3', type: 'url', target: 'https://example.com/c' },
|
||||
{
|
||||
id: 'loc3',
|
||||
type: 'url',
|
||||
target: 'https://example.com/c',
|
||||
entityRef: 'location:default/generated-loc3',
|
||||
},
|
||||
];
|
||||
locationService.queryLocations.mockResolvedValueOnce({
|
||||
items: locations,
|
||||
@@ -1115,6 +1182,7 @@ describe('createRouter readonly disabled', () => {
|
||||
id: 'foo',
|
||||
type: 'url',
|
||||
target: 'example.com',
|
||||
entityRef: 'location:default/generated-foo',
|
||||
};
|
||||
locationService.getLocationByEntity.mockResolvedValueOnce(location);
|
||||
|
||||
@@ -1132,6 +1200,7 @@ describe('createRouter readonly disabled', () => {
|
||||
id: 'foo',
|
||||
target: 'example.com',
|
||||
type: 'url',
|
||||
entityRef: 'location:default/generated-foo',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1367,6 +1436,7 @@ describe('createRouter readonly and raw json enabled', () => {
|
||||
createLocation: jest.fn(),
|
||||
listLocations: jest.fn(),
|
||||
queryLocations: jest.fn(),
|
||||
updateLocation: jest.fn(),
|
||||
deleteLocation: jest.fn(),
|
||||
getLocationByEntity: jest.fn(),
|
||||
};
|
||||
@@ -1427,7 +1497,12 @@ describe('createRouter readonly and raw json enabled', () => {
|
||||
describe('GET /locations', () => {
|
||||
it('happy path: lists locations', async () => {
|
||||
const locations: Location[] = [
|
||||
{ id: 'foo', type: 'url', target: 'example.com' },
|
||||
{
|
||||
id: 'foo',
|
||||
type: 'url',
|
||||
target: 'example.com',
|
||||
entityRef: 'location:default/generated-foo',
|
||||
},
|
||||
];
|
||||
locationService.listLocations.mockResolvedValueOnce(locations);
|
||||
|
||||
@@ -1439,7 +1514,14 @@ describe('createRouter readonly and raw json enabled', () => {
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual([
|
||||
{ data: { id: 'foo', target: 'example.com', type: 'url' } },
|
||||
{
|
||||
data: {
|
||||
id: 'foo',
|
||||
target: 'example.com',
|
||||
type: 'url',
|
||||
entityRef: 'location:default/generated-foo',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1450,6 +1532,7 @@ describe('createRouter readonly and raw json enabled', () => {
|
||||
id: 'foo',
|
||||
type: 'url',
|
||||
target: 'example.com',
|
||||
entityRef: 'location:default/generated-foo',
|
||||
};
|
||||
locationService.getLocation.mockResolvedValueOnce(location);
|
||||
|
||||
@@ -1464,6 +1547,7 @@ describe('createRouter readonly and raw json enabled', () => {
|
||||
id: 'foo',
|
||||
target: 'example.com',
|
||||
type: 'url',
|
||||
entityRef: 'location:default/generated-foo',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1492,7 +1576,11 @@ describe('createRouter readonly and raw json enabled', () => {
|
||||
};
|
||||
|
||||
locationService.createLocation.mockResolvedValue({
|
||||
location: { id: 'a', ...spec },
|
||||
location: {
|
||||
id: 'a',
|
||||
...spec,
|
||||
entityRef: 'location:default/generated-a',
|
||||
},
|
||||
entities: [],
|
||||
});
|
||||
|
||||
@@ -1508,7 +1596,11 @@ describe('createRouter readonly and raw json enabled', () => {
|
||||
expect(response.status).toEqual(201);
|
||||
expect(response.body).toEqual(
|
||||
expect.objectContaining({
|
||||
location: { id: 'a', ...spec },
|
||||
location: {
|
||||
id: 'a',
|
||||
...spec,
|
||||
entityRef: 'location:default/generated-a',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -1528,6 +1620,7 @@ describe('createRouter readonly and raw json enabled', () => {
|
||||
id: 'foo',
|
||||
type: 'url',
|
||||
target: 'example.com',
|
||||
entityRef: 'location:default/generated-foo',
|
||||
};
|
||||
locationService.getLocationByEntity.mockResolvedValueOnce(location);
|
||||
|
||||
@@ -1545,6 +1638,7 @@ describe('createRouter readonly and raw json enabled', () => {
|
||||
id: 'foo',
|
||||
target: 'example.com',
|
||||
type: 'url',
|
||||
entityRef: 'location:default/generated-foo',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1610,26 +1704,36 @@ describe('POST /locations/by-query works end to end', () => {
|
||||
id: '00000000-0000-0000-0000-000000000001',
|
||||
type: 'url',
|
||||
target: 'https://example.com/a.yaml',
|
||||
entityRef:
|
||||
'location:default/generated-17fb6f13ffb6251438be1e4f37c6482b83ede45c',
|
||||
},
|
||||
{
|
||||
id: '00000000-0000-0000-0000-000000000002',
|
||||
type: 'url',
|
||||
target: 'https://example.com/b.yaml',
|
||||
entityRef:
|
||||
'location:default/generated-50316008e1cdf0dfc8740b7691661615a3588ae5',
|
||||
},
|
||||
{
|
||||
id: '00000000-0000-0000-0000-000000000003',
|
||||
type: 'url',
|
||||
target: 'https://example.com/c.yaml',
|
||||
entityRef:
|
||||
'location:default/generated-f50fac82cafdc5ec095faee0a33ced6d9286fe08',
|
||||
},
|
||||
{
|
||||
id: '00000000-0000-0000-0000-000000000004',
|
||||
type: 'url',
|
||||
target: 'https://example.com/d.yaml',
|
||||
entityRef:
|
||||
'location:default/generated-45aa6e8abd2e13841ddf91bd04249460cbe55a47',
|
||||
},
|
||||
{
|
||||
id: '00000000-0000-0000-0000-000000000005',
|
||||
type: 'url',
|
||||
target: 'https://example.com/e.yaml',
|
||||
entityRef:
|
||||
'location:default/generated-c991bf07f54891933929eadce219b11fd32eaa5a',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1637,11 +1741,10 @@ describe('POST /locations/by-query works end to end', () => {
|
||||
await knex<DbLocationsRow>('locations').delete();
|
||||
for (const location of locations) {
|
||||
await knex<DbLocationsRow>('locations').insert({
|
||||
...location,
|
||||
location_entity_ref: computeLocationEntityRef(
|
||||
location.type,
|
||||
location.target,
|
||||
),
|
||||
id: location.id,
|
||||
type: location.type,
|
||||
target: location.target,
|
||||
location_entity_ref: location.entityRef,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1694,16 +1797,22 @@ describe('POST /locations/by-query works end to end', () => {
|
||||
id: '00000000-0000-0000-0000-000000000001',
|
||||
type: 'url',
|
||||
target: 'https://example.com/a.yaml',
|
||||
entityRef:
|
||||
'location:default/generated-17fb6f13ffb6251438be1e4f37c6482b83ede45c',
|
||||
},
|
||||
{
|
||||
id: '00000000-0000-0000-0000-000000000002',
|
||||
type: 'file',
|
||||
target: '/tmp/b.yaml',
|
||||
entityRef:
|
||||
'location:default/generated-49cac033ce5406c6fefc6ac16cf70f90852d9257',
|
||||
},
|
||||
{
|
||||
id: '00000000-0000-0000-0000-000000000003',
|
||||
type: 'url',
|
||||
target: 'https://example.com/c.yaml',
|
||||
entityRef:
|
||||
'location:default/generated-f50fac82cafdc5ec095faee0a33ced6d9286fe08',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1711,11 +1820,10 @@ describe('POST /locations/by-query works end to end', () => {
|
||||
await knex<DbLocationsRow>('locations').delete();
|
||||
for (const location of locations) {
|
||||
await knex<DbLocationsRow>('locations').insert({
|
||||
...location,
|
||||
location_entity_ref: computeLocationEntityRef(
|
||||
location.type,
|
||||
location.target,
|
||||
),
|
||||
id: location.id,
|
||||
type: location.type,
|
||||
target: location.target,
|
||||
location_entity_ref: location.entityRef,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -747,6 +747,36 @@ export async function createRouter(
|
||||
throw err;
|
||||
}
|
||||
})
|
||||
.put('/locations/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const location = await validateRequestBody(req, locationInput);
|
||||
|
||||
const auditorEvent = await auditor.createEvent({
|
||||
eventId: 'location-mutate',
|
||||
severityLevel: 'medium',
|
||||
request: req,
|
||||
meta: {
|
||||
actionType: 'update',
|
||||
id,
|
||||
location,
|
||||
},
|
||||
});
|
||||
|
||||
disallowReadonlyMode(readonlyEnabled);
|
||||
|
||||
try {
|
||||
const output = await locationService.updateLocation(id, location, {
|
||||
credentials: await httpAuth.credentials(req),
|
||||
});
|
||||
|
||||
await auditorEvent?.success({ meta: { location: output } });
|
||||
|
||||
res.status(200).json(output);
|
||||
} catch (err) {
|
||||
await auditorEvent?.fail({ error: err });
|
||||
throw err;
|
||||
}
|
||||
})
|
||||
.delete('/locations/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
|
||||
@@ -52,6 +52,11 @@ export interface LocationService {
|
||||
id: string,
|
||||
options: { credentials: BackstageCredentials },
|
||||
): Promise<Location>;
|
||||
updateLocation(
|
||||
id: string,
|
||||
location: LocationInput,
|
||||
options: { credentials: BackstageCredentials },
|
||||
): Promise<Location>;
|
||||
deleteLocation(
|
||||
id: string,
|
||||
options: { credentials: BackstageCredentials },
|
||||
@@ -98,6 +103,7 @@ export interface LocationStore {
|
||||
query?: FilterPredicate;
|
||||
}): Promise<{ items: Location[]; totalItems: number }>;
|
||||
getLocation(id: string): Promise<Location>;
|
||||
updateLocation(id: string, location: LocationInput): Promise<Location>;
|
||||
deleteLocation(id: string): Promise<void>;
|
||||
getLocationByEntity(entityRef: CompoundEntityRef | string): Promise<Location>;
|
||||
}
|
||||
|
||||
@@ -124,6 +124,7 @@ describe('CatalogImportClient', () => {
|
||||
id: 'id-0',
|
||||
type: 'url',
|
||||
target: 'http://example.com/folder/catalog-info.yaml',
|
||||
entityRef: 'location:default/generated-id-0',
|
||||
},
|
||||
entities: [
|
||||
{
|
||||
@@ -172,6 +173,7 @@ describe('CatalogImportClient', () => {
|
||||
type: 'url',
|
||||
target:
|
||||
'https://dev.azure.com/any-org/any-project/_git/any-repository?path=%2Fcatalog-info.yaml',
|
||||
entityRef: 'location:default/generated-id-0',
|
||||
},
|
||||
entities: [
|
||||
{
|
||||
@@ -221,6 +223,7 @@ describe('CatalogImportClient', () => {
|
||||
id: 'id-0',
|
||||
type: 'url',
|
||||
target: 'http://example.com/folder/catalog-info.yaml?branch=test',
|
||||
entityRef: 'location:default/generated-id-0',
|
||||
},
|
||||
entities: [
|
||||
{
|
||||
|
||||
@@ -126,6 +126,15 @@ export interface CatalogServiceMock extends CatalogService, CatalogApi {
|
||||
options?: CatalogServiceRequestOptions | CatalogRequestOptions,
|
||||
): AsyncIterable<Location_2[]>;
|
||||
// (undocumented)
|
||||
updateLocation(
|
||||
id: string,
|
||||
location: {
|
||||
type?: string;
|
||||
target: string;
|
||||
},
|
||||
options?: CatalogServiceRequestOptions | CatalogRequestOptions,
|
||||
): Promise<Location_2>;
|
||||
// (undocumented)
|
||||
validateEntity(
|
||||
entity: Entity,
|
||||
locationRef: string,
|
||||
|
||||
@@ -267,6 +267,15 @@ export interface CatalogService {
|
||||
options: CatalogServiceRequestOptions,
|
||||
): AsyncIterable<Location_2[]>;
|
||||
// (undocumented)
|
||||
updateLocation(
|
||||
id: string,
|
||||
location: {
|
||||
type?: string;
|
||||
target: string;
|
||||
},
|
||||
options: CatalogServiceRequestOptions,
|
||||
): Promise<Location_2>;
|
||||
// (undocumented)
|
||||
validateEntity(
|
||||
entity: Entity,
|
||||
locationRef: string,
|
||||
|
||||
@@ -140,6 +140,12 @@ export interface CatalogService {
|
||||
options: CatalogServiceRequestOptions,
|
||||
): Promise<void>;
|
||||
|
||||
updateLocation(
|
||||
id: string,
|
||||
location: { type?: string; target: string },
|
||||
options: CatalogServiceRequestOptions,
|
||||
): Promise<Location>;
|
||||
|
||||
getLocationByEntity(
|
||||
entityRef: string | CompoundEntityRef,
|
||||
options: CatalogServiceRequestOptions,
|
||||
@@ -327,6 +333,18 @@ class DefaultCatalogService implements CatalogService {
|
||||
);
|
||||
}
|
||||
|
||||
async updateLocation(
|
||||
id: string,
|
||||
location: { type?: string; target: string },
|
||||
options: CatalogServiceRequestOptions,
|
||||
): Promise<Location> {
|
||||
return this.#catalogApi.updateLocation(
|
||||
id,
|
||||
location,
|
||||
await this.#getOptions(options),
|
||||
);
|
||||
}
|
||||
|
||||
async getLocationByEntity(
|
||||
entityRef: string | CompoundEntityRef,
|
||||
options: CatalogServiceRequestOptions,
|
||||
|
||||
@@ -76,6 +76,7 @@ export namespace catalogServiceMock {
|
||||
getLocationByRef: jest.fn(),
|
||||
addLocation: jest.fn(),
|
||||
removeLocationById: jest.fn(),
|
||||
updateLocation: jest.fn(),
|
||||
getLocationByEntity: jest.fn(),
|
||||
validateEntity: jest.fn(),
|
||||
analyzeLocation: jest.fn(),
|
||||
|
||||
@@ -134,6 +134,12 @@ export interface CatalogServiceMock extends CatalogService, CatalogApi {
|
||||
options?: CatalogServiceRequestOptions | CatalogRequestOptions,
|
||||
): Promise<void>;
|
||||
|
||||
updateLocation(
|
||||
id: string,
|
||||
location: { type?: string; target: string },
|
||||
options?: CatalogServiceRequestOptions | CatalogRequestOptions,
|
||||
): Promise<Location>;
|
||||
|
||||
getLocationByEntity(
|
||||
entityRef: string | CompoundEntityRef,
|
||||
options?: CatalogServiceRequestOptions | CatalogRequestOptions,
|
||||
|
||||
+12
-2
@@ -72,7 +72,12 @@ describe('useUnregisterEntityDialogState', () => {
|
||||
|
||||
expect(rendered.result.current).toEqual({ type: 'loading' });
|
||||
|
||||
resolveLocation({ type: 'url', target: 'https://example.com', id: 'x' });
|
||||
resolveLocation({
|
||||
type: 'url',
|
||||
target: 'https://example.com',
|
||||
id: 'x',
|
||||
entityRef: 'location:default/generated-x',
|
||||
});
|
||||
resolveColocatedEntities([entity]);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -94,7 +99,12 @@ describe('useUnregisterEntityDialogState', () => {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
|
||||
resolveLocation({ type: 'bootstrap', target: 'bootstrap', id: 'x' });
|
||||
resolveLocation({
|
||||
type: 'bootstrap',
|
||||
target: 'bootstrap',
|
||||
id: 'x',
|
||||
entityRef: '',
|
||||
});
|
||||
resolveColocatedEntities([]);
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
@@ -78,6 +78,7 @@ export namespace catalogApiMock {
|
||||
getLocationByRef: jest.fn(),
|
||||
addLocation: jest.fn(),
|
||||
removeLocationById: jest.fn(),
|
||||
updateLocation: jest.fn(),
|
||||
getLocationByEntity: jest.fn(),
|
||||
validateEntity: jest.fn(),
|
||||
analyzeLocation: jest.fn(),
|
||||
|
||||
Reference in New Issue
Block a user