Added NestJS Playground

no-issue

This adds the barebones of a NestJS application wired up to the Admin API
behind a feature flag, so that we can experiement with how to use Nest in the
context of Ghost
This commit is contained in:
Fabien "egg" O'Carroll 2024-01-22 15:58:45 +07:00 committed by Fabien 'egg' O'Carroll
parent d2620171ea
commit 0fb0c6c2b5
36 changed files with 1209 additions and 10 deletions

View File

@ -55,6 +55,10 @@ const features = [{
title: 'New email addresses',
description: 'For self hosters, forces the usage of the mail.from config as from address for all outgoing emails',
flag: 'newEmailAddresses'
},{
title: 'NestJS Playground',
description: 'Wires up the Ghost NestJS App to the Admin API',
flag: 'NestPlayground'
}];
const AlphaFeatures: React.FC = () => {

View File

@ -381,6 +381,32 @@ async function initServices() {
debug('End: initServices');
}
/**
* Set up an dependencies that need to be injected into NestJS
*/
async function initNestDependencies() {
debug('Begin: initNestDependencies');
const GhostNestApp = require('@tryghost/ghost');
const providers = [];
providers.push({
provide: 'logger',
useValue: require('@tryghost/logging')
}, {
provide: 'SessionService',
useValue: require('./server/services/auth/session').sessionService
}, {
provide: 'AdminAuthenticationService',
useValue: require('./server/services/auth/api-key').admin
}, {
provide: 'DomainEvents',
useValue: require('@tryghost/domain-events')
});
for (const provider of providers) {
GhostNestApp.addProvider(provider);
}
debug('End: initNestDependencies');
}
/**
* Kick off recurring jobs and background services
* These are things that happen on boot, but we don't need to wait for them to finish
@ -528,6 +554,7 @@ async function bootGhost({backend = true, frontend = true, server = true} = {})
}
await initServices({config});
await initNestDependencies();
debug('End: Load Ghost Services & Apps');
// Step 5 - Mount the full Ghost app onto the minimal root app & disable maintenance mode

View File

@ -4,11 +4,13 @@ const bodyParser = require('body-parser');
const errorHandler = require('@tryghost/mw-error-handler');
const versionMatch = require('@tryghost/mw-version-match');
const labs = require('../../../../../shared/labs');
const shared = require('../../../shared');
const express = require('../../../../../shared/express');
const sentry = require('../../../../../shared/sentry');
const routes = require('./routes');
const APIVersionCompatibilityService = require('../../../../services/api-version-compatibility');
const GhostNestApp = require('@tryghost/ghost');
module.exports = function setupApiApp() {
debug('Admin API setup start');
@ -33,6 +35,14 @@ module.exports = function setupApiApp() {
// Routing
apiApp.use(routes());
apiApp.use(async (req, res, next) => {
if (!labs.isSet('NestPlayground')) {
return next();
}
const app = await GhostNestApp.getApp();
app.getHttpAdapter().getInstance()(req, res, next);
});
// API error handling
apiApp.use(errorHandler.resourceNotFound);
apiApp.use(APIVersionCompatibilityService.errorHandler);

View File

@ -36,6 +36,7 @@ const BETA_FEATURES = [
];
const ALPHA_FEATURES = [
'NestPlayground',
'urlCache',
'lexicalMultiplayer',
'websockets',

View File

@ -89,6 +89,7 @@
"@tryghost/errors": "1.3.1",
"@tryghost/express-dynamic-redirects": "0.0.0",
"@tryghost/external-media-inliner": "0.0.0",
"@tryghost/ghost": "0.0.0",
"@tryghost/helpers": "1.1.88",
"@tryghost/html-to-plaintext": "0.0.0",
"@tryghost/http-cache-utils": "0.1.11",

11
ghost/ghost/.eslintrc.js Normal file
View File

@ -0,0 +1,11 @@
module.exports = {
parser: '@typescript-eslint/parser',
plugins: ['ghost'],
extends: [
'plugin:ghost/ts'
],
rules: {
// disable file naming rule in favor or dotted notation e.g. `snippets.service.ts`
'ghost/filenames/match-exported-class': [0, null, true]
}
};

21
ghost/ghost/README.md Normal file
View File

@ -0,0 +1,21 @@
# Ghost
## Usage
## Develop
This is a monorepo package.
Follow the instructions for the top-level repo.
1. `git clone` this repo & `cd` into it as usual
2. Run `yarn` to install top-level dependencies.
## Test
- `yarn lint` run just eslint
- `yarn test` run lint and tests

43
ghost/ghost/package.json Normal file
View File

@ -0,0 +1,43 @@
{
"name": "@tryghost/ghost",
"version": "0.0.0",
"repository": "https://github.com/TryGhost/Ghost/tree/main/packages/ghost",
"author": "Ghost Foundation",
"private": true,
"main": "build/index.js",
"types": "build/index.d.ts",
"scripts": {
"dev": "tsc --watch --preserveWatchOutput --sourceMap",
"build": "tsc",
"build:ts": "yarn build",
"test:unit": "NODE_ENV=testing c8 --src src --all --reporter text --reporter cobertura mocha -r ts-node/register/transpile-only './**/*.test.ts'",
"test": "yarn test:types && yarn test:unit",
"test:types": "tsc --noEmit",
"lint:code": "eslint src/ --ext .ts --cache",
"lint": "yarn lint:code"
},
"files": [
"build"
],
"devDependencies": {
"@nestjs/testing": "10.2.8",
"@types/node": "^20.10.0",
"@types/sinon": "^17.0.3",
"c8": "8.0.1",
"mocha": "10.2.0",
"reflect-metadata": "0.1.13",
"sinon": "^17.0.1",
"ts-node": "10.9.1",
"typescript": "5.2.2"
},
"dependencies": {
"@nestjs/common": "10.2.8",
"@nestjs/core": "10.2.8",
"@nestjs/platform-express": "10.2.8",
"@tryghost/errors": "^1.2.27",
"bson-objectid": "2.0.4",
"express": "^4.18.2",
"reflect-metadata": "^0.1.14",
"rxjs": "^7.8.1"
}
}

