-
Notifications
You must be signed in to change notification settings - Fork 31
Feat: remote functions #3097
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
chrismclarke
wants to merge
13
commits into
master
Choose a base branch
from
feat/remote-functions
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+527
−1
Open
Feat: remote functions #3097
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
677a2d7
feat: wip remote function module
chrismclarke cb5a66a
chore: update readme
chrismclarke f9e2601
feat: wip debug page
chrismclarke cc8fb40
feat: function region support
chrismclarke 9fc8a7d
feat: error handling
chrismclarke dcb4d13
feat: interactive param editor
chrismclarke 1cdedc2
feat: token expiry frontend
chrismclarke 4e32fa5
chore: code tidying
chrismclarke 040dd9b
chore: improve debug messages
chrismclarke 1d5e615
chore: review tidying
chrismclarke 949b59e
Merge branch 'master' into feat/remote-functions
chrismclarke b4b2b30
Merge branch 'feat/remote-functions' of https://github.com/idemsinter…
chrismclarke 795ec1a
Merge branch 'master' into feat/remote-functions
esmeetewinkel File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| ## Overview | ||
|
|
||
|
|
||
| ## Setup (Firebase Provider) | ||
| 1. Deploy test functions from [Open App Builder Functions](https://github.com/IDEMSInternational/open-app-builder-functions) Repo | ||
|
|
||
| 2a. Update deployment config to use firebase remote_functions provider | ||
| ```ts | ||
| config.remote_functions={ | ||
| provider:'firebase' | ||
| } | ||
| ``` | ||
|
|
||
| 2b. If functions are deployed to any region other than `us-central1` | ||
| ```ts | ||
| config.firebase={ | ||
| functions:{ | ||
| region: 'europe-west1' | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| 3. Follow steps below to enable App Check which is used to ensure functions can only be invocated from trusted sources | ||
|
|
||
| ### App Check - Web | ||
|
|
||
| Follow instructions in docs to create to enable RecaptchaV3 Enterprise API on google cloud project that corresponds to firebase project, and generate a key for use on the project | ||
|
|
||
| https://firebase.google.com/docs/app-check/web/recaptcha-provider | ||
|
|
||
| Include any domains you wish to accept requests from. | ||
|
|
||
| Set the generated site key both in the firebase console for the web-app to be used, and within the deployment config. This key is public-facing, so fine to commit to source control | ||
|
|
||
| ```ts | ||
| config.firebase = { | ||
| appCheck:{recaptchaEnterpriseSiteKey:'myKey'} | ||
| } | ||
| ``` | ||
| Whilst a single key can be used across multiple apps, it is recommended to create per-app keys | ||
|
|
||
| By default only firebase functions deployed specifying appCheck will have their usage restricted to allowed sources. Additional resources such as Firestore DB can be restricted from the dashboard console. | ||
|
|
||
| Usage can be monitored both from the [Firebase Console](https://console.firebase.google.com) App Check page, and [Google Cloud Console Recaptcha](https://console.cloud.google.com/security/recaptcha/) | ||
|
|
||
| ### App Check - Android | ||
| Follow instructions to link play store project with firebase app check | ||
| https://firebase.google.com/docs/app-check/android/play-integrity-provider | ||
|
|
||
| Additional platform setup not required as integrated with `capacitor-firebase/app-check` | ||
|
|
||
| ### App Check - IOS | ||
| Follow instructions to integrate `DeviceCheck` on older devices (ios 11+) | ||
| https://firebase.google.com/docs/app-check/ios/devicecheck-provider | ||
|
|
||
| Follow instructions to integrate `App Attest` on newer devices (ios 14+) | ||
| https://firebase.google.com/docs/app-check/ios/app-attest-provider | ||
|
|
||
| ## Testing | ||
| Remote functions can be tested locally from a debug page, with support for whitelisted device tokens. | ||
| See review notes in https://github.com/IDEMSInternational/open-app-builder/pull/3097 | ||
|
|
||
| ## TODO | ||
|
|
||
| ### Functions | ||
| - [ ] Create and deploy specific test function for use in debug | ||
|
|
||
| ### Tests | ||
| - [ ] Functional testing on native | ||
| - [ ] Functional testing on web | ||
| - [ ] Service and provider unit tests | ||
|
|
||
| ### Authoring Integration | ||
| - [ ] Handle storing response in field/list for access | ||
| - [ ] Add actions to trigger | ||
| - [ ] Create feat template | ||
|
|
||
| ## Troubleshooting | ||
|
|
||
| ### Access to fetch has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. | ||
|
|
||
| By default firebase enables cors for all functions, so most likely this error is a false identification. Cors errors will also get displayed if no function matching the name has been deployed. This will likely coincide with separate internal error response from firebase | ||
|
|
||
| ```json | ||
| { | ||
| "code": "functions/internal", | ||
| "message": "internal", | ||
| "name": "FirebaseError" | ||
| } | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import { inject } from "@angular/core"; | ||
| import { Routes } from "@angular/router"; | ||
| import { RemoteFunctionService } from "./remote-function.service"; | ||
| import { TemplateActionRegistry } from "src/app/shared/components/template/services/instance/template-action.registry"; | ||
| import { RemoteFunctionActionFactory } from "./remote-function.actions"; | ||
| import { ENVIRONMENT_INITIALIZER, makeEnvironmentProviders } from "@angular/core"; | ||
|
|
||
| // Export the routes to allow debug route to be included in main app-routing.module | ||
| export const REMOTE_FUNCTION_ROUTES: Routes = [ | ||
| { | ||
| path: "", | ||
| loadComponent: () => | ||
| import("./pages/debug/remote-function-debug.page").then((m) => m.RemoteFunctionDebugPage), | ||
| }, | ||
| ]; | ||
|
|
||
| // Use initializer to register actions used within global templates | ||
| export function initialiseRemoteFunctions() { | ||
| return () => { | ||
| const service = inject(RemoteFunctionService); | ||
| const templateActionRegistry = inject(TemplateActionRegistry); | ||
| const { remote_function } = new RemoteFunctionActionFactory(service); | ||
| templateActionRegistry.register({ remote_function }); | ||
| }; | ||
| } | ||
|
|
||
| // use environment initializer to ensure init only called once (sync/non-blocking) | ||
| // if needing async logic an APP_INITIALIZER can be used instead | ||
| export function provideRemoteFunctions() { | ||
| return makeEnvironmentProviders([ | ||
| RemoteFunctionService, | ||
| { provide: ENVIRONMENT_INITIALIZER, useFactory: initialiseRemoteFunctions, multi: true }, | ||
| ]); | ||
| } |
89 changes: 89 additions & 0 deletions
89
src/app/feature/remote-function/pages/debug/remote-function-debug.page.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| <ion-content class="ion-padding"> | ||
| <h2>Remote Functions</h2> | ||
|
|
||
| <p>Trigger remote function with data parameters</p> | ||
| <div style="display: flex; gap: 16px"> | ||
| <div> | ||
| <h3>Target Function</h3> | ||
| <ion-input | ||
| fill="outline" | ||
| placeholder="Function Name" | ||
| [(ngModel)]="targetFunctionName" | ||
| ></ion-input> | ||
| </div> | ||
| <div> | ||
| <h3>Parameters</h3> | ||
| <div style="display: flex; flex-direction: column; gap: 12px"> | ||
| @for (pair of paramPairs(); track $index) { | ||
| <div style="display: flex; gap: 8px; align-items: center"> | ||
| <ion-input | ||
| fill="outline" | ||
| placeholder="Key" | ||
| [value]="pair.key()" | ||
| (ionInput)="pair.key.set($any($event.target).value)" | ||
| ></ion-input> | ||
| <ion-input | ||
| fill="outline" | ||
| placeholder="Value" | ||
| [value]="pair.value()" | ||
| (ionInput)="pair.value.set($any($event.target).value)" | ||
| ></ion-input> | ||
| <ion-button fill="clear" (click)="removeParam($index)"> | ||
| <ion-icon name="close-outline"></ion-icon> | ||
| </ion-button> | ||
| </div> | ||
| } | ||
| <ion-button fill="outline" (click)="addParam()">+ Add Param</ion-button> | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <ion-button | ||
| [disabled]="!targetFunctionName() || requestPending()" | ||
| (click)="triggerFunction(targetFunctionName())" | ||
| >Run</ion-button | ||
| > | ||
|
|
||
| @if(response().data; as responseData){ | ||
| <h4>Data</h4> | ||
| <div>{{responseData | json}}</div> | ||
| } @if(response().error; as responseError){ | ||
| <h4>Error</h4> | ||
| <div>{{responseError | json}}</div> | ||
| } | ||
|
|
||
| <hr style="background-color: #cacaca; height: 1px; width: 100%; margin: 2em 0" /> | ||
|
|
||
| @switch (config.provider) { @case('firebase'){ | ||
|
|
||
| <!-- Firebase Provider--> | ||
|
|
||
| <h2>Firebase Provider</h2> | ||
| <h3>AppCheck Token</h3> | ||
| <p>Firebase uses appcheck to ensure functions only invocated from trusted source</p> | ||
|
|
||
| @if(firebaseToken(); as token){ | ||
| <div>✅ Token Available</div> | ||
| <div style="font-size: small"> | ||
| @if(token.expireTimeMillis; as expiry){ Expires: {{expiry}} } @else{ Expiry not Available on Web | ||
| } | ||
| </div> | ||
| <ion-button style="margin-top: 1em">Refresh</ion-button> | ||
| } @if(firebaseTokenError(); as tokenError){ | ||
| <div>⚠️ Could Not Retrieve Token</div> | ||
| <div style="margin-top: 1em; font-size: small">{{tokenError}}<br /></div> | ||
| <div style="margin-top: 1em"> | ||
| Ensure the current app, site or debug token is registered in the firebase console | ||
| </div> | ||
|
|
||
| } } | ||
|
|
||
| <!-- Default Provider --> | ||
| @default { | ||
| <p>Debug page not configured for provider: {{config.provider}}</p> | ||
| } } | ||
|
|
||
| <hr style="background-color: #cacaca; height: 1px; width: 100%; margin: 2em 0" /> | ||
| <h3>Deployment Config</h3> | ||
| <div>{{config | json}}</div> | ||
| </ion-content> | ||
Empty file.
76 changes: 76 additions & 0 deletions
76
src/app/feature/remote-function/pages/debug/remote-function-debug.page.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| import { Component, computed, inject, model, OnInit, signal } from "@angular/core"; | ||
| import { RemoteFunctionService } from "../../remote-function.service"; | ||
| import { IonicModule } from "@ionic/angular"; | ||
| import { JsonPipe } from "@angular/common"; | ||
| import { FirebaseFunctionProvider } from "../../providers/firebase"; | ||
| import { FormsModule } from "@angular/forms"; | ||
|
|
||
| interface ParamSignal { | ||
| key: ReturnType<typeof signal<string>>; | ||
| value: ReturnType<typeof signal<string>>; | ||
| } | ||
|
|
||
| @Component({ | ||
| templateUrl: "remote-function-debug.page.html", | ||
| styleUrl: "remote-function-debug.page.scss", | ||
| standalone: true, | ||
| imports: [IonicModule, JsonPipe, FormsModule], | ||
| }) | ||
| export class RemoteFunctionDebugPage implements OnInit { | ||
| public service = inject(RemoteFunctionService); | ||
|
|
||
| public targetFunctionName = model<string>(""); | ||
|
|
||
| public paramPairs = signal<ParamSignal[]>([]); | ||
|
|
||
| public response = signal({ error: undefined, data: undefined }); | ||
|
|
||
| public requestPending = signal(false); | ||
|
|
||
| private firebaseProvider = computed(() => { | ||
| if (this.config.provider === "firebase") { | ||
| return this.service.provider as FirebaseFunctionProvider; | ||
| } | ||
| return undefined; | ||
| }); | ||
|
|
||
| public firebaseToken = computed(() => this.firebaseProvider()?.["appCheckToken"]()); | ||
| public firebaseTokenError = computed(() => this.firebaseProvider()?.["appCheckTokenError"]()); | ||
|
|
||
| public config = this.service["config"]; | ||
chrismclarke marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| async ngOnInit() { | ||
| await this.service.ready(); | ||
| } | ||
|
|
||
| // --- Param management --- | ||
| public addParam(key: string = "", value: string = "") { | ||
| this.paramPairs.update((pairs) => [...pairs, { key: signal(key), value: signal(value) }]); | ||
| } | ||
| public removeParam(index: number) { | ||
| this.paramPairs.update((pairs) => pairs.filter((_, i) => i !== index)); | ||
| } | ||
|
|
||
| private buildParams(): Record<string, any> { | ||
| const params: Record<string, any> = {}; | ||
| for (const { key, value } of this.paramPairs()) { | ||
| const k = key().trim(); | ||
| const v = value().trim(); | ||
| if (k) { | ||
| params[k] = v; | ||
| } | ||
| } | ||
| return params; | ||
| } | ||
|
|
||
| // --- Function --- | ||
| public async triggerFunction(name: string) { | ||
| this.requestPending.set(true); | ||
| this.response.set({ error: undefined, data: undefined }); | ||
| const params = this.buildParams(); | ||
| console.log("[Remote Function] Run", { name, params }); | ||
| const res = await this.service.provider.invoke(name, params); | ||
| this.response.set(res); | ||
| this.requestPending.set(false); | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.