Skip to content

Commit c816db4

Browse files
committed
fix(apps): address functions deployment compatibility and resolve callable signature issues
1 parent ac95c7d commit c816db4

File tree

16 files changed

+286
-78
lines changed

16 files changed

+286
-78
lines changed

apps/BUILD.bazel

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,23 @@ copy_to_bin(
1919
],
2020
)
2121

22+
copy_to_bin(
23+
name = "firebase_assets",
24+
srcs = [
25+
26+
# Firebase function files
27+
"//apps/functions:functions_files",
28+
29+
# Firebase hosted application files
30+
"//apps/code-of-conduct:application_files",
31+
".firebaserc",
32+
"deploy_wrapper_local.sh",
33+
"firebase.json",
34+
"firestore.indexes.json",
35+
"firestore.rules",
36+
],
37+
)
38+
2239
firebase.firebase_binary(
2340
name = "serve",
2441
chdir = package_name(),

apps/code-of-conduct/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ ng_project(
1717
":node_modules/@angular/compiler",
1818
":node_modules/@angular/core",
1919
":node_modules/@angular/fire",
20+
":node_modules/@angular/material",
2021
":node_modules/@angular/platform-browser",
2122
":node_modules/@angular/router",
2223
":node_modules/zone.js",

apps/code-of-conduct/app/block-user/block-user.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export class BlockUserComponent {
102102
updateUser() {
103103
this.dialogRef.disableClose = true;
104104
this.blockService
105-
.update(this.providedData.user!.username, this.blockUserForm.value)
105+
.update({username: this.providedData.user!.username, data: this.blockUserForm.value})
106106
.then(() => this.dialogRef.close(), console.error)
107107
.finally(() => {
108108
this.blockUserForm.enable();
Lines changed: 51 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,8 @@
11
import {Injectable, inject} from '@angular/core';
2-
import {
3-
updateDoc,
4-
collection,
5-
QueryDocumentSnapshot,
6-
FirestoreDataConverter,
7-
collectionSnapshots,
8-
Firestore,
9-
doc,
10-
getDoc,
11-
} from '@angular/fire/firestore';
2+
import {QueryDocumentSnapshot, FirestoreDataConverter} from '@angular/fire/firestore';
123
import {httpsCallable, Functions} from '@angular/fire/functions';
13-
import {map, shareReplay} from 'rxjs';
4+
import {map, shareReplay, from, switchMap, BehaviorSubject} from 'rxjs';
5+
import {MatSnackBar} from '@angular/material/snack-bar';
146

157
export interface BlockUserParams {
168
/** The username of the user being blocked. */
@@ -46,55 +38,64 @@ export type BlockedUserFromFirestore = BlockedUser & {
4638
export class BlockService {
4739
/** Firebase functions instance, provided from the root. */
4840
private functions = inject(Functions);
49-
/** Firebase firestore instance, provided from the root. */
50-
private firestore = inject(Firestore);
41+
/** Snackbar for displaying failure alerts. */
42+
private snackBar = inject(MatSnackBar);
43+
/** Subject to trigger refreshing the blocked users list. */
44+
private refreshBlockedUsers$ = new BehaviorSubject<void>(undefined);
5145

52-
/** Request a user to be blocked by the blocking service. */
53-
readonly block = httpsCallable(this.functions, 'blockUser');
54-
55-
/** Request a user to be unblocked by the blocking service. */
56-
readonly unblock = httpsCallable<UnblockUserParams>(this.functions, 'unblockUser');
57-
58-
/** Request a sync of all blocked users with the current Github blockings. */
59-
readonly syncUsersFromGithub = httpsCallable<void>(this.functions, 'syncUsersFromGithub');
46+
/** Request all blocked users. */
47+
getBlockedUsers = this.asCallable<void, BlockedUserFromFirestore[]>('getBlockedUsers', true);
6048

6149
/** All blocked users current blocked by the blocking service. */
62-
readonly blockedUsers = collectionSnapshots(
63-
collection(this.firestore, 'blockedUsers').withConverter(converter),
64-
).pipe(
50+
readonly blockedUsers = this.refreshBlockedUsers$.pipe(
51+
switchMap(() => from(this.getBlockedUsers())),
6552
map((blockedUsers) =>
6653
blockedUsers
67-
.map((snapshot) => snapshot.data())
54+
.map((user) => ({
55+
...user,
56+
blockUntil: user.blockUntil === false ? false : new Date(user.blockUntil),
57+
blockedOn: new Date(user.blockedOn),
58+
}))
6859
.sort((a, b) => (a.username.toLowerCase() > b.username.toLowerCase() ? 1 : -1)),
6960
),
7061
shareReplay(1),
7162
);
7263

64+
/** Request a user to be blocked. */
65+
block = this.asCallable<BlockUserParams, void>('blockUser');
66+
67+
/** Request a user to be unblocked. */
68+
unblock = this.asCallable<UnblockUserParams, void>('unblockUser');
69+
70+
/** Request a sync of all blocked users with the current Github blockings. */
71+
syncUsersFromGithub = this.asCallable<void, void>('syncUsersFromGithub');
72+
7373
/** Update the metadata for a blocked user. */
74-
async update(username: string, data: Partial<BlockedUser>) {
75-
const userDoc = await getDoc(
76-
doc(collection(this.firestore, 'blockedUsers').withConverter(converter), username),
77-
);
78-
if (userDoc.exists()) {
79-
return await updateDoc(userDoc.ref, data);
80-
}
81-
throw Error(`The entry for ${username} does not exist`);
82-
}
83-
}
74+
update = this.asCallable<{username: string; data: Partial<BlockedUser>}, void>('updateUser');
8475

85-
export const converter: FirestoreDataConverter<BlockedUser> = {
86-
toFirestore: (user: BlockedUser) => {
87-
return user;
88-
},
89-
fromFirestore: (data: QueryDocumentSnapshot<BlockedUser>) => {
90-
return {
91-
username: data.get('username'),
92-
context: data.get('context'),
93-
comments: data.get('comments'),
94-
blockedBy: data.get('blockedBy'),
95-
blockUntil:
96-
data.get('blockUntil') === false ? false : new Date(data.get('blockUntil').seconds * 1000),
97-
blockedOn: new Date(data.get('blockedOn').seconds * 1000),
76+
/**
77+
* Helper function to create a callable function that automatically refreshes the blocked users list.
78+
* @param callableName The name of the callable function to create.
79+
* @returns A function that can be called to invoke the callable function.
80+
*/
81+
private asCallable<T, R>(
82+
callableName: string,
83+
skipRefresh = false,
84+
): (callableArg: T) => Promise<R> {
85+
return async (callableArg: T) => {
86+
try {
87+
const result = await httpsCallable<T, R>(this.functions, callableName)(callableArg);
88+
if (!skipRefresh) {
89+
this.refreshBlockedUsers$.next();
90+
}
91+
return result.data;
92+
} catch (error) {
93+
const message = error instanceof Error ? error.message : 'Unknown error';
94+
this.snackBar.open(`Failed to execute ${callableName}: ${message}`, 'Dismiss', {
95+
duration: 5000,
96+
});
97+
throw error;
98+
}
9899
};
99-
},
100-
};
100+
}
101+
}
Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,66 @@
1-
<mat-table [dataSource]="blockService.blockedUsers">
1+
<mat-table [dataSource]="blockService.blockedUsers | async">
22
<ng-container matColumnDef="username">
33
<mat-header-cell *matHeaderCellDef> Username </mat-header-cell>
44
<mat-cell class="username-cell" *matCellDef="let user">
5-
<img src="https://github.com/{{user.username}}.png" alt="username">
6-
<span>{{user.username}}</span>
5+
<img src="https://github.com/{{ user.username }}.png" alt="username" />
6+
<span>{{ user.username }}</span>
77
</mat-cell>
88
</ng-container>
99

1010
<ng-container matColumnDef="blockUntil">
1111
<mat-header-cell *matHeaderCellDef> Blocked Until </mat-header-cell>
12-
<mat-cell *matCellDef="let user"> {{user.blockUntil === false ? 'Blocked Indefinitely' : user.blockUntil | date}} </mat-cell>
12+
<mat-cell *matCellDef="let user">
13+
{{ user.blockUntil === false ? 'Blocked Indefinitely' : (user.blockUntil | date) }}
14+
</mat-cell>
1315
</ng-container>
1416

1517
<ng-container matColumnDef="blockedBy">
1618
<mat-header-cell *matHeaderCellDef> Blocked By </mat-header-cell>
1719
<mat-cell class="username-cell" *matCellDef="let user">
18-
<span>{{user.blockedBy}}</span>
20+
<span>{{ user.blockedBy }}</span>
1921
</mat-cell>
2022
</ng-container>
2123

2224
<ng-container matColumnDef="actions">
2325
<mat-header-cell *matHeaderCellDef>
24-
<button matTooltip="Resync blocked users from Github into the app" [disabled]="forceSyncInProgress" mat-icon-button (click)="forceSync()">
25-
<mat-progress-spinner *ngIf="forceSyncInProgress" mode="indeterminate" diameter="24"></mat-progress-spinner>
26+
<button
27+
matTooltip="Resync blocked users from Github into the app"
28+
[disabled]="forceSyncInProgress"
29+
mat-icon-button
30+
(click)="forceSync()"
31+
>
32+
<mat-progress-spinner
33+
*ngIf="forceSyncInProgress"
34+
mode="indeterminate"
35+
diameter="24"
36+
></mat-progress-spinner>
2637
<mat-icon *ngIf="!forceSyncInProgress">sync</mat-icon>
2738
</button>
2839
</mat-header-cell>
29-
<mat-cell *matCellDef="let user;">
30-
<button matTooltip="Unblock {{user.username }} immediately" [disabled]="user.inProgress" mat-icon-button (click)="unblock(user)">
31-
<mat-progress-spinner *ngIf="user.inProgress" mode="indeterminate" diameter="24"></mat-progress-spinner>
40+
<mat-cell *matCellDef="let user">
41+
<button
42+
matTooltip="Unblock {{ user.username }} immediately"
43+
[disabled]="user.inProgress"
44+
mat-icon-button
45+
(click)="unblock(user)"
46+
>
47+
<mat-progress-spinner
48+
*ngIf="user.inProgress"
49+
mode="indeterminate"
50+
diameter="24"
51+
></mat-progress-spinner>
3252
<mat-icon *ngIf="!user.inProgress">lock_open</mat-icon>
3353
</button>
34-
<button matTooltip="Edit information for {{user.username}}" mat-icon-button (click)="editUser(user)">
54+
<button
55+
matTooltip="Edit information for {{ user.username }}"
56+
mat-icon-button
57+
(click)="editUser(user)"
58+
>
3559
<mat-icon>edit</mat-icon>
3660
</button>
3761
</mat-cell>
3862
</ng-container>
3963

4064
<mat-header-row *matHeaderRowDef="columns"></mat-header-row>
41-
<mat-row *matRowDef="let row; columns: columns;"></mat-row>
42-
</mat-table>
65+
<mat-row *matRowDef="let row; columns: columns"></mat-row>
66+
</mat-table>

apps/code-of-conduct/main.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {routes} from './app/app.routes.js';
1111
import {environment} from './environment.js';
1212
import {provideFunctions, getFunctions} from '@angular/fire/functions';
1313
import {provideFirestore, getFirestore} from '@angular/fire/firestore';
14+
import {MatSnackBarModule} from '@angular/material/snack-bar';
1415

1516
if (environment.production) {
1617
enableProdMode();
@@ -19,7 +20,7 @@ if (environment.production) {
1920
bootstrapApplication(AppComponent, {
2021
providers: [
2122
provideRouter(routes),
22-
importProvidersFrom([BrowserAnimationsModule]),
23+
importProvidersFrom([BrowserAnimationsModule, MatSnackBarModule]),
2324
provideFirebaseApp(() => initializeApp(environment.firebase)),
2425
provideAuth(() => getAuth()),
2526
provideFunctions(() => getFunctions()),

apps/deploy_wrapper_local.sh

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#!/usr/bin/env bash
2+
set -e
3+
4+
# Navigate to the directory where the script is running
5+
cd "$(dirname "$0")"
6+
7+
if [ -L functions/node_modules/firebase-functions ]; then
8+
TARGET=$(readlink functions/node_modules/firebase-functions)
9+
VIRTUAL_NODE_MODULES="functions/node_modules/$(dirname "$TARGET")"
10+
echo "Materializing firebase-functions symlink..."
11+
rm functions/node_modules/firebase-functions
12+
cp -rL "functions/node_modules/$TARGET" functions/node_modules/firebase-functions
13+
14+
echo "Creating symlinks for dependencies from $VIRTUAL_NODE_MODULES..."
15+
for dep in "$VIRTUAL_NODE_MODULES"/*; do
16+
dep_name=$(basename "$dep")
17+
if [ "$dep_name" != "firebase-functions" ]; then
18+
dep_target=$(readlink -f "$dep")
19+
rm -rf "functions/node_modules/$dep_name"
20+
ln -sf "$dep_target" "functions/node_modules/$dep_name"
21+
fi
22+
done
23+
fi
24+
25+
# Create .bin directory and symlink for firebase-functions executable
26+
# firebase-tools explicitly searches for .bin/firebase-functions to locate the SDK
27+
echo "Creating .bin/firebase-functions symlink..."
28+
mkdir -p functions/node_modules/.bin
29+
ln -sf ../firebase-functions/lib/bin/firebase-functions.js functions/node_modules/.bin/firebase-functions
30+
chmod +x functions/node_modules/.bin/firebase-functions
31+
32+
# Fix package.json to match CommonJS bundle format
33+
echo "Removing type: module from package.json..."
34+
sed -i '/"type": "module"/d' functions/package.json
35+
36+
echo "Running firebase deploy..."
37+
npx -p firebase-tools firebase deploy --project internal-200822 --config firebase.json

apps/firestore.rules

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
rules_version = '2';
22
service cloud.firestore {
33
match /databases/{database}/documents {
4+
match /blockedUsers/{user} {
5+
// Deny all reads from client, must use cloud functions for authorization
6+
allow read: if false;
7+
// Deny all writes from client, must use cloud functions
8+
allow write: if false;
9+
}
10+
11+
// Deny access to anything else by default
412
match /{document=**} {
5-
allow read, create: if request.auth != null;
6-
allow update: if request.auth != null && request.resource.data.diff(resource.data).affectedKeys().hasOnly(['blockUntil', 'comments']);
13+
allow read, write: if false;
714
}
815
}
916
}

apps/functions/BUILD.bazel

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,29 @@ package(default_visibility = ["//visibility:private"])
55

66
npm_link_all_packages()
77

8+
# TODO: Figure out a different way to deal with the file linking issues.
9+
genrule(
10+
name = "fixed_bundle",
11+
srcs = [":raw_bundle"],
12+
outs = ["bundle.js"],
13+
cmd = """
14+
for f in $(locations :raw_bundle); do
15+
if [[ $$f == *.js ]]; then
16+
cp $$f $@
17+
break
18+
fi
19+
done
20+
chmod +w $@
21+
sed -i 's/(0, import_node_module\\.createRequire)(import_meta\\.url)/(0, import_node_module.createRequire)(__filename)/g' $@
22+
sed -i -E 's/import_([a-zA-Z0-9_]+)\\.default/(import_\\1.default || import_\\1)/g' $@
23+
""",
24+
)
25+
826
copy_to_bin(
927
name = "functions_files",
1028
srcs = [
1129
"package.json",
12-
":bundle",
30+
":fixed_bundle",
1331
"//apps/functions:node_modules/firebase-tools",
1432
],
1533
visibility = ["//apps:__pkg__"],
@@ -28,7 +46,7 @@ ts_project(
2846
)
2947

3048
esbuild(
31-
name = "bundle",
49+
name = "raw_bundle",
3250
srcs = [
3351
":functions",
3452
"//apps/functions:node_modules/firebase-functions",

apps/functions/code-of-conduct/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ ts_project(
2020
name = "lib",
2121
srcs = [
2222
"blockUser.ts",
23+
"getBlockedUsers.ts",
2324
"shared.ts",
2425
"syncUsers.ts",
2526
"unblockUser.ts",
27+
"updateUser.ts",
2628
],
2729
deps = [
2830
"//apps/functions:node_modules/@octokit/auth-app",

0 commit comments

Comments
 (0)