View File

@ -0,0 +1,40 @@
import {NestApplication} from '@nestjs/core';
interface IEvent<T> {
data: T
timestamp: Date
}
interface IDomainEvents {
subscribe<T>(event: new (data: T, timestamp: Date) => IEvent<T>, fn: (_event: IEvent<T>) => void): void;
dispatch<T>(event: IEvent<T>): void
}
type EventRegistrationSpec<EventData, Subscriber> = {
Event: new (data: EventData, timestamp: Date) => IEvent<EventData>,
target: new (...args: unknown[]) => Subscriber,
methodName: string
};
const events: EventRegistrationSpec<unknown, unknown>[] = [];
export function OnEvent<T>(Event: new (data: T, timestamp: Date) => IEvent<T>) {
return function (target: object, methodName: string) {
events.push({
Event: Event as new (data: unknown, timestamp: Date) => IEvent<T>,
target: target.constructor as new (...args: unknown[]) => unknown,
methodName
});
};
}
export function registerEvents(app: NestApplication, DomainEvents: IDomainEvents) {
for (const eventSpec of events) {
DomainEvents.subscribe(eventSpec.Event, async function (event: IEvent<unknown>) {
// We have to cast to `any` here because we don't know the type - but we do know that it should have the `methodName` method
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const service = await app.resolve(eventSpec.target) as any;
await service[eventSpec.methodName](event);
});
}
}

View File

@ -0,0 +1,8 @@
import {Reflector} from '@nestjs/core';
type UserRole = 'Contributor' | 'Author' | 'Editor' | 'Admin' | 'Owner';
type APIKeyRole = 'Admin Integration' | 'Ghost Explore Integration' | 'Self-Serve Migration Integration' | 'DB Backup Integration' | 'Scheduler Integration';
export type Role = UserRole | APIKeyRole;
export const Roles = Reflector.createDecorator<Role[]>();

View File

@ -0,0 +1,129 @@
import {Actor} from './types/actor.type';
import ObjectID from 'bson-objectid';
import {now} from './helpers/date.helper';
import {BaseEvent} from './event.base';
function equals(a: unknown, b: unknown) {
if (a === null || b === null) {
return a === b;
}
if (a === undefined || b === undefined) {
return a === b;
}
if (typeof a === 'object' && Reflect.has(a, 'equals')) {
const equalsFn = Reflect.get(a, 'equals');
if (typeof equalsFn === 'function') {
return equalsFn.call(a, b);
}
}
return a === b;
}
type BaseEntityUpdatedXData = {
updatedAt: Date;
updatedBy: Actor;
} | {
updatedAt: null;
updatedBy: null;
}
type BaseEntityData = {
id: ObjectID;
deleted: boolean;
createdAt: Date;
createdBy: Actor;
} & BaseEntityUpdatedXData;
type Optional<T> = {
[K in keyof T]?: T[K]
};
export class Entity<Data> {
constructor(protected attr: Data & Optional<BaseEntityData>, actor?: Actor) {
this.attr = attr;
if (!this.attr.id) {
this.attr.id = new ObjectID();
}
if (!this.attr.createdAt) {
this.attr.createdAt = now();
}
if (actor) {
this.actor = actor;
}
if (!this.attr.createdBy) {
if (this.actor) {
this.attr.createdBy = this.actor;
} else {
// TODO: This should maybe be system user?
this.attr.createdBy = {
id: ObjectID.createFromHexString('d34d01d0d34d01d0d34d01d0'),
type: 'user',
role: 'Owner'
};
}
}
this.attr.deleted = false;
}
private events: BaseEvent<unknown>[] = [];
protected addEvent(event: BaseEvent<unknown>) {
this.events.push(event);
}
static getEventsToDispatch(entity: Entity<unknown>, fn: (events: BaseEvent<unknown>[]) => void) {
const events = entity.events;
entity.events = [];
fn(events);
}
private actor?: Actor | null;
setActor(actor: Actor) {
if (this.actor !== null) {
throw new Error(`Entity already owned by ${actor.id}`);
}
this.actor = actor;
}
get id() {
return this.attr.id;
}
get createdAt() {
return this.attr.createdAt;
}
get createdBy() {
return this.attr.createdBy;
}
get updatedAt() {
return this.attr.updatedAt;
}
get updatedBy() {
return this.attr.updatedBy;
}
get deleted() {
return this.attr.deleted;
}
delete() {
this.attr.deleted = true;
}
protected set<K extends keyof Data>(key: K, value: Data[K], actor?: Actor) {
if (equals(this.attr[key], value)) {
return;
}
(this.attr as Data)[key] = value;
if (actor) {
this.attr.updatedAt = now();
this.attr.updatedBy = actor;
} else if (this.actor) {
this.attr.updatedAt = now();
this.attr.updatedBy = this.actor;
} else {
// Maybe log a warning or smth?
}
}
}

