brillout / wildcard-api

Functions as API.
MIT License
368 stars 14 forks source link

New query language #60

Open brillout opened 4 years ago

brillout commented 4 years ago

This would be a new project entirely independent but compatible with Wildcard.

This is ambitious and I'm not sure if/when I'll finish this.

In case you ask yourself why this is necessary in the context of Wildcard: it's basically for large scale applications that need to decouple the development of several frontends from the backend development. For most users, Wildcard alone is enough.

A preview:

// Browser-side

// Note how we only depend on Wildcard here.
import { server } from '@wildcard-api/client';

// The query syntax is JSON-like.
const posts = await server.query({
  _table: 'posts',
  authorId: {
    _table: 'users',
    id: '*',
    username: 'brillout',
  },
  title: '*',
  content: '*',
});
assert(posts[0].title==='Introducing a new query language');
assert(posts[0].content.startsWith(
  'When GraphQL was announced I was super excited but after the honeymoon'
));

// Similarly for upserting data
await server.query({
  _table: 'posts',
  authorId: 'brillout',
  title: 'NQL + Wildcard = <3',
  content: 'WIP',
});

// There is a field with '*' => data retrieval
// No field with '*' => data mutation

Resolvers + Wildcard integration

const { server } = require('@wildcard-api/server');
const { resolveQuery, addResolver } = require('nql');

server.query = async function(query) {
  const { data } = await resolveQuery(query);
  return data;
};

// One resolver per table for data retrieval
addResolver('posts', async (selectFields, filterFields) => {
  // A trick is that `filterFields[fieldName]` is always an array.
  // This solves the N+1 problem!
  assert(!filterFields.id || filterFields.id.constructor === Array);

  // (No SQL injection, everything is sanitized.)
  const res = await db.sql([
    `SELECT ${selectFields.join(', ')} FROM posts`,
    // Do this for every `key in filterFields`.
    `WHERE id IN (`${filterFields.id.map(id => `'${id}'`).join(', ')})`,
  ].join(' '));

  return res;
});

// One resolver per table is enough for any graph-like query.
// With no N+1 problem.

With "object-level permissions":

const { server } = require('@wildcard-api/server');
const { resolveQuery, addPermissions } = require('nql');

server.query = async function(query) {
  // We need the context for permissions
  const context = this;
  const { data, permissionDenied } = await resolveQuery(query, context);
  if (permissionDenied) {
    return { permissionDenied };
  } else {
    return { data };
  }
};

// We define permissions programmatically.
// This is simple and powerful.
addPermissions('read', ({object, context}) => {
  const { isAdmin } = context.user;
  // Admins can read everything
  if (isAdmin) {
    return true;
  }

  // Anyone can read everything from the `posts` table
  if (object._table==='posts') {
    return true;
  }

  // Fine grained permission: anyone can read everything from
  // the `users` table except for the `password` field.
  if (object._table === 'users') {
    if (object.password) {
      return false;
    } else {
      return true;
    }
  }

  // Anything else is forbidden
  return false;
});

addPermissions('write', ({object, context}) => {
  // Same than above but for mutation
});