1119e3d77e
# Fix: URL Encoding Bug & Code Refactor
## Issue
**Bug**: URLs with encoded characters (e.g., `%20` for spaces) were
being double-encoded or incorrectly processed in
[lowercaseUrlOriginAndRemoveTrailingSlash](cci:1://file:///Users/apple/WebstormProjects/twenty/packages/twenty-shared/src/utils/url/lowercaseUrlOriginAndRemoveTrailingSlash.ts:4:0-16:2),
causing URL mismatches and potential data integrity issues.
**Build Error**: `TS2307: Cannot find module
'src/modules/messaging/message-import-manager/drivers/imap/utils/safe-decode-uri-component.util'`
## Root Cause Analysis
### 1. Missing URL Decoding
The
[lowercaseUrlOriginAndRemoveTrailingSlash](cci:1://file:///Users/apple/WebstormProjects/twenty/packages/twenty-shared/src/utils/url/lowercaseUrlOriginAndRemoveTrailingSlash.ts:4:0-16:2)
function was processing URLs without properly decoding URI components.
When URLs contained encoded characters like `%20`, `%2F`, etc., they
weren't being normalized correctly.
### 2. Invalid Cross-Package Import
The fix attempted to import
[safeDecodeURIComponent](cci:1://file:///Users/apple/WebstormProjects/twenty/packages/twenty-shared/src/utils/url/safeDecodeURIComponent.ts:0:0-6:2)
from `twenty-server`:
```typescript
import { safeDecodeURIComponent } from 'src/modules/messaging/message-import-manager/drivers/imap/utils/safe-decode-uri-component.util';
```
This failed because:
- The file resides in `twenty-shared`, a separate package
- TypeScript cannot resolve internal paths from another package
- The monorepo uses package exports (`twenty-shared/utils`) for
cross-package imports, not direct file paths
## Solution
### 1. Bug Fix: Added Safe URI Decoding
Updated
[lowercaseUrlOriginAndRemoveTrailingSlash.ts](cci:7://file:///Users/apple/WebstormProjects/twenty/packages/twenty-shared/src/utils/url/lowercaseUrlOriginAndRemoveTrailingSlash.ts:0:0-0:0)
to properly decode URL components:
```typescript
export const lowercaseUrlOriginAndRemoveTrailingSlash = (rawUrl: string) => {
const url = getURLSafely(rawUrl);
if (!isDefined(url)) {
return rawUrl;
}
const lowercaseOrigin = url.origin.toLowerCase();
const path =
safeDecodeURIComponent(url.pathname) +
safeDecodeURIComponent(url.search) +
url.hash;
return (lowercaseOrigin + path).replace(/\/$/, '');
};
```
The
[safeDecodeURIComponent](cci:1://file:///Users/apple/WebstormProjects/twenty/packages/twenty-shared/src/utils/url/safeDecodeURIComponent.ts:0:0-6:2)
wrapper handles malformed URI sequences gracefully by returning the
original string if decoding fails, preventing runtime crashes.
### 2. Refactor: Consolidated Shared Utility
**Before**: Duplicate utility existed in `twenty-server`
```
twenty-server/src/modules/messaging/.../utils/safe-decode-uri-component.util.ts
```
**After**: Single source of truth in `twenty-shared`
```
twenty-shared/src/utils/url/safeDecodeURIComponent.ts
```
This follows the established pattern in the codebase where shared
utilities live in `twenty-shared` and are imported via subpath exports:
```typescript
// In twenty-server
import { safeDecodeURIComponent } from 'twenty-shared/utils';
// In twenty-shared (local import)
import { safeDecodeURIComponent } from './safeDecodeURIComponent';
```
## Files Changed
| File | Action | Description |
|------|--------|-------------|
|
[twenty-shared/src/utils/url/safeDecodeURIComponent.ts](cci:7://file:///Users/apple/WebstormProjects/twenty/packages/twenty-shared/src/utils/url/safeDecodeURIComponent.ts:0:0-0:0)
| **Created** | New shared utility (moved from twenty-server) |
|
[twenty-shared/src/utils/url/index.ts](cci:7://file:///Users/apple/WebstormProjects/twenty/packages/twenty-shared/src/utils/url/index.ts:0:0-0:0)
| **Modified** | Added export for
[safeDecodeURIComponent](cci:1://file:///Users/apple/WebstormProjects/twenty/packages/twenty-shared/src/utils/url/safeDecodeURIComponent.ts:0:0-6:2)
|
|
[twenty-shared/src/utils/url/lowercaseUrlOriginAndRemoveTrailingSlash.ts](cci:7://file:///Users/apple/WebstormProjects/twenty/packages/twenty-shared/src/utils/url/lowercaseUrlOriginAndRemoveTrailingSlash.ts:0:0-0:0)
| **Modified** | Fixed import path, now uses local relative import |
| `twenty-server/.../imap-message-text-extractor.service.ts` |
**Modified** | Updated import to use `twenty-shared/utils` |
| `twenty-server/.../safe-decode-uri-component.util.ts` | **Deleted** |
Removed duplicate utility |
## The Utility
```typescript
// safeDecodeURIComponent.ts
export const safeDecodeURIComponent = (text: string): string => {
try {
return decodeURIComponent(text);
} catch {
return text;
}
};
```
This wrapper is necessary because `decodeURIComponent()` throws a
`URIError` on malformed sequences (e.g., `%E0%A4%A`). The safe version
returns the original string instead of crashing.
## Testing
- **342 tests passed** in `twenty-shared`
- `lowercaseUrlOriginAndRemoveTrailingSlash.test.ts` validates URL
normalization behavior
- No regressions in existing functionality
## Impact
- **Bug Fixed**: URLs with encoded characters are now properly
normalized
- **Code Quality**: Eliminated code duplication between packages
- **Maintainability**: Single source of truth for URI decoding utility
- **Build**: Resolved TS2307 compilation error
---------
Co-authored-by: Joker <apple@Apples-MacBook-Pro.local>
147 lines
10 KiB
TypeScript
147 lines
10 KiB
TypeScript
/*
|
|
* _____ _
|
|
*|_ _|_ _____ _ __ | |_ _ _
|
|
* | | \ \ /\ / / _ \ '_ \| __| | | | Auto-generated file
|
|
* | | \ V V / __/ | | | |_| |_| | Any edits to this will be overridden
|
|
* |_| \_/\_/ \___|_| |_|\__|\__, |
|
|
* |___/
|
|
*/
|
|
|
|
export { applyDiff } from './applyDiff';
|
|
export { filterOutByProperty } from './array/filterOutByProperty';
|
|
export { findById } from './array/findById';
|
|
export { findByProperty } from './array/findByProperty';
|
|
export { findOrThrow } from './array/findOrThrow';
|
|
export { getContiguousIncrementalValues } from './array/getContiguousIncrementalValues';
|
|
export { isNonEmptyArray } from './array/isNonEmptyArray';
|
|
export { mapById } from './array/mapById';
|
|
export { mapByProperty } from './array/mapByProperty';
|
|
export { sumByProperty } from './array/sumByProperty';
|
|
export { upsertIntoArrayOfObjectsComparingId } from './array/upsertIntoArrayOfObjectComparingId';
|
|
export { assertUnreachable } from './assertUnreachable';
|
|
export { computeDiffBetweenObjects } from './compute-diff-between-objects';
|
|
export { deepMerge } from './deepMerge';
|
|
export { CustomError } from './errors/CustomError';
|
|
export { extractAndSanitizeObjectStringFields } from './extractAndSanitizeObjectStringFields';
|
|
export { computeMorphRelationFieldName } from './fieldMetadata/compute-morph-relation-field-name';
|
|
export { isFieldMetadataDateKind } from './fieldMetadata/isFieldMetadataDateKind';
|
|
export { checkIfShouldComputeEmptinessFilter } from './filter/checkIfShouldComputeEmptinessFilter';
|
|
export { checkIfShouldSkipFiltering } from './filter/checkIfShouldSkipFiltering';
|
|
export { computeGqlOperationFilterForEmails } from './filter/compute-record-gql-operation-filter/for-composite-field/computeGqlOperationFilterForEmails';
|
|
export { computeGqlOperationFilterForLinks } from './filter/compute-record-gql-operation-filter/for-composite-field/computeGqlOperationFilterForLinks';
|
|
export { computeEmptyGqlOperationFilterForEmails } from './filter/computeEmptyGqlOperationFilterForEmails';
|
|
export { computeEmptyGqlOperationFilterForLinks } from './filter/computeEmptyGqlOperationFilterForLinks';
|
|
export { computeRecordGqlOperationFilter } from './filter/computeRecordGqlOperationFilter';
|
|
export { addUnitToDateTime } from './filter/dates/utils/addUnitToDateTime';
|
|
export type { FirstDayOfTheWeek } from './filter/dates/utils/firstDayOfWeekSchema';
|
|
export { firstDayOfWeekSchema } from './filter/dates/utils/firstDayOfWeekSchema';
|
|
export { getDateFromPlainDate } from './filter/dates/utils/getDateFromPlainDate';
|
|
export { getEndUnitOfDateTime } from './filter/dates/utils/getEndUnitOfDateTime';
|
|
export { getFirstDayOfTheWeekAsANumberForDateFNS } from './filter/dates/utils/getFirstDayOfTheWeekAsANumberForDateFNS';
|
|
export { getPlainDateFromDate } from './filter/dates/utils/getPlainDateFromDate';
|
|
export { getStartUnitOfDateTime } from './filter/dates/utils/getStartUnitOfDateTime';
|
|
export { relativeDateFilterAmountSchema } from './filter/dates/utils/relativeDateFilterAmountSchema';
|
|
export type { RelativeDateFilterDirection } from './filter/dates/utils/relativeDateFilterDirectionSchema';
|
|
export { relativeDateFilterDirectionSchema } from './filter/dates/utils/relativeDateFilterDirectionSchema';
|
|
export type { RelativeDateFilter } from './filter/dates/utils/relativeDateFilterSchema';
|
|
export { relativeDateFilterSchema } from './filter/dates/utils/relativeDateFilterSchema';
|
|
export { relativeDateFilterStringifiedSchema } from './filter/dates/utils/relativeDateFilterStringifiedSchema';
|
|
export type { RelativeDateFilterUnit } from './filter/dates/utils/relativeDateFilterUnitSchema';
|
|
export { relativeDateFilterUnitSchema } from './filter/dates/utils/relativeDateFilterUnitSchema';
|
|
export type { ResolvedDateFilterValue } from './filter/dates/utils/resolveDateFilter';
|
|
export { resolveDateFilter } from './filter/dates/utils/resolveDateFilter';
|
|
export type { ResolvedDateTimeFilterValue } from './filter/dates/utils/resolveDateTimeFilter';
|
|
export { resolveDateTimeFilter } from './filter/dates/utils/resolveDateTimeFilter';
|
|
export { resolveRelativeDateFilter } from './filter/dates/utils/resolveRelativeDateFilter';
|
|
export { resolveRelativeDateFilterStringified } from './filter/dates/utils/resolveRelativeDateFilterStringified';
|
|
export { resolveRelativeDateTimeFilter } from './filter/dates/utils/resolveRelativeDateTimeFilter';
|
|
export { resolveRelativeDateTimeFilterStringified } from './filter/dates/utils/resolveRelativeDateTimeFilterStringified';
|
|
export { shiftPointInTimeFromTimezoneDifferenceInMinutesWithSystemTimezone } from './filter/dates/utils/shiftPointInTimeFromTimezoneDifferenceInMinutesWithSystemTimezone';
|
|
export { subUnitFromDateTime } from './filter/dates/utils/subUnitFromDateTime';
|
|
export { isEmptinessOperand } from './filter/isEmptinessOperand';
|
|
export { turnAnyFieldFilterIntoRecordGqlFilter } from './filter/turnAnyFieldFilterIntoRecordGqlFilter';
|
|
export type {
|
|
RecordFilter,
|
|
RecordFilterGroup,
|
|
} from './filter/turnRecordFilterGroupIntoGqlOperationFilter';
|
|
export { turnRecordFilterGroupsIntoGqlOperationFilter } from './filter/turnRecordFilterGroupIntoGqlOperationFilter';
|
|
export { turnRecordFilterIntoRecordGqlOperationFilter } from './filter/turnRecordFilterIntoGqlOperationFilter';
|
|
export { combineFilters } from './filter/utils/combineFilters';
|
|
export { computeTimezoneDifferenceInMinutes } from './filter/utils/computeTimezoneDifferenceInMinutes';
|
|
export { convertViewFilterOperandToCoreOperand } from './filter/utils/convert-view-filter-operand-to-core-operand.util';
|
|
export { convertViewFilterValueToString } from './filter/utils/convertViewFilterValueToString';
|
|
export { createAnyFieldRecordFilterBaseProperties } from './filter/utils/createAnyFieldRecordFilterBaseProperties';
|
|
export {
|
|
convertGreaterThanOrEqualRatingToArrayOfRatingValues,
|
|
convertLessThanOrEqualRatingToArrayOfRatingValues,
|
|
convertRatingToRatingValue,
|
|
} from './filter/utils/fieldRatingConvertors';
|
|
export { filterSelectOptionsOfFieldMetadataItem } from './filter/utils/filterSelectOptionsOfFieldMetadataItem';
|
|
export { generateILikeFiltersForCompositeFields } from './filter/utils/generateILikeFiltersForCompositeFields';
|
|
export { getEmptyRecordGqlOperationFilter } from './filter/utils/getEmptyRecordGqlOperationFilter';
|
|
export { getFilterTypeFromFieldType } from './filter/utils/getFilterTypeFromFieldType';
|
|
export { isExpectedSubFieldName } from './filter/utils/isExpectedSubFieldName';
|
|
export { arrayOfStringsOrVariablesSchema } from './filter/utils/validation-schemas/arrayOfStringsOrVariablesSchema';
|
|
export { arrayOfUuidOrVariableSchema } from './filter/utils/validation-schemas/arrayOfUuidsOrVariablesSchema';
|
|
export {
|
|
relationFilterValueSchemaObject,
|
|
jsonRelationFilterValueSchema,
|
|
} from './filter/utils/validation-schemas/jsonRelationFilterValueSchema';
|
|
export { fromArrayToUniqueKeyRecord } from './from-array-to-unique-key-record.util';
|
|
export { fromArrayToValuesByKeyRecord } from './fromArrayToValuesByKeyRecord.util';
|
|
export { getURLSafely } from './getURLSafely';
|
|
export { getImageAbsoluteURI } from './image/getImageAbsoluteURI';
|
|
export {
|
|
sanitizeURL,
|
|
getLogoUrlFromDomainName,
|
|
} from './image/getLogoUrlFromDomainName';
|
|
export { getUniqueConstraintsFields } from './indexMetadata/getUniqueConstraintsFields';
|
|
export { fastDeepEqual } from './json/fast-deep-equal';
|
|
export { getAppPath } from './navigation/getAppPath';
|
|
export { getSettingsPath } from './navigation/getSettingsPath';
|
|
export { parseJson } from './parseJson';
|
|
export { removePropertiesFromRecord } from './removePropertiesFromRecord';
|
|
export { removeUndefinedFields } from './removeUndefinedFields';
|
|
export { safeParseRelativeDateFilterJSONStringified } from './safeParseRelativeDateFilterJSONStringified';
|
|
export { getGenericOperationName } from './sentry/getGenericOperationName';
|
|
export { getHumanReadableNameFromCode } from './sentry/getHumanReadableNameFromCode';
|
|
export { appendCopySuffix } from './strings/appendCopySuffix';
|
|
export { capitalize } from './strings/capitalize';
|
|
export { uncapitalize } from './strings/uncapitalize';
|
|
export type {
|
|
TipTapMarkType,
|
|
TipTapNodeType,
|
|
LinkMarkAttributes,
|
|
TipTapMark,
|
|
} from './tiptap/tiptap-marks';
|
|
export {
|
|
TIPTAP_MARK_TYPES,
|
|
TIPTAP_NODE_TYPES,
|
|
TIPTAP_MARKS_RENDER_ORDER,
|
|
} from './tiptap/tiptap-marks';
|
|
export type { StringPropertyKeys } from './trim-and-remove-duplicated-whitespaces-from-object-string-properties';
|
|
export { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from './trim-and-remove-duplicated-whitespaces-from-object-string-properties';
|
|
export { trimAndRemoveDuplicatedWhitespacesFromString } from './trim-and-remove-duplicated-whitespaces-from-string';
|
|
export { throwIfNotDefined } from './typeguard/throwIfNotDefined';
|
|
export { absoluteUrlSchema } from './url/absoluteUrlSchema';
|
|
export { buildSignedPath } from './url/buildSignedPath';
|
|
export { getAbsoluteUrl } from './url/getAbsoluteUrl';
|
|
export { getAbsoluteUrlOrThrow } from './url/getAbsoluteUrlOrThrow';
|
|
export { getUrlHostnameOrThrow } from './url/getUrlHostnameOrThrow';
|
|
export { isValidHostname } from './url/isValidHostname';
|
|
export { isValidUrl } from './url/isValidUrl';
|
|
export { lowercaseUrlOriginAndRemoveTrailingSlash } from './url/lowercaseUrlOriginAndRemoveTrailingSlash';
|
|
export { safeDecodeURIComponent } from './url/safeDecodeURIComponent';
|
|
export { uuidToBase36 } from './uuidToBase36';
|
|
export { assertIsDefinedOrThrow } from './validation/assertIsDefinedOrThrow';
|
|
export { isDefined } from './validation/isDefined';
|
|
export { isEmptyObject } from './validation/isEmptyObject';
|
|
export { isLabelIdentifierFieldMetadataTypes } from './validation/isLabelIdentifierFieldMetadataTypes';
|
|
export { isValidLocale } from './validation/isValidLocale';
|
|
export { isValidUuid } from './validation/isValidUuid';
|
|
export { isValidVariable } from './validation/isValidVariable';
|
|
export { normalizeLocale } from './validation/normalizeLocale';
|
|
export { getCountryCodesForCallingCode } from './validation/phones-value/getCountryCodesForCallingCode';
|
|
export { isValidCountryCode } from './validation/phones-value/isValidCountryCode';
|
|
export { resolveInput } from './variable-resolver';
|