Skip to content

Commit 3b7517c

Browse files
feat: allow internal dataset for location (#3354)
1 parent 1fdfae5 commit 3b7517c

5 files changed

Lines changed: 336 additions & 10 deletions

File tree

__tests__/schema/autocompletes.ts

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { usersFixture } from '../fixture/user';
1414
import { keywordsFixture } from '../fixture/keywords';
1515
import { Autocomplete, AutocompleteType } from '../../src/entity/Autocomplete';
1616
import { Company, CompanyType } from '../../src/entity/Company';
17+
import { DatasetLocation } from '../../src/entity/dataset/DatasetLocation';
1718
import type { MapboxResponse } from '../../src/integrations/mapbox/types';
1819
import nock from 'nock';
1920

@@ -807,6 +808,291 @@ describe('query autocompleteLocation', () => {
807808
expect(res.errors).toBeFalsy();
808809
expect(res.data.autocompleteLocation).toEqual([]);
809810
});
811+
812+
describe('internal dataset', () => {
813+
const QUERY_WITH_DATASET = /* GraphQL */ `
814+
query AutocompleteLocation(
815+
$query: String!
816+
$dataset: LocationDataset
817+
$limit: Int
818+
) {
819+
autocompleteLocation(query: $query, dataset: $dataset, limit: $limit) {
820+
id
821+
country
822+
city
823+
subdivision
824+
}
825+
}
826+
`;
827+
828+
beforeEach(async () => {
829+
await saveFixtures(con, DatasetLocation, [
830+
{
831+
id: '550e8400-e29b-41d4-a716-446655440001',
832+
country: 'United States',
833+
subdivision: 'California',
834+
city: 'San Francisco',
835+
iso2: 'US',
836+
iso3: 'USA',
837+
},
838+
{
839+
id: '550e8400-e29b-41d4-a716-446655440002',
840+
country: 'United States',
841+
subdivision: 'New York',
842+
city: 'New York City',
843+
iso2: 'US',
844+
iso3: 'USA',
845+
},
846+
{
847+
id: '550e8400-e29b-41d4-a716-446655440003',
848+
country: 'Germany',
849+
subdivision: 'Bavaria',
850+
city: 'Munich',
851+
iso2: 'DE',
852+
iso3: 'DEU',
853+
},
854+
{
855+
id: '550e8400-e29b-41d4-a716-446655440004',
856+
country: 'Germany',
857+
subdivision: null,
858+
city: 'Berlin',
859+
iso2: 'DE',
860+
iso3: 'DEU',
861+
},
862+
{
863+
id: '550e8400-e29b-41d4-a716-446655440005',
864+
country: 'United Kingdom',
865+
subdivision: null,
866+
city: null,
867+
iso2: 'GB',
868+
iso3: 'GBR',
869+
},
870+
]);
871+
});
872+
873+
it('should return locations from internal database when dataset is internal', async () => {
874+
loggedUser = '1';
875+
876+
const res = await client.query(QUERY_WITH_DATASET, {
877+
variables: { query: 'san francisco', dataset: 'internal' },
878+
});
879+
880+
expect(res.errors).toBeFalsy();
881+
expect(res.data.autocompleteLocation).toEqual([
882+
{
883+
id: '550e8400-e29b-41d4-a716-446655440001',
884+
country: 'United States',
885+
city: 'San Francisco',
886+
subdivision: 'California',
887+
},
888+
]);
889+
});
890+
891+
it('should match by country name', async () => {
892+
loggedUser = '1';
893+
894+
const res = await client.query(QUERY_WITH_DATASET, {
895+
variables: { query: 'germany', dataset: 'internal' },
896+
});
897+
898+
expect(res.errors).toBeFalsy();
899+
expect(res.data.autocompleteLocation).toHaveLength(2);
900+
// Note: ORDER BY subdivision ASC puts non-null values before null
901+
expect(res.data.autocompleteLocation).toEqual([
902+
{
903+
id: '550e8400-e29b-41d4-a716-446655440003',
904+
country: 'Germany',
905+
city: 'Munich',
906+
subdivision: 'Bavaria',
907+
},
908+
{
909+
id: '550e8400-e29b-41d4-a716-446655440004',
910+
country: 'Germany',
911+
city: 'Berlin',
912+
subdivision: null,
913+
},
914+
]);
915+
});
916+
917+
it('should match by subdivision name', async () => {
918+
loggedUser = '1';
919+
920+
const res = await client.query(QUERY_WITH_DATASET, {
921+
variables: { query: 'california', dataset: 'internal' },
922+
});
923+
924+
expect(res.errors).toBeFalsy();
925+
expect(res.data.autocompleteLocation).toEqual([
926+
{
927+
id: '550e8400-e29b-41d4-a716-446655440001',
928+
country: 'United States',
929+
city: 'San Francisco',
930+
subdivision: 'California',
931+
},
932+
]);
933+
});
934+
935+
it('should match by city name', async () => {
936+
loggedUser = '1';
937+
938+
const res = await client.query(QUERY_WITH_DATASET, {
939+
variables: { query: 'munich', dataset: 'internal' },
940+
});
941+
942+
expect(res.errors).toBeFalsy();
943+
expect(res.data.autocompleteLocation).toEqual([
944+
{
945+
id: '550e8400-e29b-41d4-a716-446655440003',
946+
country: 'Germany',
947+
city: 'Munich',
948+
subdivision: 'Bavaria',
949+
},
950+
]);
951+
});
952+
953+
it('should be case insensitive', async () => {
954+
loggedUser = '1';
955+
956+
const res = await client.query(QUERY_WITH_DATASET, {
957+
variables: { query: 'UNITED STATES', dataset: 'internal' },
958+
});
959+
960+
expect(res.errors).toBeFalsy();
961+
expect(res.data.autocompleteLocation).toHaveLength(2);
962+
});
963+
964+
it('should return empty array when no matches found', async () => {
965+
loggedUser = '1';
966+
967+
const res = await client.query(QUERY_WITH_DATASET, {
968+
variables: { query: 'nonexistent', dataset: 'internal' },
969+
});
970+
971+
expect(res.errors).toBeFalsy();
972+
expect(res.data.autocompleteLocation).toEqual([]);
973+
});
974+
975+
it('should handle null subdivision and city', async () => {
976+
loggedUser = '1';
977+
978+
const res = await client.query(QUERY_WITH_DATASET, {
979+
variables: { query: 'united kingdom', dataset: 'internal' },
980+
});
981+
982+
expect(res.errors).toBeFalsy();
983+
expect(res.data.autocompleteLocation).toEqual([
984+
{
985+
id: '550e8400-e29b-41d4-a716-446655440005',
986+
country: 'United Kingdom',
987+
city: null,
988+
subdivision: null,
989+
},
990+
]);
991+
});
992+
993+
it('should respect limit parameter', async () => {
994+
loggedUser = '1';
995+
996+
const res = await client.query(QUERY_WITH_DATASET, {
997+
variables: { query: 'united', dataset: 'internal', limit: 1 },
998+
});
999+
1000+
expect(res.errors).toBeFalsy();
1001+
expect(res.data.autocompleteLocation).toHaveLength(1);
1002+
});
1003+
1004+
it('should return Europe as a country', async () => {
1005+
loggedUser = '1';
1006+
1007+
await con.getRepository(DatasetLocation).save({
1008+
id: '550e8400-e29b-41d4-a716-446655440006',
1009+
country: 'Europe',
1010+
subdivision: null,
1011+
city: null,
1012+
iso2: 'EU',
1013+
iso3: 'EUR',
1014+
});
1015+
1016+
const res = await client.query(QUERY_WITH_DATASET, {
1017+
variables: { query: 'europe', dataset: 'internal' },
1018+
});
1019+
1020+
expect(res.errors).toBeFalsy();
1021+
expect(res.data.autocompleteLocation).toEqual([
1022+
{
1023+
id: '550e8400-e29b-41d4-a716-446655440006',
1024+
country: 'Europe',
1025+
city: null,
1026+
subdivision: null,
1027+
},
1028+
]);
1029+
});
1030+
1031+
it('should default to external (Mapbox) when dataset is not specified', async () => {
1032+
loggedUser = '1';
1033+
1034+
const mockMapboxResponse: MapboxResponse = {
1035+
type: 'FeatureCollection',
1036+
features: [
1037+
{
1038+
type: 'Feature',
1039+
geometry: {
1040+
coordinates: [-122.4194, 37.7749],
1041+
type: 'Point',
1042+
},
1043+
properties: {
1044+
name: 'San Francisco',
1045+
mapbox_id: 'place.sf',
1046+
feature_type: 'place',
1047+
place_formatted: 'San Francisco, California, United States',
1048+
context: {
1049+
country: {
1050+
id: 'country.us',
1051+
name: 'United States',
1052+
country_code: 'US',
1053+
country_code_alpha_3: 'USA',
1054+
},
1055+
region: {
1056+
id: 'region.ca',
1057+
name: 'California',
1058+
region_code: 'CA',
1059+
region_code_full: 'US-CA',
1060+
},
1061+
},
1062+
coordinates: {
1063+
latitude: 37.7749,
1064+
longitude: -122.4194,
1065+
},
1066+
language: 'en',
1067+
maki: 'marker',
1068+
metadata: {},
1069+
},
1070+
},
1071+
],
1072+
attribution: 'Mapbox',
1073+
response_id: 'test-response-id',
1074+
};
1075+
1076+
nock('https://api.mapbox.com')
1077+
.get('/search/geocode/v6/forward')
1078+
.query({
1079+
q: 'san francisco',
1080+
types: 'country,region,place',
1081+
limit: 5,
1082+
access_token: process.env.MAPBOX_ACCESS_TOKEN,
1083+
})
1084+
.reply(200, mockMapboxResponse);
1085+
1086+
// Query without specifying dataset - should use Mapbox
1087+
const res = await client.query(QUERY_WITH_DATASET, {
1088+
variables: { query: 'san francisco' },
1089+
});
1090+
1091+
expect(res.errors).toBeFalsy();
1092+
// Should return Mapbox ID, not internal database ID
1093+
expect(res.data.autocompleteLocation[0].id).toBe('place.sf');
1094+
});
1095+
});
8101096
});
8111097

