diff --git a/package.json b/package.json index 184a83b267..a124b1cbff 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,6 @@ "lodash.mapvalues": "^4.6.0", "lodash.merge": "^4.6.2", "lodash.omit": "^4.5.0", - "lodash.pick": "^4.4.0", "lodash.pickby": "^4.6.0", "lodash.snakecase": "^4.1.1", "lodash.upperfirst": "^4.3.1", @@ -137,7 +136,6 @@ "@types/lodash.kebabcase": "^4.1.7", "@types/lodash.mapvalues": "^4.6.9", "@types/lodash.omit": "^4.5.9", - "@types/lodash.pick": "^4.3.7", "@types/lodash.pickby": "^4.6.9", "@types/lodash.snakecase": "^4.1.7", "@types/lodash.upperfirst": "^4.3.7", diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useCreateActivityInDB.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useCreateActivityInDB.test.tsx index 521370dec0..7d3ad9399f 100644 --- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useCreateActivityInDB.test.tsx +++ b/packages/twenty-front/src/modules/activities/hooks/__tests__/useCreateActivityInDB.test.tsx @@ -1,7 +1,6 @@ import { type MockedResponse } from '@apollo/client/testing'; import { act, renderHook } from '@testing-library/react'; import gql from 'graphql-tag'; -import pick from 'lodash.pick'; import { useCreateActivityInDB } from '@/activities/hooks/useCreateActivityInDB'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; @@ -12,8 +11,13 @@ const mockedDate = '2024-03-15T12:00:00.000Z'; const toISOStringMock = jest.fn(() => mockedDate); global.Date.prototype.toISOString = toISOStringMock; +const { id, title, bodyV2, status, dueAt } = mockedTasks[0]; const mockedActivity = { - ...pick(mockedTasks[0], ['id', 'title', 'bodyV2', 'type', 'status', 'dueAt']), + id, + title, + bodyV2, + status, + dueAt, updatedAt: mockedDate, }; diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useOpenCreateActivityDrawer.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useOpenCreateActivityDrawer.test.tsx index 7417c87c0d..aeb135f671 100644 --- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useOpenCreateActivityDrawer.test.tsx +++ b/packages/twenty-front/src/modules/activities/hooks/__tests__/useOpenCreateActivityDrawer.test.tsx @@ -7,7 +7,6 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; import gql from 'graphql-tag'; -import pick from 'lodash.pick'; import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; import { mockedTasks } from '~/testing/mock-data/tasks'; import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems'; @@ -16,8 +15,13 @@ const mockedDate = '2024-03-15T12:00:00.000Z'; const toISOStringMock = jest.fn(() => mockedDate); global.Date.prototype.toISOString = toISOStringMock; +const { id, title, bodyV2, status, dueAt } = mockedTasks[0]; const mockedActivity = { - ...pick(mockedTasks[0], ['id', 'title', 'bodyV2', 'type', 'status', 'dueAt']), + id, + title, + bodyV2, + status, + dueAt, updatedAt: mockedDate, }; diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/getRecordFromRecordNode.ts b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordFromRecordNode.ts index 91cbb99359..426a507ed2 100644 --- a/packages/twenty-front/src/modules/object-record/cache/utils/getRecordFromRecordNode.ts +++ b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordFromRecordNode.ts @@ -1,16 +1,16 @@ -import pick from 'lodash.pick'; - import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode'; import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; import { isDefined } from 'twenty-shared/utils'; +import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; export const getRecordFromRecordNode = ({ recordNode, }: { recordNode: RecordGqlNode; }): T => { + const { id, __typename } = recordNode; + return { ...Object.fromEntries( Object.entries(recordNode).map(([fieldName, value]) => { @@ -34,6 +34,7 @@ export const getRecordFromRecordNode = ({ // RawJson field value passes through this method and does not have `id` or `__typename`. // This prevents adding an undefined `id` and `__typename` to the RawJson field value, // which is invalid JSON. - ...pick(recordNode, ['id', '__typename'] as const), + ...(isDefined(id) ? { id } : {}), + ...(isDefined(__typename) ? { __typename } : {}), } as T; }; diff --git a/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationEditDatabaseConnectionContent.tsx b/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationEditDatabaseConnectionContent.tsx index 7b1513cc0a..971f0ee6d3 100644 --- a/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationEditDatabaseConnectionContent.tsx +++ b/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationEditDatabaseConnectionContent.tsx @@ -14,7 +14,6 @@ import { ApolloError } from '@apollo/client'; import { zodResolver } from '@hookform/resolvers/zod'; import { useLingui } from '@lingui/react/macro'; import { Section } from '@react-email/components'; -import pick from 'lodash.pick'; import { FormProvider, useForm } from 'react-hook-form'; import { SettingsPath } from 'twenty-shared/types'; import { getSettingsPath } from 'twenty-shared/utils'; @@ -75,11 +74,17 @@ export const SettingsIntegrationEditDatabaseConnectionContent = ({ formConfig.formState.dirtyFields, ) as (keyof SettingsIntegrationEditConnectionFormValues)[]; + const dirtyFormValues = Object.fromEntries( + Object.entries(formValues).filter(([key]) => + dirtyFieldKeys.includes(key as keyof typeof formValues), + ), + ); + try { await updateOneDatabaseConnection({ ...formatValuesForUpdate({ databaseKey, - formValues: pick(formValues, dirtyFieldKeys), + formValues: dirtyFormValues, }), id: connection?.id ?? '', }); diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx index a3dc53e9ff..06706ca7be 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx @@ -1,6 +1,5 @@ import { zodResolver } from '@hookform/resolvers/zod'; import omit from 'lodash.omit'; -import pick from 'lodash.pick'; import { useEffect, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { useNavigate, useParams } from 'react-router-dom'; @@ -13,6 +12,7 @@ import { useUpdateOneFieldMetadataItem } from '@/object-metadata/hooks/useUpdate import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural'; import { formatFieldMetadataItemInput } from '@/object-metadata/utils/formatFieldMetadataItemInput'; import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; +import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly'; import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { FIELD_NAME_MAXIMUM_LENGTH } from '@/settings/data-model/constants/FieldNameMaximumLength'; @@ -36,7 +36,6 @@ import { Section } from 'twenty-ui/layout'; import { FieldMetadataType } from '~/generated-metadata/graphql'; import { useNavigateApp } from '~/hooks/useNavigateApp'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; -import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly'; //TODO: fix this type export type SettingsDataModelFieldEditFormValues = z.infer< @@ -151,9 +150,10 @@ export const SettingsObjectFieldEdit = () => { const otherDirtyFields = omit(dirtyFields, 'relation'); if (Object.keys(otherDirtyFields).length > 0) { - const formattedInput = pick( - formatFieldMetadataItemInput(formValues), - Object.keys(otherDirtyFields), + const formattedInput = Object.fromEntries( + Object.entries(formatFieldMetadataItemInput(formValues)).filter( + ([key]) => Object.keys(otherDirtyFields).includes(key), + ), ); await updateOneFieldMetadataItem({ diff --git a/packages/twenty-front/src/pages/settings/security/SettingsSecuritySSOIdentifyProvider.tsx b/packages/twenty-front/src/pages/settings/security/SettingsSecuritySSOIdentifyProvider.tsx index 1f15b0c1e8..ffd6e43ee0 100644 --- a/packages/twenty-front/src/pages/settings/security/SettingsSecuritySSOIdentifyProvider.tsx +++ b/packages/twenty-front/src/pages/settings/security/SettingsSecuritySSOIdentifyProvider.tsx @@ -12,7 +12,6 @@ import { ApolloError } from '@apollo/client'; import { zodResolver } from '@hookform/resolvers/zod'; import { t } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; -import pick from 'lodash.pick'; import { FormProvider, useForm } from 'react-hook-form'; import { SettingsPath } from 'twenty-shared/types'; import { getSettingsPath } from 'twenty-shared/utils'; @@ -37,13 +36,17 @@ export const SettingsSecuritySSOIdentifyProvider = () => { try { const type = form.getValues('type'); + const values = form.getValues(); + const providerKeys = Object.keys( + sSOIdentityProviderDefaultValues[type](), + ); + + const filteredValues = Object.fromEntries( + Object.entries(values).filter(([key]) => providerKeys.includes(key)), + ); + await createSSOIdentityProvider( - SSOIdentitiesProvidersParamsSchema.parse( - pick( - form.getValues(), - Object.keys(sSOIdentityProviderDefaultValues[type]()), - ), - ), + SSOIdentitiesProvidersParamsSchema.parse(filteredValues), ); navigate(SettingsPath.Security); diff --git a/packages/twenty-server/package.json b/packages/twenty-server/package.json index 51d0ccbfd3..053cc10609 100644 --- a/packages/twenty-server/package.json +++ b/packages/twenty-server/package.json @@ -135,7 +135,6 @@ "lodash.merge": "^4.6.2", "lodash.omit": "4.5.0", "lodash.omitby": "^4.6.0", - "lodash.pick": "4.4.0", "lodash.snakecase": "4.1.1", "lodash.uniq": "^4.5.0", "lodash.uniqby": "^4.7.0", diff --git a/packages/twenty-server/src/engine/api/mcp/services/tools/create.tools.service.ts b/packages/twenty-server/src/engine/api/mcp/services/tools/create.tools.service.ts index 149c2c90f7..e847ae4206 100644 --- a/packages/twenty-server/src/engine/api/mcp/services/tools/create.tools.service.ts +++ b/packages/twenty-server/src/engine/api/mcp/services/tools/create.tools.service.ts @@ -1,11 +1,11 @@ import { Injectable } from '@nestjs/common'; import { type Request } from 'express'; -import pick from 'lodash.pick'; +import { isDefined } from 'twenty-shared/utils'; -import { MetadataQueryBuilderFactory } from 'src/engine/api/rest/metadata/query-builder/metadata-query-builder.factory'; import { MCPMetadataToolsService } from 'src/engine/api/mcp/services/tools/mcp-metadata-tools.service'; import { validationSchemaManager } from 'src/engine/api/mcp/utils/get-json-schema'; +import { MetadataQueryBuilderFactory } from 'src/engine/api/rest/metadata/query-builder/metadata-query-builder.factory'; import { type ObjectName } from 'src/engine/api/rest/metadata/types/metadata-entity.type'; @Injectable() @@ -39,18 +39,22 @@ export class CreateToolsService { } async execute(request: Request, objectName: ObjectName) { + const { fields, objects } = request.body.params.arguments; + const selectors = { + ...(isDefined(fields) ? { fields } : {}), + ...(isDefined(objects) ? { objects } : {}), + }; + const requestContext = { body: request.body.params.arguments, baseUrl: this.mCPMetadataToolsService.generateBaseUrl(request), path: `/rest/metadata/${objectName}`, headers: request.headers, }; + const response = await this.mCPMetadataToolsService.send( requestContext, - await this.metadataQueryBuilderFactory.create( - requestContext, - pick(request.body.params.arguments, ['fields', 'objects']), - ), + await this.metadataQueryBuilderFactory.create(requestContext, selectors), ); return response.data.data; diff --git a/packages/twenty-server/src/engine/api/mcp/services/tools/get.tools.service.ts b/packages/twenty-server/src/engine/api/mcp/services/tools/get.tools.service.ts index 4663d5693e..b33ff9daad 100644 --- a/packages/twenty-server/src/engine/api/mcp/services/tools/get.tools.service.ts +++ b/packages/twenty-server/src/engine/api/mcp/services/tools/get.tools.service.ts @@ -1,10 +1,10 @@ import { Injectable } from '@nestjs/common'; import { type Request } from 'express'; -import pick from 'lodash.pick'; +import { isDefined } from 'twenty-shared/utils'; -import { MetadataQueryBuilderFactory } from 'src/engine/api/rest/metadata/query-builder/metadata-query-builder.factory'; import { MCPMetadataToolsService } from 'src/engine/api/mcp/services/tools/mcp-metadata-tools.service'; +import { MetadataQueryBuilderFactory } from 'src/engine/api/rest/metadata/query-builder/metadata-query-builder.factory'; import { type ObjectName } from 'src/engine/api/rest/metadata/types/metadata-entity.type'; @Injectable() @@ -74,6 +74,12 @@ export class GetToolsService { } async execute(request: Request, objectName: ObjectName) { + const { fields, objects } = request.body.params.arguments; + const selectors = { + ...(isDefined(fields) ? { fields } : {}), + ...(isDefined(objects) ? { objects } : {}), + }; + const requestContext = { body: request.body.params.arguments, baseUrl: this.mCPMetadataToolsService.generateBaseUrl(request), @@ -84,10 +90,7 @@ export class GetToolsService { const response = await this.mCPMetadataToolsService.send( requestContext, - await this.metadataQueryBuilderFactory.get( - requestContext, - pick(request.body.params.arguments, ['fields', 'objects']), - ), + await this.metadataQueryBuilderFactory.get(requestContext, selectors), ); return response.data.data; diff --git a/packages/twenty-server/src/engine/api/mcp/services/tools/update.tools.service.ts b/packages/twenty-server/src/engine/api/mcp/services/tools/update.tools.service.ts index 0d8f1da771..7e9c765040 100644 --- a/packages/twenty-server/src/engine/api/mcp/services/tools/update.tools.service.ts +++ b/packages/twenty-server/src/engine/api/mcp/services/tools/update.tools.service.ts @@ -2,11 +2,11 @@ import { Injectable } from '@nestjs/common'; import { type Request } from 'express'; import omit from 'lodash.omit'; -import pick from 'lodash.pick'; +import { isDefined } from 'twenty-shared/utils'; -import { MetadataQueryBuilderFactory } from 'src/engine/api/rest/metadata/query-builder/metadata-query-builder.factory'; import { MCPMetadataToolsService } from 'src/engine/api/mcp/services/tools/mcp-metadata-tools.service'; import { validationSchemaManager } from 'src/engine/api/mcp/utils/get-json-schema'; +import { MetadataQueryBuilderFactory } from 'src/engine/api/rest/metadata/query-builder/metadata-query-builder.factory'; import { type ObjectName } from 'src/engine/api/rest/metadata/types/metadata-entity.type'; @Injectable() @@ -70,7 +70,12 @@ export class UpdateToolsService { } async execute(request: Request, objectName: ObjectName) { - const { id, ...body } = request.body.params.arguments; + const { id, fields, objects, ...body } = request.body.params.arguments; + const selectors = { + ...(isDefined(fields) ? { fields } : {}), + ...(isDefined(objects) ? { objects } : {}), + }; + const requestContext = { body, baseUrl: this.mCPMetadataToolsService.generateBaseUrl(request), @@ -80,10 +85,7 @@ export class UpdateToolsService { const response = await this.mCPMetadataToolsService.send( requestContext, - await this.metadataQueryBuilderFactory.update( - requestContext, - pick(request.body.params.arguments, ['fields', 'objects']), - ), + await this.metadataQueryBuilderFactory.update(requestContext, selectors), ); return response.data.data; diff --git a/packages/twenty-server/src/engine/core-modules/serverless/drivers/layers/1/package.json b/packages/twenty-server/src/engine/core-modules/serverless/drivers/layers/1/package.json index 84ee87a784..888faecbb7 100644 --- a/packages/twenty-server/src/engine/core-modules/serverless/drivers/layers/1/package.json +++ b/packages/twenty-server/src/engine/core-modules/serverless/drivers/layers/1/package.json @@ -34,7 +34,6 @@ "lodash.mapvalues": "^4.6.0", "lodash.merge": "^4.6.2", "lodash.omit": "^4.5.0", - "lodash.pick": "^4.4.0", "lodash.pickby": "^4.6.0", "lodash.snakecase": "^4.1.1", "lodash.upperfirst": "^4.3.1", diff --git a/packages/twenty-server/src/engine/core-modules/serverless/drivers/layers/1/yarn.lock b/packages/twenty-server/src/engine/core-modules/serverless/drivers/layers/1/yarn.lock index d4fb8f9fc4..c5f1e2a9e0 100644 --- a/packages/twenty-server/src/engine/core-modules/serverless/drivers/layers/1/yarn.lock +++ b/packages/twenty-server/src/engine/core-modules/serverless/drivers/layers/1/yarn.lock @@ -303,15 +303,6 @@ __metadata: languageName: node linkType: hard -"@types/lodash.debounce@npm:^4.0.9": - version: 4.0.9 - resolution: "@types/lodash.debounce@npm:4.0.9" - dependencies: - "@types/lodash": "npm:*" - checksum: 10c0/9fbb24e5e52616faf60ba5c82d8c6517f4b86fc6e9ab353b4c56c0760f63d9bf53af3f2d8f6c37efa48090359fb96dba1087d497758511f6c40677002191d042 - languageName: node - linkType: hard - "@types/lodash.groupby@npm:^4.6.9": version: 4.6.9 resolution: "@types/lodash.groupby@npm:4.6.9" @@ -1796,13 +1787,6 @@ __metadata: languageName: node linkType: hard -"lodash.debounce@npm:^4.0.8": - version: 4.0.8 - resolution: "lodash.debounce@npm:4.0.8" - checksum: 10c0/762998a63e095412b6099b8290903e0a8ddcb353ac6e2e0f2d7e7d03abd4275fe3c689d88960eb90b0dde4f177554d51a690f22a343932ecbc50a5d111849987 - languageName: node - linkType: hard - "lodash.groupby@npm:^4.6.0": version: 4.6.0 resolution: "lodash.groupby@npm:4.6.0" @@ -1915,13 +1899,6 @@ __metadata: languageName: node linkType: hard -"lodash.pick@npm:^4.4.0": - version: 4.4.0 - resolution: "lodash.pick@npm:4.4.0" - checksum: 10c0/a04c460b95d1aaa44e9513d1dacf72ea74d838da843e45831de9de64c303f13cdde1859702a6f4dcef417816898ffd47c6ae0614c957ac70245bed2809b8d2e2 - languageName: node - linkType: hard - "lodash.pickby@npm:^4.6.0": version: 4.6.0 resolution: "lodash.pickby@npm:4.6.0" @@ -2531,7 +2508,6 @@ __metadata: "@types/deep-equal": "npm:^1.0.4" "@types/lodash.camelcase": "npm:^4.3.9" "@types/lodash.compact": "npm:^3.0.9" - "@types/lodash.debounce": "npm:^4.0.9" "@types/lodash.groupby": "npm:^4.6.9" "@types/lodash.identity": "npm:^3.0.9" "@types/lodash.isempty": "npm:^4.4.9" @@ -2553,7 +2529,6 @@ __metadata: lodash.camelcase: "npm:^4.3.0" lodash.chunk: "npm:^4.2.0" lodash.compact: "npm:^3.0.1" - lodash.debounce: "npm:^4.0.8" lodash.groupby: "npm:^4.6.0" lodash.identity: "npm:^3.0.0" lodash.isempty: "npm:^4.4.0" @@ -2563,7 +2538,6 @@ __metadata: lodash.mapvalues: "npm:^4.6.0" lodash.merge: "npm:^4.6.2" lodash.omit: "npm:^4.5.0" - lodash.pick: "npm:^4.4.0" lodash.pickby: "npm:^4.6.0" lodash.snakecase: "npm:^4.1.1" lodash.upperfirst: "npm:^4.3.1" diff --git a/yarn.lock b/yarn.lock index 8b053cf8a1..e1a0053d35 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21627,15 +21627,6 @@ __metadata: languageName: node linkType: hard -"@types/lodash.pick@npm:^4.3.7": - version: 4.4.9 - resolution: "@types/lodash.pick@npm:4.4.9" - dependencies: - "@types/lodash": "npm:*" - checksum: 10c0/61ae2fb9fe817a2f398863d10dc57e3cb864ebadefb7f3554d8d0c189448e08a174d6bfdd6e0220988ea084a2d47c3377908cf037e2b3ce552aa79ce0f36bc9f - languageName: node - linkType: hard - "@types/lodash.pickby@npm:^4.6.9": version: 4.6.9 resolution: "@types/lodash.pickby@npm:4.6.9" @@ -39620,13 +39611,6 @@ __metadata: languageName: node linkType: hard -"lodash.pick@npm:4.4.0, lodash.pick@npm:^4.4.0": - version: 4.4.0 - resolution: "lodash.pick@npm:4.4.0" - checksum: 10c0/a04c460b95d1aaa44e9513d1dacf72ea74d838da843e45831de9de64c303f13cdde1859702a6f4dcef417816898ffd47c6ae0614c957ac70245bed2809b8d2e2 - languageName: node - linkType: hard - "lodash.pickby@npm:^4.6.0": version: 4.6.0 resolution: "lodash.pickby@npm:4.6.0" @@ -51872,7 +51856,6 @@ __metadata: lodash.merge: "npm:^4.6.2" lodash.omit: "npm:4.5.0" lodash.omitby: "npm:^4.6.0" - lodash.pick: "npm:4.4.0" lodash.snakecase: "npm:4.1.1" lodash.uniq: "npm:^4.5.0" lodash.uniqby: "npm:^4.7.0" @@ -52107,7 +52090,6 @@ __metadata: "@types/lodash.kebabcase": "npm:^4.1.7" "@types/lodash.mapvalues": "npm:^4.6.9" "@types/lodash.omit": "npm:^4.5.9" - "@types/lodash.pick": "npm:^4.3.7" "@types/lodash.pickby": "npm:^4.6.9" "@types/lodash.snakecase": "npm:^4.1.7" "@types/lodash.upperfirst": "npm:^4.3.7" @@ -52184,7 +52166,6 @@ __metadata: lodash.mapvalues: "npm:^4.6.0" lodash.merge: "npm:^4.6.2" lodash.omit: "npm:^4.5.0" - lodash.pick: "npm:^4.4.0" lodash.pickby: "npm:^4.6.0" lodash.snakecase: "npm:^4.1.1" lodash.upperfirst: "npm:^4.3.1"