Skip to content

Commit 19348e0

Browse files
committed
add mirrors/mirroredBy key-value support for multi-transport-mode stops
- User Story 35370
1 parent 6df2593 commit 19348e0

6 files changed

Lines changed: 347 additions & 1 deletion

File tree

test-db-manager/src/types/enums.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ export enum KnownValueKey {
2222
StopOwner = 'stopOwner',
2323
OwnerContractId = 'owner-contractId',
2424
OwnerNote = 'owner-note',
25+
TimingPlaceId = 'timingPlaceId',
26+
Mirrors = 'mirrors',
27+
MirroredBy = 'mirroredBy',
2528
}
2629

2730
// Represents the values of hsl_municipality in LegacyHslMunicipalityCode table.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import compact from 'lodash/compact';
2+
import { useCallback } from 'react';
3+
import {
4+
GetStopDetailsQuery,
5+
useGetStopDetailsLazyQuery,
6+
} from '../../../../generated/graphql';
7+
import { EnrichedQuay, Quay, StopPlace } from '../../../../types';
8+
import { getStopPlacesFromQueryResult } from '../../../../utils';
9+
import { mapToEnrichedQuay } from '../../utils';
10+
11+
// TODO: Currently reuses the full GetStopDetails query which fetches all fields.
12+
// Once the mirrored data requirements are finalized, create a dedicated
13+
// GetMirroredQuayDetails query with a lighter fragment to avoid overfetching.
14+
15+
function findQuayByNetexId(
16+
data: GetStopDetailsQuery | undefined,
17+
quayNetexId: string,
18+
): { quay: Quay; stopPlace: StopPlace } | null {
19+
const stopPlaceResults = data?.stopsDb?.newestVersion ?? [];
20+
21+
for (const result of stopPlaceResults) {
22+
const [stopPlace] = getStopPlacesFromQueryResult<StopPlace>(
23+
result.TiamatStopPlace,
24+
);
25+
if (stopPlace) {
26+
const quay = compact(stopPlace.quays).find((q) => q.id === quayNetexId);
27+
if (quay) {
28+
return { quay, stopPlace };
29+
}
30+
}
31+
}
32+
33+
return null;
34+
}
35+
36+
export function useGetMirroredQuay() {
37+
const [getStopDetailsLazy] = useGetStopDetailsLazyQuery();
38+
39+
return useCallback(
40+
async (quayNetexId: string): Promise<EnrichedQuay | null> => {
41+
const { data } = await getStopDetailsLazy({
42+
variables: {
43+
where: {
44+
stop_place_quays: {
45+
quay: {
46+
netex_id: { _eq: quayNetexId },
47+
},
48+
},
49+
},
50+
},
51+
});
52+
53+
const result = findQuayByNetexId(data, quayNetexId);
54+
if (!result) {
55+
return null;
56+
}
57+
58+
return mapToEnrichedQuay(
59+
result.quay,
60+
result.stopPlace.accessibilityAssessment,
61+
);
62+
},
63+
[getStopDetailsLazy],
64+
);
65+
}

