Skip to content

Commit e77927b

Browse files
committed
Merge branch 'master' into rfontanarosa/1833/code-health-add-lint-check-for-copyright-headers
# Conflicts: # pnpm-lock.yaml
2 parents 3287335 + 28fcdd9 commit e77927b

72 files changed

Lines changed: 1661 additions & 274 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,25 @@ To deploy to your own production Firebase:
9393
### Next steps
9494

9595
For instructions on how to deploy to your own production Firebase project, see the [Ground Developer's Guide](https://github.com/google/ground-platform/wiki/Ground-Developer's-Guide).
96+
97+
## Common commands
98+
99+
All commands use [Nx](https://nx.dev) and [pnpm](https://pnpm.io). Run them from the repo root.
100+
101+
| Command | Description |
102+
| ------- | ----------- |
103+
| `nx start` | Build and start the local Firebase emulator and Angular dev server |
104+
| `nx run root:build` | Build all packages |
105+
| `nx run root:test` | Run all tests |
106+
| `nx run root:lint` | Lint all packages |
107+
| `nx run web:serve` | Serve the web app only (no emulator) |
108+
| `nx run web:serve:staging` | Serve the web app against the live staging environment |
109+
| `nx run web:test` | Run web tests |
110+
| `nx run web:extract-i18n` | Extract i18n messages from the web app |
111+
| `nx run functions:test` | Run Cloud Functions tests |
112+
| `nx run functions:emulate` | Start the Firebase emulator for functions only |
113+
| `nx run functions:logs` | Tail Cloud Functions logs |
114+
| `nx run functions:shell` | Open an interactive Cloud Functions shell |
115+
| `nx run root:export:local` | Save current emulator data to disk for use on next run |
116+
| `nx run root:deploy:staging` | Build and deploy all to staging |
117+
| `nx run root:deploy:production` | Build and deploy all to production |

docs/Gemfile.lock

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,34 @@
11
GEM
22
remote: https://rubygems.org/
33
specs:
4-
activesupport (7.1.3.4)
4+
activesupport (7.2.3.1)
55
base64
6+
benchmark (>= 0.3)
67
bigdecimal
7-
concurrent-ruby (~> 1.0, >= 1.0.2)
8+
concurrent-ruby (~> 1.0, >= 1.3.1)
89
connection_pool (>= 2.2.5)
910
drb
1011
i18n (>= 1.6, < 2)
11-
minitest (>= 5.1)
12-
mutex_m
13-
tzinfo (~> 2.0)
12+
logger (>= 1.4.2)
13+
minitest (>= 5.1, < 6)
14+
securerandom (>= 0.3)
15+
tzinfo (~> 2.0, >= 2.0.5)
1416
addressable (2.8.0)
1517
public_suffix (>= 2.0.2, < 5.0)
16-
base64 (0.2.0)
17-
bigdecimal (3.1.8)
18+
base64 (0.3.0)
19+
benchmark (0.5.0)
20+
bigdecimal (4.0.1)
1821
coffee-script (2.4.1)
1922
coffee-script-source
2023
execjs
2124
coffee-script-source (1.11.1)
2225
colorator (1.1.0)
2326
commonmarker (0.23.10)
24-
concurrent-ruby (1.3.3)
25-
connection_pool (2.4.1)
27+
concurrent-ruby (1.3.6)
28+
connection_pool (3.0.2)
2629
dnsruby (1.61.7)
2730
simpleidn (~> 0.1)
28-
drb (2.2.1)
31+
drb (2.2.3)
2932
em-websocket (0.5.2)
3033
eventmachine (>= 0.12.9)
3134
http_parser.rb (~> 0.6.0)
@@ -97,7 +100,7 @@ GEM
97100
activesupport (>= 2)
98101
nokogiri (>= 1.4)
99102
http_parser.rb (0.6.0)
100-
i18n (1.14.5)
103+
i18n (1.14.8)
101104
concurrent-ruby (~> 1.0)
102105
jekyll (3.9.3)
103106
addressable (~> 2.4)
@@ -223,8 +226,7 @@ GEM
223226
jekyll (>= 3.5, < 5.0)
224227
jekyll-feed (~> 0.9)
225228
jekyll-seo-tag (~> 2.1)
226-
minitest (5.23.1)
227-
mutex_m (0.2.0)
229+
minitest (5.27.0)
228230
net-http (0.9.1)
229231
uri (>= 0.11.1)
230232
nokogiri (1.19.1)
@@ -252,6 +254,7 @@ GEM
252254
sawyer (0.9.3)
253255
addressable (>= 2.3.5)
254256
faraday (>= 0.17.3, < 3)
257+
securerandom (0.4.1)
255258
simpleidn (0.2.1)
256259
unf (~> 0.1.4)
257260
terminal-table (1.8.0)

firestore/firestore.rules

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,8 @@ rules_version = '2';
182182
allow get: if canViewLoi(getSurvey(surveyId), resource.data);
183183
// Allow all survey users to list LOIs.
184184
allow list: if canViewSurvey(getSurvey(surveyId));
185-
// Allow if user is owner of the existing LOI or can manage survey.
186-
allow update, delete: if isLoiOwner(resource.data) || canManageSurvey(getSurvey(surveyId));
185+
// Allow if user is owner of the existing LOI and can collect data, or can manage survey.
186+
allow update, delete: if (isLoiOwner(resource.data) && canCollectData(getSurvey(surveyId))) || canManageSurvey(getSurvey(surveyId));
187187
}
188188