View File

@ -0,0 +1,6 @@
export abstract class BaseEvent<Data> {
constructor(
public readonly data: Data,
public readonly timestamp: Date = new Date()
) {}
}

View File

@ -0,0 +1,5 @@
export function now(): Date {
const date = new Date();
date.setMilliseconds(0);
return date;
}

View File

@ -0,0 +1 @@
declare module '@tryghost/errors';

View File

@ -0,0 +1,8 @@
import ObjectID from 'bson-objectid';
import {Role} from '../decorators/permissions.decorator';
export type Actor = {
id: ObjectID
role: Role
type: 'user' | 'api_key'
};

View File

@ -0,0 +1,27 @@
import assert from 'assert';
import {Greeting} from './example.entity';
describe('ExampleEntity (Greeting)', function () {
it('Can greet someone', function () {
const entity = new Greeting({
greeting: 'Bonjour'
});
const msg = entity.greet('Margot');
assert.equal(msg, 'Bonjour, Margot.');
});
it('Has a custom greeting for the recipient "world"', function () {
const entity = new Greeting({
greeting: 'Bonjour'
});
const msg = entity.greet('world');
assert.equal(msg, 'Hello, world!');
});
it('Can have its greeting updated', function () {
const entity = new Greeting({
greeting: 'Bonjour'
});
entity.greeting = 'Evening';
const msg = entity.greet('Guvner');
assert.equal(msg, 'Evening, Guvner.');
});
});

View File

@ -0,0 +1,38 @@
/**
* Entity
*
* Represents a "Business Object" in the system
*
* - As much business logic as possible should be here
* - Its interface should describe what it is and what it can do/you can do to it.
* - Handles the creation of events related to the entity
*/
import {Entity} from '../../common/entity.base';
import {ExampleEvent} from './example.event';
type GreetingData = {
greeting: string
};
export class Greeting extends Entity<GreetingData> {
get greeting() {
return this.attr.greeting;
}
set greeting(greeting: string) {
this.set('greeting', greeting);
}
greet(recipient: string) {
let message;
if (recipient.trim() === 'world') {
message = 'Hello, world!';
} else {
message = `${this.greeting}, ${recipient.trim()}.`;
}
this.addEvent(ExampleEvent.create({
message
}));
return message;
}
}

View File

@ -0,0 +1,18 @@
/**
* Event
*
* Represents an "Business Event" in the system
*
* They are serialisable (only contain data)
*/
import {BaseEvent} from '../../common/event.base';
type ExampleEventData = {
message: string
};
export class ExampleEvent extends BaseEvent<ExampleEventData> {
static create(data: ExampleEventData) {
return new ExampleEvent(data, new Date());
}
}

View File

@ -0,0 +1,13 @@
/**
* Repository
*
* These define how the service can retrieve and store entities to/from persistence
*
* They should generally be derived from a shared base interface
*/
import {Greeting} from './example.entity';
export interface ExampleRepository {
getOne(recipient: string): Promise<Greeting>
save(entity: Greeting): Promise<void>
}

View File

@ -0,0 +1,22 @@
import Sinon from 'sinon';
import {ExampleService} from './example.service';
import {Greeting} from './example.entity';
import assert from 'assert';
describe('ExampleService', function () {
it('Can greet a recipient and save the greeting', async function () {
const recipient = 'Mr Anderson';
const entity = new Greeting({greeting: 'Testing'});
const repository = {
getOne: Sinon.stub().resolves(entity),
save: Sinon.stub()
};
const service = new ExampleService(repository);
const result = await service.greet(recipient);
assert.equal(result, entity.greet(recipient));
assert(repository.save.calledWithExactly(entity));
});
});

View File

@ -0,0 +1,25 @@
/**
* Service
*
* These implement Use Cases of the system, they should use repositories, entities and other services to coordinate these Use Cases
*
* Business logic should only go in here if it does not fall in the domain of a single entity.
*/
import {Inject} from '@nestjs/common';
import {ExampleRepository} from './example.repository';
export class ExampleService {
constructor(
@Inject('ExampleRepository') private readonly repository: ExampleRepository
) {}
async greet(recipient: string): Promise<string> {
const greeting = await this.repository.getOne('Greetings');
const message = greeting.greet(recipient);
await this.repository.save(greeting);
return message;
}
}

View File

@ -0,0 +1,37 @@
/**
* Repository Implementation
*
* Can be in-memory, knex based, bookshelf based, redis based, whatever...
*
* No business logic
* No modification of Entities - they just store what they're given, or fetch what they're asked for
* They should dispatch and clear the events of an entity when tehy successfully persist it.
*/
import {Inject} from '@nestjs/common';
import {Greeting} from '../../core/example/example.entity';
import {ExampleRepository} from '../../core/example/example.repository';
interface DomainEvents {
dispatch(event: unknown): void
}
export class ExampleRepositoryInMemory implements ExampleRepository {
constructor(
@Inject('DomainEvents') private readonly events: DomainEvents
) {}
async getOne(greeting: string) {
const entity = new Greeting({
greeting: greeting.trim()
});
return entity;
}
async save(entity: Greeting) {
Greeting.getEventsToDispatch(entity, (events) => {
for (const event of events) {
this.events.dispatch(event);
}
});
}
}

View File

