Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions functions/src/common/datastore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -300,7 +301,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(
Expand All @@ -312,7 +313,7 @@ export class Datastore {
geometryObject.coordinates
);

return geometryObject;
return geometryObject as unknown as Geometry;
}

static fromFirestoreValue(coordinates: any) {
Expand Down
53 changes: 53 additions & 0 deletions functions/src/on-create-loi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -75,6 +81,9 @@ describe('onCreateLoiHandler()', () => {
mockFirestore
.doc('config/integrations/propertyGenerators/whisp')
.set(whispConfig);
mockFirestore
.doc('config/integrations/propertyGenerators/geoid')
.set(geoidConfig);
});

afterEach(() => {
Expand Down Expand Up @@ -118,4 +127,48 @@ 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,
json: () =>
Promise.resolve({ 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<QueryDocumentSnapshot | undefined>);

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<QueryDocumentSnapshot | undefined>);

const loiData = (await mockFirestore.doc(LOI_PATH).get()).data();
expect(loiData?.[l.properties]?.['geoid_geoid_code']).toBeUndefined();
});
});
20 changes: 16 additions & 4 deletions functions/src/on-create-loi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,27 @@ 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;
}

properties = updateProperties(properties, newProperties, config.prefix);
try {
const newProperties = await handler(config, geometry);
properties = updateProperties(properties, newProperties, config.prefix);
} catch (e) {
console.error(
Comment thread
rfontanarosa marked this conversation as resolved.
`LOI ${loiId}: property generator '${generatorId}' failed:`,
e
);
}

Object.keys(properties)
.filter(key => typeof properties[key] === 'object')
Expand Down
55 changes: 51 additions & 4 deletions functions/src/property-generators/geoid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,55 @@
* 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<Properties> => ({});
const defaultHeaders = { 'Content-Type': 'application/json' };

export async function geoidHandler(
Comment thread
rfontanarosa marked this conversation as resolved.
Outdated
config: PropertyGeneratorConfig,
geometry: Geometry
): Promise<Properties> {
const { headers, url } = config;

return fetchGeoidProperties(
url,
{ ...defaultHeaders, ...headers },
{ type: 'Feature', geometry, properties: {} }
);
}

async function fetchGeoidProperties(
url: string,
headers: Headers,
body: object
): Promise<Properties> {
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 responseJson = await response.json();
const id = responseJson?.id;
if (!id) {
console.error(
`geoid: response missing id field, body=${JSON.stringify(responseJson)}`
);
return {};
}
console.log(`geoid: received id=${id}`);
Comment thread
rfontanarosa marked this conversation as resolved.
Outdated

return { id };
}
4 changes: 3 additions & 1 deletion functions/src/property-generators/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
* limitations under the License.
*/

import type { Geometry } from 'geojson';

export type Properties = Record<string, string | number>;

export type Headers = Record<string, string>;
Expand All @@ -30,5 +32,5 @@ export type PropertyGeneratorConfig = {

export type PropertyGeneratorHandler = (
config: PropertyGeneratorConfig,
geometry: object
geometry: Geometry
) => Promise<Properties>;
6 changes: 3 additions & 3 deletions functions/src/property-generators/whisp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/

import { geojsonToWKT } from '@terraformer/wkt';
import { Datastore } from '../common/datastore';
import type { Geometry } from 'geojson';
import type {
Body,
Headers,
Expand All @@ -36,11 +36,11 @@ const defaultHeaders = { 'Content-Type': 'application/json' };

export async function whispHandler(
config: PropertyGeneratorConfig,
geometry: object
geometry: Geometry
): Promise<Properties> {
const { body, headers, url } = config;

const wkt = geojsonToWKT(Datastore.fromFirestoreMap(geometry));
const wkt = geojsonToWKT(geometry);

return fetchWhispProperties(
url,
Expand Down
Loading