Translated all endpoints to the new format, seeking feedback.

Signed-off-by: Aramis Sennyey <sennyeya@amazon.com>
This commit is contained in:
Aramis Sennyey
2023-01-10 13:07:12 -05:00
committed by Fredrik Adelöw
parent d2ef677825
commit 046e04a7cf
3 changed files with 331 additions and 159 deletions
+2
View File
@@ -65,11 +65,13 @@
"codeowners-utils": "^1.0.2",
"core-js": "^3.6.5",
"express": "^4.17.1",
"express-openapi": "^12.1.0",
"express-promise-router": "^4.1.0",
"fast-json-stable-stringify": "^2.1.0",
"fs-extra": "10.1.0",
"git-url-parse": "^13.0.0",
"glob": "^7.1.6",
"js-yaml": "^4.1.0",
"knex": "^2.0.0",
"lodash": "^4.17.21",
"luxon": "^3.0.0",
@@ -23,7 +23,7 @@ import {
stringifyEntityRef,
} from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { NotFoundError, serializeError } from '@backstage/errors';
import { InputError, NotFoundError, serializeError } from '@backstage/errors';
import express from 'express';
import Router from 'express-promise-router';
import { Logger } from 'winston';
@@ -50,6 +50,16 @@ import {
locationInput,
validateRequestBody,
} from './util';
import { initialize } from 'express-openapi';
import yaml from 'js-yaml';
import fs from 'fs';
import path from 'path';
class ParsingError extends Error {
toString() {
return `ParsingError: ${this.message}`;
}
}
/**
* Options used by {@link createRouter}.
@@ -94,26 +104,41 @@ export async function createRouter(
logger.info('Catalog is running in readonly mode');
}
if (refreshService) {
router.post('/refresh', async (req, res) => {
const refreshOptions: RefreshOptions = req.body;
refreshOptions.authorizationToken = getBearerToken(
req.header('authorization'),
);
const validateDependency = (
dependency: any,
next: (req: express.Request, res: express.Response) => any,
) => {
return (req: express.Request, res: express.Response) => {
if (!dependency) {
console.log('no dependency');
throw new NotFoundError(
'Dependency Error',
'Dependency not set up for this endpoint.',
);
}
return next(req, res);
};
};
await refreshService.refresh(refreshOptions);
res.status(200).end();
});
}
initialize({
app: router as any,
// NOTE: If using yaml you can provide a path relative to process.cwd() e.g.
// apiDoc: './api-v1/api-doc.yml',
apiDoc: yaml.load(
fs.readFileSync(path.resolve(__dirname, '../../openapi.yaml'), 'utf-8'),
) as any,
operations: {
RefreshEntity: validateDependency(refreshService, async (req, res) => {
const refreshOptions: RefreshOptions = req.body;
refreshOptions.authorizationToken = getBearerToken(
req.header('authorization'),
);
if (permissionIntegrationRouter) {
router.use(permissionIntegrationRouter);
}
if (entitiesCatalog) {
router
.get('/entities', async (req, res) => {
const { entities, pageInfo } = await entitiesCatalog.entities({
await refreshService!.refresh(refreshOptions);
res.status(200).end();
}),
GetEntities: validateDependency(entitiesCatalog, async (req, res) => {
const { entities, pageInfo } = await entitiesCatalog!.entities({
filter: parseEntityFilterParams(req.query),
fields: parseEntityTransformParams(req.query),
order: parseEntityOrderParams(req.query),
@@ -131,30 +156,10 @@ export async function createRouter(
// TODO(freben): encode the pageInfo in the response
res.json(entities);
})
.get('/entities/by-query', async (req, res) => {
const { items, pageInfo, totalItems } =
await entitiesCatalog.queryEntities({
...parseQueryEntitiesParams(req.query),
authorizationToken: getBearerToken(req.header('authorization')),
});
res.json({
items,
totalItems,
pageInfo: {
...(pageInfo.nextCursor && {
nextCursor: encodeCursor(pageInfo.nextCursor),
}),
...(pageInfo.prevCursor && {
prevCursor: encodeCursor(pageInfo.prevCursor),
}),
},
});
})
.get('/entities/by-uid/:uid', async (req, res) => {
}),
GetEntityByUid: validateDependency(entitiesCatalog, async (req, res) => {
const { uid } = req.params;
const { entities } = await entitiesCatalog.entities({
const { entities } = await entitiesCatalog!.entities({
filter: basicEntityFilter({ 'metadata.uid': uid }),
authorizationToken: getBearerToken(req.header('authorization')),
});
@@ -162,17 +167,10 @@ export async function createRouter(
throw new NotFoundError(`No entity with uid ${uid}`);
}
res.status(200).json(entities[0]);
})
.delete('/entities/by-uid/:uid', async (req, res) => {
const { uid } = req.params;
await entitiesCatalog.removeEntityByUid(uid, {
authorizationToken: getBearerToken(req.header('authorization')),
});
res.status(204).end();
})
.get('/entities/by-name/:kind/:namespace/:name', async (req, res) => {
}),
GetEntityByName: validateDependency(entitiesCatalog, async (req, res) => {
const { kind, namespace, name } = req.params;
const { entities } = await entitiesCatalog.entities({
const { entities } = await entitiesCatalog!.entities({
filter: basicEntityFilter({
kind: kind,
'metadata.namespace': namespace,
@@ -186,41 +184,27 @@ export async function createRouter(
);
}
res.status(200).json(entities[0]);
})
.get(
'/entities/by-name/:kind/:namespace/:name/ancestry',
}),
GetEntityAncestryByName: validateDependency(
entitiesCatalog,
async (req, res) => {
const { kind, namespace, name } = req.params;
const entityRef = stringifyEntityRef({ kind, namespace, name });
const response = await entitiesCatalog.entityAncestry(entityRef, {
const response = await entitiesCatalog!.entityAncestry(entityRef, {
authorizationToken: getBearerToken(req.header('authorization')),
});
res.status(200).json(response);
},
)
.post('/entities/by-refs', async (req, res) => {
const request = entitiesBatchRequest(req);
const token = getBearerToken(req.header('authorization'));
const response = await entitiesCatalog.entitiesBatch({
entityRefs: request.entityRefs,
fields: parseEntityTransformParams(req.query, request.fields),
authorizationToken: token,
});
res.status(200).json(response);
})
.get('/entity-facets', async (req, res) => {
const response = await entitiesCatalog.facets({
),
GetEntityFacets: validateDependency(entitiesCatalog, async (req, res) => {
const response = await entitiesCatalog!.facets({
filter: parseEntityFilterParams(req.query),
facets: parseEntityFacetParams(req.query),
authorizationToken: getBearerToken(req.header('authorization')),
});
res.status(200).json(response);
});
}
if (locationService) {
router
.post('/locations', async (req, res) => {
}),
CreateLocation: validateDependency(locationService, async (req, res) => {
const location = await validateRequestBody(req, locationInput);
const dryRun = yn(req.query.dryRun, { default: false });
@@ -234,22 +218,21 @@ export async function createRouter(
authorizationToken: getBearerToken(req.header('authorization')),
});
res.status(201).json(output);
})
.get('/locations', async (req, res) => {
}),
GetLocations: validateDependency(locationService, async (req, res) => {
const locations = await locationService.listLocations({
authorizationToken: getBearerToken(req.header('authorization')),
});
res.status(200).json(locations.map(l => ({ data: l })));
})
.get('/locations/:id', async (req, res) => {
}),
GetLocation: validateDependency(locationService, async (req, res) => {
const { id } = req.params;
const output = await locationService.getLocation(id, {
authorizationToken: getBearerToken(req.header('authorization')),
});
res.status(200).json(output);
})
.delete('/locations/:id', async (req, res) => {
}),
DeleteLocation: validateDependency(locationService, async (req, res) => {
disallowReadonlyMode(readonlyEnabled);
const { id } = req.params;
@@ -257,74 +240,108 @@ export async function createRouter(
authorizationToken: getBearerToken(req.header('authorization')),
});
res.status(204).end();
});
}
if (locationAnalyzer) {
router.post('/analyze-location', async (req, res) => {
const body = await validateRequestBody(
req,
z.object({
}),
AnalyzeLocation: validateDependency(locationService, async (req, res) => {
const body = await validateRequestBody(
req,
z.object({
location: locationInput,
catalogFilename: z.string().optional(),
}),
);
const schema = z.object({
location: locationInput,
catalogFilename: z.string().optional(),
}),
);
const schema = z.object({
location: locationInput,
catalogFilename: z.string().optional(),
});
const output = await locationAnalyzer.analyzeLocation(schema.parse(body));
res.status(200).json(output);
});
}
if (orchestrator) {
router.post('/validate-entity', async (req, res) => {
const bodySchema = z.object({
entity: z.unknown(),
location: z.string(),
});
let body: z.infer<typeof bodySchema>;
let entity: Entity;
let location: { type: string; target: string };
try {
body = await validateRequestBody(req, bodySchema);
entity = validateEntityEnvelope(body.entity);
location = parseLocationRef(body.location);
if (location.type !== 'url')
throw new TypeError(
`Invalid location ref ${body.location}, only 'url:<target>' is supported, e.g. url:https://host/path`,
);
} catch (err) {
return res.status(400).json({
errors: [serializeError(err)],
});
}
const output = await locationAnalyzer!.analyzeLocation(
schema.parse(body),
);
res.status(200).json(output);
}),
ValidateEntity: validateDependency(orchestrator, async (req, res) => {
const bodySchema = z.object({
entity: z.unknown(),
location: z.string(),
});
const processingResult = await orchestrator.process({
entity: {
...entity,
metadata: {
...entity.metadata,
annotations: {
[ANNOTATION_LOCATION]: body.location,
[ANNOTATION_ORIGIN_LOCATION]: body.location,
...entity.metadata.annotations,
let body: z.infer<typeof bodySchema>;
let entity: Entity;
let location: { type: string; target: string };
try {
body = await validateRequestBody(req, bodySchema);
entity = validateEntityEnvelope(body.entity);
location = parseLocationRef(body.location);
if (location.type !== 'url')
throw new TypeError(
`Invalid location ref ${body.location}, only 'url:<target>' is supported, e.g. url:https://host/path`,
);
} catch (err) {
return res.status(400).json({
errors: [serializeError(err)],
});
}
const processingResult = await orchestrator!.process({
entity: {
...entity,
metadata: {
...entity.metadata,
annotations: {
[ANNOTATION_LOCATION]: body.location,
[ANNOTATION_ORIGIN_LOCATION]: body.location,
...entity.metadata.annotations,
},
},
},
},
});
if (!processingResult.ok)
res.status(400).json({
errors: processingResult.errors.map(e => serializeError(e)),
});
return res.status(200).end();
});
}
router.use(errorHandler());
if (!processingResult.ok)
res.status(400).json({
errors: processingResult.errors.map(e => serializeError(e)),
});
return res.status(200).end();
}),
DeleteEntityByUid: validateDependency(
entitiesCatalog,
async (req, res) => {
const { uid } = req.params;
await entitiesCatalog!.removeEntityByUid(uid, {
authorizationToken: getBearerToken(req.header('authorization')),
});
res.status(204).end();
},
),
GetEntitiesByRefs: validateDependency(
entitiesCatalog,
async (req, res) => {
const request = entitiesBatchRequest(req);
const token = getBearerToken(req.header('authorization'));
const response = await entitiesCatalog!.entitiesBatch({
entityRefs: request.entityRefs,
fields: parseEntityTransformParams(req.query, request.fields),
authorizationToken: token,
});
res.status(200).json(response);
},
),
},
enableObjectCoercion: true,
errorMiddleware: errorHandler(),
errorTransformer: (openapiError, ajvError) => {
switch (openapiError.errorCode) {
case 'type.openapi.requestValidation':
throw new InputError(
`Invalid field ${openapiError.path}`,
new ParsingError(openapiError.message),
);
}
return {};
},
});
if (permissionIntegrationRouter) {
router.use(permissionIntegrationRouter);
}
return router;
}
+170 -17
View File
@@ -5371,11 +5371,13 @@ __metadata:
codeowners-utils: ^1.0.2
core-js: ^3.6.5
express: ^4.17.1
express-openapi: ^12.1.0
express-promise-router: ^4.1.0
fast-json-stable-stringify: ^2.1.0
fs-extra: 10.1.0
git-url-parse: ^13.0.0
glob: ^7.1.6
js-yaml: ^4.1.0
knex: ^2.0.0
lodash: ^4.17.21
luxon: ^3.0.0
@@ -17590,7 +17592,7 @@ __metadata:
languageName: node
linkType: hard
"ajv-formats@npm:^2.1.1":
"ajv-formats@npm:^2.0.2, ajv-formats@npm:^2.1.0, ajv-formats@npm:^2.1.1":
version: 2.1.1
resolution: "ajv-formats@npm:2.1.1"
dependencies:
@@ -17636,7 +17638,7 @@ __metadata:
languageName: node
linkType: hard
"ajv@npm:^8.0.0, ajv@npm:^8.10.0, ajv@npm:^8.11.0, ajv@npm:^8.12.0, ajv@npm:^8.8.0":
"ajv@npm:^8.0.0, ajv@npm:^8.1.0, ajv@npm:^8.10.0, ajv@npm:^8.11.0, ajv@npm:^8.12.0, ajv@npm:^8.3.0, ajv@npm:^8.4.0, ajv@npm:^8.8.0":
version: 8.12.0
resolution: "ajv@npm:8.12.0"
dependencies:
@@ -20392,6 +20394,13 @@ __metadata:
languageName: node
linkType: hard
"content-type@npm:^1.0.4":
version: 1.0.4
resolution: "content-type@npm:1.0.4"
checksum: 3d93585fda985d1554eca5ebd251994327608d2e200978fdbfba21c0c679914d5faf266d17027de44b34a72c7b0745b18584ecccaa7e1fdfb6a68ac7114f12e0
languageName: node
linkType: hard
"content-type@npm:~1.0.4, content-type@npm:~1.0.5":
version: 1.0.5
resolution: "content-type@npm:1.0.5"
@@ -21787,6 +21796,15 @@ __metadata:
languageName: node
linkType: hard
"difunc@npm:0.0.4":
version: 0.0.4
resolution: "difunc@npm:0.0.4"
dependencies:
esprima: ^4.0.0
checksum: 19b850dae20cba3bdf238b85fa90f79526974e7634348a3157eef3da6eb161e7f43142aa531f6be61eed1235d1f4b6a16e07912f313b5afcceee56d998c2e300
languageName: node
linkType: hard
"dir-glob@npm:^3.0.1":
version: 3.0.1
resolution: "dir-glob@npm:3.0.1"
@@ -23687,6 +23705,24 @@ __metadata:
languageName: node
linkType: hard
"express-normalize-query-params-middleware@npm:^0.5.0":
version: 0.5.1
resolution: "express-normalize-query-params-middleware@npm:0.5.1"
checksum: 20fef3a200c452cbfef25f738e8ed44704ad30f04899cce21d3984b74e4fcbeefdc5dcec3aed1a0e4e54f231c55f3cba2c3d0cc3faebfea0cab437a852bca09b
languageName: node
linkType: hard
"express-openapi@npm:^12.1.0":
version: 12.1.0
resolution: "express-openapi@npm:12.1.0"
dependencies:
express-normalize-query-params-middleware: ^0.5.0
openapi-framework: ^12.1.0
openapi-types: ^12.1.0
checksum: 6b87b7990891b1f4d4bd3e071eddd72d7433ae770d5ad0cfbbee234a0d4e712150640ec15d8fe9d1af99b49209700c58f4694045f00bd380a569a86ba5008755
languageName: node
linkType: hard
"express-prom-bundle@npm:^6.3.6":
version: 6.6.0
resolution: "express-prom-bundle@npm:6.6.0"
@@ -24584,6 +24620,15 @@ __metadata:
languageName: node
linkType: hard
"fs-routes@npm:^12.0.0":
version: 12.0.0
resolution: "fs-routes@npm:12.0.0"
peerDependencies:
glob: ">=7.1.6"
checksum: 4e97b08584c4ee04cf00215ac5175ba6f3aeaa4fb254cd108149d675edffcf90e802ead9965a5252271f26e1a47b31c341b41e209b9d210b2a584ae2a642c466
languageName: node
linkType: hard
"fs.realpath@npm:^1.0.0":
version: 1.0.0
resolution: "fs.realpath@npm:1.0.0"
@@ -24905,6 +24950,19 @@ __metadata:
languageName: node
linkType: hard
"glob@npm:*, glob@npm:8.0.3":
version: 8.0.3
resolution: "glob@npm:8.0.3"
dependencies:
fs.realpath: ^1.0.0
inflight: ^1.0.4
inherits: 2
minimatch: ^5.0.1
once: ^1.3.0
checksum: 50bcdea19d8e79d8de5f460b1939ffc2b3299eac28deb502093fdca22a78efebc03e66bf54f0abc3d3d07d8134d19a32850288b7440d77e072aa55f9d33b18c5
languageName: node
linkType: hard
"glob@npm:7.1.6":
version: 7.1.6
resolution: "glob@npm:7.1.6"
@@ -24919,19 +24977,6 @@ __metadata:
languageName: node
linkType: hard
"glob@npm:8.0.3":
version: 8.0.3
resolution: "glob@npm:8.0.3"
dependencies:
fs.realpath: ^1.0.0
inflight: ^1.0.4
inherits: 2
minimatch: ^5.0.1
once: ^1.3.0
checksum: 50bcdea19d8e79d8de5f460b1939ffc2b3299eac28deb502093fdca22a78efebc03e66bf54f0abc3d3d07d8134d19a32850288b7440d77e072aa55f9d33b18c5
languageName: node
linkType: hard
"glob@npm:^7.0.0, glob@npm:^7.1.1, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6, glob@npm:^7.1.7, glob@npm:^7.2.0":
version: 7.2.3
resolution: "glob@npm:7.2.3"
@@ -26591,6 +26636,13 @@ __metadata:
languageName: node
linkType: hard
"is-dir@npm:^1.0.0":
version: 1.0.0
resolution: "is-dir@npm:1.0.0"
checksum: b81430f22d03318b144391ec9633abdd6fdcd8fd02509bee1ab5a20b79311505ba4344282d2cab565114779ac36626fd034168b9bd47c3be12d956ae2ca76a5c
languageName: node
linkType: hard
"is-docker@npm:^2.0.0, is-docker@npm:^2.1.1":
version: 2.2.1
resolution: "is-docker@npm:2.2.1"
@@ -29360,7 +29412,7 @@ __metadata:
languageName: node
linkType: hard
"lodash.merge@npm:^4.6.2":
"lodash.merge@npm:^4.6.1, lodash.merge@npm:^4.6.2":
version: 4.6.2
resolution: "lodash.merge@npm:4.6.2"
checksum: ad580b4bdbb7ca1f7abf7e1bce63a9a0b98e370cf40194b03380a46b4ed799c9573029599caebc1b14e3f24b111aef72b96674a56cfa105e0f5ac70546cdc005
@@ -31924,6 +31976,79 @@ __metadata:
languageName: node
linkType: hard
"openapi-default-setter@npm:^12.1.0":
version: 12.1.0
resolution: "openapi-default-setter@npm:12.1.0"
dependencies:
openapi-types: ^12.1.0
checksum: 3b4543d7c091134690fcfc986258481b6211f17b4322552764b3a4a9725784202bc17419a524302dd1187ecea556dea4f2cb525f4a0258c2ab5ddec884bf00e2
languageName: node
linkType: hard
"openapi-framework@npm:^12.1.0":
version: 12.1.0
resolution: "openapi-framework@npm:12.1.0"
dependencies:
difunc: 0.0.4
fs-routes: ^12.0.0
glob: "*"
is-dir: ^1.0.0
js-yaml: ^3.10.0
openapi-default-setter: ^12.1.0
openapi-request-coercer: ^12.1.0
openapi-request-validator: ^12.1.0
openapi-response-validator: ^12.1.0
openapi-schema-validator: ^12.1.0
openapi-security-handler: ^12.1.0
openapi-types: ^12.1.0
ts-log: ^2.1.4
checksum: 4cbb35e2870604c91c01f397aa2ea8fb072f648acb9a47b830d0d67c1e68bb75eadc3e97bcbe2ea994b7374ba38866b6d91734df6e2481ce42e6625d66ccc5bd
languageName: node
linkType: hard
"openapi-jsonschema-parameters@npm:^12.1.0":
version: 12.1.0
resolution: "openapi-jsonschema-parameters@npm:12.1.0"
dependencies:
openapi-types: ^12.1.0
checksum: 9465549e132d02ad4be67326e22c9a2e3f0cf19ffa0e3c1fe69a542a675707f11c33080836b0eebd75c02501dbcb5f4130fa446d470ede3ba67b7c52e1bc341f
languageName: node
linkType: hard
"openapi-request-coercer@npm:^12.1.0":
version: 12.1.0
resolution: "openapi-request-coercer@npm:12.1.0"
dependencies:
openapi-types: ^12.1.0
ts-log: ^2.1.4
checksum: 4359d4fd58b032146838aabb9762fc2ac01771e7bea4679af5a241964f3df15ddced7968e6a8d419954d166e93ad32e08f530da0d9a8483d3c4d97be7c7c848c
languageName: node
linkType: hard
"openapi-request-validator@npm:^12.1.0":
version: 12.1.0
resolution: "openapi-request-validator@npm:12.1.0"
dependencies:
ajv: ^8.3.0
ajv-formats: ^2.1.0
content-type: ^1.0.4
openapi-jsonschema-parameters: ^12.1.0
openapi-types: ^12.1.0
ts-log: ^2.1.4
checksum: 794b1c15dbf04107ede7b8304e5b7c2fa183ad0c4bd65b3e0ee4d415cf508324932959f661120f1bfb58a23d88b5458dc0db868bc36f9ec44ea03b99ea9d42df
languageName: node
linkType: hard
"openapi-response-validator@npm:^12.1.0":
version: 12.1.0
resolution: "openapi-response-validator@npm:12.1.0"
dependencies:
ajv: ^8.4.0
openapi-types: ^12.1.0
checksum: f07036661ac846c5d508b423d01039cd5bd33d564e053fdc039046f964fad71b03a7c9254195b1d5a8a1912fcc45dce13616af97c445c6d7b040c825e1f91de1
languageName: node
linkType: hard
"openapi-sampler@npm:^1.2.1":
version: 1.2.1
resolution: "openapi-sampler@npm:1.2.1"
@@ -31934,7 +32059,28 @@ __metadata:
languageName: node
linkType: hard
"openapi-types@npm:^12.0.0":
"openapi-schema-validator@npm:^12.1.0":
version: 12.1.0
resolution: "openapi-schema-validator@npm:12.1.0"
dependencies:
ajv: ^8.1.0
ajv-formats: ^2.0.2
lodash.merge: ^4.6.1
openapi-types: ^12.1.0
checksum: 1008ef82910cc30e85132c041b269a4cc2991dad17e2d7c4cddbfb5641a15e934016bc4d1bab32f2dc79f7f3673b82b655c3634838c9ce3a8f497af3be762aa5
languageName: node
linkType: hard
"openapi-security-handler@npm:^12.1.0":
version: 12.1.0
resolution: "openapi-security-handler@npm:12.1.0"
dependencies:
openapi-types: ^12.1.0
checksum: 07f645300fdc0ca5d01790fb77e01d8e537f6b0439f55ec9af5097d3dd044e3e0fc55ce0acb9551f04256819d7738f8c3d468645e581f7b0487648eed428d519
languageName: node
linkType: hard
"openapi-types@npm:^12.0.0, openapi-types@npm:^12.1.0":
version: 12.1.0
resolution: "openapi-types@npm:12.1.0"
checksum: d8f3e2bae519aa6bcf2012f4a5592b7283fe928c06f3bdc182776ce637c7ed8d5f9f342724f68c95c9572aefe21ed5e89b18a0295a703da61af5f97bd2aea9a7
@@ -38457,6 +38603,13 @@ __metadata:
languageName: node
linkType: hard
"ts-log@npm:^2.1.4":
version: 2.2.5
resolution: "ts-log@npm:2.2.5"
checksum: 28f78ab15b8555d56c089dbc243327d8ce4331219956242a29fc4cb3bad6bb0cb8234dd17a292381a1b1dba99a7e4849a2181b2e1a303e8247e9f4ca4e284f2d
languageName: node
linkType: hard
"ts-log@npm:^2.2.3":
version: 2.2.3
resolution: "ts-log@npm:2.2.3"