@ -0,0 +1,19 @@
import assert from 'assert';
import {ExampleController} from './example.controller';
import * as sinon from 'sinon';
import {ExampleService} from '../../../core/example/example.service';
describe('ExampleController', function () {
describe('#read', function () {
it('returns the result of the greet method', async function () {
const service = Object.create(ExampleService.prototype);
service.greet = sinon.stub();
const controller = new ExampleController(service);
const result = await controller.read('egg');
assert.equal(result, service.greet.returnValues[0]);
});
});
});

View File

@ -0,0 +1,35 @@
/**
* Controller
*
* These classes are responsible for wiring HTTP Requests to the Service layer.
* They do not contain business logic.
*/
import {
Controller,
Get,
Param
} from '@nestjs/common';
import {Roles} from '../../../common/decorators/permissions.decorator';
import {ExampleService} from '../../../core/example/example.service';
@Controller('greetings')
export class ExampleController {
constructor(private readonly service: ExampleService) {}
@Roles([
'Admin',
'Author',
'Contributor',
'Editor',
'Owner',
'Admin Integration'
])
@Get(':recipient')
async read(
@Param('recipient') recipient: string
): Promise<string> {
const greeting = await this.service.greet(recipient);
return greeting;
}
}

37
ghost/ghost/src/index.ts Normal file
View File

@ -0,0 +1,37 @@
import 'reflect-metadata';
import {AppModule} from './nestjs/modules/app.module';
import {NestApplication, NestFactory} from '@nestjs/core';
import {registerEvents} from './common/decorators/handle-event.decorator';
import {ClassProvider, ValueProvider} from '@nestjs/common';
let _app: NestApplication;
export async function create() {
const app = await NestFactory.create<NestApplication>(AppModule);
const DomainEvents = await app.resolve('DomainEvents');
registerEvents(app as NestApplication, DomainEvents);
return app;
}
export async function getApp() {
if (_app) {
return _app;
}
_app = await create();
await _app.init();
return _app;
}
export async function resolve(token: string) {
const app = await getApp();
return await app.resolve(token);
}
export function addProvider(obj: ClassProvider | ValueProvider) {
AppModule.providers?.push(obj);
AppModule.exports?.push(obj.provide);
}
export {
AppModule
};

View File

@ -0,0 +1,24 @@
/**
* Listener
*
* Responsible for mapping events to perform actions on the service/provider layer.
* - "Like a Controller, but for Events not HTTP Requests"
*/
import {OnEvent} from '../common/decorators/handle-event.decorator';
import {Inject} from '@nestjs/common';
import {ExampleEvent} from '../core/example/example.event';
interface Logger {
info(message: string): void
}
export class ExampleListener {
constructor(
@Inject('logger') private logger: Logger
) {}
@OnEvent(ExampleEvent)
async logEvents(event: ExampleEvent) {
this.logger.info(`Received an event with a message: ${event.data.message}`);
}
}

View File

@ -0,0 +1,104 @@
import {ArgumentsHost, Catch, ExceptionFilter} from '@nestjs/common';
import {Response} from 'express';
interface GhostError extends Error {
statusCode?: number;
context?: string;
errorType?: string;
errorDetails?: string;
property?: string;
help?: string;
code?: string;
id?: string;
ghostErrorCode?: string;
}
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
catch(error: GhostError, host: ArgumentsHost) {
const context = host.switchToHttp();
const response = context.getResponse<Response>();
response.status(error.statusCode || 500);
response.json({
errors: [
{
message: error.message,
context: error.context || null,
type: error.errorType || null,
details: error.errorDetails || null,
property: error.property || null,
help: error.help || null,
code: error.code || null,
id: error.id || null,
ghostErrorCode: error.ghostErrorCode || null
}
]
});
}
}
/*
function prepareError(err: unknown) {
if (Array.isArray(err)) {
err = err[0];
}
// If the error is already a GhostError, it has been handled and can be returned as-is
// For everything else, we do some custom handling here
if (errors.utils.isGhostError(err)) {
return err;
}
if (!(err instanceof Error)) {
return new errors.InternalServerError({
err: err,
message: tpl(messages.genericError),
context: err.message,
statusCode: err.statusCode,
code: 'UNEXPECTED_ERROR'
});
}
// Catch bookshelf empty errors and other 404s, and turn into a Ghost 404
if (
(err.statusCode && err.statusCode === 404) ||
err.message === 'EmptyResponse'
) {
return new errors.NotFoundError({
err: err
});
// Catch handlebars / express-hbs errors, and render them as 400, rather than 500 errors as the server isn't broken
} else if (
isDependencyInStack('handlebars', err) ||
isDependencyInStack('express-hbs', err)
) {
// Temporary handling of theme errors from handlebars
// @TODO remove this when #10496 is solved properly
err = new errors.IncrrectUsageError({
err: err,
message: err.message,
statusCode: err.statusCode
});
// Catch database errors and turn them into 500 errors, but log some useful data to sentry
} else if (isDependencyInStack('mysql2', err)) {
// we don't want to return raw database errors to our users
err.sqlErrorCode = err.code;
err = new errors.InternalServerError({
err: err,
message: tpl(messages.genericError),
statusCode: err.statusCode,
code: 'UNEXPECTED_ERROR'
});
// For everything else, create a generic 500 error, with context set to the original error message
} else {
err = new errors.InternalServerError({
err: err,
message: tpl(messages.genericError),
context: err.message,
statusCode: err.statusCode,
code: 'UNEXPECTED_ERROR'
});
}
}
*/

