From 046e04a7cf4076ff30873dac6202bde98f3db35b Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Tue, 10 Jan 2023 13:07:12 -0500 Subject: [PATCH] Translated all endpoints to the new format, seeking feedback. Signed-off-by: Aramis Sennyey --- plugins/catalog-backend/package.json | 2 + .../src/service/createRouter.ts | 301 +++++++++--------- yarn.lock | 187 ++++++++++- 3 files changed, 331 insertions(+), 159 deletions(-) diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index db9e1845fe..65a3232e69 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -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", diff --git a/plugins/catalog-backend/src/service/createRouter.ts b/plugins/catalog-backend/src/service/createRouter.ts index 849096c601..6b733a0a9a 100644 --- a/plugins/catalog-backend/src/service/createRouter.ts +++ b/plugins/catalog-backend/src/service/createRouter.ts @@ -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; - 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:' 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; + 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:' 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; } diff --git a/yarn.lock b/yarn.lock index 3652a87b61..859a44fda4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"