Skip to content

fif-lang/fifql

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

18 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

fifql - Stack-based Query Language for Clojure(script) Web APIs

fifql is still under active development, and is considered *alpha*

https://img.shields.io/clojars/v/fifql.svg

fifql is a query language consisting of fif, and an exposed web server handler for querying a web server. It is meant to be a replacement for graphql, while providing the full benefits of a sandboxed stack machine.

fifql leverages the fif stack-machine as a query language, which offers an easier medium of expressing data formatted in the EDN data format. fifql works awesome with clojure and clojurescript applications on both the server-side, and on the receiving client-side.

1 Example

This example creates a new fifql server containing one custom word function which returns an integer value plus 2. It also defines a word variable ‘server-details which contains the server name and port.

(ns fifql.example.server
  (:require

   ;; High-performance Web Server
   [org.httpkit.server :as httpkit]

   ;; Routing Library
   [compojure.core :refer [GET POST defroutes]]
   [compojure.route :as route]
   
   ;; Fifql Library
   [fifql.server :refer [create-ring-request-handler]]
   [fifql.core :as fifql]))


(def server-name "A fifql Example Server")
(def server-port 8080)


;; Create our stack machine, and define some word functions
(def stack-machine
  (-> (fifql/create-stack-machine)

      (fifql/set-word 'add2 (fifql/wrap-function 1 (fn [x] (+ 2 x)))
       :doc "(n -- n) Add 2 to the value"
       :group :fifql/example)

      (fifql/set-var 'server-details {:server-port server-port :server-name server-name}
       :doc "The server details"
       :group :fifql/example)))


;; Create our ring request handler to use with Httpkit
(def fifql-handler
  (create-ring-request-handler
   :prepare-stack-machine stack-machine))


;; Create our routes. The fifql ring handler supports both GET and POST requests
(defroutes app
  (GET "/fifql" req fifql-handler)
  (POST "/fifql" req fifql-handler)
  (route/not-found "<h1>Page not Found</h1>"))


