Re-enabled general eslint rules in TS config

refs https://github.com/TryGhost/DevOps/issues/50

- when creating a TS config in our `eslint-plugin-ghost` dependency, I
  only extended the recommended config, which left out a lot of
  stylistic things we used to enforce in JS
- this fixes that by bumping the dependency to a version which extends
  those shared configs, and fixes all the code that currently goes
  against those rules
This commit is contained in:
Daniel Lockyer 2023-09-08 13:14:41 +02:00 committed by Daniel Lockyer
parent c08538ba84
commit 6dc1d08590
24 changed files with 185 additions and 197 deletions

View File

@ -1,3 +1,5 @@
/* eslint-disable no-shadow */
import AuthFrame from './AuthFrame'; import AuthFrame from './AuthFrame';
import ContentBox from './components/ContentBox'; import ContentBox from './components/ContentBox';
import PopupBox from './components/PopupBox'; import PopupBox from './components/PopupBox';
@ -32,7 +34,7 @@ const App: React.FC<AppProps> = ({scriptTag}) => {
siteUrl: options.siteUrl, siteUrl: options.siteUrl,
apiUrl: options.apiUrl!, apiUrl: options.apiUrl!,
apiKey: options.apiKey! apiKey: options.apiKey!
}) });
}, [options]); }, [options]);
const [adminApi, setAdminApi] = useState<AdminApi|null>(null); const [adminApi, setAdminApi] = useState<AdminApi|null>(null);
@ -45,7 +47,7 @@ const App: React.FC<AppProps> = ({scriptTag}) => {
return { return {
...state, ...state,
...newState ...newState
} };
}); });
}, [setFullState]); }, [setFullState]);
@ -55,7 +57,7 @@ const App: React.FC<AppProps> = ({scriptTag}) => {
// because updates to state may be asynchronous // because updates to state may be asynchronous
// so calling dispatchAction('counterUp') multiple times, may yield unexpected results if we don't use a callback function // so calling dispatchAction('counterUp') multiple times, may yield unexpected results if we don't use a callback function
setState((state) => { setState((state) => {
return SyncActionHandler({action, data, state, api, adminApi: adminApi!, options}) return SyncActionHandler({action, data, state, api, adminApi: adminApi!, options});
}); });
return; return;
} }
@ -66,7 +68,7 @@ const App: React.FC<AppProps> = ({scriptTag}) => {
setState((state) => { setState((state) => {
ActionHandler({action, data, state, api, adminApi: adminApi!, options}).then((updatedState) => { ActionHandler({action, data, state, api, adminApi: adminApi!, options}).then((updatedState) => {
setState({...updatedState}); setState({...updatedState});
}).catch(console.error); }).catch(console.error); // eslint-disable-line no-console
// No immediate changes // No immediate changes
return {}; return {};
@ -125,7 +127,7 @@ const App: React.FC<AppProps> = ({scriptTag}) => {
pagination: data.meta.pagination, pagination: data.meta.pagination,
count: count count: count
}; };
} };
/** Initialize comments setup on load, fetch data and setup state*/ /** Initialize comments setup on load, fetch data and setup state*/
const initSetup = async () => { const initSetup = async () => {

View File

@ -10,5 +10,5 @@ const AuthFrame: React.FC<Props> = ({adminUrl, onLoad}) => {
return ( return (
<iframe data-frame="admin-auth" src={adminUrl + 'auth-frame/'} style={iframeStyle} title="auth-frame" onLoad={onLoad}></iframe> <iframe data-frame="admin-auth" src={adminUrl + 'auth-frame/'} style={iframeStyle} title="auth-frame" onLoad={onLoad}></iframe>
); );
} };
export default AuthFrame; export default AuthFrame;

View File

@ -37,7 +37,7 @@ export default class IFrame extends Component<any> {
if (this.props.onResize) { if (this.props.onResize) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
(new ResizeObserver(_ => { (new ResizeObserver((_) => {
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
this.props.onResize(this.iframeRoot); this.props.onResize(this.iframeRoot);
}); });

View File

@ -18,7 +18,7 @@ const Pagination = () => {
return null; return null;
} }
const text = left === 1 ? t('Show 1 previous comment') : t('Show {{amount}} previous comments', {amount: formatNumber(left)}) const text = left === 1 ? t('Show 1 previous comment') : t('Show {{amount}} previous comments', {amount: formatNumber(left)});
return ( return (
<button className="text-md group mb-10 flex w-full items-center px-0 pb-2 pt-0 text-left font-sans font-semibold text-neutral-700 dark:text-white" data-testid="pagination-component" type="button" onClick={loadMore}> <button className="text-md group mb-10 flex w-full items-center px-0 pb-2 pt-0 text-left font-sans font-semibold text-neutral-700 dark:text-white" data-testid="pagination-component" type="button" onClick={loadMore}>

View File

@ -8,7 +8,7 @@ type Props = {
}; };
const RepliesPagination: React.FC<Props> = ({loadMore, count}) => { const RepliesPagination: React.FC<Props> = ({loadMore, count}) => {
const {t} = useAppContext(); const {t} = useAppContext();
const text = count === 1 ? t('Show 1 more reply') : t('Show {{amount}} more replies', {amount: formatNumber(count)}) const text = count === 1 ? t('Show 1 more reply') : t('Show {{amount}} more replies', {amount: formatNumber(count)});
return ( return (
<div className="flex w-full items-center justify-start"> <div className="flex w-full items-center justify-start">

View File

@ -69,7 +69,7 @@ export function formatRelativeTime(dateString: string, t: TranslationFunction):
} }
// Diff in months // Diff in months
diff = diff * 7 / 30 diff = diff * 7 / 30;
if (diff < 12) { if (diff < 12) {
if (Math.floor(diff) === 1) { if (Math.floor(diff) === 1) {
// Special case, we should compare based on dates in the future instead // Special case, we should compare based on dates in the future instead

View File

@ -3,7 +3,7 @@ import {CommentsOptions} from '../AppContext';
export function useOptions(scriptTag: HTMLElement) { export function useOptions(scriptTag: HTMLElement) {
const buildOptions = React.useCallback(() => { const buildOptions = React.useCallback(() => {
/** /**
* @type {HTMLElement} * @type {HTMLElement}
*/ */
const dataset = scriptTag.dataset; const dataset = scriptTag.dataset;

View File

@ -52,10 +52,10 @@ export abstract class BookshelfRepository<IDType, T extends Entity<IDType>> {
const existing = await this.Model.findOne({id: entity.id}, {require: false}); const existing = await this.Model.findOne({id: entity.id}, {require: false});
if (existing) { if (existing) {
existing.set(this.toPrimitive(entity)) existing.set(this.toPrimitive(entity));
await existing.save({}, {autoRefresh: false, method: 'update'}); await existing.save({}, {autoRefresh: false, method: 'update'});
} else { } else {
await this.Model.add(this.toPrimitive(entity)) await this.Model.add(this.toPrimitive(entity));
} }
} }
@ -78,7 +78,7 @@ export abstract class BookshelfRepository<IDType, T extends Entity<IDType>> {
order: this.#orderToString(order), order: this.#orderToString(order),
limit, limit,
page page
}) });
return (await Promise.all(models.map(model => this.modelToEntity(model)))).filter(entity => !!entity) as T[]; return (await Promise.all(models.map(model => this.modelToEntity(model)))).filter(entity => !!entity) as T[];
} }

View File

@ -19,7 +19,7 @@ const messages = {
}; };
function validateFilter(filter: string | null, type: 'manual' | 'automatic', isAllowedEmpty = false) { function validateFilter(filter: string | null, type: 'manual' | 'automatic', isAllowedEmpty = false) {
const allowedProperties = ['featured', 'published_at', 'tag', 'tags'] const allowedProperties = ['featured', 'published_at', 'tag', 'tags'];
if (type === 'manual') { if (type === 'manual') {
if (filter !== null) { if (filter !== null) {
throw new ValidationError({ throw new ValidationError({

View File

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import {Collection} from './Collection'; import {Collection} from './Collection';
import {Knex} from "knex"; import {Knex} from 'knex';
export interface CollectionRepository { export interface CollectionRepository {
createTransaction(fn: (transaction: Knex.Transaction) => Promise<any>): Promise<any> createTransaction(fn: (transaction: Knex.Transaction) => Promise<any>): Promise<any>

View File

@ -1,13 +1,13 @@
import logging from '@tryghost/logging'; import logging from '@tryghost/logging';
import tpl from '@tryghost/tpl'; import tpl from '@tryghost/tpl';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
import {Knex} from "knex"; import {Knex} from 'knex';
import { import {
PostsBulkUnpublishedEvent, PostsBulkUnpublishedEvent,
PostsBulkFeaturedEvent, PostsBulkFeaturedEvent,
PostsBulkUnfeaturedEvent, PostsBulkUnfeaturedEvent,
PostsBulkAddTagsEvent PostsBulkAddTagsEvent
} from "@tryghost/post-events"; } from '@tryghost/post-events';
import debugModule from '@tryghost/debug'; import debugModule from '@tryghost/debug';
import {Collection} from './Collection'; import {Collection} from './Collection';
import {CollectionRepository} from './CollectionRepository'; import {CollectionRepository} from './CollectionRepository';
@ -194,7 +194,7 @@ export class CollectionsService {
}); });
this.DomainEvents.subscribe(PostEditedEvent, async (event: PostEditedEvent) => { this.DomainEvents.subscribe(PostEditedEvent, async (event: PostEditedEvent) => {
if(this.hasPostEditRelevantChanges(event.data) === false) { if (this.hasPostEditRelevantChanges(event.data) === false) {
return; return;
} }
@ -280,7 +280,7 @@ export class CollectionsService {
featured: postEditEvent.previous.featured, featured: postEditEvent.previous.featured,
published_at: postEditEvent.previous.published_at, published_at: postEditEvent.previous.published_at,
tags: postEditEvent.previous.tags tags: postEditEvent.previous.tags
} };
return !isEqual(current, previous); return !isEqual(current, previous);
} }
@ -289,7 +289,7 @@ export class CollectionsService {
return await this.collectionsRepository.createTransaction(async (transaction) => { return await this.collectionsRepository.createTransaction(async (transaction) => {
const collections = await this.collectionsRepository.getAll({ const collections = await this.collectionsRepository.getAll({
transaction transaction
}) });
for (const collection of collections) { for (const collection of collections) {
if (collection.type === 'automatic' && collection.filter) { if (collection.type === 'automatic' && collection.filter) {
@ -423,7 +423,7 @@ export class CollectionsService {
await this.collectionsRepository.save(collection, {transaction}); await this.collectionsRepository.save(collection, {transaction});
} }
collectionsChangeLog += `Post ${postEdit.id} was updated and added to collection ${collection.slug} with filter ${collection.filter}\n` collectionsChangeLog += `Post ${postEdit.id} was updated and added to collection ${collection.slug} with filter ${collection.filter}\n`;
} else { } else {
debug(`Post ${postEdit.id} was updated but did not update any collections`); debug(`Post ${postEdit.id} was updated but did not update any collections`);
} }
@ -443,7 +443,7 @@ export class CollectionsService {
}); });
// only process collections that have a filter that includes published_at // only process collections that have a filter that includes published_at
collections = collections.filter((collection) => collection.filter?.includes('published_at')); collections = collections.filter(collection => collection.filter?.includes('published_at'));
if (!collections.length) { if (!collections.length) {
return; return;
@ -461,7 +461,7 @@ export class CollectionsService {
}); });
// only process collections that have a filter that includes featured // only process collections that have a filter that includes featured
collections = collections.filter((collection) => collection.filter?.includes('featured')); collections = collections.filter(collection => collection.filter?.includes('featured'));
if (!collections.length) { if (!collections.length) {
return; return;

View File

@ -20,12 +20,12 @@ type DonationEventModelInstance = BookshelfModelInstance & {
referrer_medium: string | null; referrer_medium: string | null;
referrer_url: string | null; referrer_url: string | null;
} }
type DonationPaymentEventModel = BookshelfModel<DonationEventModelInstance>; type DonationPaymentEventBookshelfModel = BookshelfModel<DonationEventModelInstance>;
export class DonationBookshelfRepository implements DonationRepository { export class DonationBookshelfRepository implements DonationRepository {
#Model: DonationPaymentEventModel; #Model: DonationPaymentEventBookshelfModel;
constructor({DonationPaymentEventModel}: {DonationPaymentEventModel: DonationPaymentEventModel}) { constructor({DonationPaymentEventModel}: {DonationPaymentEventModel: DonationPaymentEventBookshelfModel}) {
this.#Model = DonationPaymentEventModel; this.#Model = DonationPaymentEventModel;
} }
@ -42,7 +42,7 @@ export class DonationBookshelfRepository implements DonationRepository {
attribution_type: event.attributionType, attribution_type: event.attributionType,
referrer_source: event.referrerSource, referrer_source: event.referrerSource,
referrer_medium: event.referrerMedium, referrer_medium: event.referrerMedium,
referrer_url: event.referrerUrl, referrer_url: event.referrerUrl
}); });
} }
} }

View File

@ -1,4 +1,4 @@
import {DonationPaymentEvent} from "./DonationPaymentEvent"; import {DonationPaymentEvent} from './DonationPaymentEvent';
export type DonationRepository = { export type DonationRepository = {
save(event: DonationPaymentEvent): Promise<void>; save(event: DonationPaymentEvent): Promise<void>;

View File

@ -8,7 +8,7 @@ import {MailEventRepository} from './MailEventRepository';
/** /**
* @see https://documentation.mailgun.com/en/latest/user_manual.html#events-1 * @see https://documentation.mailgun.com/en/latest/user_manual.html#events-1
*/ */
enum EventType { enum EventType { // eslint-disable-line no-shadow
CLICKED = 'clicked', CLICKED = 'clicked',
COMPLAINED = 'complained', COMPLAINED = 'complained',
DELIVERED = 'delivered', DELIVERED = 'delivered',

View File

@ -1,4 +1,4 @@
import { PostDeletedEvent, PostAddedEvent, PostEditedEvent, TagDeletedEvent } from '@tryghost/collections'; import {PostDeletedEvent, PostAddedEvent, PostEditedEvent, TagDeletedEvent} from '@tryghost/collections';
type ModelToDomainEventInterceptorDeps = { type ModelToDomainEventInterceptorDeps = {
ModelEvents: { ModelEvents: {

View File

@ -1,5 +1,5 @@
import {Recommendation} from "./Recommendation"; import {Recommendation} from './Recommendation';
import {RecommendationRepository} from "./RecommendationRepository"; import {RecommendationRepository} from './RecommendationRepository';
import {BookshelfRepository, ModelClass, ModelInstance} from '@tryghost/bookshelf-repository'; import {BookshelfRepository, ModelClass, ModelInstance} from '@tryghost/bookshelf-repository';
import logger from '@tryghost/logging'; import logger from '@tryghost/logging';
@ -7,7 +7,7 @@ type Sentry = {
captureException(err: unknown): void; captureException(err: unknown): void;
} }
export class BookshelfRecommendationRepository extends BookshelfRepository<string, Recommendation> implements RecommendationRepository { export class BookshelfRecommendationRepository extends BookshelfRepository<string, Recommendation> implements RecommendationRepository {
sentry?: Sentry; sentry?: Sentry;
constructor(Model: ModelClass<string>, deps: {sentry?: Sentry} = {}) { constructor(Model: ModelClass<string>, deps: {sentry?: Sentry} = {}) {
@ -26,8 +26,8 @@ export class BookshelfRecommendationRepository extends BookshelfRepository<strin
url: entity.url.toString(), url: entity.url.toString(),
one_click_subscribe: entity.oneClickSubscribe, one_click_subscribe: entity.oneClickSubscribe,
created_at: entity.createdAt, created_at: entity.createdAt,
updated_at: entity.updatedAt, updated_at: entity.updatedAt
} };
} }
modelToEntity(model: ModelInstance<string>): Recommendation | null { modelToEntity(model: ModelInstance<string>): Recommendation | null {
@ -42,8 +42,8 @@ export class BookshelfRecommendationRepository extends BookshelfRepository<strin
url: new URL(model.get('url') as string), url: new URL(model.get('url') as string),
oneClickSubscribe: model.get('one_click_subscribe') as boolean, oneClickSubscribe: model.get('one_click_subscribe') as boolean,
createdAt: model.get('created_at') as Date, createdAt: model.get('created_at') as Date,
updatedAt: model.get('updated_at') as Date | null, updatedAt: model.get('updated_at') as Date | null
}) });
} catch (err) { } catch (err) {
logger.error(err); logger.error(err);
this.sentry?.captureException(err); this.sentry?.captureException(err);
@ -52,7 +52,7 @@ export class BookshelfRecommendationRepository extends BookshelfRepository<strin
} }
entityFieldToColumn(field: keyof Recommendation): string { entityFieldToColumn(field: keyof Recommendation): string {
const mapping = { const mapping = {
id: 'id', id: 'id',
title: 'title', title: 'title',
reason: 'reason', reason: 'reason',
@ -62,7 +62,7 @@ export class BookshelfRecommendationRepository extends BookshelfRepository<strin
url: 'url', url: 'url',
oneClickSubscribe: 'one_click_subscribe', oneClickSubscribe: 'one_click_subscribe',
createdAt: 'created_at', createdAt: 'created_at',
updatedAt: 'updated_at', updatedAt: 'updated_at'
} as Record<keyof Recommendation, string>; } as Record<keyof Recommendation, string>;
return mapping[field]; return mapping[field];
} }

View File

@ -1,8 +1,8 @@
import {Recommendation} from "./Recommendation"; import {Recommendation} from './Recommendation';
import {RecommendationRepository} from "./RecommendationRepository"; import {RecommendationRepository} from './RecommendationRepository';
import {InMemoryRepository} from '@tryghost/in-memory-repository'; import {InMemoryRepository} from '@tryghost/in-memory-repository';
export class InMemoryRecommendationRepository extends InMemoryRepository<string, Recommendation> implements RecommendationRepository { export class InMemoryRecommendationRepository extends InMemoryRepository<string, Recommendation> implements RecommendationRepository {
toPrimitive(entity: Recommendation): object { toPrimitive(entity: Recommendation): object {
return entity; return entity;
} }

View File

@ -1,5 +1,5 @@
import ObjectId from "bson-objectid"; import ObjectId from 'bson-objectid';
import errors from "@tryghost/errors"; import errors from '@tryghost/errors';
export type AddRecommendation = { export type AddRecommendation = {
title: string title: string
@ -16,16 +16,16 @@ type RecommendationConstructorData = AddRecommendation & {id: string, createdAt:
export type RecommendationCreateData = AddRecommendation & {id?: string, createdAt?: Date, updatedAt?: Date|null} export type RecommendationCreateData = AddRecommendation & {id?: string, createdAt?: Date, updatedAt?: Date|null}
export class Recommendation { export class Recommendation {
id: string id: string;
title: string title: string;
reason: string|null reason: string|null;
excerpt: string|null // Fetched from the site meta data excerpt: string|null; // Fetched from the site meta data
featuredImage: URL|null // Fetched from the site meta data featuredImage: URL|null; // Fetched from the site meta data
favicon: URL|null // Fetched from the site meta data favicon: URL|null; // Fetched from the site meta data
url: URL url: URL;
oneClickSubscribe: boolean oneClickSubscribe: boolean;
createdAt: Date createdAt: Date;
updatedAt: Date|null updatedAt: Date|null;
#deleted: boolean; #deleted: boolean;
@ -50,14 +50,14 @@ export class Recommendation {
static validate(properties: AddRecommendation) { static validate(properties: AddRecommendation) {
if (properties.url.protocol !== 'http:' && properties.url.protocol !== 'https:') { if (properties.url.protocol !== 'http:' && properties.url.protocol !== 'https:') {
throw new errors.ValidationError({ throw new errors.ValidationError({
message: 'url must be a valid URL', message: 'url must be a valid URL'
}); });
} }
if (properties.featuredImage !== null) { if (properties.featuredImage !== null) {
if (properties.featuredImage.protocol !== 'http:' && properties.featuredImage.protocol !== 'https:') { if (properties.featuredImage.protocol !== 'http:' && properties.featuredImage.protocol !== 'https:') {
throw new errors.ValidationError({ throw new errors.ValidationError({
message: 'Featured image must be a valid URL', message: 'Featured image must be a valid URL'
}); });
} }
} }
@ -65,32 +65,32 @@ export class Recommendation {
if (properties.favicon !== null) { if (properties.favicon !== null) {
if (properties.favicon.protocol !== 'http:' && properties.favicon.protocol !== 'https:') { if (properties.favicon.protocol !== 'http:' && properties.favicon.protocol !== 'https:') {
throw new errors.ValidationError({ throw new errors.ValidationError({
message: 'Favicon must be a valid URL', message: 'Favicon must be a valid URL'
}); });
} }
} }
if (properties.title.length === 0) { if (properties.title.length === 0) {
throw new errors.ValidationError({ throw new errors.ValidationError({
message: 'Title must not be empty', message: 'Title must not be empty'
}); });
} }
if (properties.title.length > 2000) { if (properties.title.length > 2000) {
throw new errors.ValidationError({ throw new errors.ValidationError({
message: 'Title must be less than 2000 characters', message: 'Title must be less than 2000 characters'
}); });
} }
if (properties.reason && properties.reason.length > 2000) { if (properties.reason && properties.reason.length > 2000) {
throw new errors.ValidationError({ throw new errors.ValidationError({
message: 'Reason must be less than 2000 characters', message: 'Reason must be less than 2000 characters'
}); });
} }
if (properties.excerpt && properties.excerpt.length > 2000) { if (properties.excerpt && properties.excerpt.length > 2000) {
throw new errors.ValidationError({ throw new errors.ValidationError({
message: 'Excerpt must be less than 2000 characters', message: 'Excerpt must be less than 2000 characters'
}); });
} }
} }
@ -117,7 +117,7 @@ export class Recommendation {
url: data.url, url: data.url,
oneClickSubscribe: data.oneClickSubscribe, oneClickSubscribe: data.oneClickSubscribe,
createdAt: data.createdAt ?? new Date(), createdAt: data.createdAt ?? new Date(),
updatedAt: data.updatedAt ?? null, updatedAt: data.updatedAt ?? null
}; };
this.validate(d); this.validate(d);

View File

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import {AddRecommendation, EditRecommendation, Recommendation} from "./Recommendation"; import {AddRecommendation, EditRecommendation, Recommendation} from './Recommendation';
import {RecommendationService} from "./RecommendationService"; import {RecommendationService} from './RecommendationService';
import errors from '@tryghost/errors'; import errors from '@tryghost/errors';
type Frame = { type Frame = {
@ -19,7 +19,7 @@ function validateString(object: any, key: string, {required = true, nullable = f
} }
if (object[key] !== undefined && object[key] !== null) { if (object[key] !== undefined && object[key] !== null) {
if (typeof object[key] !== "string") { if (typeof object[key] !== 'string') {
throw new errors.BadRequestError({message: `${key} must be a string`}); throw new errors.BadRequestError({message: `${key} must be a string`});
} }
return object[key]; return object[key];
@ -33,7 +33,7 @@ function validateBoolean(object: any, key: string, {required = true} = {}): bool
throw new errors.BadRequestError({message: `${key} must be an object`}); throw new errors.BadRequestError({message: `${key} must be an object`});
} }
if (object[key] !== undefined) { if (object[key] !== undefined) {
if (typeof object[key] !== "boolean") { if (typeof object[key] !== 'boolean') {
throw new errors.BadRequestError({message: `${key} must be a boolean`}); throw new errors.BadRequestError({message: `${key} must be a boolean`});
} }
return object[key]; return object[key];
@ -66,7 +66,7 @@ function validateInteger(object: any, key: string, {required = true, nullable =
} }
if (object[key] !== undefined && object[key] !== null) { if (object[key] !== undefined && object[key] !== null) {
if (typeof object[key] === "string") { if (typeof object[key] === 'string') {
// Try to cast to a number // Try to cast to a number
const parsed = parseInt(object[key]); const parsed = parseInt(object[key]);
if (isNaN(parsed) || !isFinite(parsed)) { if (isNaN(parsed) || !isFinite(parsed)) {
@ -75,7 +75,7 @@ function validateInteger(object: any, key: string, {required = true, nullable =
return parsed; return parsed;
} }
if (typeof object[key] !== "number") { if (typeof object[key] !== 'number') {
throw new errors.BadRequestError({message: `${key} must be a number`}); throw new errors.BadRequestError({message: `${key} must be a number`});
} }
return object[key]; return object[key];
@ -84,7 +84,6 @@ function validateInteger(object: any, key: string, {required = true, nullable =
} }
} }
export class RecommendationController { export class RecommendationController {
service: RecommendationService; service: RecommendationService;
@ -106,23 +105,22 @@ export class RecommendationController {
} }
#getFramePage(frame: Frame): number { #getFramePage(frame: Frame): number {
const page = validateInteger(frame.options, "page", {required: false, nullable: true}) ?? 1; const page = validateInteger(frame.options, 'page', {required: false, nullable: true}) ?? 1;
if (page < 1) { if (page < 1) {
throw new errors.BadRequestError({message: "page must be greater or equal to 1"}); throw new errors.BadRequestError({message: 'page must be greater or equal to 1'});
} }
return page; return page;
} }
#getFrameLimit(frame: Frame, defaultLimit = 15): number { #getFrameLimit(frame: Frame, defaultLimit = 15): number {
const limit = validateInteger(frame.options, "limit", {required: false, nullable: true}) ?? defaultLimit; const limit = validateInteger(frame.options, 'limit', {required: false, nullable: true}) ?? defaultLimit;
if (limit < 1) { if (limit < 1) {
throw new errors.BadRequestError({message: "limit must be greater or equal to 1"}); throw new errors.BadRequestError({message: 'limit must be greater or equal to 1'});
} }
return limit; return limit;
} }
#getFrameRecommendation(frame: Frame): AddRecommendation { #getFrameRecommendation(frame: Frame): AddRecommendation {
if (!frame.data || !frame.data.recommendations || !frame.data.recommendations[0]) { if (!frame.data || !frame.data.recommendations || !frame.data.recommendations[0]) {
throw new errors.BadRequestError(); throw new errors.BadRequestError();
@ -131,15 +129,15 @@ export class RecommendationController {
const recommendation = frame.data.recommendations[0]; const recommendation = frame.data.recommendations[0];
const cleanedRecommendation: AddRecommendation = { const cleanedRecommendation: AddRecommendation = {
title: validateString(recommendation, "title") ?? '', title: validateString(recommendation, 'title') ?? '',
url: validateURL(recommendation, "url")!, url: validateURL(recommendation, 'url')!,
// Optional fields // Optional fields
oneClickSubscribe: validateBoolean(recommendation, "one_click_subscribe", {required: false}) ?? false, oneClickSubscribe: validateBoolean(recommendation, 'one_click_subscribe', {required: false}) ?? false,
reason: validateString(recommendation, "reason", {required: false, nullable: true}) ?? null, reason: validateString(recommendation, 'reason', {required: false, nullable: true}) ?? null,
excerpt: validateString(recommendation, "excerpt", {required: false, nullable: true}) ?? null, excerpt: validateString(recommendation, 'excerpt', {required: false, nullable: true}) ?? null,
featuredImage: validateURL(recommendation, "featured_image", {required: false, nullable: true}) ?? null, featuredImage: validateURL(recommendation, 'featured_image', {required: false, nullable: true}) ?? null,
favicon: validateURL(recommendation, "favicon", {required: false, nullable: true}) ?? null, favicon: validateURL(recommendation, 'favicon', {required: false, nullable: true}) ?? null
}; };
// Create a new recommendation // Create a new recommendation
@ -153,23 +151,22 @@ export class RecommendationController {
const recommendation = frame.data.recommendations[0]; const recommendation = frame.data.recommendations[0];
const cleanedRecommendation: EditRecommendation = { const cleanedRecommendation: EditRecommendation = {
title: validateString(recommendation, "title", {required: false}) ?? undefined, title: validateString(recommendation, 'title', {required: false}) ?? undefined,
url: validateURL(recommendation, "url", {required: false}) ?? undefined, url: validateURL(recommendation, 'url', {required: false}) ?? undefined,
oneClickSubscribe: validateBoolean(recommendation, "one_click_subscribe", {required: false}), oneClickSubscribe: validateBoolean(recommendation, 'one_click_subscribe', {required: false}),
reason: validateString(recommendation, "reason", {required: false, nullable: true}), reason: validateString(recommendation, 'reason', {required: false, nullable: true}),
excerpt: validateString(recommendation, "excerpt", {required: false, nullable: true}), excerpt: validateString(recommendation, 'excerpt', {required: false, nullable: true}),
featuredImage: validateURL(recommendation, "featured_image", {required: false, nullable: true}), featuredImage: validateURL(recommendation, 'featured_image', {required: false, nullable: true}),
favicon: validateURL(recommendation, "favicon", {required: false, nullable: true}), favicon: validateURL(recommendation, 'favicon', {required: false, nullable: true})
}; };
// Create a new recommendation // Create a new recommendation
return cleanedRecommendation; return cleanedRecommendation;
} }
#returnRecommendations(recommendations: Recommendation[], meta?: any) { #returnRecommendations(recommendations: Recommendation[], meta?: any) {
return { return {
data: recommendations.map(r => { data: recommendations.map((r) => {
return { return {
id: r.id, id: r.id,
title: r.title, title: r.title,
@ -184,7 +181,7 @@ export class RecommendationController {
}; };
}), }),
meta meta
} };
} }
#buildPagination({page, limit, count}: {page: number, limit: number, count: number}) { #buildPagination({page, limit, count}: {page: number, limit: number, count: number}) {
@ -197,7 +194,7 @@ export class RecommendationController {
pages, pages,
prev: page > 1 ? page - 1 : null, prev: page > 1 ? page - 1 : null,
next: page < pages ? page + 1 : null next: page < pages ? page + 1 : null
} };
} }
async addRecommendation(frame: Frame) { async addRecommendation(frame: Frame) {
@ -226,8 +223,8 @@ export class RecommendationController {
const limit = this.#getFrameLimit(frame, 5); const limit = this.#getFrameLimit(frame, 5);
const order = [ const order = [
{ {
field: "createdAt" as const, field: 'createdAt' as const,
direction: "desc" as const direction: 'desc' as const
} }
]; ];

View File

@ -1,17 +1,17 @@
import {OrderOption} from "@tryghost/bookshelf-repository"; import {OrderOption} from '@tryghost/bookshelf-repository';
import {Recommendation} from "./Recommendation"; import {Recommendation} from './Recommendation';
export interface RecommendationRepository { export interface RecommendationRepository {
save(entity: Recommendation): Promise<void>; save(entity: Recommendation): Promise<void>;
getById(id: string): Promise<Recommendation | null>; getById(id: string): Promise<Recommendation | null>;
getAll({filter, order}?: {filter?: string, order?: OrderOption<Recommendation>}): Promise<Recommendation[]>; getAll({filter, order}?: {filter?: string, order?: OrderOption<Recommendation>}): Promise<Recommendation[]>;
getPage({ filter, order, page, limit }: { getPage({filter, order, page, limit}: {
filter?: string; filter?: string;
order?: OrderOption<Recommendation>; order?: OrderOption<Recommendation>;
page: number; page: number;
limit: number; limit: number;
}): Promise<Recommendation[]>; }): Promise<Recommendation[]>;
getCount({ filter }?: { getCount({filter}?: {
filter?: string; filter?: string;
}): Promise<number>; }): Promise<number>;
}; };

View File

@ -1,9 +1,9 @@
import {OrderOption} from "@tryghost/bookshelf-repository"; import {OrderOption} from '@tryghost/bookshelf-repository';
import {AddRecommendation, Recommendation} from "./Recommendation"; import {AddRecommendation, Recommendation} from './Recommendation';
import {RecommendationRepository} from "./RecommendationRepository"; import {RecommendationRepository} from './RecommendationRepository';
import {WellknownService} from "./WellknownService"; import {WellknownService} from './WellknownService';
import errors from "@tryghost/errors"; import errors from '@tryghost/errors';
import tpl from "@tryghost/tpl"; import tpl from '@tryghost/tpl';
type MentionSendingService = { type MentionSendingService = {
sendAll(options: {url: URL, links: URL[]}): Promise<void> sendAll(options: {url: URL, links: URL[]}): Promise<void>
@ -15,8 +15,8 @@ type RecommendationEnablerService = {
} }
const messages = { const messages = {
notFound: "Recommendation with id {id} not found" notFound: 'Recommendation with id {id} not found'
} };
export class RecommendationService { export class RecommendationService {
repository: RecommendationRepository; repository: RecommendationRepository;
@ -57,12 +57,12 @@ export class RecommendationService {
} }
private sendMentionToRecommendation(recommendation: Recommendation) { private sendMentionToRecommendation(recommendation: Recommendation) {
this.mentionSendingService.sendAll({ this.mentionSendingService.sendAll({
url: this.wellknownService.getURL(), url: this.wellknownService.getURL(),
links: [ links: [
recommendation.url recommendation.url
] ]
}).catch(console.error); }).catch(console.error); // eslint-disable-line no-console
} }
async addRecommendation(addRecommendation: AddRecommendation) { async addRecommendation(addRecommendation: AddRecommendation) {
@ -118,12 +118,12 @@ export class RecommendationService {
async listRecommendations({page, limit, filter, order}: { page: number; limit: number | 'all', filter?: string, order?: OrderOption<Recommendation> } = {page: 1, limit: 'all'}) { async listRecommendations({page, limit, filter, order}: { page: number; limit: number | 'all', filter?: string, order?: OrderOption<Recommendation> } = {page: 1, limit: 'all'}) {
if (limit === 'all') { if (limit === 'all') {
return await this.repository.getAll({filter, order}) return await this.repository.getAll({filter, order});
} }
return await this.repository.getPage({page, limit, filter, order}) return await this.repository.getPage({page, limit, filter, order});
} }
async countRecommendations({filter}: { filter?: string }) { async countRecommendations({filter}: { filter?: string }) {
return await this.repository.getCount({filter}) return await this.repository.getCount({filter});
} }
} }

View File

@ -1,5 +1,5 @@
import {Recommendation} from "./Recommendation" import {Recommendation} from './Recommendation';
import fs from "fs/promises"; import fs from 'fs/promises';
import _path from 'path'; import _path from 'path';
type UrlUtils = { type UrlUtils = {
@ -14,8 +14,8 @@ type Options = {
} }
export class WellknownService { export class WellknownService {
dir: string dir: string;
urlUtils: UrlUtils urlUtils: UrlUtils;
constructor({dir, urlUtils}: Options) { constructor({dir, urlUtils}: Options) {
this.dir = dir; this.dir = dir;
@ -27,8 +27,8 @@ export class WellknownService {
url: recommendation.url, url: recommendation.url,
reason: recommendation.reason, reason: recommendation.reason,
updated_at: (recommendation.updatedAt ?? recommendation.createdAt).toISOString(), updated_at: (recommendation.updatedAt ?? recommendation.createdAt).toISOString(),
created_at: (recommendation.createdAt).toISOString(), created_at: (recommendation.createdAt).toISOString()
} };
} }
getPath() { getPath() {

View File

@ -106,7 +106,7 @@
"devDependencies": { "devDependencies": {
"concurrently": "8.2.1", "concurrently": "8.2.1",
"eslint": "8.44.0", "eslint": "8.44.0",
"eslint-plugin-ghost": "3.3.0", "eslint-plugin-ghost": "3.3.2",
"eslint-plugin-react": "7.33.0", "eslint-plugin-react": "7.33.0",
"husky": "8.0.3", "husky": "8.0.3",
"lint-staged": "14.0.1", "lint-staged": "14.0.1",

145
yarn.lock
View File

@ -8854,90 +8854,89 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@typescript-eslint/eslint-plugin@6.1.0": "@typescript-eslint/eslint-plugin@6.6.0":
version "6.1.0" version "6.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.1.0.tgz#96f3ca6615717659d06c9f7161a1d14ab0c49c66" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.6.0.tgz#19ba09aa34fd504696445100262e5a9e1b1d7024"
integrity sha512-qg7Bm5TyP/I7iilGyp6DRqqkt8na00lI6HbjWZObgk3FFSzH5ypRwAHXJhJkwiRtTcfn+xYQIMOR5kJgpo6upw== integrity sha512-CW9YDGTQnNYMIo5lMeuiIG08p4E0cXrXTbcZ2saT/ETE7dWUrNxlijsQeU04qAAKkILiLzdQz+cGFxCJjaZUmA==
dependencies: dependencies:
"@eslint-community/regexpp" "^4.5.1" "@eslint-community/regexpp" "^4.5.1"
"@typescript-eslint/scope-manager" "6.1.0" "@typescript-eslint/scope-manager" "6.6.0"
"@typescript-eslint/type-utils" "6.1.0" "@typescript-eslint/type-utils" "6.6.0"
"@typescript-eslint/utils" "6.1.0" "@typescript-eslint/utils" "6.6.0"
"@typescript-eslint/visitor-keys" "6.1.0" "@typescript-eslint/visitor-keys" "6.6.0"
debug "^4.3.4" debug "^4.3.4"
graphemer "^1.4.0" graphemer "^1.4.0"
ignore "^5.2.4" ignore "^5.2.4"
natural-compare "^1.4.0" natural-compare "^1.4.0"
natural-compare-lite "^1.4.0"
semver "^7.5.4" semver "^7.5.4"
ts-api-utils "^1.0.1" ts-api-utils "^1.0.1"
"@typescript-eslint/parser@6.1.0": "@typescript-eslint/parser@6.6.0":
version "6.1.0" version "6.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.1.0.tgz#3135bf65dca5340d8650703eb8cb83113e156ee5" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.6.0.tgz#fe323a7b4eafb6d5ea82b96216561810394a739e"
integrity sha512-hIzCPvX4vDs4qL07SYzyomamcs2/tQYXg5DtdAfj35AyJ5PIUqhsLf4YrEIFzZcND7R2E8tpQIZKayxg8/6Wbw== integrity sha512-setq5aJgUwtzGrhW177/i+DMLqBaJbdwGj2CPIVFFLE0NCliy5ujIdLHd2D1ysmlmsjdL2GWW+hR85neEfc12w==
dependencies: dependencies:
"@typescript-eslint/scope-manager" "6.1.0" "@typescript-eslint/scope-manager" "6.6.0"
"@typescript-eslint/types" "6.1.0" "@typescript-eslint/types" "6.6.0"
"@typescript-eslint/typescript-estree" "6.1.0" "@typescript-eslint/typescript-estree" "6.6.0"
"@typescript-eslint/visitor-keys" "6.1.0" "@typescript-eslint/visitor-keys" "6.6.0"
debug "^4.3.4" debug "^4.3.4"
"@typescript-eslint/scope-manager@6.1.0": "@typescript-eslint/scope-manager@6.6.0":
version "6.1.0" version "6.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.1.0.tgz#a6cdbe11630614f8c04867858a42dd56590796ed" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.6.0.tgz#57105d4419d6de971f7d2c30a2ff4ac40003f61a"
integrity sha512-AxjgxDn27hgPpe2rQe19k0tXw84YCOsjDJ2r61cIebq1t+AIxbgiXKvD4999Wk49GVaAcdJ/d49FYel+Pp3jjw== integrity sha512-pT08u5W/GT4KjPUmEtc2kSYvrH8x89cVzkA0Sy2aaOUIw6YxOIjA8ilwLr/1fLjOedX1QAuBpG9XggWqIIfERw==
dependencies: dependencies:
"@typescript-eslint/types" "6.1.0" "@typescript-eslint/types" "6.6.0"
"@typescript-eslint/visitor-keys" "6.1.0" "@typescript-eslint/visitor-keys" "6.6.0"
"@typescript-eslint/type-utils@6.1.0": "@typescript-eslint/type-utils@6.6.0":
version "6.1.0" version "6.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.1.0.tgz#21cc6c3bc1980b03f9eb4e64580d0c5be6f08215" resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.6.0.tgz#14f651d13b884915c4fca0d27adeb652a4499e86"
integrity sha512-kFXBx6QWS1ZZ5Ni89TyT1X9Ag6RXVIVhqDs0vZE/jUeWlBv/ixq2diua6G7ece6+fXw3TvNRxP77/5mOMusx2w== integrity sha512-8m16fwAcEnQc69IpeDyokNO+D5spo0w1jepWWY2Q6y5ZKNuj5EhVQXjtVAeDDqvW6Yg7dhclbsz6rTtOvcwpHg==
dependencies: dependencies:
"@typescript-eslint/typescript-estree" "6.1.0" "@typescript-eslint/typescript-estree" "6.6.0"
"@typescript-eslint/utils" "6.1.0" "@typescript-eslint/utils" "6.6.0"
debug "^4.3.4" debug "^4.3.4"
ts-api-utils "^1.0.1" ts-api-utils "^1.0.1"
"@typescript-eslint/types@6.1.0": "@typescript-eslint/types@6.6.0":
version "6.1.0" version "6.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.1.0.tgz#2d607c62827bb416ada5c96ebfa2ef84e45a8dfa" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.6.0.tgz#95e7ea650a2b28bc5af5ea8907114a48f54618c2"
integrity sha512-+Gfd5NHCpDoHDOaU/yIF3WWRI2PcBRKKpP91ZcVbL0t5tQpqYWBs3z/GGhvU+EV1D0262g9XCnyqQh19prU0JQ== integrity sha512-CB6QpJQ6BAHlJXdwUmiaXDBmTqIE2bzGTDLADgvqtHWuhfNP3rAOK7kAgRMAET5rDRr9Utt+qAzRBdu3AhR3sg==
"@typescript-eslint/typescript-estree@6.1.0": "@typescript-eslint/typescript-estree@6.6.0":
version "6.1.0" version "6.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.1.0.tgz#ea382f6482ba698d7e993a88ce5391ea7a66c33d" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.6.0.tgz#373c420d2e12c28220f4a83352280a04823a91b7"
integrity sha512-nUKAPWOaP/tQjU1IQw9sOPCDavs/iU5iYLiY/6u7gxS7oKQoi4aUxXS1nrrVGTyBBaGesjkcwwHkbkiD5eBvcg== integrity sha512-hMcTQ6Al8MP2E6JKBAaSxSVw5bDhdmbCEhGW/V8QXkb9oNsFkA4SBuOMYVPxD3jbtQ4R/vSODBsr76R6fP3tbA==
dependencies: dependencies:
"@typescript-eslint/types" "6.1.0" "@typescript-eslint/types" "6.6.0"
"@typescript-eslint/visitor-keys" "6.1.0" "@typescript-eslint/visitor-keys" "6.6.0"
debug "^4.3.4" debug "^4.3.4"
globby "^11.1.0" globby "^11.1.0"
is-glob "^4.0.3" is-glob "^4.0.3"
semver "^7.5.4" semver "^7.5.4"
ts-api-utils "^1.0.1" ts-api-utils "^1.0.1"
"@typescript-eslint/utils@6.1.0": "@typescript-eslint/utils@6.6.0":
version "6.1.0" version "6.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.1.0.tgz#1641843792b4e3451cc692e2c73055df8b26f453" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.6.0.tgz#2d686c0f0786da6362d909e27a9de1c13ba2e7dc"
integrity sha512-wp652EogZlKmQoMS5hAvWqRKplXvkuOnNzZSE0PVvsKjpexd/XznRVHAtrfHFYmqaJz0DFkjlDsGYC9OXw+OhQ== integrity sha512-mPHFoNa2bPIWWglWYdR0QfY9GN0CfvvXX1Sv6DlSTive3jlMTUy+an67//Gysc+0Me9pjitrq0LJp0nGtLgftw==
dependencies: dependencies:
"@eslint-community/eslint-utils" "^4.4.0" "@eslint-community/eslint-utils" "^4.4.0"
"@types/json-schema" "^7.0.12" "@types/json-schema" "^7.0.12"
"@types/semver" "^7.5.0" "@types/semver" "^7.5.0"
"@typescript-eslint/scope-manager" "6.1.0" "@typescript-eslint/scope-manager" "6.6.0"
"@typescript-eslint/types" "6.1.0" "@typescript-eslint/types" "6.6.0"
"@typescript-eslint/typescript-estree" "6.1.0" "@typescript-eslint/typescript-estree" "6.6.0"
semver "^7.5.4" semver "^7.5.4"
"@typescript-eslint/visitor-keys@6.1.0": "@typescript-eslint/visitor-keys@6.6.0":
version "6.1.0" version "6.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.1.0.tgz#d2b84dff6b58944d3257ea03687e269a788c73be" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.6.0.tgz#1109088b4346c8b2446f3845db526374d9a3bafc"
integrity sha512-yQeh+EXhquh119Eis4k0kYhj9vmFzNpbhM3LftWQVwqVjipCkwHBQOZutcYW+JVkjtTG9k8nrZU1UoNedPDd1A== integrity sha512-L61uJT26cMOfFQ+lMZKoJNbAEckLe539VhTxiGHrWl5XSKQgA0RTBZJW2HFPy5T0ZvPVSD93QsrTKDkfNwJGyQ==
dependencies: dependencies:
"@typescript-eslint/types" "6.1.0" "@typescript-eslint/types" "6.6.0"
eslint-visitor-keys "^3.4.1" eslint-visitor-keys "^3.4.1"
"@tyriar/fibonacci-heap@^2.0.7": "@tyriar/fibonacci-heap@^2.0.7":
@ -16773,10 +16772,10 @@ eslint-plugin-babel@5.3.1:
dependencies: dependencies:
eslint-rule-composer "^0.3.0" eslint-rule-composer "^0.3.0"
eslint-plugin-ember@11.10.0: eslint-plugin-ember@11.11.1:
version "11.10.0" version "11.11.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-ember/-/eslint-plugin-ember-11.10.0.tgz#46a696ebabcfefcf8212eb0eb2b11d61360c70fc" resolved "https://registry.yarnpkg.com/eslint-plugin-ember/-/eslint-plugin-ember-11.11.1.tgz#bc49d76ed8ec43e646d222f15181f4d18c3c3218"
integrity sha512-/5VanfpfzIdmWgXWyQ6ylAJWITu8mXivRce06h0uoifVpUoGaBdAkwuto/PLGfDxWdi43xWUFLb5Tpkhx2MoFg== integrity sha512-dvsDa4LkDkGqCE2bzBIguRMi1g40JVwRWMSHmn8S7toRDxSOU3M7yromgi5eSAJX2O2vEvJZ9QnR15YDbvNfVQ==
dependencies: dependencies:
"@ember-data/rfc395-data" "^0.0.4" "@ember-data/rfc395-data" "^0.0.4"
"@glimmer/syntax" "^0.84.2" "@glimmer/syntax" "^0.84.2"
@ -16809,21 +16808,21 @@ eslint-plugin-filenames@allouis/eslint-plugin-filenames#15dc354f4e3d155fc2d6ae08
lodash.snakecase "4.1.1" lodash.snakecase "4.1.1"
lodash.upperfirst "4.3.1" lodash.upperfirst "4.3.1"
eslint-plugin-ghost@3.3.0: eslint-plugin-ghost@3.3.2:
version "3.3.0" version "3.3.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-ghost/-/eslint-plugin-ghost-3.3.0.tgz#576fcbb308e94b5e601ce8806ec63d354c5c114d" resolved "https://registry.yarnpkg.com/eslint-plugin-ghost/-/eslint-plugin-ghost-3.3.2.tgz#b789c20e645d285743bc8c852f92b9898474eb98"
integrity sha512-VpL9FVI3kvY5BqM06qer67oDboDO/iHxl2mWnOZY1SwUtdcWuSfb81OQvmggilUc3UhjWahLuCHM9QzVtcOObA== integrity sha512-Ae3u4lTo2ApB8wBdaEShJVEuqNSYG1IiarAFvee8bSx8Ykwj0vRxpyy9MRgwaFc6Lre7dcaIJ60iWVBoO4MwwA==
dependencies: dependencies:
"@kapouer/eslint-plugin-no-return-in-loop" "1.0.0" "@kapouer/eslint-plugin-no-return-in-loop" "1.0.0"
"@typescript-eslint/eslint-plugin" "6.1.0" "@typescript-eslint/eslint-plugin" "6.6.0"
"@typescript-eslint/parser" "6.1.0" "@typescript-eslint/parser" "6.6.0"
eslint-plugin-ember "11.10.0" eslint-plugin-ember "11.11.1"
eslint-plugin-filenames allouis/eslint-plugin-filenames#15dc354f4e3d155fc2d6ae082dbfc26377539a18 eslint-plugin-filenames allouis/eslint-plugin-filenames#15dc354f4e3d155fc2d6ae082dbfc26377539a18
eslint-plugin-mocha "7.0.1" eslint-plugin-mocha "7.0.1"
eslint-plugin-n "16.0.1" eslint-plugin-n "16.0.2"
eslint-plugin-sort-imports-es6-autofix "0.6.0" eslint-plugin-sort-imports-es6-autofix "0.6.0"
eslint-plugin-unicorn "42.0.0" eslint-plugin-unicorn "42.0.0"
typescript "5.1.6" typescript "5.2.2"
eslint-plugin-i18next@6.0.3: eslint-plugin-i18next@6.0.3:
version "6.0.3" version "6.0.3"
@ -16841,10 +16840,10 @@ eslint-plugin-mocha@7.0.1:
eslint-utils "^2.0.0" eslint-utils "^2.0.0"
ramda "^0.27.0" ramda "^0.27.0"
eslint-plugin-n@16.0.1: eslint-plugin-n@16.0.2:
version "16.0.1" version "16.0.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-n/-/eslint-plugin-n-16.0.1.tgz#baa62bb3af52940a53ba15386348ad9b0b425ada" resolved "https://registry.yarnpkg.com/eslint-plugin-n/-/eslint-plugin-n-16.0.2.tgz#5b2c0ad8dd9b724244d30fad2cc49ff4308a2152"
integrity sha512-CDmHegJN0OF3L5cz5tATH84RPQm9kG+Yx39wIqIwPR2C0uhBGMWfbbOtetR83PQjjidA5aXMu+LEFw1jaSwvTA== integrity sha512-Y66uDfUNbBzypsr0kELWrIz+5skicECrLUqlWuXawNSLUq3ltGlCwu6phboYYOTSnoTdHgTLrc+5Ydo6KjzZog==
dependencies: dependencies:
"@eslint-community/eslint-utils" "^4.4.0" "@eslint-community/eslint-utils" "^4.4.0"
builtins "^5.0.1" builtins "^5.0.1"
@ -23801,11 +23800,6 @@ napi-build-utils@^1.0.1:
resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806"
integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==
natural-compare-lite@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4"
integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==
natural-compare@^1.4.0: natural-compare@^1.4.0:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@ -30512,11 +30506,6 @@ typescript-memoize@^1.0.0-alpha.3, typescript-memoize@^1.0.1:
resolved "https://registry.yarnpkg.com/typescript-memoize/-/typescript-memoize-1.1.1.tgz#02737495d5df6ebf72c07ba0d002e8f4cf5ccfa0" resolved "https://registry.yarnpkg.com/typescript-memoize/-/typescript-memoize-1.1.1.tgz#02737495d5df6ebf72c07ba0d002e8f4cf5ccfa0"
integrity sha512-GQ90TcKpIH4XxYTI2F98yEQYZgjNMOGPpOgdjIBhaLaWji5HPWlRnZ4AeA1hfBxtY7bCGDJsqDDHk/KaHOl5bA== integrity sha512-GQ90TcKpIH4XxYTI2F98yEQYZgjNMOGPpOgdjIBhaLaWji5HPWlRnZ4AeA1hfBxtY7bCGDJsqDDHk/KaHOl5bA==
typescript@5.1.6:
version "5.1.6"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274"
integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==
typescript@5.2.2, typescript@^5.0.4: typescript@5.2.2, typescript@^5.0.4:
version "5.2.2" version "5.2.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78"