View File

@ -0,0 +1,10 @@
import {Catch, NotFoundException, ExceptionFilter, ArgumentsHost} from '@nestjs/common';
@Catch(NotFoundException)
export class NotFoundFallthroughExceptionFilter implements ExceptionFilter {
catch(exception: NotFoundException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const next = ctx.getNext();
next();
}
}

View File

@ -0,0 +1,102 @@
import {
Injectable,
CanActivate,
ExecutionContext,
Inject
} from '@nestjs/common';
import {Request, Response} from 'express';
import {Actor} from '../../common/types/actor.type';
import ObjectID from 'bson-objectid';
// Here we extend the express Request interface with our new type
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Express {
// eslint-disable-next-line no-shadow
export interface Request {
actor?: Actor
}
}
}
interface SessionService {
// We use any because we've not got types for bookshelf models
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getUserForSession(req: Request, res: Response): Promise<any>
}
interface AuthenticationService {
// We use any because we've not got types for bookshelf models
// eslint-disable-next-line @typescript-eslint/no-explicit-any
authenticateWithToken(url: string, token: string, ignoreMaxAge: boolean): Promise<any>
}
@Injectable()
export class AdminAPIAuthentication implements CanActivate {
constructor(
@Inject('SessionService') private sessionService: SessionService,
@Inject('AdminAuthenticationService') private authService: AuthenticationService
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const response = context.switchToHttp().getResponse<Response>();
const user = await this.sessionService.getUserForSession(request, response);
if (user) {
await this.setUserActor(user, request);
return true;
}
if (!request.headers || !request.headers.authorization) {
return false;
}
const [scheme, token] = request.headers.authorization.split(' ');
if (!/^Ghost$/i.test(scheme)) {
return false;
}
const {apiKey, user: apiUser} = await this.authService.authenticateWithToken(
request.originalUrl,
token,
false
);
if (user) {
await this.setUserActor(apiUser, request);
return true;
}
if (apiKey) {
await apiKey.related('role').fetch();
const json = apiKey.toJSON();
request.actor = {
id: ObjectID.createFromHexString(json.integration.id),
role: json.role.name,
type: 'api_key'
};
return true;
}
return false;
}
// This is `any` because again it represents a bookshelf model
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private async setUserActor(user: any, request: Request) {
await user.related('roles').fetch();
const json = user.toJSON();
request.actor = {
// BS To work around Owner id === 1
id: ObjectID.createFromHexString(
json.id === '1' ? 'DEAD01D0DEAD01D0DEAD01D0' : json.id
),
role: json.roles[0].name,
type: 'user'
};
}
}

View File

@ -0,0 +1,26 @@
import {
Injectable,
CanActivate,
ExecutionContext,
Inject
} from '@nestjs/common';
import {Reflector} from '@nestjs/core';
import {Roles} from '../../common/decorators/permissions.decorator';
import {Request} from 'express';
@Injectable()
export class PermissionsGuard implements CanActivate {
constructor(@Inject(Reflector) private reflector: Reflector) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const roles = this.reflector.get(Roles, context.getHandler());
const request = context.switchToHttp().getRequest<Request>();
const role = request.actor?.role;
if (role && roles.includes(role)) {
return true;
}
return false;
}
}

View File

@ -0,0 +1,71 @@
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler
} from '@nestjs/common';
import {Observable} from 'rxjs';
import {tap} from 'rxjs/operators';
import {Request, Response} from 'express';
@Injectable()
export class LocationHeaderInterceptor implements NestInterceptor {
intercept<T>(context: ExecutionContext, next: CallHandler): Observable<T> {
if (context.getType() !== 'http') {
return next.handle();
}
const [
req,
res
]: [Request, Response] = context.getArgs();
if (req.method !== 'POST') {
return next.handle();
}
function getLocationHeader(responseData: unknown) {
if (typeof responseData !== 'object' || responseData === null) {
return;
}
const keys = Object.keys(responseData);
if (keys.length !== 1) {
return;
}
const data: unknown = Reflect.get(responseData, keys[0]);
if (!Array.isArray(data)) {
return;
}
if (data.length !== 1) {
return;
}
const id = data[0].id;
if (!id || typeof id !== 'string') {
return;
}
const url = new URL('https://ghost.io');
url.protocol = req.secure ? 'https:' : 'http:';
// We use `any` here because we haven't yet extended the express Request object with the vhost plugin types
// eslint-disable-next-line @typescript-eslint/no-explicit-any
url.host = (req as any).vhost ? (req as any).vhost.host : req.get('host');
url.pathname = req.path;
url.pathname += `${id}/`;
return url;
}
return next.handle().pipe(
tap((data) => {
const location = getLocationHeader(data);
if (location) {
res.set('location', location.href);
}
})
);
}
}

View File

@ -0,0 +1,17 @@
import {Module} from '@nestjs/common';
import {ExampleController} from '../../http/admin/controllers/example.controller';
import {ExampleService} from '../../core/example/example.service';
import {ExampleRepositoryInMemory} from '../../db/in-memory/example.repository.in-memory';
@Module({
controllers: [ExampleController],
exports: [ExampleService],
providers: [
ExampleService,
{
provide: 'ExampleRepository',
useClass: ExampleRepositoryInMemory
}
]
})
export class AdminAPIModule {}

View File

