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 ContentBox from './components/ContentBox';
import PopupBox from './components/PopupBox';
@ -32,7 +34,7 @@ const App: React.FC<AppProps> = ({scriptTag}) => {
siteUrl: options.siteUrl,
apiUrl: options.apiUrl!,
apiKey: options.apiKey!
})
});
}, [options]);
const [adminApi, setAdminApi] = useState<AdminApi|null>(null);
@ -45,7 +47,7 @@ const App: React.FC<AppProps> = ({scriptTag}) => {
return {
...state,
...newState
}
};
});
}, [setFullState]);
@ -55,7 +57,7 @@ const App: React.FC<AppProps> = ({scriptTag}) => {
// 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
setState((state) => {
return SyncActionHandler({action, data, state, api, adminApi: adminApi!, options})
return SyncActionHandler({action, data, state, api, adminApi: adminApi!, options});
});
return;
}
@ -66,7 +68,7 @@ const App: React.FC<AppProps> = ({scriptTag}) => {
setState((state) => {
ActionHandler({action, data, state, api, adminApi: adminApi!, options}).then((updatedState) => {
setState({...updatedState});
}).catch(console.error);
}).catch(console.error); // eslint-disable-line no-console
// No immediate changes
return {};
@ -125,7 +127,7 @@ const App: React.FC<AppProps> = ({scriptTag}) => {
pagination: data.meta.pagination,
count: count
};
}
};
/** Initialize comments setup on load, fetch data and setup state*/
const initSetup = async () => {

View File

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

View File

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

View File

@ -18,7 +18,7 @@ const Pagination = () => {
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 (
<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 {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 (
<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 = diff * 7 / 30
diff = diff * 7 / 30;
if (diff < 12) {
if (Math.floor(diff) === 1) {
// Special case, we should compare based on dates in the future instead

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});
if (existing) {
existing.set(this.toPrimitive(entity))
existing.set(this.toPrimitive(entity));
await existing.save({}, {autoRefresh: false, method: 'update'});
} 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),
limit,
page
})
});
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) {
const allowedProperties = ['featured', 'published_at', 'tag', 'tags']
const allowedProperties = ['featured', 'published_at', 'tag', 'tags'];
if (type === 'manual') {
if (filter !== null) {
throw new ValidationError({

View File

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

View File

@ -1,13 +1,13 @@
import logging from '@tryghost/logging';
import tpl from '@tryghost/tpl';
import isEqual from 'lodash/isEqual';
import {Knex} from "knex";
import {Knex} from 'knex';
import {
PostsBulkUnpublishedEvent,
PostsBulkFeaturedEvent,
PostsBulkUnfeaturedEvent,
PostsBulkAddTagsEvent
} from "@tryghost/post-events";
} from '@tryghost/post-events';
import debugModule from '@tryghost/debug';
import {Collection} from './Collection';
import {CollectionRepository} from './CollectionRepository';
@ -194,7 +194,7 @@ export class CollectionsService {
});
this.DomainEvents.subscribe(PostEditedEvent, async (event: PostEditedEvent) => {
if(this.hasPostEditRelevantChanges(event.data) === false) {
if (this.hasPostEditRelevantChanges(event.data) === false) {
return;
}
@ -280,7 +280,7 @@ export class CollectionsService {
featured: postEditEvent.previous.featured,
published_at: postEditEvent.previous.published_at,
tags: postEditEvent.previous.tags
}
};
return !isEqual(current, previous);
}
@ -289,7 +289,7 @@ export class CollectionsService {
return await this.collectionsRepository.createTransaction(async (transaction) => {
const collections = await this.collectionsRepository.getAll({
transaction
})
});
for (const collection of collections) {
if (collection.type === 'automatic' && collection.filter) {
@ -423,7 +423,7 @@ export class CollectionsService {
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 {
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
collections = collections.filter((collection) => collection.filter?.includes('published_at'));
collections = collections.filter(collection => collection.filter?.includes('published_at'));
if (!collections.length) {
return;
@ -461,7 +461,7 @@ export class CollectionsService {
});
// 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) {
return;

View File

@ -20,12 +20,12 @@ type DonationEventModelInstance = BookshelfModelInstance & {
referrer_medium: string | null;
referrer_url: string | null;
}
type DonationPaymentEventModel = BookshelfModel<DonationEventModelInstance>;
type DonationPaymentEventBookshelfModel = BookshelfModel<DonationEventModelInstance>;
export class DonationBookshelfRepository implements DonationRepository {
#Model: DonationPaymentEventModel;
#Model: DonationPaymentEventBookshelfModel;
constructor({DonationPaymentEventModel}: {DonationPaymentEventModel: DonationPaymentEventModel}) {
constructor({DonationPaymentEventModel}: {DonationPaymentEventModel: DonationPaymentEventBookshelfModel}) {
this.#Model = DonationPaymentEventModel;
}
@ -42,7 +42,7 @@ export class DonationBookshelfRepository implements DonationRepository {
attribution_type: event.attributionType,
referrer_source: event.referrerSource,
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 = {
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
*/
enum EventType {
enum EventType { // eslint-disable-line no-shadow
CLICKED = 'clicked',
COMPLAINED = 'complained',
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 = {
ModelEvents: {

View File

@ -1,5 +1,5 @@
import {Recommendation} from "./Recommendation";
import {RecommendationRepository} from "./RecommendationRepository";
import {Recommendation} from './Recommendation';
import {RecommendationRepository} from './RecommendationRepository';
import {BookshelfRepository, ModelClass, ModelInstance} from '@tryghost/bookshelf-repository';
import logger from '@tryghost/logging';
@ -26,8 +26,8 @@ export class BookshelfRecommendationRepository extends BookshelfRepository<strin
url: entity.url.toString(),
one_click_subscribe: entity.oneClickSubscribe,
created_at: entity.createdAt,
updated_at: entity.updatedAt,
}
updated_at: entity.updatedAt
};
}
modelToEntity(model: ModelInstance<string>): Recommendation | null {
@ -42,8 +42,8 @@ export class BookshelfRecommendationRepository extends BookshelfRepository<strin
url: new URL(model.get('url') as string),
oneClickSubscribe: model.get('one_click_subscribe') as boolean,
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) {
logger.error(err);
this.sentry?.captureException(err);
@ -62,7 +62,7 @@ export class BookshelfRecommendationRepository extends BookshelfRepository<strin
url: 'url',
oneClickSubscribe: 'one_click_subscribe',
createdAt: 'created_at',
updatedAt: 'updated_at',
updatedAt: 'updated_at'
} as Record<keyof Recommendation, string>;
return mapping[field];
}

View File

@ -1,5 +1,5 @@
import {Recommendation} from "./Recommendation";
import {RecommendationRepository} from "./RecommendationRepository";
import {Recommendation} from './Recommendation';
import {RecommendationRepository} from './RecommendationRepository';
import {InMemoryRepository} from '@tryghost/in-memory-repository';
export class InMemoryRecommendationRepository extends InMemoryRepository<string, Recommendation> implements RecommendationRepository {

View File

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

View File

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

View File

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

View File

@ -1,9 +1,9 @@
import {OrderOption} from "@tryghost/bookshelf-repository";
import {AddRecommendation, Recommendation} from "./Recommendation";
import {RecommendationRepository} from "./RecommendationRepository";
import {WellknownService} from "./WellknownService";
import errors from "@tryghost/errors";
import tpl from "@tryghost/tpl";
import {OrderOption} from '@tryghost/bookshelf-repository';
import {AddRecommendation, Recommendation} from './Recommendation';
import {RecommendationRepository} from './RecommendationRepository';
import {WellknownService} from './WellknownService';
import errors from '@tryghost/errors';
import tpl from '@tryghost/tpl';
type MentionSendingService = {
sendAll(options: {url: URL, links: URL[]}): Promise<void>
@ -15,8 +15,8 @@ type RecommendationEnablerService = {
}
const messages = {
notFound: "Recommendation with id {id} not found"
}
notFound: 'Recommendation with id {id} not found'
};
export class RecommendationService {
repository: RecommendationRepository;
@ -62,7 +62,7 @@ export class RecommendationService {
links: [
recommendation.url
]
}).catch(console.error);
}).catch(console.error); // eslint-disable-line no-console
}
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'}) {
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 }) {
return await this.repository.getCount({filter})
return await this.repository.getCount({filter});
}
}

View File

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

View File

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

145
yarn.lock
View File

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