Compare commits

..

11 Commits

Author SHA1 Message Date
Faruk AYDIN
7787436fd4 fix: Add uncaught exceptions log 2022-05-12 23:17:05 +02:00
Faruk AYDIN
8c9bf9c7ef fix: Update bullmq version 2022-05-12 22:45:58 +02:00
Ömer Faruk Aydın
cc1892a5cf Merge pull request #330 from automatisch/feature/slack-new-message-to-channel-trigger
feat: Implement new message to a channel for slack w/o webhook
2022-05-11 13:59:26 +02:00
Faruk AYDIN
c59bb85793 feat: Implement new message to a channel for slack w/o webhook 2022-05-11 13:49:59 +02:00
Ömer Faruk Aydın
a88d208cd6 Merge pull request #329 from automatisch/show-only-associated-flows
feat: filter flows by appKey in app flows
2022-05-09 14:21:41 +02:00
Ali BARIN
91e209e961 feat: filter flows by appKey in app flows 2022-05-09 14:15:40 +02:00
Faruk AYDIN
7c543ff8ed feat: Return only related flows with given app 2022-05-08 17:14:29 +02:00
Ömer Faruk Aydın
9f56fd7c3b Merge pull request #327 from automatisch/new-issue-github-trigger
feat: add new issue trigger in GitHub
2022-05-08 00:11:38 +02:00
Ali BARIN
3a63fc376d feat: add new issue trigger in GitHub 2022-05-07 23:04:29 +02:00
Ömer Faruk Aydın
445ed9239d Merge pull request #326 from automatisch/rename-schedule-as-scheduler
chore: rename Schedule integration as Scheduler
2022-05-07 21:28:04 +02:00
Ali BARIN
9d5060f902 chore: rename Schedule integration as Scheduler 2022-05-07 21:25:15 +02:00
38 changed files with 399 additions and 238 deletions

View File

@@ -31,7 +31,7 @@
"ajv-formats": "^2.1.1",
"axios": "0.24.0",
"bcrypt": "^5.0.1",
"bullmq": "^1.76.1",
"bullmq": "^1.82.0",
"copyfiles": "^2.4.1",
"cors": "^2.8.5",
"crypto-js": "^4.1.1",

View File

@@ -1,13 +1,16 @@
import { IJSONObject } from '@automatisch/types';
import ListRepos from './data/list-repos';
import ListBranches from './data/list-branches';
import ListLabels from './data/list-labels';
export default class Data {
listRepos: ListRepos;
listBranches: ListBranches;
listLabels: ListLabels;
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
this.listRepos = new ListRepos(connectionData);
this.listBranches = new ListBranches(connectionData, parameters);
this.listLabels = new ListLabels(connectionData, parameters);
}
}

View File

@@ -0,0 +1,36 @@
import { Octokit } from 'octokit';
import type { IJSONObject } from '@automatisch/types';
import { assignOwnerAndRepo } from '../utils';
export default class ListLabels {
client?: Octokit;
repoOwner?: string;
repo?: string;
constructor(connectionData: IJSONObject, parameters?: IJSONObject) {
if (connectionData.accessToken) {
this.client = new Octokit({
auth: connectionData.accessToken as string,
});
}
assignOwnerAndRepo(this, parameters?.repo as string);
}
get options() {
return {
owner: this.repoOwner,
repo: this.repo,
};
}
async run() {
const labels = await this.client.paginate(this.client.rest.issues.listLabelsForRepo, this.options);
return labels.map((label) => ({
value: label.name,
name: label.name,
}));
}
}

View File

