Output function name based on constant string

Hi,

I’m using ReScript to work with Firebase functions.

I have two parts:

  • A callable function defined in Node.js
  • A frontend application calling the function in the browser.

The way Firebase works is that you can export functions from your index.js in Node:

import {onCall, HttpsError} from "firebase-functions/v2/https";

export const myAction = onCall(request => { return 42 })

And then the client can do something like:

import { getFunctions, httpsCallable } from "firebase/functions";

const functions = getFunctions();
const callMyAction = httpsCallable(functions, 'myAction ');

Notice that myAction needs to line up in both the server and the client.
So it is a bit of a magic string deal here.

Can I define my function name as a constant string in ReScript (in a shared file that both the client and server use)?
Using it on the client is easy, but can I somehow tell rescript that it needs to output my export const with the same constant name?

One option that comes to mind is to bind to module as an external, which would let you treat index.js’s exports as a dictionary:

// Firebase.res
type request
type response = JSON.t
type apiCallback

@module("firebase-functions/v2/https")
external onCall: (request => response) => apiCallback = "onCall"

// ApiDefinition.res
let myAction = "myAction"
let myOtherAction = "myOtherAction"

// ApiImpl.res
let myAction = Firebase.onCall(_ => JSON.Number(42.0))
let myOtherAction = Firebase.onCall(_ => JSON.String("Hello!"))

// Index.res
type module_ = {
  mutable exports: Js.Dict.t<Firebase.apiCallback>
}

external module_: module_ = "module"

module_.exports = Js.Dict.fromArray([
  (ApiDefinition.myAction, ApiImpl.myAction),
  (ApiDefinition.myOtherAction, ApiImpl.myOtherAction),
])

Thanks, works but forces me to use commonjs. Some of my other code is esm, any chance I can do something similar there?

That’ll depend somewhat on firebase. One possibility would be to try a default export, since that’s what cjs modules are interpreted as in esm. In other words, replace

type module_ = {
  mutable exports: Js.Dict.t<Firebase.apiCallback>
}

external module_: module_ = "module"

module_.exports = Js.Dict.fromArray([
  (ApiDefinition.myAction, ApiImpl.myAction),
  (ApiDefinition.myOtherAction, ApiImpl.myOtherAction),
])

with

let default = Js.Dict.fromArray([
  (ApiDefinition.myAction, ApiImpl.myAction),
  (ApiDefinition.myOtherAction, ApiImpl.myOtherAction),
])

and see if firebase can still interpret what you’re doing.

Yeah firebase is quite brittle when it some to this.

What I need is:

// exposes createOrder
export * from "./CreateOrder.mjs"

leads to

functions[europe-west3-createOrder]

When trying:

import * as Domain from "../shared/Domain.mjs";
import * as Js_dict from "rescript/lib/es6/js_dict.js";
import * as CreateOrder from "./CreateOrder.mjs";

var $$default = Js_dict.fromArray([[
        Domain.FunctionNames.createOrder,
        CreateOrder.createOrder
      ]]);

export {
  $$default as default,
}

it leads to

functions[europe-west3-default-createOrder]

Actually, I probably want to generate some code for my client based on my Rescript function files.

On the server side I have something like:

let createOrder: Firebase.Functions.callableFunction<
  Domain.createOrderRequest,
  string,
> = Firebase.Functions.Https.onCall(
  ~opts={
    region: Domain.firebaseRegion,
    allowedCors: Common.allowedCors,
  },
  ~handler=request => {
    let requestData: createOrderRequest = request.data
    Console.log(requestData)
    Promise.resolve("https://nojaf.com")
  },
)

If I can somehow process all files and detect any let binding that has type callableFunction and generate my client side API.

Something like:

let createOrder: Firebase.Functions.httpsCallable<
  Domain.createOrderRequest,
  string,
> = Functions.httpsCallable(functions, ~name="createOrder")

if I can generate this, it will always match my server code.

Is there any way to get the ReScript (typed) AST?

This might actually be fixed by tweaking the import, though at that point it’d be kinda moot.

// exposes createOrder
export createOrder from "./CreateOrder.mjs"

On the codegen side, you might be able to make use of the generic transform of GitHub - zth/rescript-embed-lang: A general purpose PPX and library for embedding other languages into ReScript, via code generation. to define an API. Would need to be felt out, though. More generally, ppxs were the typical was to work with the AST, but it involves dipping down into ocaml land.