@ -0,0 +1,38 @@
import {DynamicModule} from '@nestjs/common';
import {APP_FILTER, APP_GUARD, APP_INTERCEPTOR} from '@nestjs/core';
import {AdminAPIModule} from './admin-api.module';
import {NotFoundFallthroughExceptionFilter} from '../filters/not-found-fallthrough.filter';
import {ExampleListener} from '../../listeners/example.listener';
import {AdminAPIAuthentication} from '../guards/admin-api-authentication.guard';
import {PermissionsGuard} from '../guards/permissions.guard';
import {LocationHeaderInterceptor} from '../interceptors/location-header.interceptor';
import {GlobalExceptionFilter} from '../filters/global-exception.filter';
class AppModuleClass {}
export const AppModule: DynamicModule = {
global: true,
module: AppModuleClass,
imports: [AdminAPIModule],
exports: [],
controllers: [],
providers: [
ExampleListener,
{
provide: APP_FILTER,
useClass: GlobalExceptionFilter
}, {
provide: APP_FILTER,
useClass: NotFoundFallthroughExceptionFilter
}, {
provide: APP_GUARD,
useClass: AdminAPIAuthentication
}, {
provide: APP_GUARD,
useClass: PermissionsGuard
}, {
provide: APP_INTERCEPTOR,
useClass: LocationHeaderInterceptor
}
]
};

View File

@ -0,0 +1,7 @@
module.exports = {
parser: '@typescript-eslint/parser',
plugins: ['ghost'],
extends: [
'plugin:ghost/test'
]
};

11
ghost/ghost/tsconfig.json Normal file
View File

@ -0,0 +1,11 @@
{
"extends": "../tsconfig.json",
"include": [
"src/**/*"
],
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"outDir": "build"
}
}

223
yarn.lock
View File