@@ -649,6 +649,98 @@
"name": "Test trigger"
}
]
},
{
"name": "New issue",
"key": "newIssue",
"description": "Triggers when a new issue is created",
"substeps": [
{
"key": "chooseAccount",
"name": "Choose account"
},
{
"key": "chooseTrigger",
"name": "Set up a trigger",
"arguments": [
{
"label": "Repo",
"key": "repo",
"type": "dropdown",
"required": false,
"variables": false,
"source": {
"type": "query",
"name": "getData",
"arguments": [
{
"name": "key",
"value": "listRepos"
}
]
}
},
{
"label": "Which types of issues should this trigger on?",
"key": "issueType",
"type": "dropdown",
"description": "Defaults to any issue you can see.",
"required": true,
"variables": false,
"value": "all",
"options": [
{
"label": "Any issue you can see",
"value": "all"
},
{
"label": "Only issues assigned to you",
"value": "assigned"
},
{
"label": "Only issues created by you",
"value": "created"
},
{
"label": "Only issues you're mentioned in",
"value": "mentioned"
},
{
"label": "Only issues you're subscribed to",
"value": "subscribed"
}
]
},
{
"label": "Label",
"key": "label",
"type": "dropdown",
"description": "Only trigger on issues when this label is added.",
"required": false,
"variables": false,
"dependsOn": ["parameters.repo"],
"source": {
"type": "query",
"name": "getData",
"arguments": [
{
"name": "key",
"value": "listLabels"
},
{
"name": "parameters.repo",
"value": "{parameters.repo}"
}
]
}
}
]
},
{
"key": "testStep",
"name": "Test trigger"
}
]
}
]
}

View File

@@ -11,6 +11,7 @@ import NewCommitComment from './triggers/new-commit-comment';
import NewLabel from './triggers/new-label';
import NewCollaborator from './triggers/new-collaborator';
import NewRelease from './triggers/new-release';
import NewIssue from './triggers/new-issue';
export default class Triggers {
newRepository: NewRepository;
@@ -25,6 +26,7 @@ export default class Triggers {
newLabel: NewLabel;
newCollaborator: NewCollaborator;
newRelease: NewRelease;
newIssue: NewIssue;
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
this.newRepository = new NewRepository(connectionData);
@@ -39,5 +41,6 @@ export default class Triggers {
this.newLabel = new NewLabel(connectionData, parameters);
this.newCollaborator = new NewCollaborator(connectionData, parameters);
this.newRelease = new NewRelease(connectionData, parameters);
this.newIssue = new NewIssue(connectionData, parameters);
}
}

View File

@@ -0,0 +1,88 @@
import { Octokit } from 'octokit';
import { DateTime } from 'luxon';
import { IJSONObject } from '@automatisch/types';
import { assignOwnerAndRepo } from '../utils';
export default class NewIssue {
client?: Octokit;
connectionData?: IJSONObject;
repoOwner?: string;
repo?: string;
hasRepo?: boolean;
label?: string;
issueType?: string;
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
if (connectionData.accessToken) {
this.client = new Octokit({
auth: connectionData.accessToken as string,
});
}
assignOwnerAndRepo(this, parameters?.repo as string);
}
get options() {
return {
labels: this.label,
}
}
async listRepoIssues(options = {}, paginate = false) {
const listRepoIssues = this.client.rest.issues.listForRepo;
const extendedOptions = {
...this.options,
repo: this.repo,
owner: this.repoOwner,
filter: this.issueType,
...options,
};
if (paginate) {
return await this.client.paginate(listRepoIssues, extendedOptions);
}
return (await listRepoIssues(extendedOptions)).data;
}
async listIssues(options = {}, paginate = false) {
const listIssues = this.client.rest.issues.listForAuthenticatedUser;
const extendedOptions = {
...this.options,
...options,
};
if (paginate) {
return await this.client.paginate(listIssues, extendedOptions);
}
return (await listIssues(extendedOptions)).data;
}
async run(startTime: Date) {
const options = {
since: DateTime.fromJSDate(startTime).toISO(),
};
if (this.hasRepo) {
return await this.listRepoIssues(options, true);
}
return await this.listIssues(options, true);
}
async testRun() {
const options = {
per_page: 1,
};
if (this.hasRepo) {
return await this.listRepoIssues(options, false);
}
return await this.listIssues(options, false);
}
}

View File

