From 428869df92e96ece7616c8e469b065739fe0e333 Mon Sep 17 00:00:00 2001 From: Roberto Fontanarosa Date: Fri, 3 Apr 2026 15:13:11 +0200 Subject: [PATCH 01/10] Improved error handler with integrations, fixed some types --- functions/src/common/datastore.ts | 5 +++-- functions/src/on-create-loi.ts | 15 ++++++++++++--- functions/src/property-generators/types.ts | 4 +++- functions/src/property-generators/whisp.ts | 6 +++--- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/functions/src/common/datastore.ts b/functions/src/common/datastore.ts index f7612930f..5ad4dd22d 100644 --- a/functions/src/common/datastore.ts +++ b/functions/src/common/datastore.ts @@ -17,6 +17,7 @@ import { UserRecord } from 'firebase-admin/auth'; import { firestore } from 'firebase-admin'; import { DocumentData, FieldPath, GeoPoint } from 'firebase-admin/firestore'; +import type { Geometry } from 'geojson'; import { registry } from '@ground/lib'; import { GroundProtos } from '@ground/proto'; @@ -288,7 +289,7 @@ export class Datastore { * * @returns GeoJSON geometry object (with geometry as list of lists) */ - static fromFirestoreMap(geoJsonGeometry: any): any { + static fromFirestoreMap(geoJsonGeometry: object): Geometry { const geometryObject = geoJsonGeometry as pseudoGeoJsonGeometry; if (!geometryObject) { throw new Error( @@ -300,7 +301,7 @@ export class Datastore { geometryObject.coordinates ); - return geometryObject; + return geometryObject as unknown as Geometry; } static fromFirestoreValue(coordinates: any) { diff --git a/functions/src/on-create-loi.ts b/functions/src/on-create-loi.ts index 405002672..784e5c0f5 100644 --- a/functions/src/on-create-loi.ts +++ b/functions/src/on-create-loi.ts @@ -64,15 +64,24 @@ export async function onCreateLoiHandler( const propertyGenerators = await db.fetchPropertyGenerators(); for (const propertyGeneratorDoc of propertyGenerators.docs) { + const generatorId = propertyGeneratorDoc.id; const config = propertyGeneratorDoc.data() as PropertyGeneratorConfig; - const handler = propertyGeneratorHandlers[propertyGeneratorDoc.id]; + const handler = propertyGeneratorHandlers[generatorId]; - if (!handler || !enabledIntegrationIds.has(propertyGeneratorDoc.id)) + if (!handler) { continue; + } - const newProperties = await handler(config, geometry); + if (!enabledIntegrationIds.has(generatorId)) { + continue; + } + try { + const newProperties = await handler(config, geometry); properties = updateProperties(properties, newProperties, config.prefix); + } catch (e) { + console.error(`LOI ${loiId}: property generator '${generatorId}' failed:`, e); + } Object.keys(properties) .filter(key => typeof properties[key] === 'object') diff --git a/functions/src/property-generators/types.ts b/functions/src/property-generators/types.ts index a53606546..327312223 100644 --- a/functions/src/property-generators/types.ts +++ b/functions/src/property-generators/types.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import type { Geometry } from 'geojson'; + export type Properties = Record; export type Headers = Record; @@ -30,5 +32,5 @@ export type PropertyGeneratorConfig = { export type PropertyGeneratorHandler = ( config: PropertyGeneratorConfig, - geometry: object + geometry: Geometry ) => Promise; diff --git a/functions/src/property-generators/whisp.ts b/functions/src/property-generators/whisp.ts index 8e96c189c..f8013d551 100644 --- a/functions/src/property-generators/whisp.ts +++ b/functions/src/property-generators/whisp.ts @@ -15,7 +15,7 @@ */ import { geojsonToWKT } from '@terraformer/wkt'; -import { Datastore } from '../common/datastore'; +import type { Geometry } from 'geojson'; import type { Body, Headers, @@ -36,11 +36,11 @@ const defaultHeaders = { 'Content-Type': 'application/json' }; export async function whispHandler( config: PropertyGeneratorConfig, - geometry: object + geometry: Geometry ): Promise { const { body, headers, url } = config; - const wkt = geojsonToWKT(Datastore.fromFirestoreMap(geometry)); + const wkt = geojsonToWKT(geometry); return fetchWhispProperties( url, From 6ebea60d389e606a1823237dc9002a9c70dbcb2f Mon Sep 17 00:00:00 2001 From: Roberto Fontanarosa Date: Fri, 3 Apr 2026 15:14:01 +0200 Subject: [PATCH 02/10] GeoID handler implementation --- functions/src/on-create-loi.spec.ts | 55 ++++++++++++++++++++ functions/src/property-generators/geoid.ts | 59 ++++++++++++++++++++-- 2 files changed, 110 insertions(+), 4 deletions(-) diff --git a/functions/src/on-create-loi.spec.ts b/functions/src/on-create-loi.spec.ts index 7e89692cd..6f9163405 100644 --- a/functions/src/on-create-loi.spec.ts +++ b/functions/src/on-create-loi.spec.ts @@ -65,6 +65,12 @@ describe('onCreateLoiHandler()', () => { url: 'https://whisp.example.com/api', }; + const geoidConfig = { + name: 'geoid', + prefix: 'geoid_', + url: 'https://geoid.example.com/api', + }; + beforeEach(() => { mockFirestore = createMockFirestore(); stubAdminApi(mockFirestore); @@ -75,6 +81,9 @@ describe('onCreateLoiHandler()', () => { mockFirestore .doc('config/integrations/propertyGenerators/whisp') .set(whispConfig); + mockFirestore + .doc('config/integrations/propertyGenerators/geoid') + .set(geoidConfig); }); afterEach(() => { @@ -118,4 +127,50 @@ describe('onCreateLoiHandler()', () => { [pr.numericValue]: 100, }); }); + + it('runs geoid property generator and updates LOI properties when integration is enabled', async () => { + mockFirestore.doc(JOB_PATH).set({ + [j.enabledIntegrations]: [{ [intgr.id]: 'geoid' }], + }); + spyOn(globalThis, 'fetch').and.returnValue( + Promise.resolve({ + ok: true, + text: () => + Promise.resolve( + "type='Feature' id='019d4e5a-d6bb-7000-99d2-3c0d4081586b'" + ), + } as Response) + ); + + await onCreateLoiHandler({ + data: newDocumentSnapshot(loiDoc) as unknown as QueryDocumentSnapshot, + params: { surveyId: SURVEY_ID, loiId: LOI_ID }, + } as unknown as FirestoreEvent); + + const loiData = (await mockFirestore.doc(LOI_PATH).get()).data(); + expect(loiData?.[l.properties]?.['geoid_id']).toEqual({ + [pr.stringValue]: '019d4e5a-d6bb-7000-99d2-3c0d4081586b', + }); + }); + + it('skips geoid property generator when fetch fails', async () => { + mockFirestore.doc(JOB_PATH).set({ + [j.enabledIntegrations]: [{ [intgr.id]: 'geoid' }], + }); + spyOn(globalThis, 'fetch').and.returnValue( + Promise.resolve({ + ok: false, + status: 500, + text: () => Promise.resolve('Internal Server Error'), + } as Response) + ); + + await onCreateLoiHandler({ + data: newDocumentSnapshot(loiDoc) as unknown as QueryDocumentSnapshot, + params: { surveyId: SURVEY_ID, loiId: LOI_ID }, + } as unknown as FirestoreEvent); + + const loiData = (await mockFirestore.doc(LOI_PATH).get()).data(); + expect(loiData?.[l.properties]?.['geoid_geoid_code']).toBeUndefined(); + }); }); diff --git a/functions/src/property-generators/geoid.ts b/functions/src/property-generators/geoid.ts index f1c6860dc..37f7bad15 100644 --- a/functions/src/property-generators/geoid.ts +++ b/functions/src/property-generators/geoid.ts @@ -14,8 +14,59 @@ * limitations under the License. */ -import type { Properties, PropertyGeneratorHandler } from './types'; +import type { Geometry } from 'geojson'; +import type { Headers, Properties, PropertyGeneratorConfig } from './types'; -// TODO: Implement geoid property generation. -export const geoidHandler: PropertyGeneratorHandler = - async (): Promise => ({}); +const CATALOG_ID = 'ground'; +const COLLECTION_ID = 'ground'; + +const defaultHeaders = { 'Content-Type': 'application/json' }; + +const GEOID_ID_PATTERN = /\bid='([^']+)'/; + +export async function geoidHandler( + config: PropertyGeneratorConfig, + geometry: Geometry +): Promise { + const { headers, url } = config; + const endpoint = `${url}/features/catalogs/${CATALOG_ID}/collections/${COLLECTION_ID}/items`; + + return fetchGeoidProperties( + endpoint, + { ...defaultHeaders, ...headers }, + { type: 'Feature', geometry, properties: {} } + ); +} + +async function fetchGeoidProperties( + url: string, + headers: Headers, + body: object +): Promise { + const bodyJson = JSON.stringify(body); + console.log(`geoid: POST ${url} body=${bodyJson}`); + + const response = await fetch(url, { + method: 'POST', + headers, + body: bodyJson, + }); + + if (!response.ok) { + const errorBody = await response.text(); + console.error( + `geoid: request failed with status ${response.status}: ${errorBody}` + ); + return {}; + } + + const responseText = await response.text(); + const id = GEOID_ID_PATTERN.exec(responseText)?.[1]; + if (!id) { + console.error(`geoid: response missing id field, body=${responseText}`); + return {}; + } + console.log(`geoid: received id=${id}`); + + return { id }; +} From 98b9ce78f64a0b7bc51622de50110b83fd1e94a8 Mon Sep 17 00:00:00 2001 From: Roberto Fontanarosa Date: Fri, 3 Apr 2026 15:19:17 +0200 Subject: [PATCH 03/10] fix indentation --- functions/src/on-create-loi.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/functions/src/on-create-loi.ts b/functions/src/on-create-loi.ts index 784e5c0f5..1c8703e99 100644 --- a/functions/src/on-create-loi.ts +++ b/functions/src/on-create-loi.ts @@ -80,7 +80,10 @@ export async function onCreateLoiHandler( const newProperties = await handler(config, geometry); properties = updateProperties(properties, newProperties, config.prefix); } catch (e) { - console.error(`LOI ${loiId}: property generator '${generatorId}' failed:`, e); + console.error( + `LOI ${loiId}: property generator '${generatorId}' failed:`, + e + ); } Object.keys(properties) From 66b7554300f3cacec88f9ac3ab6da212c52f9626 Mon Sep 17 00:00:00 2001 From: Roberto Fontanarosa Date: Fri, 3 Apr 2026 15:35:41 +0200 Subject: [PATCH 04/10] fix missing whitespaces --- functions/src/on-create-loi.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/src/on-create-loi.ts b/functions/src/on-create-loi.ts index 1c8703e99..1de30ec24 100644 --- a/functions/src/on-create-loi.ts +++ b/functions/src/on-create-loi.ts @@ -78,7 +78,7 @@ export async function onCreateLoiHandler( try { const newProperties = await handler(config, geometry); - properties = updateProperties(properties, newProperties, config.prefix); + properties = updateProperties(properties, newProperties, config.prefix); } catch (e) { console.error( `LOI ${loiId}: property generator '${generatorId}' failed:`, From d1eca26ce76fbb3fde866442311ef4542f7f53be Mon Sep 17 00:00:00 2001 From: Roberto Fontanarosa Date: Thu, 16 Apr 2026 10:05:20 +0200 Subject: [PATCH 05/10] fixed geoid response type --- functions/src/property-generators/geoid.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/functions/src/property-generators/geoid.ts b/functions/src/property-generators/geoid.ts index 37f7bad15..f1e3e7636 100644 --- a/functions/src/property-generators/geoid.ts +++ b/functions/src/property-generators/geoid.ts @@ -22,8 +22,6 @@ const COLLECTION_ID = 'ground'; const defaultHeaders = { 'Content-Type': 'application/json' }; -const GEOID_ID_PATTERN = /\bid='([^']+)'/; - export async function geoidHandler( config: PropertyGeneratorConfig, geometry: Geometry @@ -60,10 +58,12 @@ async function fetchGeoidProperties( return {}; } - const responseText = await response.text(); - const id = GEOID_ID_PATTERN.exec(responseText)?.[1]; + const responseJson = await response.json(); + const id = responseJson?.id; if (!id) { - console.error(`geoid: response missing id field, body=${responseText}`); + console.error( + `geoid: response missing id field, body=${JSON.stringify(responseJson)}` + ); return {}; } console.log(`geoid: received id=${id}`); From 90bdf7924ebe2f8c6f88606839f1de063649a487 Mon Sep 17 00:00:00 2001 From: Roberto Fontanarosa Date: Thu, 16 Apr 2026 11:58:48 +0200 Subject: [PATCH 06/10] removed hardcoded configuration --- functions/src/property-generators/geoid.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/functions/src/property-generators/geoid.ts b/functions/src/property-generators/geoid.ts index f1e3e7636..f160c2e65 100644 --- a/functions/src/property-generators/geoid.ts +++ b/functions/src/property-generators/geoid.ts @@ -17,9 +17,6 @@ import type { Geometry } from 'geojson'; import type { Headers, Properties, PropertyGeneratorConfig } from './types'; -const CATALOG_ID = 'ground'; -const COLLECTION_ID = 'ground'; - const defaultHeaders = { 'Content-Type': 'application/json' }; export async function geoidHandler( @@ -27,10 +24,9 @@ export async function geoidHandler( geometry: Geometry ): Promise { const { headers, url } = config; - const endpoint = `${url}/features/catalogs/${CATALOG_ID}/collections/${COLLECTION_ID}/items`; return fetchGeoidProperties( - endpoint, + url, { ...defaultHeaders, ...headers }, { type: 'Feature', geometry, properties: {} } ); From c1208cc6e448eec783c5512b29d8a0544856ff8d Mon Sep 17 00:00:00 2001 From: Roberto Fontanarosa Date: Thu, 16 Apr 2026 15:33:46 +0200 Subject: [PATCH 07/10] fixed test --- functions/src/on-create-loi.spec.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/functions/src/on-create-loi.spec.ts b/functions/src/on-create-loi.spec.ts index 6f9163405..1a72cb0a5 100644 --- a/functions/src/on-create-loi.spec.ts +++ b/functions/src/on-create-loi.spec.ts @@ -135,10 +135,8 @@ describe('onCreateLoiHandler()', () => { spyOn(globalThis, 'fetch').and.returnValue( Promise.resolve({ ok: true, - text: () => - Promise.resolve( - "type='Feature' id='019d4e5a-d6bb-7000-99d2-3c0d4081586b'" - ), + json: () => + Promise.resolve({ id: '019d4e5a-d6bb-7000-99d2-3c0d4081586b' }), } as Response) ); From 6e61a91b09ec694a2a5b3e01a9ecaf54a1bc2766 Mon Sep 17 00:00:00 2001 From: Roberto Fontanarosa Date: Fri, 17 Apr 2026 00:19:36 +0200 Subject: [PATCH 08/10] renamed geoid into geoId, used firebase-functions logger instead of console --- functions/src/on-create-loi.spec.ts | 22 +++++++++---------- .../{geoid.ts => geo-id.ts} | 19 ++++++++-------- functions/src/property-generators/index.ts | 4 ++-- 3 files changed, 23 insertions(+), 22 deletions(-) rename functions/src/property-generators/{geoid.ts => geo-id.ts} (77%) diff --git a/functions/src/on-create-loi.spec.ts b/functions/src/on-create-loi.spec.ts index 1a72cb0a5..a48dfd62e 100644 --- a/functions/src/on-create-loi.spec.ts +++ b/functions/src/on-create-loi.spec.ts @@ -65,9 +65,9 @@ describe('onCreateLoiHandler()', () => { url: 'https://whisp.example.com/api', }; - const geoidConfig = { - name: 'geoid', - prefix: 'geoid_', + const geoIdConfig = { + name: 'geoId', + prefix: 'geoId_', url: 'https://geoid.example.com/api', }; @@ -82,8 +82,8 @@ describe('onCreateLoiHandler()', () => { .doc('config/integrations/propertyGenerators/whisp') .set(whispConfig); mockFirestore - .doc('config/integrations/propertyGenerators/geoid') - .set(geoidConfig); + .doc('config/integrations/propertyGenerators/geoId') + .set(geoIdConfig); }); afterEach(() => { @@ -128,9 +128,9 @@ describe('onCreateLoiHandler()', () => { }); }); - it('runs geoid property generator and updates LOI properties when integration is enabled', async () => { + it('runs geoId property generator and updates LOI properties when integration is enabled', async () => { mockFirestore.doc(JOB_PATH).set({ - [j.enabledIntegrations]: [{ [intgr.id]: 'geoid' }], + [j.enabledIntegrations]: [{ [intgr.id]: 'geoId' }], }); spyOn(globalThis, 'fetch').and.returnValue( Promise.resolve({ @@ -146,14 +146,14 @@ describe('onCreateLoiHandler()', () => { } as unknown as FirestoreEvent); const loiData = (await mockFirestore.doc(LOI_PATH).get()).data(); - expect(loiData?.[l.properties]?.['geoid_id']).toEqual({ + expect(loiData?.[l.properties]?.['geoId_id']).toEqual({ [pr.stringValue]: '019d4e5a-d6bb-7000-99d2-3c0d4081586b', }); }); - it('skips geoid property generator when fetch fails', async () => { + it('skips geoId property generator when fetch fails', async () => { mockFirestore.doc(JOB_PATH).set({ - [j.enabledIntegrations]: [{ [intgr.id]: 'geoid' }], + [j.enabledIntegrations]: [{ [intgr.id]: 'geoId' }], }); spyOn(globalThis, 'fetch').and.returnValue( Promise.resolve({ @@ -169,6 +169,6 @@ describe('onCreateLoiHandler()', () => { } as unknown as FirestoreEvent); const loiData = (await mockFirestore.doc(LOI_PATH).get()).data(); - expect(loiData?.[l.properties]?.['geoid_geoid_code']).toBeUndefined(); + expect(loiData?.[l.properties]?.['geoId_id']).toBeUndefined(); }); }); diff --git a/functions/src/property-generators/geoid.ts b/functions/src/property-generators/geo-id.ts similarity index 77% rename from functions/src/property-generators/geoid.ts rename to functions/src/property-generators/geo-id.ts index f160c2e65..5755f75a1 100644 --- a/functions/src/property-generators/geoid.ts +++ b/functions/src/property-generators/geo-id.ts @@ -14,31 +14,32 @@ * limitations under the License. */ +import * as logger from 'firebase-functions/logger'; import type { Geometry } from 'geojson'; import type { Headers, Properties, PropertyGeneratorConfig } from './types'; const defaultHeaders = { 'Content-Type': 'application/json' }; -export async function geoidHandler( +export async function geoIdHandler( config: PropertyGeneratorConfig, geometry: Geometry ): Promise { const { headers, url } = config; - return fetchGeoidProperties( + return fetchGeoIdProperties( url, { ...defaultHeaders, ...headers }, { type: 'Feature', geometry, properties: {} } ); } -async function fetchGeoidProperties( +async function fetchGeoIdProperties( url: string, headers: Headers, body: object ): Promise { const bodyJson = JSON.stringify(body); - console.log(`geoid: POST ${url} body=${bodyJson}`); + logger.debug(`geoId: POST ${url} body=${bodyJson}`); const response = await fetch(url, { method: 'POST', @@ -48,8 +49,8 @@ async function fetchGeoidProperties( if (!response.ok) { const errorBody = await response.text(); - console.error( - `geoid: request failed with status ${response.status}: ${errorBody}` + logger.error( + `geoId: request failed with status ${response.status}: ${errorBody}` ); return {}; } @@ -57,12 +58,12 @@ async function fetchGeoidProperties( const responseJson = await response.json(); const id = responseJson?.id; if (!id) { - console.error( - `geoid: response missing id field, body=${JSON.stringify(responseJson)}` + logger.error( + `geoId: response missing id field, body=${JSON.stringify(responseJson)}` ); return {}; } - console.log(`geoid: received id=${id}`); + logger.debug(`geoId: received id=${id}`); return { id }; } diff --git a/functions/src/property-generators/index.ts b/functions/src/property-generators/index.ts index 03541288e..c9b808396 100644 --- a/functions/src/property-generators/index.ts +++ b/functions/src/property-generators/index.ts @@ -22,7 +22,7 @@ export type { PropertyGeneratorHandler, } from './types'; -import { geoidHandler } from './geoid'; +import { geoIdHandler } from './geo-id'; import { whispHandler } from './whisp'; import type { PropertyGeneratorHandler } from './types'; @@ -30,6 +30,6 @@ export const propertyGeneratorHandlers: Record< string, PropertyGeneratorHandler > = { - geoid: geoidHandler, + geoId: geoIdHandler, whisp: whispHandler, }; From e36aea82d929928d288826843409b9976afccd90 Mon Sep 17 00:00:00 2001 From: Roberto Fontanarosa Date: Fri, 17 Apr 2026 00:19:55 +0200 Subject: [PATCH 09/10] fixed misspelled geoId --- .../job-integration-editor/job-integration-editor.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/app/components/shared/job-integration-editor/job-integration-editor.component.ts b/web/src/app/components/shared/job-integration-editor/job-integration-editor.component.ts index e81143d3a..206db9a38 100644 --- a/web/src/app/components/shared/job-integration-editor/job-integration-editor.component.ts +++ b/web/src/app/components/shared/job-integration-editor/job-integration-editor.component.ts @@ -42,7 +42,7 @@ export class JobIntegrationEditorComponent { description: $localize`:@@app.cards.whispIntegration.description:Enable Whisp integration for this job.`, }, { - id: 'geoid', + id: 'geoId', title: $localize`:@@app.cards.geoidIntegration.title:GeoID integration`, description: $localize`:@@app.cards.geoidIntegration.description:Enable GeoID integration for this job.`, }, From be135ffacc0e980150f0e8e0bdb3a7628fec4adc Mon Sep 17 00:00:00 2001 From: Roberto Fontanarosa Date: Thu, 21 May 2026 12:03:24 +0200 Subject: [PATCH 10/10] use logger instead of console.log --- functions/src/on-create-loi.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/functions/src/on-create-loi.ts b/functions/src/on-create-loi.ts index 1de30ec24..69cac9585 100644 --- a/functions/src/on-create-loi.ts +++ b/functions/src/on-create-loi.ts @@ -18,6 +18,7 @@ import { FirestoreEvent, QueryDocumentSnapshot, } from 'firebase-functions/v2/firestore'; +import * as logger from 'firebase-functions/logger'; import { getDatastore } from './common/context'; import { broadcastSurveyUpdate } from './common/broadcast-survey-update'; import { GroundProtos } from '@ground/proto'; @@ -80,8 +81,8 @@ export async function onCreateLoiHandler( const newProperties = await handler(config, geometry); properties = updateProperties(properties, newProperties, config.prefix); } catch (e) { - console.error( - `LOI ${loiId}: property generator '${generatorId}' failed:`, + logger.error( + `onCreateLoi: loiId=${loiId} property generator '${generatorId}' failed:`, e ); }