@ -3718,6 +3718,11 @@
tslib "^2.3.1"
upath "^2.0.1"
"@lukeed/csprng@^1.0.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@lukeed/csprng/-/csprng-1.1.0.tgz#1e3e4bd05c1cc7a0b2ddbd8a03f39f6e4b5e6cfe"
integrity sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==
"@mdx-js/react@^2.1.5":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@mdx-js/react/-/react-2.3.0.tgz#4208bd6d70f0d0831def28ef28c26149b03180b3"
@ -3768,6 +3773,45 @@
pump "^3.0.0"
tar-fs "^2.1.1"
"@nestjs/common@10.2.8":
version "10.2.8"
resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-10.2.8.tgz#f8934e6353440d6e51c89c0cf1b0f9aef54e8729"
integrity sha512-rmpwcdvq2IWMmsUVP8rsdKub6uDWk7dwCYo0aif50JTwcvcxzaP3iKVFKoSgvp0RKYu8h15+/AEOfaInmPpl0Q==
dependencies:
uid "2.0.2"
iterare "1.2.1"
tslib "2.6.2"
"@nestjs/core@10.2.8":
version "10.2.8"
resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-10.2.8.tgz#7b3abcf375113faffeef989a3a945c2494d390ec"
integrity sha512-9+MZ2s8ixfY9Bl/M9ofChiyYymcwdK9ZWNH4GDMF7Am7XRAQ1oqde6MYGG05rhQwiVXuTwaYLlXciJKfsrg5qg==
dependencies:
uid "2.0.2"
"@nuxtjs/opencollective" "0.3.2"
fast-safe-stringify "2.1.1"
iterare "1.2.1"
path-to-regexp "3.2.0"
tslib "2.6.2"
"@nestjs/platform-express@10.2.8":
version "10.2.8"
resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-10.2.8.tgz#c5af1fe3afb6e9858fc5610fd11a247635187eff"
integrity sha512-WoSSVtwIRc5AdGMHWVzWZK4JZLT0f4o2xW8P9gQvcX+omL8W1kXCfY8GQYXNBG84XmBNYH8r0FtC8oMe/lH5NQ==
dependencies:
body-parser "1.20.2"
cors "2.8.5"
express "4.18.2"
multer "1.4.4-lts.1"
tslib "2.6.2"
"@nestjs/testing@10.2.8":
version "10.2.8"
resolved "https://registry.yarnpkg.com/@nestjs/testing/-/testing-10.2.8.tgz#9bf0a05770b5afacf85aaf4abd99caa2284c3dd5"
integrity sha512-9Kj5IQhM67/nj/MT6Wi2OmWr5YQnCMptwKVFrX1TDaikpY12196v7frk0jVjdT7wms7rV07GZle9I2z0aSjqtQ==
dependencies:
tslib "2.6.2"
"@newrelic/aws-sdk@^7.1.0":
version "7.1.0"
resolved "https://registry.yarnpkg.com/@newrelic/aws-sdk/-/aws-sdk-7.1.0.tgz#7e934a44150e87dd88ab3b15690df03a20dfd718"
@ -3887,6 +3931,15 @@
nx "16.8.1"
tslib "^2.3.0"
"@nuxtjs/opencollective@0.3.2":
version "0.3.2"
resolved "https://registry.yarnpkg.com/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz#620ce1044f7ac77185e825e1936115bb38e2681c"
integrity sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==
dependencies:
chalk "^4.1.0"
consola "^2.15.0"
node-fetch "^2.6.1"
"@nx/nx-darwin-arm64@16.8.1":
version "16.8.1"
resolved "https://registry.yarnpkg.com/@nx/nx-darwin-arm64/-/nx-darwin-arm64-16.8.1.tgz#fd85ed007d63d232700272cd07138ecac046525d"
@ -7234,7 +7287,7 @@
"@tryghost/mongo-knex@^0.9.0":
version "0.9.0"
resolved "https://registry.npmjs.org/@tryghost/mongo-knex/-/mongo-knex-0.9.0.tgz#34463ceaa23b8e5e4c7ff859d9d5cc8f300ceade"
resolved "https://registry.yarnpkg.com/@tryghost/mongo-knex/-/mongo-knex-0.9.0.tgz#34463ceaa23b8e5e4c7ff859d9d5cc8f300ceade"
integrity sha512-1dksBf+nVyfVRssFC3/Tn1KqMhKfLhsjCnxnLv8vW5ZxSw39U6Kyp97u4BWithx31M/g3Q8nfCVg8hIgYVyt7w==
dependencies:
debug "^4.3.3"
@ -7332,7 +7385,7 @@
got "13.0.0"
lodash "^4.17.21"
"@tryghost/root-utils@0.3.24":
"@tryghost/root-utils@0.3.24", "@tryghost/root-utils@^0.3.24":
version "0.3.24"
resolved "https://registry.yarnpkg.com/@tryghost/root-utils/-/root-utils-0.3.24.tgz#91653fbadc882fb8510844f163a2231c87f30fab"
integrity sha512-EzYM3dR/3xyvJHm37RumiIzeGEBRwnnQtQzswXpzn46Rooz7PA7NSjUbLZ8j2K3t0ee+CsPNuyzmzZl+Ih1P2g==
@ -7340,7 +7393,7 @@
caller "^1.0.1"
find-root "^1.1.0"
"@tryghost/root-utils@^0.3.24", "@tryghost/root-utils@^0.3.25":
"@tryghost/root-utils@^0.3.25":
version "0.3.25"
resolved "https://registry.yarnpkg.com/@tryghost/root-utils/-/root-utils-0.3.25.tgz#d6e289004d2ee990f0baa337c007aba8eb6cc2c5"
integrity sha512-UvqoDFo64rWEvZTqP7P4PfB7a4AuE8V3KpN/IbEIBbZw4wG7lnINn67r6EdOXQA4U7fB4lIw9Z82ZZwVT5MkPg==
@ -7884,6 +7937,13 @@
dependencies:
undici-types "~5.26.4"
"@types/node@^20.10.0":
version "20.10.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.0.tgz#16ddf9c0a72b832ec4fcce35b8249cf149214617"
integrity sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==
dependencies:
undici-types "~5.26.4"
"@types/node@^8.0.0":
version "8.10.66"
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.66.tgz#dd035d409df322acc83dff62a602f12a5783bbb3"
@ -8035,6 +8095,13 @@
dependencies:
"@types/sinonjs__fake-timers" "*"
"@types/sinon@^17.0.3":
version "17.0.3"
resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-17.0.3.tgz#9aa7e62f0a323b9ead177ed23a36ea757141a5fa"
integrity sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==
dependencies:
"@types/sinonjs__fake-timers" "*"
"@types/sinonjs__fake-timers@*":
version "8.1.2"
resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz#bf2e02a3dbd4aecaf95942ecd99b7402e03fad5e"
@ -10436,6 +10503,24 @@ bn.js@^5.0.0, bn.js@^5.1.1:
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70"
integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==
body-parser@1.20.1:
version "1.20.1"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668"
integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==
dependencies:
bytes "3.1.2"
content-type "~1.0.4"
debug "2.6.9"
depd "2.0.0"
destroy "1.2.0"
http-errors "2.0.0"
iconv-lite "0.4.24"
on-finished "2.4.1"
qs "6.11.0"
raw-body "2.5.1"
type-is "~1.6.18"
unpipe "1.0.0"
body-parser@1.20.2, body-parser@^1.19.0:
version "1.20.2"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd"
@ -11486,7 +11571,7 @@ busboy@^0.2.11:
dicer "0.2.5"
readable-stream "1.1.x"
busboy@^1.6.0:
busboy@^1.0.0, busboy@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893"
integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==
@ -12641,6 +12726,11 @@ connect@^3.6.6:
parseurl "~1.3.3"
utils-merge "1.0.1"
consola@^2.15.0:
version "2.15.3"
resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550"
integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==
console-browserify@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336"
@ -16641,6 +16731,43 @@ express-unless@^2.1.3:
resolved "https://registry.yarnpkg.com/express-unless/-/express-unless-2.1.3.tgz#f951c6cca52a24da3de32d42cfd4db57bc0f9a2e"
integrity sha512-wj4tLMyCVYuIIKHGt0FhCtIViBcwzWejX0EjNxveAa6dG+0XBCQhMbx+PnkLkFCxLC69qoFrxds4pIyL88inaQ==
express@4.18.2:
version "4.18.2"
resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59"
integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==
dependencies:
accepts "~1.3.8"
array-flatten "1.1.1"
body-parser "1.20.1"
content-disposition "0.5.4"
content-type "~1.0.4"
cookie "0.5.0"
cookie-signature "1.0.6"
debug "2.6.9"
depd "2.0.0"
encodeurl "~1.0.2"
escape-html "~1.0.3"
etag "~1.8.1"
finalhandler "1.2.0"
fresh "0.5.2"
http-errors "2.0.0"
merge-descriptors "1.0.1"
methods "~1.1.2"
on-finished "2.4.1"
parseurl "~1.3.3"
path-to-regexp "0.1.7"
proxy-addr "~2.0.7"
qs "6.11.0"
range-parser "~1.2.1"
safe-buffer "5.2.1"
send "0.18.0"
serve-static "1.15.0"
setprototypeof "1.2.0"
statuses "2.0.1"
type-is "~1.6.18"
utils-merge "1.0.1"
vary "~1.1.2"
express@4.18.3, express@^4.10.7, express@^4.17.1, express@^4.17.2, express@^4.17.3, express@^4.18.2:
version "4.18.3"
resolved "https://registry.yarnpkg.com/express/-/express-4.18.3.tgz#6870746f3ff904dee1819b82e4b51509afffb0d4"
@ -16821,7 +16948,7 @@ fast-ordered-set@^1.0.0, fast-ordered-set@^1.0.2:
dependencies:
blank-object "^1.0.1"
fast-safe-stringify@^2.1.1:
fast-safe-stringify@2.1.1, fast-safe-stringify@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884"
integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==
@ -20005,6 +20132,11 @@ istextorbinary@^2.5.1:
editions "^2.2.0"
textextensions "^2.5.0"
iterare@1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/iterare/-/iterare-1.2.1.tgz#139c400ff7363690e33abffa33cbba8920f00042"
integrity sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==
iterate-iterator@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/iterate-iterator/-/iterate-iterator-1.0.2.tgz#551b804c9eaa15b847ea6a7cdc2f5bf1ec150f91"
@ -22909,6 +23041,19 @@ multer@1.4.4, multer@^1.4.4:
type-is "^1.6.4"
xtend "^4.0.0"
multer@1.4.4-lts.1:
version "1.4.4-lts.1"
resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.4-lts.1.tgz#24100f701a4611211cfae94ae16ea39bb314e04d"
integrity sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg==
dependencies:
append-field "^1.0.0"
busboy "^1.0.0"
concat-stream "^1.5.2"
mkdirp "^0.5.4"
object-assign "^4.1.1"
type-is "^1.6.4"
xtend "^4.0.0"
mustache@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64"
@ -24315,6 +24460,11 @@ path-to-regexp@0.1.7:
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==
path-to-regexp@3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.2.0.tgz#fa7877ecbc495c601907562222453c43cc204a5f"
integrity sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==
path-to-regexp@^1.0.0, path-to-regexp@^1.7.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
@ -26081,6 +26231,16 @@ range-parser@~1.2.1:
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
raw-body@2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857"
integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==
dependencies:
bytes "3.1.2"
http-errors "2.0.0"
iconv-lite "0.4.24"
unpipe "1.0.0"
raw-body@2.5.2:
version "2.5.2"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a"
@ -26475,6 +26635,11 @@ redis-parser@^3.0.0:
dependencies:
redis-errors "^1.0.0"
reflect-metadata@^0.1.14:
version "0.1.14"
resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.14.tgz#24cf721fe60677146bb77eeb0e1f9dece3d65859"
integrity sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==
reframe.js@4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/reframe.js/-/reframe.js-4.0.2.tgz#07f5f8fb40e0eb6f2d783ce952030d3e14dca7b7"
@ -27730,6 +27895,18 @@ sinon@17.0.0:
nise "^5.1.5"
supports-color "^7.2.0"
sinon@^17.0.1:
version "17.0.1"
resolved "https://registry.yarnpkg.com/sinon/-/sinon-17.0.1.tgz#26b8ef719261bf8df43f925924cccc96748e407a"
integrity sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==
dependencies:
"@sinonjs/commons" "^3.0.0"
"@sinonjs/fake-timers" "^11.2.2"
"@sinonjs/samsam" "^8.0.0"
diff "^5.1.0"
nise "^5.1.5"
supports-color "^7.2.0"
sinon@^9.0.0:
version "9.2.4"
resolved "https://registry.yarnpkg.com/sinon/-/sinon-9.2.4.tgz#e55af4d3b174a4443a8762fa8421c2976683752b"
@ -29564,6 +29741,25 @@ ts-interface-checker@^0.1.9:
resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699"
integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==
ts-node@10.9.1:
version "10.9.1"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b"
integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==
dependencies:
"@cspotcode/source-map-support" "^0.8.0"
"@tsconfig/node10" "^1.0.7"
"@tsconfig/node12" "^1.0.7"
"@tsconfig/node14" "^1.0.0"
"@tsconfig/node16" "^1.0.2"
acorn "^8.4.1"
acorn-walk "^8.1.1"
arg "^4.1.0"
create-require "^1.1.0"
diff "^4.0.1"
make-error "^1.1.1"
v8-compile-cache-lib "^3.0.1"
yn "3.1.1"
ts-node@10.9.2:
version "10.9.2"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f"
@ -29592,16 +29788,16 @@ tsconfig-paths@^4.1.2:
minimist "^1.2.6"
strip-bom "^3.0.0"
tslib@2.6.2, tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.5.0:
version "2.6.2"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
tslib@^1.11.1, tslib@^1.13.0, tslib@^1.9.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.5.0:
version "2.6.2"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
tsscmp@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb"
@ -29764,6 +29960,13 @@ uid-safe@~2.1.5:
dependencies:
random-bytes "~1.0.0"
uid@2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/uid/-/uid-2.0.2.tgz#4b5782abf0f2feeefc00fa88006b2b3b7af3e3b9"
integrity sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==
dependencies:
"@lukeed/csprng" "^1.0.0"
unbox-primitive@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"