├── .gitignore ├── CHANGELOG.md ├── EASY-INSTALL-V1.5.SQL ├── Makefile ├── README.md ├── docs ├── console.md └── http.md ├── examples ├── test_ejs.sql ├── test_handlebars.sql ├── test_lodash.sql ├── test_momentjs.sql ├── test_mustache.sql └── test_underscore.sql ├── modules ├── base64.js └── http.js ├── package.json ├── supascript--1.0--1.1.sql ├── supascript--1.0.sql ├── supascript--1.1--1.2.sql ├── supascript--1.1.sql ├── supascript--1.2.sql ├── supascript--1.3.sql ├── supascript--1.4.sql ├── supascript--1.5.sql └── supascript.control /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | ## 1.5 3 | - `ALTER TABLE supascript_log ENABLE ROW LEVEL SECURITY;` 4 | - `ALTER TABLE supascript_js_modules ENABLE ROW LEVEL SECURITY;` 5 | Note: This changes the security model for functions called from `anon` and `authenticated` user roles. 6 | ## 1.4 7 | ### Documentation Updated 01 May 2021 8 | - Major overhaul of the README.md file 9 | ### Fixed 10 | - added `ALTER PUBLICATION supabase_realtime ADD TABLE supascript_log` so log file can be queried with the realtime engine 11 | ## 1.3 12 | ### Fixed 13 | - better handling for logging of the current query name (function name) 14 | ### New 15 | - added EASY-INSTALL-V1.3.SQL for quick copy-paste installation into Supabase 16 | - added package.json for publishing to npm 17 | ## 1.2 18 | ### New 19 | - added `console` object to log debug objects to `SUPASCRIPT_LOG` table 20 | - console.log() 21 | - console.info() 22 | - console.warn() 23 | - console.error() 24 | - console.assert() 25 | - console.time() 26 | - console.timeEnd() 27 | ## 1.1 28 | ### New 29 | - added `require("http")` built-in module to handle web requests 30 | -------------------------------------------------------------------------------- /EASY-INSTALL-V1.5.SQL: -------------------------------------------------------------------------------- 1 | ------------------------------ 2 | -- Install SupaScript v1.5 --- 3 | ------------------------------ 4 | CREATE EXTENSION IF NOT EXISTS PLV8; 5 | CREATE EXTENSION IF NOT EXISTS HTTP; 6 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 7 | 8 | DROP FUNCTION IF EXISTS supascript_init(); 9 | 10 | SET PLV8.START_PROC = 'supascript_init'; 11 | ALTER DATABASE POSTGRES SET PLV8.START_PROC TO 'supascript_init'; 12 | 13 | CREATE TABLE IF NOT EXISTS supascript_log 14 | ( 15 | id uuid primary key default uuid_generate_v4(), 16 | created timestamp with time zone DEFAULT CURRENT_TIMESTAMP, 17 | _catalog text DEFAULT CURRENT_CATALOG, 18 | _user text DEFAULT CURRENT_USER, 19 | _schema text DEFAULT CURRENT_SCHEMA, 20 | _schemas name[] DEFAULT CURRENT_SCHEMAS(true), 21 | _pid int DEFAULT PG_BACKEND_PID(), 22 | log_type text, 23 | query text, 24 | content jsonb 25 | ); 26 | 27 | CREATE TABLE IF NOT EXISTS supascript_js_modules (module text UNIQUE PRIMARY KEY, 28 | autoload BOOL DEFAULT FALSE, 29 | source text); 30 | 31 | CREATE OR REPLACE FUNCTION supascript_init() RETURNS VOID 32 | AS $$ 33 | 34 | this.moduleCache = {}; 35 | 36 | // this handles "TypeError: Do not know how to serialize a BigInt" 37 | function toJson(data) { 38 | if (data !== undefined) { 39 | return JSON.stringify(data, (_, v) => typeof v === 'bigint' ? `${v}#bigint` : v) 40 | .replace(/"(-?\d+)#bigint"/g, (_, a) => a); 41 | } 42 | } 43 | 44 | this.console = { 45 | timers:{}, 46 | write_to_log: function() { 47 | const arr = []; 48 | for (let i=0; i < arguments.length; i++) { 49 | if (!(i === 1 && arguments[0] === 'ASSERT')) { 50 | const arg = JSON.parse(toJson(arguments[i])); // required to handle bigint 51 | arr.push(arg); 52 | } 53 | } 54 | let query = ''; 55 | try { 56 | query = JSON.stringify(sql('select current_query()')[0].current_query); 57 | if (query.length > 23 && query.substr(0,23).toLowerCase() === '"with pgrst_source as (') { 58 | query = query.substr(23); 59 | let index = query.indexOf('AS pgrst_scalar'); 60 | if (index < 0) { index = 999; }; 61 | query = query.substr(0,index); 62 | query = query.replace(new RegExp(String.fromCharCode(92, 92, 34), 'g'), String.fromCharCode(34)); 63 | } 64 | } catch (queryParseError) { 65 | query = 'query parse error'; 66 | } 67 | const log_type = arr.shift(); 68 | sql(`insert into supascript_log (content, log_type, query) values ($1, $2, $3)`,[arr, log_type, query]); 69 | }, 70 | log: function() { 71 | console.write_to_log('LOG', ...arguments); 72 | }, 73 | info: function() { 74 | console.write_to_log('INFO', ...arguments); 75 | }, 76 | warn: function() { 77 | console.write_to_log('WARN', ...arguments); 78 | }, 79 | assert: function() { 80 | if (arguments[0] === false) { 81 | // arguments.shift(); // remove assert expression 82 | console.write_to_log('ASSERT', ...arguments); // log rest of arguments (1 to n) 83 | } 84 | }, 85 | error: function() { 86 | console.write_to_log('ERROR', ...arguments); 87 | }, 88 | time: function(label = 'DEFAULT_TIMER') { 89 | this.timers[label] = +new Date(); 90 | }, 91 | timeEnd: function(label = 'DEFAULT_TIMER') { 92 | console.write_to_log('TIMER',label,+new Date() - this.timers[label]); 93 | delete this.timers[label]; 94 | } 95 | 96 | }; 97 | 98 | // execute a Postgresql function 99 | // i.e. exec('my_function',['parm1', 123, {"item_name": "test json object"}]) 100 | this.exec = function(function_name, parms) { 101 | var func = plv8.find_function(function_name); 102 | return func(...parms); 103 | } 104 | 105 | this.load = function(key, source) { 106 | var module = {exports: {}}; 107 | try { 108 | eval("(function(module, exports) {" + source + "; })")(module, module.exports); 109 | } catch (err) { 110 | plv8.elog(ERROR, `eval error in source: ${err} (SOURCE): ${source}`); 111 | } 112 | 113 | // store in cache 114 | moduleCache[key] = module.exports; 115 | return module.exports; 116 | }; 117 | 118 | // execute a sql statement against the Postgresql database with optional args 119 | // i.e. sql('select * from people where first_name = $1 and last_name = $2', ['John', 'Smith']) 120 | this.sql = function(sql_statement, args) { 121 | if (args) { 122 | return plv8.execute(sql_statement, args); 123 | } else { 124 | return plv8.execute(sql_statement); 125 | } 126 | }; 127 | 128 | // emulate node.js "require", with automatic download from the internet via CDN sites 129 | // optional autoload (boolean) parameter allows the module to be preloaded later 130 | // i.e. var myModule = require('https://some.cdn.com/module_content.js', true) 131 | this.require = function(module, autoload) { 132 | if (module === 'http' || module === 'https') { 133 | // emulate NodeJS require('http') 134 | module = 'https://raw.githubusercontent.com/burggraf/SupaScript/main/modules/http.js'; 135 | } 136 | if(moduleCache[module]) 137 | return moduleCache[module]; 138 | var rows = plv8.execute( 139 | 'select source from supascript_js_modules where module = $1', 140 | [module] 141 | ); 142 | 143 | if (rows.length === 0 && module.substr(0,4) === 'http') { 144 | try { 145 | source = plv8.execute(`SELECT content FROM http_get('${module}');`)[0].content; 146 | } catch (err) { 147 | plv8.elog(ERROR, `Could not load module through http: ${module}`, JSON.stringify(err)); 148 | } 149 | try { 150 | /* the line below is written purely for esthetic reasons, so as not to mess up the online source editor */ 151 | /* when using standard regExp expressions, the single-quote char messes up the code highlighting */ 152 | /* in the editor and everything looks funky */ 153 | const quotedSource = source.replace(new RegExp(String.fromCharCode(39), 'g'), String.fromCharCode(39, 39)); 154 | 155 | plv8.execute(`insert into supascript_js_modules (module, autoload, source) values ('${module}', ${autoload ? true : false}, '${quotedSource}')`); 156 | } catch (err) { 157 | plv8.elog(ERROR, `Error inserting module into supascript_js_modules: ${err} ${module}, ${autoload ? true : false}, '${plv8.quote_literal(source)}'`); 158 | } 159 | return load(module, source); 160 | } else if(rows.length === 0) { 161 | plv8.elog(NOTICE, `Could not load module: ${module}`); 162 | return null; 163 | } else { 164 | return load(module, rows[0].source); 165 | } 166 | 167 | }; 168 | 169 | // Grab modules worth auto-loading at context start and let them cache 170 | var query = `select module, source from supascript_js_modules where autoload = true`; 171 | plv8.execute(query).forEach(function(row) { 172 | this.load(row.module, row.source); 173 | }); 174 | $$ LANGUAGE PLV8; 175 | ALTER TABLE supascript_log ENABLE ROW LEVEL SECURITY; 176 | ALTER TABLE supascript_js_modules ENABLE ROW LEVEL SECURITY; 177 | ALTER PUBLICATION supabase_realtime ADD TABLE supascript_log; 178 | 179 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | EXTENSION = supascript 2 | DATA = supascript--1.3.sql 3 | 4 | PG_CONFIG = pg_config 5 | PGXS := $(shell $(PG_CONFIG) --pgxs) 6 | include $(PGXS) 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SupaScript: NodeJS for PostgreSQL and Supabase 2 | 3 | - JavaScript-based, NodeJS-like, Deno-inspired extensions for Supabase. 4 | - Use `require()` to import node js modules into plv8 PostgreSQL from any location on the web, similar to [Deno](https://deno.land/). 5 | - `sql()` helper function to give easy access to PostgreSQL databases 6 | - `exec()` helper function to make it easy to call other functions written in SupaScript (PLV8), PLPGSQL, SQL or any other supported language 7 | - `console` emulation for `console.log()` and other `console` functions [See the docs](./docs/console.md) 8 | - (See [SupaScriptConsole](https://github.com/burggraf/SupaScriptConsole) client library for viewing console.log output in your browser console in real time) 9 | - Built-in support (like NodeJS built-ins) for: 10 | - `require("http")` [see documentation](./docs/http.md): easy interface to make web calls and send headers, etc. for GET, POST, PUT, PATCH, DELETE, HEAD, HEADER 11 | - Polyfills 12 | - Base64: btoa(), atob() 13 | - Packaged as a PostgreSQL extension or just use the EASY-INSTALL-Vx.y.SQL script to install it 14 | 15 | ## Make writing PostgreSQL modules fun again 16 | Can you write JavaScript in your sleep? Me too. 17 | Can you write PlpgSql queries to save your life? Me either. 18 | 19 | Enter our masked hero from heaven: [PLV8](https://plv8.github.io) 20 | 21 | So how do I write nifty JavaScript modules for Supabase / Postgres? Use SupaScript! 22 | 23 | ## Installation 24 | (If you're using Supabase, you can safely use method 1 below.) 25 | 26 | Method 1: Copy the contents of the file `EASY-INSTALL-Vx.yy.SQL` into a SQL window in your database and run it. 27 | - The PLV8 extension must be available on your server 28 | - The HTTP extension must be available on your server 29 | 30 | Method 2: Regular extension installation method (if SUPASCRIPT is already available on your server along with PLV8 and HTTP) 31 | ```sql 32 | CREATE EXTENSION SUPASCRIPT CASCADE 33 | ``` 34 | 35 | ## Quick Sample: 36 | Sample function: 37 | ```sql 38 | create or replace function test_underscore() 39 | returns json as $$ 40 | const _ = require('https://cdn.jsdelivr.net/npm/underscore@1.12.1/underscore-min.js'); 41 | const retval = _.map([1, 2, 3], function(num){ return num * 3; }); 42 | return retval; 43 | $$ language plv8; 44 | ``` 45 | The sample above uses the `require()` function to dynamically load the underscore library: 46 | ```js 47 | const module_name = require(, ); 48 | ``` 49 | where 50 | 51 | * `url-or-module-name`: either the public url of the node js module or a module name you've put into the plv8_js_modules table manually. 52 | 53 | * `autoload`: (optional) boolean: true if you want this module to be loaded automatically when the plv8 extension starts up, otherwise false 54 | 55 | ## Writing SupaScript functions on the server using JavaScript V8 56 | 57 | Functions are stored in your PostgreSQL database, so you write them (create them) as SQL statements: 58 | ```sql 59 | create or replace function hello_javascript(name text) 60 | returns json as $$ 61 | const retval = { message: `Hello, my good friend ${name}!` }; 62 | return retval; 63 | $$ language plv8; 64 | ``` 65 | 66 | ### Let's break this down 67 | ```sql 68 | create or replace function hello_javascript(name text) 69 | ``` 70 | Since we're creating a function (or updating it later using `replace`), all functions are written using the create/replace syntax. The name of the function here is simply `hello_javascript` and the parameter the function accepts is called `name` and it's type is `text`. You can, of course, accept multiple parameters separated by commas, but each must have a type, such as 'hello_javascript(name text, age int, big_blob_of_json json)`, etc. 71 | 72 | ```sql 73 | returns json 74 | ``` 75 | You need to add a return type to your function. Here we are saying this function returns a `JSON Object`. You can return text, int, or even void if you don't really need a return value. Any valid data type can be returned from a function. **Tip: if you have multiple items you want to return from a function, you probably want to return json -- this allows you the most flexibility.** 76 | 77 | ```sql 78 | as $$ 79 | // JAVASCRIPT CODE HERE 80 | $$ 81 | ``` 82 | Everything in between the $$ and $$ is pure JavaScript V8 code. Simple. 83 | ```sql 84 | language plv8; 85 | ``` 86 | You need to end your function with `language plv8;` to tell PostgreSQL to use the PLV8 (JavaScript V8) engine to run this function. You can use uppercase or lowercase here (`LANGUAGE PLV8;'). 87 | 88 | Now, you've got all that JavaScript goodness flowing, and it hits you -- What? I can't access the huge world of Node JS libraries? What do you expect me to do -- write JavaScript from scratch like an animal? Forget it! 89 | 90 | Enter **SupaScript require()**. 91 | 92 | ## Using require() 93 | If you've used NodeJS before, you're in love with require(). Since you don't have access to a file system, though, you can't use npm install. So we need to have a way to load those neato node_modules. How do we do it? 94 | 95 | ### Method 1: load from the web automatically 96 | This is the easiest **(and preferred)** method. 97 | 98 | ``` 99 | const module = require('https://url-to-public-function'); 100 | ``` 101 | Here's how we'd use the popular Moment JS library: 102 | ``` 103 | create or replace function test_momentjs() 104 | returns json as $$ 105 | const moment = require('https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.js', false); 106 | const retval = moment().add(7, 'days'); 107 | 108 | return retval; 109 | $$ language plv8; 110 | ``` 111 | Then just call this function from SQL: 112 | ``` 113 | select test_momentjs(); 114 | ``` 115 | 116 | Where do I find the url for the function or library I want to use? Hunt around on the library documentation page to find a CDN version of the library or look for documentation that shows how to load the library in HTML with a