8121098
describe('query autocompleteCompany', () => {

src/common/schema/autocompletes.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import { AutocompleteType } from '../../entity/Autocomplete';
33
import { CompanyType } from '../../entity/Company';
44
import { normalizeContentForDeduplication } from '../../entity/posts/hooks';
55

6+
export enum LocationDataset {
7+
Internal = 'internal',
8+
External = 'external',
9+
}
10+
611
export const autocompleteBaseSchema = z.object({
712
query: z.string().trim().toLowerCase().normalize().min(1).max(100).nonempty(),
813
limit: z.number().min(1).max(50).default(20),
@@ -20,3 +25,9 @@ export const autocompleteKeywordsSchema = z.object({
2025
query: z.string().transform(normalizeContentForDeduplication),
2126
limit: z.number().min(1).max(50).default(20),
2227
});
28+
29+
export const autocompleteLocationSchema = z.object({
30+
query: z.string().trim().toLowerCase().normalize().min(1),
31+
limit: z.number().min(1).max(50).default(5),
32+
dataset: z.enum(LocationDataset).default(LocationDataset.External),
33+
});

src/integrations/mapbox/clients.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ export class MapboxClient implements IMapboxClient {
3737
});
3838
}
3939

40-
async autocomplete(query: string): Promise<MapboxResponse> {
40+
async autocomplete(query: string, limit = 5): Promise<MapboxResponse> {
4141
return this.garmr.execute(async () => {
42-
const url = `${this.baseUrl}?q=${encodeURIComponent(query)}&types=country,region,place&limit=5&access_token=${this.accessToken}`;
42+
const url = `${this.baseUrl}?q=${encodeURIComponent(query)}&types=country,region,place&limit=${limit}&access_token=${this.accessToken}`;
4343

4444
const response = await fetch(url);
4545

src/integrations/mapbox/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,5 @@ export interface MapboxResponse {
5656

5757
export interface IMapboxClient extends IGarmrClient {
5858
geocode(query: string): Promise<MapboxResponse>;
59-
autocomplete(query: string): Promise<MapboxResponse>;
59+
autocomplete(query: string, limit?: number): Promise<MapboxResponse>;
6060
}

0 commit comments

Comments
 (0)