@@ -9,6 +9,7 @@ export default class NewNotification {
connectionData?: IJSONObject;
repoOwner?: string;
repo?: string;
hasRepo?: boolean;
baseOptions = {
all: true,
participating: false,
@@ -24,10 +25,6 @@ export default class NewNotification {
assignOwnerAndRepo(this, parameters?.repo as string);
}
get hasRepo() {
return this.repoOwner && this.repo;
}
async listRepoNotifications(options = {}, paginate = false) {
const listRepoNotifications = this.client.rest.activity.listRepoNotificationsForAuthenticatedUser;

View File

@@ -1,8 +1,9 @@
export function assignOwnerAndRepo<T extends { repoOwner?: string; repo?: string; }>(object: T, repoFullName: string): T {
export function assignOwnerAndRepo<T extends { repoOwner?: string; repo?: string; hasRepo?: boolean; }>(object: T, repoFullName: string): T {
if (object && repoFullName) {
const [repoOwner, repo] = repoFullName.split('/');
object.repoOwner = repoOwner;
object.repo = repo;
object.hasRepo = true;
}
return object;

View File

@@ -1,13 +0,0 @@
import { IJSONObject } from '@automatisch/types';
import FindProjectMergeRequests from './actions/find-project-merge-requests';
export default class Actions {
findProjectMergeRequests: FindProjectMergeRequests;
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
this.findProjectMergeRequests = new FindProjectMergeRequests(
connectionData,
parameters
);
}
}

View File

@@ -1,35 +0,0 @@
import { Gitlab } from '@gitbeaker/node';
import { IJSONObject } from '@automatisch/types';
export default class FindProjectMergeRequests {
client: any;
projectId: number;
state: string;
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
if (connectionData?.accessToken) {
this.client = new Gitlab({
host: `https://${connectionData.host}`,
oauthToken: connectionData?.accessToken as string,
});
}
if (parameters.project) {
this.projectId = parameters.project as number;
}
if (parameters.state) {
this.state = parameters.state as string;
}
}
async run() {
const mergeRequests = await this.client.MergeRequests.all({
state: this.state,
projectId: this.projectId,
maxPages: 1,
});
return { data: mergeRequests };
}
}

View File

@@ -1,10 +0,0 @@
import { IJSONObject } from '@automatisch/types';
import ListProjects from './data/list-projects';
export default class Data {
listProjects: ListProjects;
constructor(connectionData: IJSONObject, parameters?: IJSONObject) {
this.listProjects = new ListProjects(connectionData, parameters);
}
}

View File

@@ -1,26 +0,0 @@
import { Gitlab } from '@gitbeaker/node';
import type { IJSONObject } from '@automatisch/types';
export default class ListProjects {
client?: any;
constructor(connectionData: IJSONObject, parameters?: IJSONObject) {
if (connectionData?.accessToken) {
this.client = new Gitlab({
host: `https://${connectionData.host}`,
oauthToken: connectionData?.accessToken as string,
});
}
}
async run() {
const projects = await this.client.Projects.all({
membership: true,
});
return projects.map((project: any) => ({
value: project.id,
name: project.name_with_namespace,
}));
}
}

View File

@@ -1,25 +1,15 @@
import Authentication from './authentication';
import {
IService,
IAuthentication,
IApp,
IJSONObject,
} from '@automatisch/types';
import Authentication from './authentication';
import Actions from './actions';
import Data from './data';
export default class Gitlab implements IService {
authenticationClient: IAuthentication;
actions: Actions;
data: Data;
constructor(
appData: IApp,
connectionData: IJSONObject,
parameters: IJSONObject
) {
constructor(appData: IApp, connectionData: IJSONObject) {
this.authenticationClient = new Authentication(appData, connectionData);
this.actions = new Actions(connectionData, parameters);
this.data = new Data(connectionData, parameters);
}
}

View File

@@ -234,76 +234,5 @@
}
]
}
],
"actions": [
{
"name": "Find project merge requests",
"key": "findProjectMergeRequests",
"description": "Find merge requests for a project.",
"substeps": [
{
"key": "chooseAccount",
"name": "Choose account"
},
{
"key": "setupAction",
"name": "Set up action",
"arguments": [
{
"label": "Project",
"key": "project",
"type": "dropdown",
"required": true,
"description": "Search for merge requests in this project.",
"variables": false,
"source": {
"type": "query",
"name": "getData",
"arguments": [
{
"name": "key",
"value": "listProjects"
}
]
}
},
{
"label": "State",
"key": "state",
"type": "dropdown",
"required": true,
"description": "Filter merge requests by their state.",
"variables": false,
"options": [
{
"label": "All",
"value": "all"
},
{
"label": "Opened",
"value": "opened"
},
{
"label": "Closed",
"value": "closed"
},
{
"label": "Locked",
"value": "locked"
},
{
"label": "Merged",
"value": "merged"
}
]
}
]
},
{
"key": "testStep",
"name": "Test action"
}
]
}
]
}

