0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-11 02:12:21 -05:00

Structured the common folder slightly

- gave everything a type in it's file name
- grouped things into folders
- subject to much change!
This commit is contained in:
Hannah Wolfe 2023-12-06 17:27:54 +00:00 committed by Fabien "egg" O'Carroll
parent 38e5935d40
commit c559c59dd2
19 changed files with 277 additions and 14 deletions

View file

@ -1,6 +1,6 @@
import {Actor} from './actor';
import {Actor} from '../types/actor.type';
import ObjectID from 'bson-objectid';
import {now} from './date';
import {now} from '../helpers/date.helper';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function equals(a: any, b: any) {

View file

@ -1,5 +1,5 @@
import ObjectID from 'bson-objectid';
import {Entity} from './entity';
import {Entity} from '../base/entity.base';
export type Page = {
page: number;

View file

@ -1,4 +1,4 @@
import {Entity} from '../../common/entity';
import {Entity} from '../../common/base/entity.base';
type SnippetData = {
name: string;

View file

@ -1,7 +1,7 @@
import ObjectID from 'bson-objectid';
import {Snippet} from './snippet.entity';
import {SnippetsRepository} from './snippets.repository.interface';
import {OrderOf, Page} from '../../common/repository';
import {OrderOf, Page} from '../../common/interfaces/repository.interface';
import nql from '@tryghost/nql';
export class SnippetsRepositoryInMemory implements SnippetsRepository {

View file

@ -1,5 +1,5 @@
import {Snippet} from './snippet.entity';
import {Repository} from '../../common/repository';
import {Repository} from '../../common/interfaces/repository.interface';
export interface SnippetsRepository extends Repository<Snippet, string, []> {

View file

@ -1,10 +1,10 @@
import {Inject} from '@nestjs/common';
import {Knex} from 'knex';
import ObjectID from 'bson-objectid';
import {OrderOf, Page, Repository} from '../../common/repository';
import {OrderOf, Page, Repository} from '../../common/interfaces/repository.interface';
import assert from 'assert';
import nql from '@tryghost/nql';
import {Entity} from '../../common/entity';
import {Entity} from '../../common/base/entity.base';
export abstract class BaseKnexRepository<T extends Entity<unknown>, F, O extends OrderOf<Fields>, Fields extends string[], Row> implements Repository<T, F, Fields> {
constructor(@Inject('knex') private readonly knex: Knex) {}

View file

@ -1,6 +1,6 @@
import {SnippetsRepository} from '../../core/snippets/snippets.repository.interface';
import {Snippet} from '../../core/snippets/snippet.entity';
import {OrderOf} from '../../common/repository';
import {OrderOf} from '../../common/interfaces/repository.interface';
import {BaseKnexRepository} from './knex.repository';
import ObjectID from 'bson-objectid';
import {Knex} from 'knex';
@ -93,4 +93,3 @@ export class KnexSnippetsRepository
});
}
}

View file

@ -0,0 +1,24 @@
import {Snippet} from '../../../core/snippets/snippet.entity';
export class SnippetDTO {
id: string;
name: string;
lexical?: string|null;
mobiledoc?: string|null;
created_at: Date;
updated_at: Date|null;
constructor(data: Snippet, options: {formats?: 'mobiledoc'|'lexical'}) {
this.id = data.id.toString();
this.name = data.name;
if (options.formats === 'mobiledoc') {
this.mobiledoc = data.mobiledoc || null;
} else {
this.lexical = data.lexical || null;
}
this.created_at = data.createdAt;
this.updated_at = data.updatedAt || null;
}
}

View file

@ -0,0 +1,67 @@
import ObjectId from 'bson-objectid';
import {Test} from '@nestjs/testing';
import {Snippet} from '../../../core/snippets/snippet.entity';
import {SnippetsController} from './snippets.controller';
import {SnippetsService} from '../../../core/snippets/snippets.service';
import {SnippetsRepositoryInMemory} from '../../../core/snippets/snippets.repository.inmemory';
import assert from 'assert/strict';
import sinon from 'sinon';
describe('SnippetsController', () => {
let snippetsController: SnippetsController;
let snippetsService: SnippetsService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
controllers: [SnippetsController],
providers: [
{
provide: 'SnippetsRepository',
useClass: SnippetsRepositoryInMemory
},
SnippetsService
]
}).compile();
snippetsService = moduleRef.get<SnippetsService>(SnippetsService);
snippetsController = moduleRef.get<SnippetsController>(SnippetsController);
});
describe('browse', () => {
it('should return an array of snippets', async () => {
const serviceSnippetResult = {
snippets: [
new Snippet({
id: ObjectId(),
deleted: false,
name: 'Test',
mobiledoc: '{"version":"0.3.1","atoms":[],"cards":[],"markups":[],"sections":[[1,"p",[[0,[],0,"Test"]]]]}',
lexical: undefined,
createdAt: new Date(),
updatedAt: null,
createdBy: ObjectId(),
updatedBy: null
})
],
pagination: {
page: 1,
limit: 15,
pages: 1,
total: 1
}
};
snippetsService.getAll = sinon.stub().returns(serviceSnippetResult);
const response = await snippetsController.browse();
assert.equal(response.snippets.length, 1);
assert.equal(Object.keys(response.snippets[0]).length, 6);
assert.equal(response.snippets[0].id, serviceSnippetResult.snippets[0].id.toString());
assert.equal(response.snippets[0].name, serviceSnippetResult.snippets[0].name);
assert.equal(response.snippets[0].mobiledoc, serviceSnippetResult.snippets[0].mobiledoc);
assert.equal(response.snippets[0].lexical, undefined);
assert.equal(response.snippets[0].created_at, serviceSnippetResult.snippets[0].createdAt);
assert.equal(response.snippets[0].updated_at, serviceSnippetResult.snippets[0].updatedAt);
});
});
});

