@open-mercato/shared 0.8.1-develop.7275.1.f772b944d4 → 0.8.1-develop.7295.1.d0e0e33014
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -1
- package/AGENTS.md +6 -3
- package/build.mjs +4 -1
- package/dist/lib/crud/errors.js +6 -1
- package/dist/lib/crud/errors.js.map +2 -2
- package/dist/lib/crud/factory.js +22 -13
- package/dist/lib/crud/factory.js.map +2 -2
- package/dist/lib/custom-fields/kinds.js +113 -0
- package/dist/lib/custom-fields/kinds.js.map +7 -0
- package/dist/lib/encryption/indexDoc.js +12 -9
- package/dist/lib/encryption/indexDoc.js.map +2 -2
- package/dist/lib/location/countries.generated.js +265 -0
- package/dist/lib/location/countries.generated.js.map +7 -0
- package/dist/lib/location/countries.js +2 -13
- package/dist/lib/location/countries.js.map +2 -2
- package/dist/lib/navigation/pageReload.js +11 -0
- package/dist/lib/navigation/pageReload.js.map +7 -0
- package/dist/lib/query/encrypted-sort.js +4 -1
- package/dist/lib/query/encrypted-sort.js.map +2 -2
- package/dist/lib/query/engine.js +97 -10
- package/dist/lib/query/engine.js.map +3 -3
- package/dist/lib/schedule/interval.js +33 -0
- package/dist/lib/schedule/interval.js.map +7 -0
- package/dist/lib/schedule/invalidScheduleValue.js +19 -0
- package/dist/lib/schedule/invalidScheduleValue.js.map +7 -0
- package/dist/lib/search/config.js.map +2 -2
- package/dist/lib/search/containment.js +32 -0
- package/dist/lib/search/containment.js.map +7 -0
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/package.json +3 -2
- package/scripts/generate-countries.mjs +75 -0
- package/src/lib/crud/__tests__/crud-factory.test.ts +177 -0
- package/src/lib/crud/__tests__/errors.test.ts +31 -1
- package/src/lib/crud/errors.ts +16 -0
- package/src/lib/crud/factory.ts +37 -15
- package/src/lib/custom-fields/__tests__/kinds.test.ts +208 -0
- package/src/lib/custom-fields/kinds.ts +205 -0
- package/src/lib/encryption/__tests__/indexDoc.custom-field-kinds.test.ts +127 -0
- package/src/lib/encryption/__tests__/indexDoc.test.ts +39 -0
- package/src/lib/encryption/indexDoc.ts +16 -7
- package/src/lib/location/__tests__/countries.test.ts +25 -0
- package/src/lib/location/countries.generated.ts +264 -0
- package/src/lib/location/countries.ts +2 -23
- package/src/lib/navigation/__tests__/pageReload.test.ts +58 -0
- package/src/lib/navigation/pageReload.ts +43 -0
- package/src/lib/query/__tests__/engine.test.ts +208 -16
- package/src/lib/query/encrypted-sort.ts +8 -1
- package/src/lib/query/engine.ts +172 -19
- package/src/lib/schedule/__tests__/interval.test.ts +37 -0
- package/src/lib/schedule/interval.ts +45 -0
- package/src/lib/schedule/invalidScheduleValue.ts +33 -0
- package/src/lib/search/__tests__/config.test.ts +27 -0
- package/src/lib/search/__tests__/containment.test.ts +61 -0
- package/src/lib/search/config.ts +28 -6
- package/src/lib/search/containment.ts +66 -0
|
@@ -39,6 +39,45 @@ describe('encryption/indexDoc', () => {
|
|
|
39
39
|
expect(decryptCustomFieldValueMock).toHaveBeenCalled()
|
|
40
40
|
})
|
|
41
41
|
|
|
42
|
+
test('decryptIndexDocCustomFields passes each key its resolved kind (#5968)', async () => {
|
|
43
|
+
const doc = { id: '1', 'cf:note': 'enc', 'cf:count': 'enc', cf_order_ref: 'enc' }
|
|
44
|
+
const kinds = { note: 'text', count: 'integer', 'order-ref': 'text', order_ref: 'text' }
|
|
45
|
+
|
|
46
|
+
await decryptIndexDocCustomFields(doc, { tenantId: 't1', organizationId: 'org1' }, {} as any, undefined, kinds)
|
|
47
|
+
|
|
48
|
+
expect(decryptCustomFieldValueMock).toHaveBeenCalledTimes(3)
|
|
49
|
+
const kindsPassed = decryptCustomFieldValueMock.mock.calls.map((call) => call[4]?.kind).sort()
|
|
50
|
+
expect(kindsPassed).toEqual(['integer', 'text', 'text'])
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('decryptIndexDocCustomFields passes a null kind when no map is supplied (#5968)', async () => {
|
|
54
|
+
const doc = { id: '1', 'cf:note': 'enc' }
|
|
55
|
+
|
|
56
|
+
await decryptIndexDocCustomFields(doc, { tenantId: 't1', organizationId: 'org1' }, {} as any)
|
|
57
|
+
|
|
58
|
+
expect(decryptCustomFieldValueMock).toHaveBeenCalledWith('enc', 't1', {}, undefined, { kind: null })
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('decryptIndexDocForSearch forwards the kind map to the cf decryption (#5968)', async () => {
|
|
62
|
+
const service = {
|
|
63
|
+
isEnabled: () => true,
|
|
64
|
+
decryptEntityPayload: jest.fn(async () => ({})),
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await decryptIndexDocForSearch(
|
|
68
|
+
'example:todo',
|
|
69
|
+
{ id: '1', 'cf:note': 'enc' },
|
|
70
|
+
{ tenantId: 't1', organizationId: 'org1' },
|
|
71
|
+
service as any,
|
|
72
|
+
undefined,
|
|
73
|
+
{ note: 'multiline' },
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
expect(decryptCustomFieldValueMock).toHaveBeenCalledWith(
|
|
77
|
+
'enc', 't1', service, undefined, { kind: 'multiline' },
|
|
78
|
+
)
|
|
79
|
+
})
|
|
80
|
+
|
|
42
81
|
test('decryptIndexDocForSearch merges decrypted entity payload and decrypts cf keys', async () => {
|
|
43
82
|
decryptCustomFieldValueMock.mockImplementation(async (value: unknown) => (value === 'enc' ? 'dec' : value))
|
|
44
83
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { decryptCustomFieldValue } from './customFieldValues'
|
|
2
|
+
import { resolveCustomFieldKind, type CustomFieldKindMap } from '../custom-fields/kinds'
|
|
2
3
|
import type { TenantDataEncryptionService } from './tenantDataEncryptionService'
|
|
3
4
|
|
|
4
5
|
export type IndexDocScope = {
|
|
@@ -10,12 +11,15 @@ async function decryptValue(
|
|
|
10
11
|
value: unknown,
|
|
11
12
|
scope: IndexDocScope,
|
|
12
13
|
service: TenantDataEncryptionService | null,
|
|
13
|
-
cache
|
|
14
|
+
cache: Map<string | null, string | null> | undefined,
|
|
15
|
+
kind: string | null,
|
|
14
16
|
): Promise<unknown> {
|
|
15
17
|
if (Array.isArray(value)) {
|
|
16
|
-
return Promise.all(
|
|
18
|
+
return Promise.all(
|
|
19
|
+
value.map((entry) => decryptCustomFieldValue(entry, scope.tenantId, service, cache, { kind })),
|
|
20
|
+
)
|
|
17
21
|
}
|
|
18
|
-
return decryptCustomFieldValue(value, scope.tenantId, service, cache)
|
|
22
|
+
return decryptCustomFieldValue(value, scope.tenantId, service, cache, { kind })
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
export async function decryptIndexDocCustomFields(
|
|
@@ -23,6 +27,7 @@ export async function decryptIndexDocCustomFields(
|
|
|
23
27
|
scope: IndexDocScope,
|
|
24
28
|
service: TenantDataEncryptionService | null,
|
|
25
29
|
cache?: Map<string | null, string | null>,
|
|
30
|
+
kinds?: CustomFieldKindMap | null,
|
|
26
31
|
): Promise<Record<string, unknown>> {
|
|
27
32
|
// HybridQueryEngine aliases cf keys as `cf_<key>` (sanitized), while index docs use `cf:<key>`.
|
|
28
33
|
// Support both shapes to keep decryption consistent across query paths.
|
|
@@ -33,7 +38,10 @@ export async function decryptIndexDocCustomFields(
|
|
|
33
38
|
await Promise.all(
|
|
34
39
|
keys.map(async (key) => {
|
|
35
40
|
try {
|
|
36
|
-
|
|
41
|
+
// Without the field's kind, `decryptCustomFieldValue` falls back to `JSON.parse` and
|
|
42
|
+
// retypes string-typed values whose plaintext looks like JSON — `"123"` becomes 123
|
|
43
|
+
// and `"true"` becomes true (issue #5968). An unresolved kind keeps the legacy path.
|
|
44
|
+
working[key] = await decryptValue(working[key], scope, service, cache, resolveCustomFieldKind(kinds, key))
|
|
37
45
|
} catch {
|
|
38
46
|
// ignore; keep original value
|
|
39
47
|
}
|
|
@@ -48,12 +56,13 @@ export async function decryptIndexDocForSearch(
|
|
|
48
56
|
scope: IndexDocScope,
|
|
49
57
|
service: TenantDataEncryptionService | null,
|
|
50
58
|
cache?: Map<string | null, string | null>,
|
|
59
|
+
kinds?: CustomFieldKindMap | null,
|
|
51
60
|
): Promise<Record<string, unknown>> {
|
|
52
61
|
if (!service || typeof service.decryptEntityPayload !== 'function') {
|
|
53
|
-
return decryptIndexDocCustomFields(doc, scope, service, cache)
|
|
62
|
+
return decryptIndexDocCustomFields(doc, scope, service, cache, kinds)
|
|
54
63
|
}
|
|
55
64
|
if (service.isEnabled?.() === false) {
|
|
56
|
-
return decryptIndexDocCustomFields(doc, scope, service, cache)
|
|
65
|
+
return decryptIndexDocCustomFields(doc, scope, service, cache, kinds)
|
|
57
66
|
}
|
|
58
67
|
|
|
59
68
|
let working: Record<string, unknown> = doc
|
|
@@ -72,7 +81,7 @@ export async function decryptIndexDocForSearch(
|
|
|
72
81
|
await decryptEntity('customers:customer_entity')
|
|
73
82
|
}
|
|
74
83
|
|
|
75
|
-
return decryptIndexDocCustomFields(working, scope, service, cache)
|
|
84
|
+
return decryptIndexDocCustomFields(working, scope, service, cache, kinds)
|
|
76
85
|
}
|
|
77
86
|
|
|
78
87
|
export async function encryptIndexDocForStorage(
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
1
3
|
import {
|
|
2
4
|
COUNTRY_PRIORITY,
|
|
3
5
|
ISO_COUNTRIES,
|
|
@@ -6,7 +8,30 @@ import {
|
|
|
6
8
|
resolveCountryName,
|
|
7
9
|
} from '../countries'
|
|
8
10
|
|
|
11
|
+
const PACKAGE_ROOT = join(__dirname, '..', '..', '..', '..')
|
|
12
|
+
const DIST_COUNTRIES = join(PACKAGE_ROOT, 'dist/lib/location/countries.js')
|
|
13
|
+
const DIST_GENERATED = join(PACKAGE_ROOT, 'dist/lib/location/countries.generated.js')
|
|
14
|
+
|
|
9
15
|
describe('ISO_COUNTRIES', () => {
|
|
16
|
+
it('does not import language-subtag-registry at runtime (Node ESM / production)', () => {
|
|
17
|
+
const source = readFileSync(join(__dirname, '../countries.ts'), 'utf8')
|
|
18
|
+
expect(source).not.toMatch(/language-subtag-registry/)
|
|
19
|
+
const generated = readFileSync(join(__dirname, '../countries.generated.ts'), 'utf8')
|
|
20
|
+
expect(generated).toMatch(/AUTO-GENERATED/)
|
|
21
|
+
expect(generated).toMatch(/code: "PL"/)
|
|
22
|
+
expect(generated).toMatch(/code: "DE"/)
|
|
23
|
+
|
|
24
|
+
expect(existsSync(DIST_COUNTRIES)).toBe(true)
|
|
25
|
+
expect(existsSync(DIST_GENERATED)).toBe(true)
|
|
26
|
+
const distCountries = readFileSync(DIST_COUNTRIES, 'utf8')
|
|
27
|
+
const distGenerated = readFileSync(DIST_GENERATED, 'utf8')
|
|
28
|
+
expect(distCountries).not.toMatch(/language-subtag-registry/)
|
|
29
|
+
expect(distGenerated).not.toMatch(/language-subtag-registry/)
|
|
30
|
+
expect(distCountries).toMatch(/from "\.\/countries\.generated\.js"/)
|
|
31
|
+
expect(distGenerated).toMatch(/code: "PL"/)
|
|
32
|
+
expect(distGenerated).toMatch(/code: "DE"/)
|
|
33
|
+
})
|
|
34
|
+
|
|
10
35
|
it('includes Kosovo, which the language-subtag registry does not list', () => {
|
|
11
36
|
const kosovo = ISO_COUNTRIES.filter((entry) => entry.code === 'XK')
|
|
12
37
|
expect(kosovo).toEqual([{ code: 'XK', name: 'Kosovo' }])
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// AUTO-GENERATED by scripts/generate-countries.mjs — do not edit by hand.
|
|
2
|
+
// Regenerate via `node packages/shared/scripts/generate-countries.mjs` or the shared package build.
|
|
3
|
+
|
|
4
|
+
export const REGISTRY_COUNTRIES: Array<{ code: string; name: string }> = [
|
|
5
|
+
{ code: "AC", name: "Ascension Island" },
|
|
6
|
+
{ code: "AD", name: "Andorra" },
|
|
7
|
+
{ code: "AE", name: "United Arab Emirates" },
|
|
8
|
+
{ code: "AF", name: "Afghanistan" },
|
|
9
|
+
{ code: "AG", name: "Antigua and Barbuda" },
|
|
10
|
+
{ code: "AI", name: "Anguilla" },
|
|
11
|
+
{ code: "AL", name: "Albania" },
|
|
12
|
+
{ code: "AM", name: "Armenia" },
|
|
13
|
+
{ code: "AO", name: "Angola" },
|
|
14
|
+
{ code: "AQ", name: "Antarctica" },
|
|
15
|
+
{ code: "AR", name: "Argentina" },
|
|
16
|
+
{ code: "AS", name: "American Samoa" },
|
|
17
|
+
{ code: "AT", name: "Austria" },
|
|
18
|
+
{ code: "AU", name: "Australia" },
|
|
19
|
+
{ code: "AW", name: "Aruba" },
|
|
20
|
+
{ code: "AX", name: "Åland Islands" },
|
|
21
|
+
{ code: "AZ", name: "Azerbaijan" },
|
|
22
|
+
{ code: "BA", name: "Bosnia and Herzegovina" },
|
|
23
|
+
{ code: "BB", name: "Barbados" },
|
|
24
|
+
{ code: "BD", name: "Bangladesh" },
|
|
25
|
+
{ code: "BE", name: "Belgium" },
|
|
26
|
+
{ code: "BF", name: "Burkina Faso" },
|
|
27
|
+
{ code: "BG", name: "Bulgaria" },
|
|
28
|
+
{ code: "BH", name: "Bahrain" },
|
|
29
|
+
{ code: "BI", name: "Burundi" },
|
|
30
|
+
{ code: "BJ", name: "Benin" },
|
|
31
|
+
{ code: "BL", name: "Saint Barthélemy" },
|
|
32
|
+
{ code: "BM", name: "Bermuda" },
|
|
33
|
+
{ code: "BN", name: "Brunei Darussalam" },
|
|
34
|
+
{ code: "BO", name: "Bolivia" },
|
|
35
|
+
{ code: "BQ", name: "Bonaire, Sint Eustatius and Saba" },
|
|
36
|
+
{ code: "BR", name: "Brazil" },
|
|
37
|
+
{ code: "BS", name: "Bahamas" },
|
|
38
|
+
{ code: "BT", name: "Bhutan" },
|
|
39
|
+
{ code: "BV", name: "Bouvet Island" },
|
|
40
|
+
{ code: "BW", name: "Botswana" },
|
|
41
|
+
{ code: "BY", name: "Belarus" },
|
|
42
|
+
{ code: "BZ", name: "Belize" },
|
|
43
|
+
{ code: "CA", name: "Canada" },
|
|
44
|
+
{ code: "CC", name: "Cocos (Keeling) Islands" },
|
|
45
|
+
{ code: "CD", name: "The Democratic Republic of the Congo" },
|
|
46
|
+
{ code: "CF", name: "Central African Republic" },
|
|
47
|
+
{ code: "CG", name: "Congo" },
|
|
48
|
+
{ code: "CH", name: "Switzerland" },
|
|
49
|
+
{ code: "CI", name: "Côte d'Ivoire" },
|
|
50
|
+
{ code: "CK", name: "Cook Islands" },
|
|
51
|
+
{ code: "CL", name: "Chile" },
|
|
52
|
+
{ code: "CM", name: "Cameroon" },
|
|
53
|
+
{ code: "CN", name: "China" },
|
|
54
|
+
{ code: "CO", name: "Colombia" },
|
|
55
|
+
{ code: "CP", name: "Clipperton Island" },
|
|
56
|
+
{ code: "CQ", name: "Sark" },
|
|
57
|
+
{ code: "CR", name: "Costa Rica" },
|
|
58
|
+
{ code: "CU", name: "Cuba" },
|
|
59
|
+
{ code: "CV", name: "Cabo Verde, Cape Verde" },
|
|
60
|
+
{ code: "CW", name: "Curaçao" },
|
|
61
|
+
{ code: "CX", name: "Christmas Island" },
|
|
62
|
+
{ code: "CY", name: "Cyprus" },
|
|
63
|
+
{ code: "CZ", name: "Czechia, Czech Republic" },
|
|
64
|
+
{ code: "DE", name: "Germany" },
|
|
65
|
+
{ code: "DG", name: "Diego Garcia" },
|
|
66
|
+
{ code: "DJ", name: "Djibouti" },
|
|
67
|
+
{ code: "DK", name: "Denmark" },
|
|
68
|
+
{ code: "DM", name: "Dominica" },
|
|
69
|
+
{ code: "DO", name: "Dominican Republic" },
|
|
70
|
+
{ code: "DZ", name: "Algeria" },
|
|
71
|
+
{ code: "EA", name: "Ceuta, Melilla" },
|
|
72
|
+
{ code: "EC", name: "Ecuador" },
|
|
73
|
+
{ code: "EE", name: "Estonia" },
|
|
74
|
+
{ code: "EG", name: "Egypt" },
|
|
75
|
+
{ code: "EH", name: "Western Sahara" },
|
|
76
|
+
{ code: "ER", name: "Eritrea" },
|
|
77
|
+
{ code: "ES", name: "Spain" },
|
|
78
|
+
{ code: "ET", name: "Ethiopia" },
|
|
79
|
+
{ code: "EU", name: "European Union" },
|
|
80
|
+
{ code: "EZ", name: "Eurozone" },
|
|
81
|
+
{ code: "FI", name: "Finland" },
|
|
82
|
+
{ code: "FJ", name: "Fiji" },
|
|
83
|
+
{ code: "FK", name: "Falkland Islands (Malvinas)" },
|
|
84
|
+
{ code: "FM", name: "Federated States of Micronesia" },
|
|
85
|
+
{ code: "FO", name: "Faroe Islands" },
|
|
86
|
+
{ code: "FR", name: "France" },
|
|
87
|
+
{ code: "GA", name: "Gabon" },
|
|
88
|
+
{ code: "GB", name: "United Kingdom" },
|
|
89
|
+
{ code: "GD", name: "Grenada" },
|
|
90
|
+
{ code: "GE", name: "Georgia" },
|
|
91
|
+
{ code: "GF", name: "French Guiana" },
|
|
92
|
+
{ code: "GG", name: "Guernsey" },
|
|
93
|
+
{ code: "GH", name: "Ghana" },
|
|
94
|
+
{ code: "GI", name: "Gibraltar" },
|
|
95
|
+
{ code: "GL", name: "Greenland" },
|
|
96
|
+
{ code: "GM", name: "Gambia" },
|
|
97
|
+
{ code: "GN", name: "Guinea" },
|
|
98
|
+
{ code: "GP", name: "Guadeloupe" },
|
|
99
|
+
{ code: "GQ", name: "Equatorial Guinea" },
|
|
100
|
+
{ code: "GR", name: "Greece" },
|
|
101
|
+
{ code: "GS", name: "South Georgia and the South Sandwich Islands" },
|
|
102
|
+
{ code: "GT", name: "Guatemala" },
|
|
103
|
+
{ code: "GU", name: "Guam" },
|
|
104
|
+
{ code: "GW", name: "Guinea-Bissau" },
|
|
105
|
+
{ code: "GY", name: "Guyana" },
|
|
106
|
+
{ code: "HK", name: "Hong Kong" },
|
|
107
|
+
{ code: "HM", name: "Heard Island and McDonald Islands" },
|
|
108
|
+
{ code: "HN", name: "Honduras" },
|
|
109
|
+
{ code: "HR", name: "Croatia" },
|
|
110
|
+
{ code: "HT", name: "Haiti" },
|
|
111
|
+
{ code: "HU", name: "Hungary" },
|
|
112
|
+
{ code: "IC", name: "Canary Islands" },
|
|
113
|
+
{ code: "ID", name: "Indonesia" },
|
|
114
|
+
{ code: "IE", name: "Ireland" },
|
|
115
|
+
{ code: "IL", name: "Israel" },
|
|
116
|
+
{ code: "IM", name: "Isle of Man" },
|
|
117
|
+
{ code: "IN", name: "India" },
|
|
118
|
+
{ code: "IO", name: "British Indian Ocean Territory" },
|
|
119
|
+
{ code: "IQ", name: "Iraq" },
|
|
120
|
+
{ code: "IR", name: "Islamic Republic of Iran" },
|
|
121
|
+
{ code: "IS", name: "Iceland" },
|
|
122
|
+
{ code: "IT", name: "Italy" },
|
|
123
|
+
{ code: "JE", name: "Jersey" },
|
|
124
|
+
{ code: "JM", name: "Jamaica" },
|
|
125
|
+
{ code: "JO", name: "Jordan" },
|
|
126
|
+
{ code: "JP", name: "Japan" },
|
|
127
|
+
{ code: "KE", name: "Kenya" },
|
|
128
|
+
{ code: "KG", name: "Kyrgyzstan" },
|
|
129
|
+
{ code: "KH", name: "Cambodia" },
|
|
130
|
+
{ code: "KI", name: "Kiribati" },
|
|
131
|
+
{ code: "KM", name: "Comoros" },
|
|
132
|
+
{ code: "KN", name: "Saint Kitts and Nevis" },
|
|
133
|
+
{ code: "KP", name: "Democratic People's Republic of Korea" },
|
|
134
|
+
{ code: "KR", name: "Republic of Korea" },
|
|
135
|
+
{ code: "KW", name: "Kuwait" },
|
|
136
|
+
{ code: "KY", name: "Cayman Islands" },
|
|
137
|
+
{ code: "KZ", name: "Kazakhstan" },
|
|
138
|
+
{ code: "LA", name: "Lao People's Democratic Republic" },
|
|
139
|
+
{ code: "LB", name: "Lebanon" },
|
|
140
|
+
{ code: "LC", name: "Saint Lucia" },
|
|
141
|
+
{ code: "LI", name: "Liechtenstein" },
|
|
142
|
+
{ code: "LK", name: "Sri Lanka" },
|
|
143
|
+
{ code: "LR", name: "Liberia" },
|
|
144
|
+
{ code: "LS", name: "Lesotho" },
|
|
145
|
+
{ code: "LT", name: "Lithuania" },
|
|
146
|
+
{ code: "LU", name: "Luxembourg" },
|
|
147
|
+
{ code: "LV", name: "Latvia" },
|
|
148
|
+
{ code: "LY", name: "Libya" },
|
|
149
|
+
{ code: "MA", name: "Morocco" },
|
|
150
|
+
{ code: "MC", name: "Monaco" },
|
|
151
|
+
{ code: "MD", name: "Moldova" },
|
|
152
|
+
{ code: "ME", name: "Montenegro" },
|
|
153
|
+
{ code: "MF", name: "Saint Martin (French part)" },
|
|
154
|
+
{ code: "MG", name: "Madagascar" },
|
|
155
|
+
{ code: "MH", name: "Marshall Islands" },
|
|
156
|
+
{ code: "MK", name: "North Macedonia" },
|
|
157
|
+
{ code: "ML", name: "Mali" },
|
|
158
|
+
{ code: "MM", name: "Myanmar" },
|
|
159
|
+
{ code: "MN", name: "Mongolia" },
|
|
160
|
+
{ code: "MO", name: "Macao" },
|
|
161
|
+
{ code: "MP", name: "Northern Mariana Islands" },
|
|
162
|
+
{ code: "MQ", name: "Martinique" },
|
|
163
|
+
{ code: "MR", name: "Mauritania" },
|
|
164
|
+
{ code: "MS", name: "Montserrat" },
|
|
165
|
+
{ code: "MT", name: "Malta" },
|
|
166
|
+
{ code: "MU", name: "Mauritius" },
|
|
167
|
+
{ code: "MV", name: "Maldives" },
|
|
168
|
+
{ code: "MW", name: "Malawi" },
|
|
169
|
+
{ code: "MX", name: "Mexico" },
|
|
170
|
+
{ code: "MY", name: "Malaysia" },
|
|
171
|
+
{ code: "MZ", name: "Mozambique" },
|
|
172
|
+
{ code: "NA", name: "Namibia" },
|
|
173
|
+
{ code: "NC", name: "New Caledonia" },
|
|
174
|
+
{ code: "NE", name: "Niger" },
|
|
175
|
+
{ code: "NF", name: "Norfolk Island" },
|
|
176
|
+
{ code: "NG", name: "Nigeria" },
|
|
177
|
+
{ code: "NI", name: "Nicaragua" },
|
|
178
|
+
{ code: "NL", name: "Netherlands" },
|
|
179
|
+
{ code: "NO", name: "Norway" },
|
|
180
|
+
{ code: "NP", name: "Nepal" },
|
|
181
|
+
{ code: "NR", name: "Nauru" },
|
|
182
|
+
{ code: "NU", name: "Niue" },
|
|
183
|
+
{ code: "NZ", name: "New Zealand" },
|
|
184
|
+
{ code: "OM", name: "Oman" },
|
|
185
|
+
{ code: "PA", name: "Panama" },
|
|
186
|
+
{ code: "PE", name: "Peru" },
|
|
187
|
+
{ code: "PF", name: "French Polynesia" },
|
|
188
|
+
{ code: "PG", name: "Papua New Guinea" },
|
|
189
|
+
{ code: "PH", name: "Philippines" },
|
|
190
|
+
{ code: "PK", name: "Pakistan" },
|
|
191
|
+
{ code: "PL", name: "Poland" },
|
|
192
|
+
{ code: "PM", name: "Saint Pierre and Miquelon" },
|
|
193
|
+
{ code: "PN", name: "Pitcairn" },
|
|
194
|
+
{ code: "PR", name: "Puerto Rico" },
|
|
195
|
+
{ code: "PS", name: "State of Palestine" },
|
|
196
|
+
{ code: "PT", name: "Portugal" },
|
|
197
|
+
{ code: "PW", name: "Palau" },
|
|
198
|
+
{ code: "PY", name: "Paraguay" },
|
|
199
|
+
{ code: "QA", name: "Qatar" },
|
|
200
|
+
{ code: "RE", name: "Réunion" },
|
|
201
|
+
{ code: "RO", name: "Romania" },
|
|
202
|
+
{ code: "RS", name: "Serbia" },
|
|
203
|
+
{ code: "RU", name: "Russian Federation" },
|
|
204
|
+
{ code: "RW", name: "Rwanda" },
|
|
205
|
+
{ code: "SA", name: "Saudi Arabia" },
|
|
206
|
+
{ code: "SB", name: "Solomon Islands" },
|
|
207
|
+
{ code: "SC", name: "Seychelles" },
|
|
208
|
+
{ code: "SD", name: "Sudan" },
|
|
209
|
+
{ code: "SE", name: "Sweden" },
|
|
210
|
+
{ code: "SG", name: "Singapore" },
|
|
211
|
+
{ code: "SH", name: "Saint Helena, Ascension and Tristan da Cunha" },
|
|
212
|
+
{ code: "SI", name: "Slovenia" },
|
|
213
|
+
{ code: "SJ", name: "Svalbard and Jan Mayen" },
|
|
214
|
+
{ code: "SK", name: "Slovakia" },
|
|
215
|
+
{ code: "SL", name: "Sierra Leone" },
|
|
216
|
+
{ code: "SM", name: "San Marino" },
|
|
217
|
+
{ code: "SN", name: "Senegal" },
|
|
218
|
+
{ code: "SO", name: "Somalia" },
|
|
219
|
+
{ code: "SR", name: "Suriname" },
|
|
220
|
+
{ code: "SS", name: "South Sudan" },
|
|
221
|
+
{ code: "ST", name: "Sao Tome and Principe" },
|
|
222
|
+
{ code: "SV", name: "El Salvador" },
|
|
223
|
+
{ code: "SX", name: "Sint Maarten (Dutch part)" },
|
|
224
|
+
{ code: "SY", name: "Syrian Arab Republic" },
|
|
225
|
+
{ code: "SZ", name: "Eswatini, eSwatini, Swaziland" },
|
|
226
|
+
{ code: "TA", name: "Tristan da Cunha" },
|
|
227
|
+
{ code: "TC", name: "Turks and Caicos Islands" },
|
|
228
|
+
{ code: "TD", name: "Chad" },
|
|
229
|
+
{ code: "TF", name: "French Southern Territories" },
|
|
230
|
+
{ code: "TG", name: "Togo" },
|
|
231
|
+
{ code: "TH", name: "Thailand" },
|
|
232
|
+
{ code: "TJ", name: "Tajikistan" },
|
|
233
|
+
{ code: "TK", name: "Tokelau" },
|
|
234
|
+
{ code: "TL", name: "Timor-Leste" },
|
|
235
|
+
{ code: "TM", name: "Turkmenistan" },
|
|
236
|
+
{ code: "TN", name: "Tunisia" },
|
|
237
|
+
{ code: "TO", name: "Tonga" },
|
|
238
|
+
{ code: "TR", name: "Türkiye, Turkey" },
|
|
239
|
+
{ code: "TT", name: "Trinidad and Tobago" },
|
|
240
|
+
{ code: "TV", name: "Tuvalu" },
|
|
241
|
+
{ code: "TW", name: "Taiwan, Province of China" },
|
|
242
|
+
{ code: "TZ", name: "United Republic of Tanzania" },
|
|
243
|
+
{ code: "UA", name: "Ukraine" },
|
|
244
|
+
{ code: "UG", name: "Uganda" },
|
|
245
|
+
{ code: "UM", name: "United States Minor Outlying Islands" },
|
|
246
|
+
{ code: "UN", name: "United Nations" },
|
|
247
|
+
{ code: "US", name: "United States" },
|
|
248
|
+
{ code: "UY", name: "Uruguay" },
|
|
249
|
+
{ code: "UZ", name: "Uzbekistan" },
|
|
250
|
+
{ code: "VA", name: "Holy See (Vatican City State)" },
|
|
251
|
+
{ code: "VC", name: "Saint Vincent and the Grenadines" },
|
|
252
|
+
{ code: "VE", name: "Venezuela" },
|
|
253
|
+
{ code: "VG", name: "British Virgin Islands" },
|
|
254
|
+
{ code: "VI", name: "U.S. Virgin Islands" },
|
|
255
|
+
{ code: "VN", name: "Viet Nam" },
|
|
256
|
+
{ code: "VU", name: "Vanuatu" },
|
|
257
|
+
{ code: "WF", name: "Wallis and Futuna" },
|
|
258
|
+
{ code: "WS", name: "Samoa" },
|
|
259
|
+
{ code: "YE", name: "Yemen" },
|
|
260
|
+
{ code: "YT", name: "Mayotte" },
|
|
261
|
+
{ code: "ZA", name: "South Africa" },
|
|
262
|
+
{ code: "ZM", name: "Zambia" },
|
|
263
|
+
{ code: "ZW", name: "Zimbabwe" },
|
|
264
|
+
]
|
|
@@ -1,32 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
type RegistryEntry = {
|
|
4
|
-
Type: string
|
|
5
|
-
Subtag?: string
|
|
6
|
-
Description?: string[]
|
|
7
|
-
Deprecated?: string
|
|
8
|
-
}
|
|
1
|
+
import { REGISTRY_COUNTRIES } from './countries.generated'
|
|
9
2
|
|
|
10
3
|
export type IsoCountry = {
|
|
11
4
|
code: string
|
|
12
5
|
name: string
|
|
13
6
|
}
|
|
14
7
|
|
|
15
|
-
|
|
16
|
-
if (entry.Type !== 'region') return false
|
|
17
|
-
if (!entry.Subtag || !/^[A-Z]{2}$/.test(entry.Subtag)) return false
|
|
18
|
-
if (entry.Deprecated) return false
|
|
19
|
-
if (!entry.Description || !entry.Description.length) return false
|
|
20
|
-
if (entry.Description[0] === 'Private use') return false
|
|
21
|
-
return true
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const RAW_COUNTRIES: IsoCountry[] = (registry as RegistryEntry[])
|
|
25
|
-
.filter(isIsoAlpha2)
|
|
26
|
-
.map((entry) => ({
|
|
27
|
-
code: entry.Subtag,
|
|
28
|
-
name: entry.Description.join(', '),
|
|
29
|
-
}))
|
|
8
|
+
const RAW_COUNTRIES: IsoCountry[] = REGISTRY_COUNTRIES
|
|
30
9
|
|
|
31
10
|
// Codes in common use that the language-subtag registry does not list as region subtags.
|
|
32
11
|
// XK is user-assigned rather than ISO 3166-1 assigned, so it never appears in the registry.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* These helpers exist so a boundary-crossing portal navigation re-runs the server layout that
|
|
3
|
+
* computes the portal chrome and the session. Which document API each one uses is the whole
|
|
4
|
+
* contract: `assign` keeps the current page in session history, `replace` does not, so they are
|
|
5
|
+
* the document-loading counterparts of `router.push` and `router.replace` respectively. Swapping
|
|
6
|
+
* one for the other is invisible on screen and only surfaces as a broken Back button, so pin both.
|
|
7
|
+
*
|
|
8
|
+
* This runs in the node environment on purpose: jsdom exposes `window.location` as a
|
|
9
|
+
* non-configurable getter whose `assign`/`replace` are non-writable, so the real navigation calls
|
|
10
|
+
* cannot be observed there. A synthetic `window` lets the helper bodies run for real.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const assign = jest.fn()
|
|
14
|
+
const replace = jest.fn()
|
|
15
|
+
|
|
16
|
+
describe('page reload navigation helpers', () => {
|
|
17
|
+
let navigateWithPageReload: (path: string) => void
|
|
18
|
+
let replaceWithPageReload: (path: string) => void
|
|
19
|
+
|
|
20
|
+
beforeAll(() => {
|
|
21
|
+
Object.defineProperty(globalThis, 'window', {
|
|
22
|
+
configurable: true,
|
|
23
|
+
writable: true,
|
|
24
|
+
value: { location: { assign, replace } },
|
|
25
|
+
})
|
|
26
|
+
// Imported after the synthetic window exists, so the module resolves against it.
|
|
27
|
+
const helpers = require('../pageReload')
|
|
28
|
+
navigateWithPageReload = helpers.navigateWithPageReload
|
|
29
|
+
replaceWithPageReload = helpers.replaceWithPageReload
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
afterAll(() => {
|
|
33
|
+
delete (globalThis as { window?: unknown }).window
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
assign.mockReset()
|
|
38
|
+
replace.mockReset()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('navigateWithPageReload', () => {
|
|
42
|
+
it('loads the path and keeps the current page in session history', () => {
|
|
43
|
+
navigateWithPageReload('/acme/portal/dashboard')
|
|
44
|
+
|
|
45
|
+
expect(assign).toHaveBeenCalledWith('/acme/portal/dashboard')
|
|
46
|
+
expect(replace).not.toHaveBeenCalled()
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('replaceWithPageReload', () => {
|
|
51
|
+
it('loads the path without leaving the redirecting page in session history', () => {
|
|
52
|
+
replaceWithPageReload('/acme/portal/login')
|
|
53
|
+
|
|
54
|
+
expect(replace).toHaveBeenCalledWith('/acme/portal/login')
|
|
55
|
+
expect(assign).not.toHaveBeenCalled()
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boundary-crossing navigation helpers: they reload the document instead of handing the route to
|
|
3
|
+
* the client router.
|
|
4
|
+
*
|
|
5
|
+
* The portal's `(frontend)` layout — the server component that decides the portal chrome and
|
|
6
|
+
* resolves the customer session — sits *above* the `[...slug]` segment so portal navigation does
|
|
7
|
+
* not remount the client subtree. A client-side `router.push`/`router.replace` therefore never
|
|
8
|
+
* re-runs that layout, and any navigation that crosses the public/authenticated boundary would
|
|
9
|
+
* keep rendering chrome computed for the page it came from: authenticated chrome over the login
|
|
10
|
+
* page after a logout, or logged-out chrome over the dashboard after a login.
|
|
11
|
+
*
|
|
12
|
+
* Use these helpers for every boundary-crossing navigation (login, logout, signup completion,
|
|
13
|
+
* invite acceptance, auth guards redirecting to login) so the layout re-runs and recomputes both
|
|
14
|
+
* the chrome and the session. Same-side navigation should keep using the client router.
|
|
15
|
+
*
|
|
16
|
+
* Pick the one that matches the client-router call it stands in for, so session history behaves
|
|
17
|
+
* the same way: `navigateWithPageReload` for `router.push`, `replaceWithPageReload` for
|
|
18
|
+
* `router.replace`.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Full page load that adds a session-history entry — the document-loading counterpart of `router.push`. */
|
|
22
|
+
export function navigateWithPageReload(path: string): void {
|
|
23
|
+
window.location.assign(path)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Full page load that replaces the current session-history entry — the document-loading
|
|
28
|
+
* counterpart of `router.replace`.
|
|
29
|
+
*
|
|
30
|
+
* Use this where the client-router call being replaced was a `router.replace`: an auth guard
|
|
31
|
+
* sending an unauthenticated visitor to the login page, or the portal landing page forwarding a
|
|
32
|
+
* signed-in customer to the dashboard.
|
|
33
|
+
*
|
|
34
|
+
* Note that for those call sites specifically, `navigateWithPageReload` was measured to behave
|
|
35
|
+
* identically: they redirect from an effect during page load, and a browser treats a navigation
|
|
36
|
+
* started then as a client redirect, replacing the entry rather than pushing it whichever method
|
|
37
|
+
* is used. This helper exists so the call site states which semantics it means instead of relying
|
|
38
|
+
* on that heuristic — and so a redirect that later moves behind a user gesture, where the
|
|
39
|
+
* distinction does bite, keeps the behaviour it was written with.
|
|
40
|
+
*/
|
|
41
|
+
export function replaceWithPageReload(path: string): void {
|
|
42
|
+
window.location.replace(path)
|
|
43
|
+
}
|