catalog-backend: relations working e2e
Co-authored-by: blam <ben@blam.sh>
This commit is contained in:
@@ -32,21 +32,21 @@ const schema = yup.object<Partial<GroupEntityV1alpha1>>({
|
||||
// one element and there is no simple workaround -_-
|
||||
// the cast is there to convince typescript that the array itself is
|
||||
// required without using .required()
|
||||
ancestors: yup.array(yup.string().required()).test({
|
||||
name: 'isDefined',
|
||||
message: 'ancestors must be defined',
|
||||
test: v => Boolean(v),
|
||||
}) as yup.ArraySchema<string, object>,
|
||||
children: yup.array(yup.string().required()).test({
|
||||
name: 'isDefined',
|
||||
message: 'children must be defined',
|
||||
test: v => Boolean(v),
|
||||
}) as yup.ArraySchema<string, object>,
|
||||
descendants: yup.array(yup.string().required()).test({
|
||||
name: 'isDefined',
|
||||
message: 'descendants must be defined',
|
||||
test: v => Boolean(v),
|
||||
}) as yup.ArraySchema<string, object>,
|
||||
// ancestors: yup.array(yup.string().required()).test({
|
||||
// name: 'isDefined',
|
||||
// message: 'ancestors must be defined',
|
||||
// test: v => Boolean(v),
|
||||
// }) as yup.ArraySchema<string, object>,
|
||||
// children: yup.array(yup.string().required()).test({
|
||||
// name: 'isDefined',
|
||||
// message: 'children must be defined',
|
||||
// test: v => Boolean(v),
|
||||
// }) as yup.ArraySchema<string, object>,
|
||||
// descendants: yup.array(yup.string().required()).test({
|
||||
// name: 'isDefined',
|
||||
// message: 'descendants must be defined',
|
||||
// test: v => Boolean(v),
|
||||
// }) as yup.ArraySchema<string, object>,
|
||||
})
|
||||
.required(),
|
||||
});
|
||||
|
||||
@@ -37,11 +37,11 @@ const schema = yup.object<Partial<UserEntityV1alpha1>>({
|
||||
// element and there is no simple workaround -_-
|
||||
// the cast is there to convince typescript that the array itself is
|
||||
// required without using .required()
|
||||
memberOf: yup.array(yup.string().required()).test({
|
||||
name: 'isDefined',
|
||||
message: 'memberOf must be defined',
|
||||
test: v => Boolean(v),
|
||||
}) as yup.ArraySchema<string, object>,
|
||||
// memberOf: yup.array(yup.string().required()).test({
|
||||
// name: 'isDefined',
|
||||
// message: 'memberOf must be defined',
|
||||
// test: v => Boolean(v),
|
||||
// }) as yup.ArraySchema<string, object>,
|
||||
})
|
||||
.required(),
|
||||
});
|
||||
|
||||
@@ -29,7 +29,11 @@ import limiterFactory from 'p-limit';
|
||||
import { Logger } from 'winston';
|
||||
import type { Database, DbEntityResponse, EntityFilters } from '../database';
|
||||
import { durationText } from '../util/timing';
|
||||
import type { EntitiesCatalog } from './types';
|
||||
import type {
|
||||
EntitiesCatalog,
|
||||
EntityMutationRequest,
|
||||
EntityMutationResponse,
|
||||
} from './types';
|
||||
|
||||
type BatchContext = {
|
||||
kind: string;
|
||||
@@ -63,7 +67,7 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog {
|
||||
return items.map(i => i.entity);
|
||||
}
|
||||
|
||||
async addOrUpdateEntity(
|
||||
private async addOrUpdateEntity(
|
||||
entity: Entity,
|
||||
locationId?: string,
|
||||
): Promise<Entity> {
|
||||
@@ -96,15 +100,6 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog {
|
||||
});
|
||||
}
|
||||
|
||||
async addEntities(entities: Entity[], locationId?: string): Promise<void> {
|
||||
await this.database.transaction(async tx => {
|
||||
await this.database.addEntities(
|
||||
tx,
|
||||
entities.map(entity => ({ locationId, entity })),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async removeEntityByUid(uid: string): Promise<void> {
|
||||
return await this.database.transaction(async tx => {
|
||||
const entityResponse = await this.database.entityByUid(tx, uid);
|
||||
@@ -138,27 +133,31 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog {
|
||||
* @param entities Some entities
|
||||
* @param locationId The location that they all belong to
|
||||
*/
|
||||
async batchAddOrUpdateEntities(entities: Entity[], locationId?: string) {
|
||||
async batchAddOrUpdateEntities(
|
||||
requests: EntityMutationRequest[],
|
||||
locationId?: string,
|
||||
): Promise<EntityMutationResponse[]> {
|
||||
// Group the entities by unique kind+namespace combinations
|
||||
const entitiesByKindAndNamespace = groupBy(entities, entity => {
|
||||
const entitiesByKindAndNamespace = groupBy(requests, ({ entity }) => {
|
||||
const name = getEntityName(entity);
|
||||
return `${name.kind}:${name.namespace}`.toLowerCase();
|
||||
});
|
||||
|
||||
const limiter = limiterFactory(BATCH_CONCURRENCY);
|
||||
const tasks: Promise<void>[] = [];
|
||||
const tasks: Promise<EntityMutationResponse[]>[] = [];
|
||||
|
||||
for (const groupEntities of Object.values(entitiesByKindAndNamespace)) {
|
||||
const { kind, namespace } = getEntityName(groupEntities[0]);
|
||||
for (const groupRequests of Object.values(entitiesByKindAndNamespace)) {
|
||||
const { kind, namespace } = getEntityName(groupRequests[0].entity);
|
||||
|
||||
// Go through the new entities in reasonable chunk sizes (sometimes,
|
||||
// sources produce tens of thousands of entities, and those are too large
|
||||
// batch sizes to reasonably send to the database)
|
||||
for (const batch of chunk(groupEntities, BATCH_SIZE)) {
|
||||
for (const batch of chunk(groupRequests, BATCH_SIZE)) {
|
||||
tasks.push(
|
||||
limiter(async () => {
|
||||
const first = serializeEntityRef(batch[0]);
|
||||
const last = serializeEntityRef(batch[batch.length - 1]);
|
||||
const first = serializeEntityRef(batch[0].entity);
|
||||
const last = serializeEntityRef(batch[batch.length - 1].entity);
|
||||
const modifiedEntityIds: EntityMutationResponse[] = [];
|
||||
this.logger.debug(
|
||||
`Considering batch ${first}-${last} (${batch.length} entries)`,
|
||||
);
|
||||
@@ -171,8 +170,16 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog {
|
||||
batch,
|
||||
context,
|
||||
);
|
||||
if (toAdd.length) await this.batchAdd(toAdd, context);
|
||||
if (toUpdate.length) await this.batchUpdate(toUpdate, context);
|
||||
if (toAdd.length) {
|
||||
modifiedEntityIds.push(
|
||||
...(await this.batchAdd(toAdd, context)),
|
||||
);
|
||||
}
|
||||
if (toUpdate.length) {
|
||||
modifiedEntityIds.push(
|
||||
...(await this.batchUpdate(toUpdate, context)),
|
||||
);
|
||||
}
|
||||
break;
|
||||
} catch (e) {
|
||||
if (e instanceof ConflictError && attempt < BATCH_ATTEMPTS) {
|
||||
@@ -184,16 +191,18 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return modifiedEntityIds;
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(tasks);
|
||||
return (await Promise.all(tasks)).flat();
|
||||
}
|
||||
|
||||
// Set the relations originating from an entity using the DB layer
|
||||
async setRelations(
|
||||
private async setRelations(
|
||||
originatingEntityId: string,
|
||||
relations: EntityRelationSpec[],
|
||||
): Promise<void> {
|
||||
@@ -207,15 +216,15 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog {
|
||||
// produce the list of entities to be added, and the list of entities to be
|
||||
// updated
|
||||
private async analyzeBatch(
|
||||
newEntities: Entity[],
|
||||
requests: EntityMutationRequest[],
|
||||
{ kind, namespace }: BatchContext,
|
||||
): Promise<{
|
||||
toAdd: Entity[];
|
||||
toUpdate: Entity[];
|
||||
toAdd: EntityMutationRequest[];
|
||||
toUpdate: EntityMutationRequest[];
|
||||
}> {
|
||||
const markTimestamp = process.hrtime();
|
||||
|
||||
const names = newEntities.map(e => e.metadata.name);
|
||||
const names = requests.map(({ entity }) => entity.metadata.name);
|
||||
const oldEntities = await this.entities({
|
||||
kind: kind,
|
||||
'metadata.namespace': namespace,
|
||||
@@ -226,18 +235,19 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog {
|
||||
oldEntities.map(e => [e.metadata.name, e]),
|
||||
);
|
||||
|
||||
const toAdd: Entity[] = [];
|
||||
const toUpdate: Entity[] = [];
|
||||
const toAdd: EntityMutationRequest[] = [];
|
||||
const toUpdate: EntityMutationRequest[] = [];
|
||||
|
||||
for (const newEntity of newEntities) {
|
||||
for (const request of requests) {
|
||||
const newEntity = request.entity;
|
||||
const oldEntity = oldEntitiesByName.get(newEntity.metadata.name);
|
||||
if (!oldEntity) {
|
||||
toAdd.push(newEntity);
|
||||
toAdd.push(request);
|
||||
} else if (entityHasChanges(oldEntity, newEntity)) {
|
||||
// TODO(freben): This currently uses addOrUpdateEntity under the hood,
|
||||
// but should probably calculate the end result entity right here
|
||||
// instead and call a dedicated batch update database method instead
|
||||
toUpdate.push(newEntity);
|
||||
toUpdate.push(request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,28 +262,55 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog {
|
||||
|
||||
// Efficiently adds the given entities to storage, under the assumption that
|
||||
// they do not conflict with any existing entities
|
||||
private async batchAdd(entities: Entity[], { locationId }: BatchContext) {
|
||||
private async batchAdd(
|
||||
requests: EntityMutationRequest[],
|
||||
{ locationId }: BatchContext,
|
||||
): Promise<EntityMutationResponse[]> {
|
||||
const markTimestamp = process.hrtime();
|
||||
|
||||
await this.addEntities(entities, locationId);
|
||||
const res = await this.database.transaction(
|
||||
async tx =>
|
||||
await this.database.addEntities(
|
||||
tx,
|
||||
requests.map(({ entity }) => ({ locationId, entity })),
|
||||
),
|
||||
);
|
||||
|
||||
const entityIds = res.map(({ entity }) => ({
|
||||
entityId: entity.metadata.uid!,
|
||||
}));
|
||||
|
||||
for (const [index, { entityId }] of entityIds.entries()) {
|
||||
await this.setRelations(entityId, requests[index].relations);
|
||||
}
|
||||
|
||||
this.logger.debug(
|
||||
`Added ${entities.length} entities in ${durationText(markTimestamp)}`,
|
||||
`Added ${requests.length} entities in ${durationText(markTimestamp)}`,
|
||||
);
|
||||
|
||||
return entityIds;
|
||||
}
|
||||
|
||||
// Efficiently updates the given entities into storage, under the assumption
|
||||
// that there already exist entities with the same names
|
||||
private async batchUpdate(entities: Entity[], { locationId }: BatchContext) {
|
||||
private async batchUpdate(
|
||||
requests: EntityMutationRequest[],
|
||||
{ locationId }: BatchContext,
|
||||
): Promise<EntityMutationResponse[]> {
|
||||
const markTimestamp = process.hrtime();
|
||||
|
||||
const responseIds: EntityMutationResponse[] = [];
|
||||
// TODO(freben): Still not batched
|
||||
for (const entity of entities) {
|
||||
await this.addOrUpdateEntity(entity, locationId);
|
||||
for (const entity of requests) {
|
||||
const res = await this.addOrUpdateEntity(entity.entity, locationId);
|
||||
const entityId = res.metadata.uid!;
|
||||
responseIds.push({ entityId });
|
||||
await this.setRelations(entityId, entity.relations);
|
||||
}
|
||||
|
||||
this.logger.debug(
|
||||
`Updated ${entities.length} entities in ${durationText(markTimestamp)}`,
|
||||
`Updated ${requests.length} entities in ${durationText(markTimestamp)}`,
|
||||
);
|
||||
|
||||
return responseIds;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,10 +21,17 @@ import type { EntityFilters } from '../database';
|
||||
// Entities
|
||||
//
|
||||
|
||||
export type EntityMutationRequest = {
|
||||
entity: Entity;
|
||||
relations: EntityRelationSpec[];
|
||||
};
|
||||
|
||||
export type EntityMutationResponse = {
|
||||
entityId: string;
|
||||
};
|
||||
|
||||
export type EntitiesCatalog = {
|
||||
entities(filters?: EntityFilters): Promise<Entity[]>;
|
||||
addOrUpdateEntity(entity: Entity, locationId?: string): Promise<Entity>;
|
||||
addEntities(entities: Entity[], locationId?: string): Promise<void>;
|
||||
removeEntityByUid(uid: string): Promise<void>;
|
||||
|
||||
/**
|
||||
@@ -34,15 +41,9 @@ export type EntitiesCatalog = {
|
||||
* @param locationId The location that they all belong to
|
||||
*/
|
||||
batchAddOrUpdateEntities(
|
||||
entities: Entity[],
|
||||
entities: EntityMutationRequest[],
|
||||
locationId?: string,
|
||||
): Promise<void>;
|
||||
|
||||
// Same as the DB layer
|
||||
setRelations(
|
||||
entityUid: string,
|
||||
relations: EntityRelationSpec[],
|
||||
): Promise<void>;
|
||||
): Promise<EntityMutationResponse[]>;
|
||||
};
|
||||
|
||||
//
|
||||
|
||||
@@ -31,10 +31,7 @@ describe('HigherOrderOperations', () => {
|
||||
beforeAll(() => {
|
||||
entitiesCatalog = {
|
||||
entities: jest.fn(),
|
||||
addOrUpdateEntity: jest.fn(),
|
||||
addEntities: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
setRelations: jest.fn(),
|
||||
batchAddOrUpdateEntities: jest.fn(),
|
||||
};
|
||||
locationsCatalog = {
|
||||
@@ -72,7 +69,6 @@ describe('HigherOrderOperations', () => {
|
||||
locationReader.read.mockResolvedValue({
|
||||
entities: [],
|
||||
errors: [],
|
||||
relations: [],
|
||||
});
|
||||
|
||||
const result = await higherOrderOperation.addLocation(spec);
|
||||
@@ -116,7 +112,6 @@ describe('HigherOrderOperations', () => {
|
||||
locationReader.read.mockResolvedValue({
|
||||
entities: [],
|
||||
errors: [],
|
||||
relations: [],
|
||||
});
|
||||
|
||||
const result = await higherOrderOperation.addLocation(spec);
|
||||
@@ -144,9 +139,8 @@ describe('HigherOrderOperations', () => {
|
||||
|
||||
locationsCatalog.locations.mockResolvedValue([]);
|
||||
locationReader.read.mockResolvedValue({
|
||||
entities: [{ entity, location }],
|
||||
entities: [{ entity, location, relations: [] }],
|
||||
errors: [{ error: new Error('abcd'), location }],
|
||||
relations: [],
|
||||
});
|
||||
|
||||
await expect(higherOrderOperation.addLocation(spec)).rejects.toThrow(
|
||||
@@ -193,9 +187,8 @@ describe('HigherOrderOperations', () => {
|
||||
{ currentStatus: locationStatus, data: location },
|
||||
]);
|
||||
locationReader.read.mockResolvedValue({
|
||||
entities: [{ entity: desc, location }],
|
||||
entities: [{ entity: desc, location, relations: [] }],
|
||||
errors: [],
|
||||
relations: [],
|
||||
});
|
||||
entitiesCatalog.batchAddOrUpdateEntities.mockResolvedValue(undefined);
|
||||
|
||||
@@ -239,9 +232,8 @@ describe('HigherOrderOperations', () => {
|
||||
{ currentStatus: locationStatus, data: location },
|
||||
]);
|
||||
locationReader.read.mockResolvedValue({
|
||||
entities: [{ entity: desc, location }],
|
||||
entities: [{ entity: desc, location, relations: [] }],
|
||||
errors: [],
|
||||
relations: [],
|
||||
});
|
||||
entitiesCatalog.entities.mockResolvedValue([]);
|
||||
entitiesCatalog.addEntities.mockResolvedValue(undefined);
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
import { Entity, Location, LocationSpec } from '@backstage/catalog-model';
|
||||
import { Location, LocationSpec } from '@backstage/catalog-model';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Logger } from 'winston';
|
||||
import { EntitiesCatalog, LocationsCatalog } from '../catalog';
|
||||
@@ -94,16 +94,16 @@ export class HigherOrderOperations implements HigherOrderOperation {
|
||||
if (!previousLocation) {
|
||||
await this.locationsCatalog.addLocation(location);
|
||||
}
|
||||
const outputEntities: Entity[] = [];
|
||||
for (const entity of readerOutput.entities) {
|
||||
const out = await this.entitiesCatalog.addOrUpdateEntity(
|
||||
entity.entity,
|
||||
location.id,
|
||||
);
|
||||
outputEntities.push(out);
|
||||
}
|
||||
const writtenEntities = await this.entitiesCatalog.batchAddOrUpdateEntities(
|
||||
readerOutput.entities,
|
||||
location.id,
|
||||
);
|
||||
|
||||
return { location, entities: outputEntities };
|
||||
const entities = await this.entitiesCatalog.entities({
|
||||
'metadata.uid': writtenEntities.map(e => e.entityId),
|
||||
});
|
||||
|
||||
return { location, entities };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,7 +166,7 @@ export class HigherOrderOperations implements HigherOrderOperation {
|
||||
|
||||
try {
|
||||
await this.entitiesCatalog.batchAddOrUpdateEntities(
|
||||
readerOutput.entities.map(e => e.entity),
|
||||
readerOutput.entities,
|
||||
location.id,
|
||||
);
|
||||
} catch (e) {
|
||||
|
||||
@@ -18,6 +18,7 @@ import { UrlReader } from '@backstage/backend-common';
|
||||
import {
|
||||
Entity,
|
||||
EntityPolicy,
|
||||
EntityRelationSpec,
|
||||
ENTITY_DEFAULT_NAMESPACE,
|
||||
LocationSpec,
|
||||
} from '@backstage/catalog-model';
|
||||
@@ -64,7 +65,6 @@ export class LocationReaders implements LocationReader {
|
||||
const output: ReadLocationResult = {
|
||||
entities: [],
|
||||
errors: [],
|
||||
relations: [],
|
||||
};
|
||||
let items: CatalogProcessorResult[] = [result.location(location, false)];
|
||||
|
||||
@@ -79,11 +79,21 @@ export class LocationReaders implements LocationReader {
|
||||
await this.handleData(item, emit);
|
||||
} else if (item.type === 'entity') {
|
||||
if (rulesEnforcer.isAllowed(item.entity, item.location)) {
|
||||
const entity = await this.handleEntity(item, emit);
|
||||
const relations = Array<EntityRelationSpec>();
|
||||
|
||||
const entity = await this.handleEntity(item, emitResult => {
|
||||
if (emitResult.type === 'relation') {
|
||||
relations.push(emitResult.relation);
|
||||
return;
|
||||
}
|
||||
emit(emitResult);
|
||||
});
|
||||
|
||||
if (entity) {
|
||||
output.entities.push({
|
||||
entity,
|
||||
location: item.location,
|
||||
relations,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -100,10 +110,6 @@ export class LocationReaders implements LocationReader {
|
||||
location: item.location,
|
||||
error: item.error,
|
||||
});
|
||||
} else if (item.type === 'relation') {
|
||||
output.relations.push({
|
||||
relation: item.relation,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,11 +132,23 @@ export class LocationReaders implements LocationReader {
|
||||
) {
|
||||
const { processors, logger } = this.options;
|
||||
|
||||
const validatedEmit: CatalogProcessorEmit = emitResult => {
|
||||
if (emitResult.type === 'relation') {
|
||||
throw new Error('readLocation may not emit entity relations');
|
||||
}
|
||||
|
||||
emit(emitResult);
|
||||
};
|
||||
|
||||
for (const processor of processors) {
|
||||
if (processor.readLocation) {
|
||||
try {
|
||||
if (
|
||||
await processor.readLocation(item.location, item.optional, emit)
|
||||
await processor.readLocation(
|
||||
item.location,
|
||||
item.optional,
|
||||
validatedEmit,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -153,10 +171,20 @@ export class LocationReaders implements LocationReader {
|
||||
) {
|
||||
const { processors, logger } = this.options;
|
||||
|
||||
const validatedEmit: CatalogProcessorEmit = emitResult => {
|
||||
if (emitResult.type === 'relation') {
|
||||
throw new Error('parseData may not emit entity relations');
|
||||
}
|
||||
|
||||
emit(emitResult);
|
||||
};
|
||||
|
||||
for (const processor of processors) {
|
||||
if (processor.parseData) {
|
||||
try {
|
||||
if (await processor.parseData(item.data, item.location, emit)) {
|
||||
if (
|
||||
await processor.parseData(item.data, item.location, validatedEmit)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -250,10 +278,18 @@ export class LocationReaders implements LocationReader {
|
||||
`Encountered error at location ${item.location.type} ${item.location.target}, ${item.error}`,
|
||||
);
|
||||
|
||||
const validatedEmit: CatalogProcessorEmit = emitResult => {
|
||||
if (emitResult.type === 'relation') {
|
||||
throw new Error('handleError may not emit entity relations');
|
||||
}
|
||||
|
||||
emit(emitResult);
|
||||
};
|
||||
|
||||
for (const processor of processors) {
|
||||
if (processor.handleError) {
|
||||
try {
|
||||
await processor.handleError(item.error, item.location, emit);
|
||||
await processor.handleError(item.error, item.location, validatedEmit);
|
||||
} catch (e) {
|
||||
const message = `Processor ${processor.constructor.name} threw an error while handling another error at ${item.location.type} ${item.location.target}, ${e}`;
|
||||
emit(result.generalError(item.location, message));
|
||||
|
||||
@@ -120,6 +120,7 @@ export type CatalogProcessorEntityResult = {
|
||||
export type CatalogProcessorRelationResult = {
|
||||
type: 'relation';
|
||||
relation: EntityRelationSpec;
|
||||
entityRef?: string;
|
||||
};
|
||||
|
||||
export type CatalogProcessorErrorResult = {
|
||||
|
||||
@@ -52,13 +52,13 @@ export type LocationReader = {
|
||||
|
||||
export type ReadLocationResult = {
|
||||
entities: ReadLocationEntity[];
|
||||
relations: { relation: EntityRelationSpec }[];
|
||||
errors: ReadLocationError[];
|
||||
};
|
||||
|
||||
export type ReadLocationEntity = {
|
||||
location: LocationSpec;
|
||||
entity: Entity;
|
||||
relations: EntityRelationSpec[];
|
||||
};
|
||||
|
||||
export type ReadLocationError = {
|
||||
|
||||
@@ -32,10 +32,7 @@ describe('createRouter', () => {
|
||||
beforeAll(async () => {
|
||||
entitiesCatalog = {
|
||||
entities: jest.fn(),
|
||||
addOrUpdateEntity: jest.fn(),
|
||||
addEntities: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
setRelations: jest.fn(),
|
||||
batchAddOrUpdateEntities: jest.fn(),
|
||||
};
|
||||
locationsCatalog = {
|
||||
|
||||
@@ -49,8 +49,13 @@ export async function createRouter(
|
||||
})
|
||||
.post('/entities', async (req, res) => {
|
||||
const body = await requireRequestBody(req);
|
||||
const result = await entitiesCatalog.addOrUpdateEntity(body as Entity);
|
||||
res.status(200).send(result);
|
||||
const [result] = await entitiesCatalog.batchAddOrUpdateEntities([
|
||||
{ entity: body as Entity, relations: [] },
|
||||
]);
|
||||
const entity = await entitiesCatalog.entities({
|
||||
'metadata.uid': result.entityId,
|
||||
});
|
||||
res.status(200).send(entity);
|
||||
})
|
||||
.get('/entities/by-uid/:uid', async (req, res) => {
|
||||
const { uid } = req.params;
|
||||
|
||||
Reference in New Issue
Block a user