View file

@ -0,0 +1,123 @@
import {Body, Controller, Delete, Get, HttpCode, Param, Post, Put, Query, UseFilters, UseInterceptors} from '@nestjs/common';
import {SnippetsService} from '../../../core/snippets/snippets.service';
import {SnippetDTO} from './snippet.dto';
import {Pagination} from '../../../common/types/pagination.type';
import ObjectID from 'bson-objectid';
import {now} from '../../../common/helpers/date.helper';
import {LocationHeaderInterceptor} from '../../interceptors/location-header.interceptor';
import {GlobalExceptionFilter} from '../../filters/global-exception.filter';
import {NotFoundError} from '@tryghost/errors';
@Controller('snippets')
@UseInterceptors(LocationHeaderInterceptor)
@UseFilters(GlobalExceptionFilter)
export class SnippetsController {
constructor(private readonly service: SnippetsService) {}
@Get(':id')
async read(
@Param('id') id: 'string',
@Query('formats') formats?: 'mobiledoc' | 'lexical'
): Promise<{snippets: [SnippetDTO]}> {
const snippet = await this.service.getOne(ObjectID.createFromHexString(id));
if (snippet === null) {
throw new NotFoundError({
context: 'Snippet not found.',
message: 'Resource not found error, cannot read snippet.'
});
}
return {
snippets: [new SnippetDTO(snippet, {formats})]
};
}
@Delete(':id')
@HttpCode(204)
async destroy(
@Param('id') id: 'string'
) {
const snippet = await this.service.delete(ObjectID.createFromHexString(id));
if (snippet === null) {
throw new NotFoundError({
context: 'Resource could not be found.',
message: 'Resource not found error, cannot delete snippet.'
});
}
return {};
}
@Put(':id')
async edit(
@Param('id') id: 'string',
@Body() body: any,
@Query('formats') formats?: 'mobiledoc' | 'lexical'
): Promise<{snippets: [SnippetDTO]}> {
const snippet = await this.service.update(ObjectID.createFromHexString(id), body.snippets[0]);
if (snippet === null) {
throw new NotFoundError({
context: 'Snippet not found.',
message: 'Resource not found error, cannot read snippet.'
});
}
return {
snippets: [new SnippetDTO(snippet, {formats})]
};
}
@Post('')
async add(
@Body() body: any,
@Query('formats') formats?: 'mobiledoc' | 'lexical'
): Promise<{snippets: [SnippetDTO]}> {
const snippet = await this.service.create({
...body.snippets[0],
updatedAt: now()
});
return {
snippets: [new SnippetDTO(snippet, {formats})]
};
}
@Get('')
async browse(
@Query('formats') formats?: 'mobiledoc' | 'lexical',
@Query('filter') filter?: string,
@Query('page') page: number = 1,
@Query('limit') limit: number | 'all' = 15
): Promise<{snippets: SnippetDTO[], meta: {pagination: Pagination;};}> {
let snippets;
let total;
if (limit === 'all') {
snippets = await this.service.getAll({
filter
});
total = snippets.length;
} else {
const result = await this.service.getPage({
filter,
page,
limit
});
total = result.count;
snippets = result.data;
}
const pages = limit === 'all' ? 0 : Math.ceil(total / limit);
const snippetDTOs = snippets.map(snippet => new SnippetDTO(snippet, {formats}));
return {
snippets: snippetDTOs,
meta: {
pagination: {
page,
limit,
total,
pages,
prev: page > 1 ? page - 1 : null,
next: page < pages ? page + 1 : null
}
}
};
}
}

