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:
parent
d2620171ea
commit
0fb0c6c2b5
@ -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 = () => {
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -36,6 +36,7 @@ const BETA_FEATURES = [
|
||||
];
|
||||
|
||||
const ALPHA_FEATURES = [
|
||||
'NestPlayground',
|
||||
'urlCache',
|
||||
'lexicalMultiplayer',
|
||||
'websockets',
|
||||
|
@ -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
11
ghost/ghost/.eslintrc.js
Normal 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
21
ghost/ghost/README.md
Normal 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
43
ghost/ghost/package.json
Normal 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"
|
||||
}
|
||||
}
|
40
ghost/ghost/src/common/decorators/handle-event.decorator.ts
Normal file
40
ghost/ghost/src/common/decorators/handle-event.decorator.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
@ -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[]>();
|
129
ghost/ghost/src/common/entity.base.ts
Normal file
129
ghost/ghost/src/common/entity.base.ts
Normal 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?
|
||||
}
|
||||
}
|
||||
}
|
6
ghost/ghost/src/common/event.base.ts
Normal file
6
ghost/ghost/src/common/event.base.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export abstract class BaseEvent<Data> {
|
||||
constructor(
|
||||
public readonly data: Data,
|
||||
public readonly timestamp: Date = new Date()
|
||||
) {}
|
||||
}
|
5
ghost/ghost/src/common/helpers/date.helper.ts
Normal file
5
ghost/ghost/src/common/helpers/date.helper.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export function now(): Date {
|
||||
const date = new Date();
|
||||
date.setMilliseconds(0);
|
||||
return date;
|
||||
}
|
1
ghost/ghost/src/common/libraries.defintitions.ts
Normal file
1
ghost/ghost/src/common/libraries.defintitions.ts
Normal file
@ -0,0 +1 @@
|
||||
declare module '@tryghost/errors';
|
8
ghost/ghost/src/common/types/actor.type.ts
Normal file
8
ghost/ghost/src/common/types/actor.type.ts
Normal 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'
|
||||
};
|
27
ghost/ghost/src/core/example/example.entity.test.ts
Normal file
27
ghost/ghost/src/core/example/example.entity.test.ts
Normal 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.');
|
||||
});
|
||||
});
|
38
ghost/ghost/src/core/example/example.entity.ts
Normal file
38
ghost/ghost/src/core/example/example.entity.ts
Normal 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;
|
||||
}
|
||||
}
|
18
ghost/ghost/src/core/example/example.event.ts
Normal file
18
ghost/ghost/src/core/example/example.event.ts
Normal 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());
|
||||
}
|
||||
}
|
13
ghost/ghost/src/core/example/example.repository.ts
Normal file
13
ghost/ghost/src/core/example/example.repository.ts
Normal 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>
|
||||
}
|
22
ghost/ghost/src/core/example/example.service.test.ts
Normal file
22
ghost/ghost/src/core/example/example.service.test.ts
Normal 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));
|
||||
});
|
||||
});
|
25
ghost/ghost/src/core/example/example.service.ts
Normal file
25
ghost/ghost/src/core/example/example.service.ts
Normal 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;
|
||||
}
|
||||
}
|
37
ghost/ghost/src/db/in-memory/example.repository.in-memory.ts
Normal file
37
ghost/ghost/src/db/in-memory/example.repository.in-memory.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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]);
|
||||
});
|
||||
});
|
||||
});
|
35
ghost/ghost/src/http/admin/controllers/example.controller.ts
Normal file
35
ghost/ghost/src/http/admin/controllers/example.controller.ts
Normal 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
37
ghost/ghost/src/index.ts
Normal 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
|
||||
};
|
24
ghost/ghost/src/listeners/example.listener.ts
Normal file
24
ghost/ghost/src/listeners/example.listener.ts
Normal 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}`);
|
||||
}
|
||||
}
|
104
ghost/ghost/src/nestjs/filters/global-exception.filter.ts
Normal file
104
ghost/ghost/src/nestjs/filters/global-exception.filter.ts
Normal 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'
|
||||
});
|
||||
}
|
||||
}
|
||||
*/
|
@ -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();
|
||||
}
|
||||
}
|
102
ghost/ghost/src/nestjs/guards/admin-api-authentication.guard.ts
Normal file
102
ghost/ghost/src/nestjs/guards/admin-api-authentication.guard.ts
Normal 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'
|
||||
};
|
||||
}
|
||||
}
|
26
ghost/ghost/src/nestjs/guards/permissions.guard.ts
Normal file
26
ghost/ghost/src/nestjs/guards/permissions.guard.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
17
ghost/ghost/src/nestjs/modules/admin-api.module.ts
Normal file
17
ghost/ghost/src/nestjs/modules/admin-api.module.ts
Normal 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 {}
|
38
ghost/ghost/src/nestjs/modules/app.module.ts
Normal file
38
ghost/ghost/src/nestjs/modules/app.module.ts
Normal 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
|
||||
}
|
||||
]
|
||||
};
|
7
ghost/ghost/test/.eslintrc.js
Normal file
7
ghost/ghost/test/.eslintrc.js
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['ghost'],
|
||||
extends: [
|
||||
'plugin:ghost/test'
|
||||
]
|
||||
};
|
11
ghost/ghost/tsconfig.json
Normal file
11
ghost/ghost/tsconfig.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"outDir": "build"
|
||||
}
|
||||
}
|
223
yarn.lock
223
yarn.lock
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user