ui/src/utils/knownValueKey.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,6 @@ export enum KnownValueKey {
2323
OwnerContractId = 'owner-contractId',
2424
OwnerNote = 'owner-note',
2525
TimingPlaceId = 'timingPlaceId',
26+
Mirrors = 'mirrors',
27+
MirroredBy = 'mirroredBy',
2628
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
export * from './stopPlace';
21
export * from './alternativeNames';
32
export * from './buildSearchStopByLabelOrNameFilter';
3+
export * from './mirrorRelation';
4+
export * from './stopPlace';
45
export * from './transportMode';
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import { KnownValueKey } from '../knownValueKey';
2+
import {
3+
addMirroredBy,
4+
getMirrorParentId,
5+
getMirroredByIds,
6+
isMirrorChild,
7+
isMirrorParent,
8+
removeMirroredBy,
9+
setMirrorParent,
10+
} from './mirrorRelation';
11+
12+
describe('Mirror relation utils', () => {
13+
describe('getMirrorParentId', () => {
14+
it('should return parent netexId when mirrors key exists', () => {
15+
const quay = {
16+
keyValues: [{ key: KnownValueKey.Mirrors, values: ['NSR:Quay:123'] }],
17+
};
18+
expect(getMirrorParentId(quay)).toBe('NSR:Quay:123');
19+
});
20+
21+
it('should return null when no mirrors key exists', () => {
22+
const quay = {
23+
keyValues: [{ key: 'someOtherKey', values: ['value'] }],
24+
};
25+
expect(getMirrorParentId(quay)).toBeNull();
26+
});
27+
28+
it('should return null for empty keyValues', () => {
29+
expect(getMirrorParentId({ keyValues: [] })).toBeNull();
30+
expect(getMirrorParentId({ keyValues: undefined })).toBeNull();
31+
});
32+
});
33+
34+
describe('getMirroredByIds', () => {
35+
it('should return list of child netexIds', () => {
36+
const quay = {
37+
keyValues: [
38+
{
39+
key: KnownValueKey.MirroredBy,
40+
values: ['NSR:Quay:456', 'NSR:Quay:789'],
41+
},
42+
],
43+
};
44+
expect(getMirroredByIds(quay)).toEqual(['NSR:Quay:456', 'NSR:Quay:789']);
45+
});
46+
47+
it('should return empty array when no mirroredBy key', () => {
48+
const quay = { keyValues: [] };
49+
expect(getMirroredByIds(quay)).toEqual([]);
50+
});
51+
52+
it('should filter out null/undefined values', () => {
53+
const quay = {
54+
keyValues: [
55+
{
56+
key: KnownValueKey.MirroredBy,
57+
values: ['NSR:Quay:456', null, 'NSR:Quay:789'],
58+
},
59+
],
60+
};
61+
expect(getMirroredByIds(quay)).toEqual(['NSR:Quay:456', 'NSR:Quay:789']);
62+
});
63+
});
64+
65+
describe('isMirrorChild', () => {
66+
it('should return true when quay has mirrors key', () => {
67+
const quay = {
68+
keyValues: [{ key: KnownValueKey.Mirrors, values: ['NSR:Quay:123'] }],
69+
};
70+
expect(isMirrorChild(quay)).toBe(true);
71+
});
72+
73+
it('should return false when quay has no mirrors key', () => {
74+
expect(isMirrorChild({ keyValues: [] })).toBe(false);
75+
});
76+
});
77+
78+
describe('isMirrorParent', () => {
79+
it('should return true when quay has mirroredBy key with values', () => {
80+
const quay = {
81+
keyValues: [
82+
{ key: KnownValueKey.MirroredBy, values: ['NSR:Quay:456'] },
83+
],
84+
};
85+
expect(isMirrorParent(quay)).toBe(true);
86+
});
87+
88+
it('should return false when mirroredBy is empty', () => {
89+
const quay = {
90+
keyValues: [{ key: KnownValueKey.MirroredBy, values: [] }],
91+
};
92+
expect(isMirrorParent(quay)).toBe(false);
93+
});
94+
95+
it('should return false when no mirroredBy key', () => {
96+
expect(isMirrorParent({ keyValues: [] })).toBe(false);
97+
});
98+
});
99+
100+
describe('setMirrorParent', () => {
101+
it('should set mirrors key on empty keyValues', () => {
102+
const result = setMirrorParent([], 'NSR:Quay:123');
103+
expect(result).toEqual([
104+
{ key: KnownValueKey.Mirrors, values: ['NSR:Quay:123'] },
105+
]);
106+
});
107+
108+
it('should update existing mirrors key', () => {
109+
const existing = [
110+
{ key: KnownValueKey.Mirrors, values: ['NSR:Quay:old'] },
111+
];
112+
const result = setMirrorParent(existing, 'NSR:Quay:new');
113+
expect(result).toEqual([
114+
{ key: KnownValueKey.Mirrors, values: ['NSR:Quay:new'] },
115+
]);
116+
});
117+
118+
it('should preserve other keyValues', () => {
119+
const existing = [{ key: 'otherKey', values: ['otherValue'] }];
120+
const result = setMirrorParent(existing, 'NSR:Quay:123');
121+
expect(result).toEqual([
122+
{ key: 'otherKey', values: ['otherValue'] },
123+
{ key: KnownValueKey.Mirrors, values: ['NSR:Quay:123'] },
124+
]);
125+
});
126+
});
127+
128+
describe('addMirroredBy', () => {
129+
it('should add first child to empty keyValues', () => {
130+
const result = addMirroredBy([], 'NSR:Quay:456');
131+
expect(result).toEqual([
132+
{ key: KnownValueKey.MirroredBy, values: ['NSR:Quay:456'] },
133+
]);
134+
});
135+
136+
it('should append to existing mirroredBy list', () => {
137+
const existing = [
138+
{ key: KnownValueKey.MirroredBy, values: ['NSR:Quay:456'] },
139+
];
140+
const result = addMirroredBy(existing, 'NSR:Quay:789');
141+
expect(result).toEqual([
142+
{
143+
key: KnownValueKey.MirroredBy,
144+
values: ['NSR:Quay:456', 'NSR:Quay:789'],
145+
},
146+
]);
147+
});
148+
149+
it('should not duplicate existing child', () => {
150+
const existing = [
151+
{ key: KnownValueKey.MirroredBy, values: ['NSR:Quay:456'] },
152+
];
153+
const result = addMirroredBy(existing, 'NSR:Quay:456');
154+
expect(result).toEqual([
155+
{ key: KnownValueKey.MirroredBy, values: ['NSR:Quay:456'] },
156+
]);
157+
});
158+
159+
it('should preserve other keyValues', () => {
160+
const existing = [{ key: 'otherKey', values: ['otherValue'] }];
161+
const result = addMirroredBy(existing, 'NSR:Quay:456');
162+
expect(result).toEqual([
163+
{ key: 'otherKey', values: ['otherValue'] },
164+
{ key: KnownValueKey.MirroredBy, values: ['NSR:Quay:456'] },
165+
]);
166+
});
167+
});
168+
169+
describe('removeMirroredBy', () => {
170+
it('should remove child from mirroredBy list', () => {
171+
const existing = [
172+
{
173+
key: KnownValueKey.MirroredBy,
174+
values: ['NSR:Quay:456', 'NSR:Quay:789'],
175+
},
176+
];
177+
const result = removeMirroredBy(existing, 'NSR:Quay:456');
178+
expect(result).toEqual([
179+
{ key: KnownValueKey.MirroredBy, values: ['NSR:Quay:789'] },
180+
]);
181+
});
182+
183+
it('should result in empty values when removing last child', () => {
184+
const existing = [
185+
{ key: KnownValueKey.MirroredBy, values: ['NSR:Quay:456'] },
186+
];
187+
const result = removeMirroredBy(existing, 'NSR:Quay:456');
188+
expect(result).toEqual([{ key: KnownValueKey.MirroredBy, values: [] }]);
189+
});
190+
191+
it('should be no-op when child not in list', () => {
192+
const existing = [
193+
{ key: KnownValueKey.MirroredBy, values: ['NSR:Quay:456'] },
194+
];
195+
const result = removeMirroredBy(existing, 'NSR:Quay:999');
196+
expect(result).toEqual([
197+
{ key: KnownValueKey.MirroredBy, values: ['NSR:Quay:456'] },
198+
]);
199+
});
200+
});
201+
});
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import omit from 'lodash/omit';
2+
import {
3+
StopRegistryKeyValues,
4+
StopRegistryKeyValuesInput,
5+
} from '../../generated/graphql';
6+
import { ElementWithKeyValues, findKeyValue } from '../findKeyValue';
7+
import { KnownValueKey } from '../knownValueKey';
8+
import { setKeyValue } from './stopPlace';
9+
10+
export function stripKeyValueTypenames(
11+
keyValues: ReadonlyArray<StopRegistryKeyValues | null>,
12+
): StopRegistryKeyValuesInput[] {
13+
return keyValues
14+
.filter(Boolean)
15+
.map((kv) => omit(kv, '__typename') as StopRegistryKeyValuesInput);
16+
}
17+
18+
export function getMirrorParentId(quay: ElementWithKeyValues): string | null {
19+
return findKeyValue(quay, KnownValueKey.Mirrors);
20+
}
21+
22+
export function getMirroredByIds(quay: ElementWithKeyValues): string[] {
23+
const entry = quay.keyValues?.find(
24+
(kv) => kv?.key === KnownValueKey.MirroredBy,
25+
);
26+
return (entry?.values?.filter(Boolean) as string[]) ?? [];
27+
}
28+
29+
export function isMirrorChild(quay: ElementWithKeyValues): boolean {
30+
return getMirrorParentId(quay) !== null;
31+
}
32+
33+
export function isMirrorParent(quay: ElementWithKeyValues): boolean {
34+
return getMirroredByIds(quay).length > 0;
35+
}
36+
37+
export function setMirrorParent(
38+
childKeyValues: ReadonlyArray<StopRegistryKeyValues | null> | undefined,
39+
parentNetexId: string,
40+
): (StopRegistryKeyValues | null)[] {
41+
return setKeyValue(childKeyValues, KnownValueKey.Mirrors, [parentNetexId]);
42+
}
43+
44+
export function addMirroredBy(
45+
parentKeyValues: ReadonlyArray<StopRegistryKeyValues | null> | undefined,
46+
childNetexId: string,
47+
): (StopRegistryKeyValues | null)[] {
48+
const existing = parentKeyValues?.find(
49+
(kv) => kv?.key === KnownValueKey.MirroredBy,
50+
);
51+
const currentIds = (existing?.values?.filter(Boolean) as string[]) ?? [];
52+
53+
if (currentIds.includes(childNetexId)) {
54+
return (parentKeyValues ?? []).slice();
55+
}
56+
57+
return setKeyValue(parentKeyValues, KnownValueKey.MirroredBy, [
58+
...currentIds,
59+
childNetexId,
60+
]);
61+
}
62+
63+
export function removeMirroredBy(
64+
parentKeyValues: ReadonlyArray<StopRegistryKeyValues | null> | undefined,
65+
childNetexId: string,
66+
): (StopRegistryKeyValues | null)[] {
67+
const existing = parentKeyValues?.find(
68+
(kv) => kv?.key === KnownValueKey.MirroredBy,
69+
);
70+
const currentIds = (existing?.values?.filter(Boolean) as string[]) ?? [];
71+
const updated = currentIds.filter((id) => id !== childNetexId);
72+
73+
return setKeyValue(parentKeyValues, KnownValueKey.MirroredBy, updated);
74+
}

0 commit comments

Comments
 (0)