View File

Before

Width:  |  Height:  |  Size: 345 B

After

Width:  |  Height:  |  Size: 345 B

View File

@@ -5,7 +5,7 @@ import {
IJSONObject,
} from '@automatisch/types';
export default class Schedule implements IService {
export default class Scheduler implements IService {
triggers: Triggers;
constructor(

View File

@@ -1,8 +1,8 @@
{
"name": "Schedule",
"key": "schedule",
"iconUrl": "{BASE_URL}/apps/schedule/assets/favicon.svg",
"docUrl": "https://automatisch.io/docs/schedule",
"name": "Scheduler",
"key": "scheduler",
"iconUrl": "{BASE_URL}/apps/scheduler/assets/favicon.svg",
"docUrl": "https://automatisch.io/docs/scheduler",
"primaryColor": "0059F7",
"requiresAuthentication": false,
"triggers": [

View File

@@ -5,11 +5,13 @@ import {
IJSONObject,
} from '@automatisch/types';
import Authentication from './authentication';
import Triggers from './triggers';
import Actions from './actions';
import Data from './data';
export default class Slack implements IService {
authenticationClient: IAuthentication;
triggers: Triggers;
actions: Actions;
data: Data;
@@ -20,6 +22,7 @@ export default class Slack implements IService {
) {
this.authenticationClient = new Authentication(appData, connectionData);
this.data = new Data(connectionData);
this.triggers = new Triggers(connectionData, parameters);
this.actions = new Actions(connectionData, parameters);
}
}

View File

@@ -97,6 +97,65 @@
]
}
],
"triggers": [
{
"name": "New message posted to a channel",
"key": "newMessageToChannel",
"description": "Triggers when a new message is posted to a channel",
"substeps": [
{
"key": "chooseAccount",
"name": "Choose account"
},
{
"key": "chooseTrigger",
"name": "Set up a trigger",
"arguments": [
{
"label": "Channel",
"key": "channel",
"type": "dropdown",
"required": true,
"variables": false,
"source": {
"type": "query",
"name": "getData",
"arguments": [
{
"name": "key",
"value": "listChannels"
}
]
}
},
{
"label": "Trigger for Bot Messages?",
"key": "triggerForBotMessages",
"type": "dropdown",
"description": "Should this flow trigger for bot messages?",
"required": true,
"value": true,
"variables": false,
"options": [
{
"label": "Yes",
"value": true
},
{
"label": "No",
"value": false
}
]
}
]
},
{
"key": "testStep",
"name": "Test trigger"
}
]
}
],
"actions": [
{
"name": "Send a message to channel",

View File

@@ -0,0 +1,13 @@
import { IJSONObject } from '@automatisch/types';
import NewMessageToChannel from './triggers/new-message-to-channel';
export default class Triggers {
newMessageToChannel: NewMessageToChannel;
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
this.newMessageToChannel = new NewMessageToChannel(
connectionData,
parameters
);
}
}

View File

@@ -0,0 +1,47 @@
import { IJSONObject } from '@automatisch/types';
import axios, { AxiosInstance } from 'axios';
export default class NewMessageToChannel {
httpClient: AxiosInstance;
parameters: IJSONObject;
connectionData: IJSONObject;
BASE_URL = 'https://slack.com/api';
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
this.httpClient = axios.create({ baseURL: this.BASE_URL });
this.connectionData = connectionData;
this.parameters = parameters;
}
async run() {
// TODO: Fix after webhook implementation.
}
async testRun() {
const headers = {
Authorization: `Bearer ${this.connectionData.accessToken}`,
};
const params = {
channel: this.parameters.channel,
};
const response = await this.httpClient.get('/conversations.history', {
headers,
params,
});
let lastMessage;
if (this.parameters.triggerForBotMessages) {
lastMessage = response.data.messages[0];
} else {
lastMessage = response.data.messages.find(
(message: IJSONObject) =>
!Object.prototype.hasOwnProperty.call(message, 'bot_id')
);
}
return [lastMessage];
}
}

View File

@@ -1,15 +1,21 @@
import Context from '../../types/express/context';
const getFlows = async (
_parent: unknown,
_params: unknown,
context: Context
) => {
const flows = await context.currentUser
type Params = {
appKey?: string;
};
const getFlows = async (_parent: unknown, params: Params, context: Context) => {
const flowsQuery = context.currentUser
.$relatedQuery('flows')
.withGraphJoined('[steps.[connection]]')
.orderBy('created_at', 'desc');
if (params.appKey) {
flowsQuery.where('steps.app_key', params.appKey);
}
const flows = await flowsQuery;
return flows;
};

View File

@@ -5,7 +5,7 @@ type Query {
getAppConnections(key: AvailableAppsEnumType!): [Connection]
testConnection(id: String!): Connection
getFlow(id: String!): Flow
getFlows: [Flow]
getFlows(appKey: String): [Flow]
getStepWithTestExecutions(stepId: String!): [Step]
getExecutions(limit: Int!, offset: Int!): ExecutionConnection
getExecutionSteps(
@@ -68,7 +68,6 @@ type ActionSubstepArgument {
variables: Boolean
source: ActionSubstepArgumentSource
dependsOn: [String]
options: [TriggerSubstepArgumentOption]
}
type ActionSubstepArgumentSource {
@@ -77,11 +76,6 @@ type ActionSubstepArgumentSource {
arguments: [ActionSubstepArgumentSourceArgument]
}
type TriggerSubstepArgumentOption {
label: String
value: JSONObject
}
type ActionSubstepArgumentSourceArgument {
name: String
value: String

View File

@@ -1,2 +1,11 @@
import './config/orm';
import './workers/processor';
process
.on('unhandledRejection', (reason, p) => {
console.error(reason, 'Unhandled Rejection at Promise', p);
})
.on('uncaughtException', (err) => {
console.error(err, 'Uncaught Exception thrown');
process.exit(1);
});

View File

@@ -4,8 +4,13 @@ import { GET_FLOWS } from 'graphql/queries/get-flows';
import AppFlowRow from 'components/AppFlowRow';
import type { IFlow } from '@automatisch/types';
export default function AppFlows(): React.ReactElement {
const { data } = useQuery(GET_FLOWS);
type AppFlowsProps = {
appKey: string;
}
export default function AppFlows(props: AppFlowsProps): React.ReactElement {
const { appKey } = props;
const { data } = useQuery(GET_FLOWS, { variables: { appKey }});
const appFlows: IFlow[] = data?.getFlows || [];
return (

View File

@@ -11,7 +11,7 @@ interface ControlledAutocompleteProps extends AutocompleteProps<IFieldDropdownOp
description?: string;
}
const getOption = (options: readonly IFieldDropdownOption[], value: string) => options.find(option => option.value === value);
const getOption = (options: readonly IFieldDropdownOption[], value: string) => options.find(option => option.value === value) || null;
function ControlledAutocomplete(props: ControlledAutocompleteProps): React.ReactElement {
const { control } = useFormContext();
@@ -45,7 +45,7 @@ function ControlledAutocomplete(props: ControlledAutocompleteProps): React.React
value={getOption(options, field.value)}
onChange={(event, selectedOption, reason, details) => {
const typedSelectedOption = selectedOption as IFieldDropdownOption;
if (Object.prototype.hasOwnProperty.call(typedSelectedOption, 'value')) {
if (typedSelectedOption !== null && Object.prototype.hasOwnProperty.call(typedSelectedOption, 'value')) {
controllerOnChange(typedSelectedOption.value);
} else {
controllerOnChange(typedSelectedOption);

View File

@@ -69,11 +69,13 @@ function generateValidationSchema(substeps: ISubstep[]) {
// if the field depends on another field, add the dependsOn required validation
if (dependsOn?.length > 0) {
for (const dependsOnKey of dependsOn) {
const missingDependencyValueMessage = `We're having trouble loading '${key}' data as required field '${dependsOnKey}' is missing.`;
// TODO: make `dependsOnKey` agnostic to the field. However, nested validation schema is not supported.
// So the fields under the `parameters` key are subject to their siblings only and thus, `parameters.` is removed.
substepArgumentValidations[key] = substepArgumentValidations[key].when(`${dependsOnKey.replace('parameters.', '')}`, {
is: (value: string) => Boolean(value) === false,
then: (schema) => schema.required(`We're having trouble loading '${key}' data as required field '${dependsOnKey}' is missing.`),
then: (schema) => schema.notOneOf([''], missingDependencyValueMessage).required(missingDependencyValueMessage),
});
}
}

View File

@@ -94,10 +94,6 @@ export const GET_APPS = gql`
description
variables
dependsOn
options {
label
value
}
source {
type
name

View File

@@ -1,8 +1,8 @@
import { gql } from '@apollo/client';
export const GET_FLOWS = gql`
query GetFlows {
getFlows {
query GetFlows($appKey: String) {
getFlows(appKey: $appKey) {
id
name
}

View File

@@ -155,7 +155,7 @@ export default function Application(): React.ReactElement {
</Box>
<Routes>
<Route path={`${URLS.FLOWS}/*`} element={<AppFlows />} />
<Route path={`${URLS.FLOWS}/*`} element={<AppFlows appKey={appKey} />} />
<Route path={`${URLS.CONNECTIONS}/*`} element={<AppConnections appKey={appKey} />} />

View File

@@ -6490,15 +6490,15 @@ bull@^4.7.0:
semver "^7.3.2"
uuid "^8.3.0"
bullmq@^1.76.1:
version "1.76.1"
resolved "https://registry.yarnpkg.com/bullmq/-/bullmq-1.76.1.tgz#202cfe2a9eee0cd00f3f4995c04b6fe5f1bc1995"
integrity sha512-K+TAT2mbuDEz99IuC/rY+vwmA6jpUH0w5y61pdNx0xVdHdAWn6dd9Vs0MnCObZDHCdbNgGvFumfOjCzHjD4VVg==
bullmq@^1.82.0:
version "1.82.0"
resolved "https://registry.yarnpkg.com/bullmq/-/bullmq-1.82.0.tgz#3dbc4affbf0cb9ab88af750ad5d7ca10df5c8677"
integrity sha512-jKbmYS9+IQSubCk1FRYZRrhJxbzC5zWQjmHMBzF642YpyV360A0Lm2+oblfRX2D6cN0aDmjS8yfFcPl96b3Zcw==
dependencies:
cron-parser "^2.18.0"
cron-parser "^4.2.1"
get-port "^5.1.1"
glob "^7.2.0"
ioredis "^4.28.2"
ioredis "^4.28.5"
lodash "^4.17.21"
msgpackr "^1.4.6"
semver "^6.3.0"
@@ -7534,14 +7534,6 @@ create-require@^1.1.0:
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
cron-parser@^2.18.0:
version "2.18.0"
resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-2.18.0.tgz#de1bb0ad528c815548371993f81a54e5a089edcf"
integrity sha512-s4odpheTyydAbTBQepsqd2rNWGa2iV3cyo8g7zbI2QQYGLVsfbhmwukayS1XHppe02Oy1fg7mg6xoaraVJeEcg==
dependencies:
is-nan "^1.3.0"
moment-timezone "^0.5.31"
cron-parser@^4.2.1:
version "4.3.0"
resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-4.3.0.tgz#16c3932fa62d0c30708d4200f510d6ca26bf35a2"
@@ -11062,7 +11054,7 @@ invariant@^2.2.4:
dependencies:
loose-envify "^1.0.0"
ioredis@^4.28.2, ioredis@^4.28.5:
ioredis@^4.28.5:
version "4.28.5"
resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.28.5.tgz#5c149e6a8d76a7f8fa8a504ffc85b7d5b6797f9f"
integrity sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==
@@ -11292,14 +11284,6 @@ is-module@^1.0.0:
resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=
is-nan@^1.3.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d"
integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==
dependencies:
call-bind "^1.0.0"
define-properties "^1.1.3"
is-negative-zero@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150"
@@ -13391,18 +13375,6 @@ modify-values@^1.0.0:
resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022"
integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==
moment-timezone@^0.5.31:
version "0.5.34"
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.34.tgz#a75938f7476b88f155d3504a9343f7519d9a405c"
integrity sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg==
dependencies:
moment ">= 2.9.0"
"moment@>= 2.9.0":
version "2.29.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
morgan@^1.10.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7"