;; Start the web server. Note that any server that supports ring
;; request handlers are supported.
(defn start
  []
  (httpkit/run-server #'app {:port server-port}))


(defn -main [& args]
  (start))

Querying the server via a curl command:

# HTTP GET Examples

$ curl http://localhost/fifql?query=2,add2
{:input-string "2,add2", :stack (4), :stdout [], :stderr []}

$ curl http://localhost/fifql?query=server-details
{:input-string "server-details", :stack ({:server-port 8080, :server-name "A fifql Example Server"}), :stdout [], :stderr []}

$ curl http://localhost/fifql?query=server-details,:server-port,get
{:input-string "server-details,:server-port,get", :stack (8080), :stdout [], :stderr []}


# HTTP POST Examples

$ curl -d "2 2 +" -X POST http://localhost:8080/fifql -H "Accept: application/edn"
{:input-string "2 2 +", :stack (4), :stdout [], :stderr []}


$ curl -d "\"Hello World!\" println" -X POST http://localhost:8080/fifql -H "Accept: application/edn"
{:input-string "\"Hello World!\" println", :stack (), :stdout ["Hello World!\r\n"], :stderr []}

We can also query using clojure and clojurescript. An example within clojure:

(require '[fifql.client :refer [query sform]])

(def some-value 10)

(query "http://localhost:8080/fifql"
       (sform %= some-value 2 +))
;; {:input-string "10 2 +", :stack (12), :stdout [], :stderr []}

Note that we can inline clojure evaluations within the `sform` function by escaping with a preceding %= symbol.

Similarly, there is a clojurescript equivalent query that involves a callback:

(ns fifql.cljs.example
 (:require [fifql.client :refer [query sform] :include-macros true]))

(query "http://localhost:8080/fifql"
       (sform 2 2 +)
       (fn [result] (.log js/console result)))
;; {:input-string "2 2 +", :stack (4), :stdout [], :stderr []}

2 Requirements

fif-ql requires clojure 1.9+

3 Installation

For the latest version, please visit clojars.org

4 Introduction

fifql is inspired by GraphQL and offers more flexibility when performing queries. Instead of forcing the user to tie into a particular schema, why not let them come up with thier own schemas from fundamental data structures?

Sometimes it can be unclear what a user wants from an API, so this gives them complete freedom on how the data should be retrieved from the system.

Additionally, the fif stack language is already sandboxed and includes additional security to prevent malicious intent.

As an example, assume that I want to retrieve the first 10 users from a user-listing

In GraphQL, this query would look like this:

{userListing(first: 10) {
  totalCount
  items {
    name
    id
  }
  endCursor
  hasNextPage
}}

In fifql, this query is constructed from a few word functions, namely example/user-listing, example/user-count, and example/users-after?

;; What key value pairs do we want from each user in the user-listing?
def user-keys [:name :id]

;; Grab the first 10 values in the user listing, and place in the word variable 'ulisting
{:first 10} example/user-listing *ulisting <> setg

;; Construct our end cursor, place in the word variable 'end-cursor
ulisting last :id get *end-cursor <> setg

;; Construct our data to be returned on the stack
{:total-count (example/user-count)

 ;; map over the user-listing selecting only the key-value pairs that we want
 :items ((user-keys select-keys) ulisting map vec)

 :end-cursor (end-cursor str)
 :has-next-page? (end-cursor example/users-after?)} ?

;;
;; Notes:
;; '?' is used to 'realize' the data, this is a fundamental fif concept.

The result of the first element of the stack:

{:total-count 43
 :items [{:name "Ben" :id 1} {:name "John" :id 2} ...]
 :end-cursor "9"
 :has-next-page? true}

An important note to make about the fifql version of the query. The user has chosen how to represent the data for themselves, leaving them with full control. This takes unneeded burden off of the API development.

In the event that such queries become commonplace, the API can be extended to include more personalized and concrete functions for the client ie.

{:first 10} example/user-page

;; generates the same query result as the query above.

As a result, this makes fifql more flexible and a much more powerful alternative to GraphQL.

4.1 Spooky Scary Stack-machines

If you’re not familiar with stack programming, a lot of what has been presented here might look scary and unconventional. Stack-programming made a prime appearance when the programming language Forth was developed, and it has remained an often overlooked alternative in modern software development outside of embedded systems.

Stack-programming is great as a query language due to how it presents a lot fewer surprises. Values are simply pushed and popped off of a stack. The resulting stack is then returned to the user who performed the query. It couldn’t get much simpler than that.

That being said there are several more advantages

4.1.1 Interop is easy

Clojure functions are easily adapted to work in the fif stack machine. No need to write a schema and write a bunch of resolvers, just write clojure functions and wrap them into word functions.

4.1.2 Presented in the EDN data format

Since everything is done in the EDN data format, there is no need for complicated keyword to string conversions when working within clojure and clojurescript.

4.1.3 Testing is easy

Stack-machines developed can be easily tested, with a ton of examples available in the fif repository source code.

4.1.4 More advanced language features are rewarding, but not required

You can take full advantage of fifql without having to learn the ins and outs of the entire fif language. In the event that you would like to learn more, you can check out the fif playground and get more accustomed to what is possible.

4.1.5 Mutations are just more word functions

GraphQL makes a distinction with Queries and Mutations. This distinction does not exist in fifql, since it’s just another word function.

5 Getting the full scoop on ring requests in fifql

The fifql ring request handler has been designed to allow you to dictate which stack-machine a request is privileged to use.

As an example, I will write a :prepare-stack-machine function that will only allow a user to use a set of word functions that can mutate the server.

(def guest-stack-machine
     (-> (fifql/create-stack-machine)
         (import-guest-libs)))


(def admin-stack-machine
     (-> guest-stack-machine
         (import-admin-libs)))


(def fifql-handler
  (create-ring-request-handler
   :prepare-stack-machine
   (fn [req]
       (if (-> req :session :admin?)
           admin-stack-machine
           guest-stack-machine))))

create-ring-request-handler can also optionally include the :post-response function which can manipulate the response while having full access to the evaluated stack machine.

As an example, i’ll check the stack-machine for a username and password, and manipulate the user’s session if they provide the correct credentials.

(require '[fifql.core :refer [get-var]])

;; ..building on the last example

(def fifql-handler
     (create-ring-request-handler
      ;;..
      :post-response
      (fn [sm request response]
         (let [username (get-var sm 'username)
               password (get-var sm 'password)]

            ;; Set the session to an admin session if the
            ;; stack-machine has the correct `username and `password
            ;; set.
            (if (and (= username "admin") (= password "123"))
              (assoc-in response [:session :admin?] true)
              response)))))

An example client request

(require '[fifql.client :refer [sform query]])


(defn authenticate-script [username password]
  (sform 
   def username %= username
   def password %= password))


(query "http://localhost:8080/fifql" (authenticate-script "admin" "123"))

6 NodeJS express request handler

fifql also has support for ExpressJS for use with clojurescript in NodeJS. Here is an example of a ExpressJS server.

(ns fifql.dev.server
  (:require
   [fif.core :as fif]
   [fifql.core :as fifql]
   [fifql.server :refer [create-express-request-handler
                         add-request-middleware!]]))


(def express (js/require "express"))
(def app (express))

(def server-name "A fifql Example Server")
(def server-port 8081)
(def server-details
  {:server-port server-port :server-name server-name})


;; Create our stack machine, and define some word functions
(def stack-machine
  (-> (fifql/create-stack-machine)

      (fifql/set-word 'add2 (fifql/wrap-function 1 (fn [x] (+ 2 x)))
       :doc "(n -- n) Add 2 to the value"
       :group :fifql/example)

      (fifql/set-var 'server-details server-details
       :doc "The server details"
       :group :fifql/example)))


;; Generate our fifql handler
(def fifql-handler
  (create-express-request-handler
   :prepare-stack-machine stack-machine))

;; Create some routes. Note: we also add our own middleware
(doto app
  (add-request-middleware!)
  (.get "/fifql" fifql-handler)
  (.post "/fifql" fifql-handler))


(defn -main [& args]
  (.listen app server-port
           #(.log js/console (str "Server Started on port " server-port))))


(set! *main-cli-fn* -main)

Starting this server and running the the same examples from the introduction reveals the same output:

$ curl -d "2 add2" http://localhost:8081/fifql -H "Content-Type: application/fif"
{:input-string "2 add2", :stack (4), :stdout [], :stderr []}

6.1 Differences between Ring and ExpressJS Request Handlers

  • The returned :stderr key for responses is unused in NodeJS, since clojurescript does not map to it. The key remains in the returned response for consistency.
  • When overloading the :post-response function handler in the ExpressJS request handler does not require you to return the response object due to the nature of ExpressJS’s design.