Skip to content

Commit b35c15e

Browse files
Merge pull request #14 from MinistryPlatform-Community/feat/google-places-mp-config-key
feat(google-places): source API key from MP config first, env var as fallback
2 parents 3343f0d + c3be28b commit b35c15e

3 files changed

Lines changed: 54 additions & 10 deletions

File tree

.env.example

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,13 @@ MINISTRY_PLATFORM_DEV_CLIENT_SECRET=
4343
# Enables address autocomplete on the Address Line 1 field in the Add/Edit
4444
# Family tool (and any other tool that uses GooglePlacesService).
4545
#
46-
# When unset, the tool falls back to a plain text input — no errors, no UI
47-
# change beyond losing the suggestion dropdown.
46+
# Key resolution order (first non-empty wins):
47+
# 1. MinistryPlatform dp_Configuration_Settings
48+
# (Application_Code='COMMON', Key_Name='GoogleMapsAPIKey')
49+
# 2. This GOOGLE_PLACES_API_KEY environment variable
50+
# 3. If neither is set, the feature is disabled — the tool falls back to
51+
# a plain text input with no errors and no UI change beyond losing the
52+
# suggestion dropdown.
4853
#
4954
# Get a key at https://console.cloud.google.com and enable the "Places API
5055
# (New)" — the v1 REST endpoints. Calls go through our server actions, so

src/app/(web)/tools/addeditfamily/actions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ export async function placeAutocomplete(
100100
await getSession();
101101
if (input.trim().length < 3) return [];
102102
const service = await GooglePlacesService.getInstance();
103-
if (!service.isEnabled()) return [];
103+
if (!(await service.isEnabled())) return [];
104104
return service.autocomplete(input, sessionToken);
105105
}
106106

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,79 @@
1+
import { MPHelper } from "@/lib/providers/ministry-platform";
12
import { GooglePlacesProvider } from "@/lib/providers/google-places";
23
import type { PlacePrediction, PlaceDetails } from "@/lib/providers/google-places";
34

45
export class GooglePlacesService {
56
private static instance: GooglePlacesService;
7+
private mp: MPHelper | null = null;
68
private provider: GooglePlacesProvider | null = null;
9+
// undefined = not yet resolved; null = resolved with no key (feature disabled)
10+
private resolvedKey: string | null | undefined = undefined;
711

812
private constructor() {}
913

1014
public static async getInstance(): Promise<GooglePlacesService> {
1115
if (!GooglePlacesService.instance) {
1216
GooglePlacesService.instance = new GooglePlacesService();
17+
GooglePlacesService.instance.mp = new MPHelper();
1318
}
1419
return GooglePlacesService.instance;
1520
}
1621

17-
public isEnabled(): boolean {
18-
return Boolean(process.env.GOOGLE_PLACES_API_KEY);
22+
/**
23+
* Resolves the Google Places API key with this precedence:
24+
* 1. MP dp_Configuration_Settings (Application_Code='COMMON', Key_Name='GoogleMapsAPIKey')
25+
* 2. GOOGLE_PLACES_API_KEY environment variable
26+
* 3. null (feature disabled)
27+
*
28+
* Cached on the singleton so MP is queried at most once per process lifetime.
29+
*/
30+
private async resolveApiKey(): Promise<string | null> {
31+
if (this.resolvedKey !== undefined) return this.resolvedKey;
32+
33+
try {
34+
const rows = await this.mp!.getTableRecords<{ Value: string | null }>({
35+
table: "dp_Configuration_Settings",
36+
select: "Value",
37+
filter: "Application_Code='COMMON' AND Key_Name='GoogleMapsAPIKey'",
38+
top: 1,
39+
});
40+
const mpKey = rows[0]?.Value?.trim();
41+
if (mpKey) {
42+
this.resolvedKey = mpKey;
43+
return this.resolvedKey;
44+
}
45+
} catch {
46+
// Swallow lookup failures (e.g. table permissions) and fall through to env var.
47+
}
48+
49+
const envKey = process.env.GOOGLE_PLACES_API_KEY?.trim();
50+
this.resolvedKey = envKey ? envKey : null;
51+
return this.resolvedKey;
52+
}
53+
54+
public async isEnabled(): Promise<boolean> {
55+
return (await this.resolveApiKey()) !== null;
1956
}
2057

21-
private getProvider(): GooglePlacesProvider {
58+
private async getProvider(): Promise<GooglePlacesProvider> {
2259
if (this.provider) return this.provider;
23-
const apiKey = process.env.GOOGLE_PLACES_API_KEY;
60+
const apiKey = await this.resolveApiKey();
2461
if (!apiKey) {
2562
throw new Error(
26-
"GOOGLE_PLACES_API_KEY is not configured. Add it to .env.local to enable address autocomplete.",
63+
"Google Places API key is not configured. Set the 'GoogleMapsAPIKey' setting in MinistryPlatform (Application_Code='COMMON') or define GOOGLE_PLACES_API_KEY in .env.local.",
2764
);
2865
}
2966
this.provider = new GooglePlacesProvider(apiKey);
3067
return this.provider;
3168
}
3269

3370
async autocomplete(input: string, sessionToken: string): Promise<PlacePrediction[]> {
34-
return this.getProvider().autocomplete(input, sessionToken);
71+
const provider = await this.getProvider();
72+
return provider.autocomplete(input, sessionToken);
3573
}
3674

3775
async getPlaceDetails(placeId: string, sessionToken: string): Promise<PlaceDetails> {
38-
return this.getProvider().getPlaceDetails(placeId, sessionToken);
76+
const provider = await this.getProvider();
77+
return provider.getPlaceDetails(placeId, sessionToken);
3978
}
4079
}

0 commit comments

Comments
 (0)