189189
// Apply survey-level permissions to submission documents.
@@ -194,8 +194,8 @@ rules_version = '2';
194194
allow get: if canViewSubmission(getSurvey(surveyId), resource.data);
195195
// Allow all survey users to list submissions.
196196
allow list: if canViewSurvey(getSurvey(surveyId));
197-
// Allow if user is owner of the existing submission or can manage survey.
198-
allow update, delete: if isSubmissionOwner(resource.data) || canManageSurvey(getSurvey(surveyId));
197+
// Allow if user is owner of the existing submission and can collect data, or can manage survey.
198+
allow update, delete: if (isSubmissionOwner(resource.data) && canCollectData(getSurvey(surveyId))) || canManageSurvey(getSurvey(surveyId));
199199
}
200200

201201
// Apply survey-level permissions to job documents.

functions/src/common/broadcast-survey-update.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ import * as admin from 'firebase-admin';
2525
* See also: https://firebase.google.com/docs/cloud-messaging/concept-options#collapsible_and_non-collapsible_messages
2626
*/
2727
export async function broadcastSurveyUpdate(topic: string): Promise<string> {
28+
if (process.env.FUNCTIONS_EMULATOR === 'true') {
29+
console.debug(`Skipping FCM message to ${topic} (emulator mode)`);
30+
return '';
31+
}
32+
2833
console.debug(`Sending message to ${topic}`);
2934

3035
return admin.messaging().send({ topic });

functions/src/handlers.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import cors from 'cors';
1818
import { DecodedIdToken } from 'firebase-admin/auth';
19-
import { onRequest, Request } from 'firebase-functions/v2/https';
19+
import { onRequest, HttpsOptions, Request } from 'firebase-functions/v2/https';
2020
import type { Response } from 'express';
2121
import { StatusCodes } from 'http-status-codes';
2222
import { getDecodedIdToken } from './common/auth';
@@ -74,8 +74,11 @@ export type HttpsRequestHandler = (
7474
/**
7575
* A synchronous HTTPS request handler. The HTTPS request is closed as soon as the handler resolves.
7676
*/
77-
export function onHttpsRequest(handler: HttpsRequestHandler) {
78-
return onRequest((req: Request, res: Response) =>
77+
export function onHttpsRequest(
78+
handler: HttpsRequestHandler,
79+
options: HttpsOptions = {}
80+
) {
81+
return onRequest(options, (req: Request, res: Response) =>
7982
corsMiddleware(req, res, () =>
8083
cookieParser()(
8184
req as any,

functions/src/import-geojson.spec.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { Blob, FormData } from 'formdata-node';
2828
import { StatusCodes } from 'http-status-codes';
2929
import { invokeCallbackAsync } from './handlers';
3030
import { SURVEY_ORGANIZER_ROLE } from './common/auth';
31-
import { resetDatastore } from './common/context';
31+
import { getDatastore, resetDatastore } from './common/context';
3232
import { Firestore } from 'firebase-admin/firestore';
3333
import { registry } from '@ground/lib';
3434
import { GroundProtos } from '@ground/proto';
@@ -274,13 +274,37 @@ describe('importGeoJson()', () => {
274274
await invokeCallbackAsync(importGeoJsonCallback, req, res, {
275275
email,
276276
} as DecodedIdToken);
277-
} catch (error) {
278-
console.log(error);
277+
} catch (err) {
278+
console.log(err);
279279
}
280280

281281
// Check post-conditions.
282282
expect(res.status).toHaveBeenCalledOnceWith(expectedStatus);
283283
expect(await loiData(surveyId)).toEqual(expected);
284284
})
285285
);
286+
287+
it('surfaces errors thrown during async file processing', async () => {
288+
// Make fetchSurvey reject to simulate an unexpected async error in the file handler.
289+
const db = getDatastore();
290+
spyOn(db, 'fetchSurvey').and.returnValue(
291+
Promise.reject(new Error('Database connection failed'))
292+
);
293+
294+
const req = await createPostRequestSpy(
295+
{ url: '/importGeoJson' },
296+
createPostData(surveyId, jobId, geoJsonWithPoint)
297+
);
298+
const res = createResponseSpy();
299+
300+
try {
301+
await invokeCallbackAsync(importGeoJsonCallback, req, res, {
302+
email,
303+
} as DecodedIdToken);
304+
} catch {
305+
// Expected to reject.
306+
}
307+
308+
expect(res.status).toHaveBeenCalledWith(StatusCodes.INTERNAL_SERVER_ERROR);
309+
});
286310
});

functions/src/import-geojson.ts

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -68,41 +68,45 @@ export function importGeoJsonCallback(
6868

6969
// This code will process each file uploaded.
7070
busboy.on('file', async (_fieldname, fileStream) => {
71-
const { survey: surveyId, job: jobId } = params;
72-
if (!surveyId || !jobId) {
73-
return error(StatusCodes.BAD_REQUEST, 'Missing survey and/or job ID');
74-
}
75-
const survey = await db.fetchSurvey(surveyId);
76-
if (!survey.exists) {
77-
return error(StatusCodes.NOT_FOUND, `Survey ${surveyId} not found`);
78-
}
79-
if (!canImport(user, survey)) {
80-
return error(
81-
StatusCodes.FORBIDDEN,
82-
`User does not have permission to import into survey ${surveyId}`
71+
try {
72+
const { survey: surveyId, job: jobId } = params;
73+
if (!surveyId || !jobId) {
74+
return error(StatusCodes.BAD_REQUEST, 'Missing survey and/or job ID');
75+
}
76+
const survey = await db.fetchSurvey(surveyId);
77+
if (!survey.exists) {
78+
return error(StatusCodes.NOT_FOUND, `Survey ${surveyId} not found`);
79+
}
80+
if (!canImport(user, survey)) {
81+
return error(
82+
StatusCodes.FORBIDDEN,
83+
`User does not have permission to import into survey ${surveyId}`
84+
);
85+
}
86+
87+
console.debug(
88+
`Importing GeoJSON into survey '${surveyId}', job '${jobId}'`
8389
);
84-
}
8590

86-
console.debug(
87-
`Importing GeoJSON into survey '${surveyId}', job '${jobId}'`
88-
);
91+
const parser = JSONStream.parse(['features', true], undefined);
8992

90-
const parser = JSONStream.parse(['features', true], undefined);
91-
92-
fileStream.pipe(
93-
parser
94-
.on('header', (data: any) => {
95-
try {
96-
onGeoJsonType(data.type);
97-
if (data.crs) onGeoJsonCrs(data.crs);
98-
} catch (error: any) {
99-
busboy.emit('error', error);
100-
}
101-
})
102-
.on('data', (data: any) => {
103-
if (!hasError) onGeoJsonFeature(data, surveyId, jobId);
104-
})
105-
);
93+
fileStream.pipe(
94+
parser
95+
.on('header', (data: any) => {
96+
try {
97+
onGeoJsonType(data.type);
98+
if (data.crs) onGeoJsonCrs(data.crs);
99+
} catch (error: any) {
100+
busboy.emit('error', error);
101+
}
102+
})
103+
.on('data', (data: any) => {
104+
if (!hasError) onGeoJsonFeature(data, surveyId, jobId);
105+
})
106+
);
107+
} catch (err) {
108+
busboy.emit('error', err);
109+
}
106110
});
107111

108112
// Handle non-file fields in the task. survey and job must appear

functions/src/index.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,17 @@ export const onCreatePasslistEntry = onDocumentCreated(
7070

7171
export const importGeoJson = onHttpsRequestAsync(importGeoJsonCallback);
7272

73-
export const exportCsv = onHttpsRequest(exportCsvHandler);
74-
75-
export const exportGeojson = onHttpsRequest(exportGeojsonHandler);
73+
export const exportCsv = onHttpsRequest(exportCsvHandler, {
74+
memory: '4GiB',
75+
timeoutSeconds: 3600,
76+
cpu: 2,
77+
});
78+
79+
export const exportGeojson = onHttpsRequest(exportGeojsonHandler, {
80+
memory: '4GiB',
81+
timeoutSeconds: 3600,
82+
cpu: 2,
83+
});
7684

7785
export const onCreateLoi = onDocumentCreated(
7886
loiPathTemplate,

0 commit comments

Comments
 (0)