JSPM

  • Created
  • Published
  • Downloads 48
  • Score
    100M100P100Q76398F
  • License ISC

expose async functions as an api

Package Exports

  • renraku/x/api/api-context.js
  • renraku/x/api/api-error.js
  • renraku/x/identities/as-api.js
  • renraku/x/identities/as-shape.js
  • renraku/x/identities/as-topic.js
  • renraku/x/remote/loopback-json-remote.js
  • renraku/x/servelet/make-json-http-servelet.js
  • renraku/x/types/symbols/augment-symbol.js

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (renraku) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

連絡
れんらく
R·E·N·R·A·K·U

npm install renraku

🔆 enlightened typescript api library
    🛎️ simple — expose async functions
    🎭 shapeshifting — client objects impersonate serverside api
    🛡 flexible auth — set auth policies for each group of functions
    🔧 testability — run your functions anywhere for testing or dev
    🧠 sophisticated types — painstakingly engineered for integrity
    🌐 compatible — exposes standard json rpc
    ⚠️ experimental — live on the edge


⛩️ RENRAKU STEP-BY-STEP

you can skip this tutorial and just read the working s/example/ code (which is used for testing purposes, so mind the relative import paths)

  1. let's build a really simple api together.
    we'll integrate auth like it's a real app,
    run a node server, and call it from the browser

  2. we'll start with two functions

    export async function sayHello(name: string) {
      return `Hello ${name}, welcome!`
    }
    
    export async function sayGoodbye(name: string) {
      return `Goodbye ${name}, see you later.`
    }

    this will soon become our api

  3. now let's spice that up with some auth

    export interface ExampleAuth {
      doctorate: boolean
    }
    
    export async function sayHello(auth: ExampleAuth, name: string) {
      if (auth.doctorate) return `Hello Dr. ${name}, welcome!`
      else return `Hello ${name}, welcome!`
    }
    
    export async function sayGoodbye(auth: ExampleAuth, name: string) {
      if (auth.doctorate) return `Goodbye Dr. ${name}, see you later.`
      else return `Goodbye ${name}, see you later.`
    }

    now we're greeting users who have a doctorate differently than otherwise

    these functions are now properly conforming renraku procedures

    • all renraku functions must always accept a first argument called auth
    • the auth argument is reserved for processed auth data (usually a user's details and privileges)
  4. we formalize those functions into a renraku "topic"

    import {asTopic} from "renraku/x/identities/as-topic.js"
    
    export interface ExampleAuth {
      doctorate: boolean
    }
    
    export const greeterTopic = asTopic<ExampleAuth>()({
     //                                         ↑
     //          ⚠️ curried for magical typescript inference ⚠️
    
      async sayHello(auth, name: string) {
        if (auth.doctorate) return `Hello Dr. ${name}, welcome!`
        else return `Hello ${name}, welcome!`
      },
    
      async sayGoodbye(auth, name: string) {
        if (auth.doctorate) return `Goodbye Dr. ${name}, see you later.`
        else return `Goodbye ${name}, see you later.`
      },
    })

    we named this topic "greeter"

    • every function in the same renraku topic must share the same auth type.
    • some renraku library functions, like asTopic, are curried up and you have to invoke them twice, like asTopic()(topic).
      it looks weird, but you desparately want this: it circumvents a typescript limitation, allowing you to specify your auth type while also allowing your topic type to be inferred (so you can avoid maintaining a separate interface for your topic)
    • and if you like, you can group your topic functions into arbitrarily-nested objects, like this
      const greeterTopic = asTopic<ExampleAuth>()({
        async sayBoo(auth) {return "BOO!"},
        nestedGroupA: {
          nestedGroupB: {
            async sayYolo(auth) {return "#YOLO"},
          }
        }
      })
  5. we assemble topics into an api object

    import {asApi} from "renraku/x/identities/as-api.js"
    import {apiContext} from "renraku/x/api/api-context.js"
    
    export interface ExampleMeta {
      token: string
    }
    
    export const exampleApi = () => asApi({
      greeter: apiContext<ExampleMeta, ExampleAuth>()({
        expose: greeterTopic,
        policy: {processAuth: async (meta, request) => ({doctorate: meta.token === "abc"})},
      })
    })

    an api contains api contexts

    • an api context contains a topic and the policy which processes the auth for that topic
    • our example processAuth is stupid-simple: if the token is "abc", the user has a doctorate.
      of course in a real app, this is where we might do token verification, and query our database about the user and whatnot
    • and yes, you can group your api-contexts into arbitrarily-nested objects
    • however you cannot nest a context under another context (so that auth policies cannot conflict)
  6. we expose the api on a nodejs server

    import {makeJsonHttpServelet} from "renraku/x/servelet/make-json-http-servelet.js"
    import {makeNodeHttpServer} from "renraku/x/server/make-node-http-server.js"
    
    const servelet = makeJsonHttpServelet(exampleApi())
    const server = makeNodeHttpServer(servelet)
    server.listen(8001)

    now our server is up and running

    • the servelet simply accepts requests and returns responses.
      it runs the auth processing and executes the appropriate topic function
    • then we make and start a standard node http server with the servelet
  7. clientside: we define a shape object for our api

    import {asShape} from "renraku/x/identities/as-shape.js"
    import {_augment} from "renraku/x/types/symbols/augment-symbol.js"
    
    export const exampleShape = asShape<ReturnType<typeof exampleApi>>({
      greeter: {
        [_augment]: {getMeta: async() => ({token: "abc"})},
        sayHello: true,
        sayGoodbye: true,
      }
    })

    the shape outlines your api and auth meta data for each topic

    • typescript will enforce that the shape matches your topic exactly
    • each topic must be given an _augment object with a getMeta function.
      this specifies what meta data will be sent with each request to the topic
    • this runtime shape object is vital for generating remotes
  8. clientside: we generate a remote, and start calling functions from the browser

    import {generateJsonBrowserRemote} from "renraku/x/remote/generate-json-browser-remote.js"
    void async function main() {
    
      const {greeter} = generateJsonBrowserRemote({
        headers: {},
        shape: exampleShape,
        link: "http://localhost:8001",
      })
    
      // execute an http json remote procedure call
      const result1 = await greeter.sayHello("Chase")
      const result2 = await greeter.sayGoodbye("Moskal")
    
      console.log(result1) // "Hello Dr. Chase, welcome!"
      console.log(result2) // "Goodbye Dr. Moskal, see you later."
    }()

⛩️ RENRAKU FOR ARCHITECTURE, DEVELOPMENT, AND TESTING

  • your frontend systems should be agnostic about how they receive business logic
    so you can freely pass in remote or local functionality

    import {Business} from "renraku/x/types/primitives/business.js"
    async function makeMyFrontendSystem({greeter}: {
    
        // accept business logic,
        // but you don't even want to know if it's remote or local
        greeter: Business<GreeterTopic>
    
      }) {
      const result = greeter.sayHello("Chase")
      console.log(result)
    }

    i like to run my whole serverside in loopback within the browser, for rapid development

  • transform a topic to business logic for direct usage

    import {asTopic} from "renraku/x/identities/as-topic.js"
    import {toBusiness} from "renraku/x/transform/to-business.js"
    
    interface ExampleAuth {
      doctorate: boolean
    }
    
    const greeterTopic = asTopic<ExampleAuth>()({
      async sayHello(auth, name: string) {
        if (auth.doctorate) return `Hello Dr. ${name}, welcome!`
        else return `Hello ${name}, welcome!`
      }
    })
    
    // now we can curry the topic for local usage
    // we specify the auth provided with each call
    const greeter = toBusiness<ExampleAuth>()({
      topic: greeter,
      getAuth: async() => ({doctorate: true}),
    })
    
    // now we can call functions, and the auth is baked-in
    void async function main() {
      const result = await greeter.sayHello("Chase")
      console.log(result) // "Hello Dr. Chase, welcome!"
    }()
    • this is useful for running simple tests
  • full loopback for full-stack testing

    import {loopbackJsonRemote} from "renraku/x/remote/loopback-json-remote.js"
    
    // spin up the servelet on the clientside
    // (servelet is normally on the serverside)
    const servelet = makeJsonHttpServelet(exampleApi())
    
    // generate a "loopback" remote which directly calls the servelet
    // instead of any network activity
    const {greeter} = loopbackJsonRemote({
      servelet,
      shape: exampleShape,
      link: "http://localhost:8001",
    })
    
    // execute locally, no network activity
    const result1 = await greeter.sayHello("Chase")
    const result2 = await greeter.sayGoodbye("Moskal")
    
    console.log(result1) // "Hello Dr. Chase, welcome!"
    console.log(result2) // "Goodbye Dr. Moskal, see you later."
    • a loopback fully excercises all facilities, clientside and serverside, emulating http transactions, and running your auth processing — all without any real network activity

⛩️ RENRAKU ERROR HANDLING

  • thrown exceptions will trigger exceptions on the clientside
  • if you throw a renraku ApiError, the message and the http status code will be sent to the client
    import {ApiError} from "renraku/x/api/api-error.js"
    
    // later, somewhere in your topic functionality
    throw new ApiError(403, "forbidden; user must be qualified with a doctorate")
  • for all other thrown exceptions, the details are censored from the client, and a generic 500 ApiError is sent instead

📖 RENRAKU TERMINOLOGY

  • topic — business logic functions. organized into objects, recursive
  • meta — data sent with each request
  • auth — processed auth data, passed to each business function
  • api — server definition containing topics. is a recursive collection of api contexts
  • api-context — binds a topic together with an auth policy
  • policy — defines how meta data is processed into auth data by the serverside
  • servelet — a function which executes an api, accepts a request and returns a response
  • shape — data structure describes an api's surface area and meta augmentations
  • augment — defines how the client sends meta data with requests
  • remote — a clientside proxy that mimics the shape of an api, whose functions execute remote calls


⛩️ RENRAKU WANTS YOU

contributions welcome

please consider posting an issue for any problems, questions, or comments you may have

and give me your github stars!
  // chase


      — RENRAKU means "contact" —