diff --git a/packages/nodes-base/nodes/Notion/test/GenericFunctions.test.ts b/packages/nodes-base/nodes/Notion/test/GenericFunctions.test.ts index 9e83fb44869..4d4cb491af4 100644 --- a/packages/nodes-base/nodes/Notion/test/GenericFunctions.test.ts +++ b/packages/nodes-base/nodes/Notion/test/GenericFunctions.test.ts @@ -1,3 +1,4 @@ +import fc from 'fast-check'; import type { MockProxy } from 'vitest-mock-extended'; import { mock } from 'vitest-mock-extended'; import type { IExecuteFunctions, INode, INodeParameterResourceLocator } from 'n8n-workflow'; @@ -23,6 +24,7 @@ import { } from '../shared/GenericFunctions'; import { versionDescription as versionDescriptionV1 } from '../v1/VersionDescription'; import { versionDescription as versionDescriptionV2 } from '../v2/VersionDescription'; +import { wordFrom } from '@test/text-families'; import type { Mock } from 'vitest'; const collectNotionUrlExpressions = (value: unknown): string[] => { @@ -603,6 +605,15 @@ describe('Test Notion, simplifyObjects', () => { }, }); + const keyFor = (propertyName: string, nodeVersion: number, text = 'x') => { + const [result] = simplifyObjects( + [page({ [propertyName]: richText(text) })], + false, + nodeVersion, + ); + return Object.keys(result).find((key) => result[key] === text); + }; + describe('v3 keeps change-case v5 Unicode-aware keys', () => { it('preserves non-ASCII characters in simplified property keys', () => { const result = simplifyObjects([page({ Prénom: richText('Jean') })], false, 3); @@ -622,6 +633,14 @@ describe('Test Notion, simplifyObjects', () => { expect(result[0]).toHaveProperty(expectedKey, 'x'); }); + + it('keeps any accented word as its key', () => { + fc.assert( + fc.property(wordFrom('latin-accented'), (name) => { + expect(keyFor(name, 3)).toBe(`property_${name}`); + }), + ); + }); }); describe('earlier versions fold to the pre-v5 ASCII shape', () => { @@ -663,5 +682,13 @@ describe('Test Notion, simplifyObjects', () => { expect(result[0]).toHaveProperty(expectedKey, 'x'); }); + + it('folds any accented word to an ASCII key', () => { + fc.assert( + fc.property(wordFrom('latin-accented'), (name) => { + expect(keyFor(name, 2)).toMatch(/^property_[a-z_]*$/); + }), + ); + }); }); }); diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 57b196a9e14..bb1309fd7ea 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -926,6 +926,9 @@ "@types/ssh2-sftp-client": "^9.0.5", "@types/uuid": "catalog:", "@types/xml2js": "catalog:", + "@unicode/unicode-18.0.0": "2.0.7", + "confusables": "1.1.1", + "fast-check": "catalog:", "n8n-containers": "workspace:*", "n8n-core": "workspace:*", "nock": "catalog:", diff --git a/packages/nodes-base/test/text-families/index.ts b/packages/nodes-base/test/text-families/index.ts new file mode 100644 index 00000000000..9604d634ad0 --- /dev/null +++ b/packages/nodes-base/test/text-families/index.ts @@ -0,0 +1,60 @@ +import bidiControl from '@unicode/unicode-18.0.0/Binary_Property/Bidi_Control/symbols'; +import changesWhenCasemapped from '@unicode/unicode-18.0.0/Binary_Property/Changes_When_Casemapped/symbols'; +import emojiPresentation from '@unicode/unicode-18.0.0/Binary_Property/Emoji_Presentation/symbols'; +import whiteSpace from '@unicode/unicode-18.0.0/Binary_Property/White_Space/symbols'; +import combiningDiacriticalMarks from '@unicode/unicode-18.0.0/Block/Combining_Diacritical_Marks/symbols'; +import halfwidthAndFullwidthForms from '@unicode/unicode-18.0.0/Block/Halfwidth_And_Fullwidth_Forms/symbols'; +import decimalNumber from '@unicode/unicode-18.0.0/General_Category/Decimal_Number/symbols'; +import format from '@unicode/unicode-18.0.0/General_Category/Format/symbols'; +import arabic from '@unicode/unicode-18.0.0/Script/Arabic/symbols'; +import cyrillic from '@unicode/unicode-18.0.0/Script/Cyrillic/symbols'; +import devanagari from '@unicode/unicode-18.0.0/Script/Devanagari/symbols'; +import greek from '@unicode/unicode-18.0.0/Script/Greek/symbols'; +import han from '@unicode/unicode-18.0.0/Script/Han/symbols'; +import hangul from '@unicode/unicode-18.0.0/Script/Hangul/symbols'; +import hebrew from '@unicode/unicode-18.0.0/Script/Hebrew/symbols'; +import hiragana from '@unicode/unicode-18.0.0/Script/Hiragana/symbols'; +import katakana from '@unicode/unicode-18.0.0/Script/Katakana/symbols'; +import latin from '@unicode/unicode-18.0.0/Script/Latin/symbols'; +import { alphabetMap } from 'confusables'; +import fc from 'fast-check'; + +const isAscii = (text: string) => !/\P{ASCII}/u.test(text); + +const families = { + 'latin-accented': latin.filter((c) => /^\p{Ll}$/u.test(c) && !isAscii(c)), + greek, + cyrillic, + hebrew, + arabic, + devanagari, + han, + hiragana, + katakana, + hangul, + 'combining-marks': combiningDiacriticalMarks, + fullwidth: halfwidthAndFullwidthForms.filter((c) => /^[\p{L}\p{Nd}]$/u.test(c)), + /** Letters whose case mapping changes the length or does not round-trip, such as ß, fi and İ. */ + 'case-mapping': changesWhenCasemapped.filter( + (c) => + c.toUpperCase().length !== c.length || + c.toLowerCase().length !== c.length || + c.toLowerCase().toUpperCase() !== c.toUpperCase() || + c.toUpperCase().toLowerCase() !== c.toLowerCase(), + ), + emoji: emojiPresentation, + digits: decimalNumber.filter((c) => !isAscii(c)), + whitespace: whiteSpace, + /** Format characters other than bidi controls: soft hyphen, zero width space and joiners, byte order mark. */ + invisible: format.filter((c) => !bidiControl.includes(c)), + 'bidi-controls': bidiControl, + /** Non-ASCII characters the `confusables` package maps to ASCII letters. */ + confusables: [...'abcdefghijklmnopqrstuvwxyz'].flatMap((letter) => + (alphabetMap.get(letter) ?? []).filter((c) => !isAscii(c)), + ), +} satisfies Record; + +export type FamilyName = keyof typeof families; + +export const wordFrom = (family: FamilyName, minLength = 1, maxLength = 8) => + fc.string({ unit: fc.constantFrom(...families[family]), minLength, maxLength }); diff --git a/packages/nodes-base/test/text-families/unicode.d.ts b/packages/nodes-base/test/text-families/unicode.d.ts new file mode 100644 index 00000000000..21c11d4276c --- /dev/null +++ b/packages/nodes-base/test/text-families/unicode.d.ts @@ -0,0 +1,5 @@ +// The Unicode data package ships no typings for its per-property modules. +declare module '@unicode/unicode-18.0.0/*/symbols' { + const symbols: string[]; + export default symbols; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 383246bd88c..29d18b56b95 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7284,9 +7284,18 @@ importers: '@types/xml2js': specifier: 'catalog:' version: 0.4.14 + '@unicode/unicode-18.0.0': + specifier: 2.0.7 + version: 2.0.7 '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.9(@vitest/browser@4.1.10)(vitest@4.1.9) + confusables: + specifier: 1.1.1 + version: 1.1.1 + fast-check: + specifier: 'catalog:' + version: 3.23.2 n8n-containers: specifier: workspace:* version: link:../testing/containers @@ -14959,6 +14968,9 @@ packages: resolution: {integrity: sha512-jIXhD0eWQ1JA6ln/5Dltyx22UxWNrw0hZmhy2rlv6m6KgF7kplHx3g0fzi09lNmTJQRR91OlemYp3xFnvDK9og==} engines: {node: '>=20.0.0'} + '@unicode/unicode-18.0.0@2.0.7': + resolution: {integrity: sha512-sxWcUPwfbWVcSt+RPixHI468bs97A2+ftrC9b6wsGEkUTp2eod0qp7J59A+ngd4mFRISElQhPkYClhtXwNuLQw==} + '@unrs/resolver-binding-android-arm-eabi@1.9.2': resolution: {integrity: sha512-tS+lqTU3N0kkthU+rYp0spAYq15DU8ld9kXkaKg9sbQqJNF+WPMuNHZQGCgdxrUOEO0j22RKMwRVhF1HTl+X8A==} cpu: [arm] @@ -16634,6 +16646,10 @@ packages: config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + confusables@1.1.1: + resolution: {integrity: sha512-BzFtzUrufackm00Wb2zvrZV0ItRqPdWaUprU5FXHeZiJRrOWxGmXmQl/muGTF9EQl+MdBXz+Irk99meskGZmXw==} + engines: {node: '>=10', npm: '>=6'} + consola@3.4.2: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} @@ -29026,7 +29042,7 @@ snapshots: mongodb: 6.21.0(@aws-sdk/credential-providers@3.808.0)(gcp-metadata@5.3.0(supports-color@8.1.1))(socks@2.8.9) mysql2: 3.23.1(@types/node@20.19.41) pdf-parse: 2.4.5 - pg: 8.21.0 + pg: 8.21.0(pg-native@3.8.0) playwright: 1.62.1 puppeteer: 24.41.0(bufferutil@4.0.9)(supports-color@8.1.1)(typescript@7.0.2)(utf-8-validate@5.0.10) redis: 4.6.14 @@ -33694,6 +33710,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@unicode/unicode-18.0.0@2.0.7': {} + '@unrs/resolver-binding-android-arm-eabi@1.9.2': optional: true @@ -35618,6 +35636,8 @@ snapshots: ini: 1.3.8 proto-list: 1.2.4 + confusables@1.1.1: {} + consola@3.4.2: {} console-control-strings@1.1.0: @@ -41480,14 +41500,9 @@ snapshots: libpq: 1.11.0 pg-types: 2.2.0 - pg-pool@3.14.0(pg@8.21.0(pg-native@3.8.0)): - dependencies: - pg: 8.21.0(pg-native@3.8.0) - pg-pool@3.14.0(pg@8.21.0): dependencies: - pg: 8.21.0 - optional: true + pg: 8.21.0(pg-native@3.8.0) pg-promise@11.9.1(pg-native@3.8.0): dependencies: @@ -41513,21 +41528,10 @@ snapshots: postgres-date: 1.0.7 postgres-interval: 1.2.0 - pg@8.21.0: - dependencies: - pg-connection-string: 2.14.0 - pg-pool: 3.14.0(pg@8.21.0) - pg-protocol: 1.15.0 - pg-types: 2.2.0 - pgpass: 1.0.5 - optionalDependencies: - pg-cloudflare: 1.4.0 - optional: true - pg@8.21.0(pg-native@3.8.0): dependencies: pg-connection-string: 2.14.0 - pg-pool: 3.14.0(pg@8.21.0(pg-native@3.8.0)) + pg-pool: 3.14.0(pg@8.21.0) pg-protocol: 1.15.0 pg-types: 2.2.0 pgpass: 1.0.5