View file

@ -0,0 +1,50 @@
import {Snippet} from '../../../core/snippets/snippet.entity';
import ObjectId from 'bson-objectid';
import assert from 'assert/strict';
import {SnippetDTO} from './snippet.dto';
describe('BrowseSnippetDTO', () => {
it('constructs a BrowseSnippetDTO object from a Snippet object with mobiledoc field', async () => {
const snippet = new Snippet({
id: ObjectId(),
deleted: false,
name: 'Test',
mobiledoc: '{"version":"0.3.1","atoms":[],"cards":[],"markups":[],"sections":[[1,"p",[[0,[],0,"Test"]]]]}',
lexical: undefined,
createdAt: new Date(),
updatedAt: null,
createdBy: ObjectId(),
updatedBy: null
});
const browseSnippetDTO = new SnippetDTO(snippet, {formats: 'mobiledoc'});
assert(browseSnippetDTO, 'BrowseSnippetDTO object is not null');
assert.equal(browseSnippetDTO.id, snippet.id.toString());
assert.equal(browseSnippetDTO.name, snippet.name);
assert.equal(browseSnippetDTO.mobiledoc, snippet.mobiledoc);
assert.equal(browseSnippetDTO.lexical, undefined);
});
it('constructs a BrowseSnippetDTO object from a Snippet object with lexical field', async () => {
const snippet = new Snippet({
deleted: false,
id: ObjectId(),
name: 'Test',
mobiledoc: undefined,
lexical: `{"root":{"children":[{"type":"html","version":1,"html":"<p>hey!</p>"}],"direction":null,"format":"","indent":0,"type":"root","version":1}}`,
createdAt: new Date(),
updatedAt: null,
createdBy: ObjectId(),
updatedBy: null
});
const browseSnippetDTO = new SnippetDTO(snippet, {formats: 'lexical'});
assert(browseSnippetDTO, 'BrowseSnippetDTO object is not null');
assert.equal(browseSnippetDTO.id, snippet.id.toString());
assert.equal(browseSnippetDTO.name, snippet.name);
assert.equal(browseSnippetDTO.mobiledoc, undefined);
assert.equal(browseSnippetDTO.lexical, snippet.lexical);
});
});

View file

@ -1,9 +1,9 @@
import {Body, Controller, Delete, Get, HttpCode, Param, Post, Put, Query, UseFilters, UseInterceptors} from '@nestjs/common';
import {SnippetsService} from '../../core/snippets/snippets.service';
import {SnippetDTO} from './snippet.dto';
import {Pagination} from '../../common/pagination.type';
import {Pagination} from '../../common/types/pagination.type';
import ObjectID from 'bson-objectid';
import {now} from '../../common/date';
import {now} from '../../common/helpers/date.helper';
import {LocationHeaderInterceptor} from '../interceptors/location-header.interceptor';
import {GlobalExceptionFilter} from '../filters/global-exception.filter';
import {NotFoundError} from '@tryghost/errors';

View file

@ -1,7 +1,7 @@
import 'reflect-metadata';
import {AppModule} from './nestjs/AppModule';
import {NestApplication, NestFactory} from '@nestjs/core';
import {registerEvents} from './common/handle-event.decorator';
import {registerEvents} from './common/decorators/handle-event.decorator';
export async function create() {
const app = await NestFactory.create(AppModule);

View file

@ -1,6 +1,6 @@
import {MilestoneCreatedEvent} from '@tryghost/milestones';
import {SlackNotifications} from '@tryghost/slack-notifications';
import {OnEvent} from '../common/handle-event.decorator';
import {OnEvent} from '../common/decorators/handle-event.decorator';
import {Inject} from '@nestjs/common';
interface IConfig {