1.1.13 • Published 1 day ago

bridg v1.1.13

Weekly downloads
-
License
ISC
Repository
github
Last release
1 day ago

Bridg - Query your DB directly from the frontend

Chat

Bridg let's you query your database from the client, like Firebase or Supabase, but with the power and type-safety of Prisma.

<input
  placeholder="Search for blogs.."
  onChange={async (e) => {
    const query = e.target.value;
    const blogs = await db.blog.findMany({ where: { title: { contains: query } } });
    setSearchResults(blogs);
  }}
/>

Getting Started
Querying Your Database
Protecting Your Data

Supported Databases

MongoDB, Postgres, MySQL (& Planetscale), SQLite, Microsoft SQL Server, Azure SQL, MariaDB, AWS Aurora, CockroachDB

Getting started

Example Projects

Next.js (basic) Simple Next.js example with SQLite database (Codesandbox)

Next.js (blogging app) - Next.js, next-auth authentication, CRUD examples, SQLite

create-react-app (serverless) - CRA + Postgres + Netlify function (for Bridg)

React Native - Expo App + Postgres + Netlify

Vue.js - Simple Vue / Nuxt example with SQLite database

Want an example project for your favorite framework? Feel free to create an issue, or a PR with a sample.

Add Bridg to an existing project

  1. Configure your project to use Prisma
    • Add the extendedWhereUnique preview feature to your schema.prisma
    • Note: If you run into issues, try using ^prisma@4.10.1, Prisma 5 support is currently in the works
generator client {
  provider        = "your-client"
  previewFeatures = ["extendedWhereUnique"]
}
  1. Install Bridg: npm install bridg
  2. Add the following script to your package.json :
{
  "scripts": {
    "generate": "npx prisma generate && npm explore bridg -- npm run generate"
  }
}
  1. Generate the client: npm run generate
    • This will need to be ran any time you change your DB schema
    • There is a known issue with the generate command on the default Windows command prompt. Please opt to use WSL until I get it fixed.
  2. Expose an API endpoint at /api/bridg to handle requests:
// Example Next.js API handler, translate to your JS API framework of choice
import { handleRequest } from 'bridg/server/request-handler';
import { PrismaClient } from '@prisma/client';

const db = new PrismaClient();

// allows all requests, don't ship like this, ya dingus
const rules = { default: true };

export default async function handler(req, res) {
  // Mock authentication, replace with any auth system you want
  const userId = 'authenticated-user-id';

  // pass the request (req.body), your prisma client (db),
  // the user making the request (uid), and your database rules (rules)
  const { data, status } = await handleRequest(req.body, { db, uid: userId, rules });

  return res.status(status).json(data);
}

Note on applications with separate server / client:

This library has yet to be tested with apps running a server & client as separate projects, but it should 🤷‍♂️ work. You would want to install Bridg and Prisma on your server, run the generate script, and copy the Bridg client (node_modules/bridg/index.js & index.d.ts) and Prisma types to your client application.

Querying Your Database

Bridg is built on top of Prisma, you can check out the basics of executing CRUD queries here.

The Prisma documentation is excellent and is highly recommended if you haven't used Prisma in the past.

For security reasons, some functionality isn't available, but I'm working towards full compatibility with Prisma. Currently upsert, connectOrCreate, set, and discconnect are unavailable inside of nested queries.

Executing queries works like so:

import db from 'bridg';

const data = await db.tableName.crudMethod(args);

The following are simplified examples. If you're thinking "I wonder if I could do X with this..", the answer is probably yes. You will just need to search for "pagination with Prisma", or for whatever you're trying to achieve.

Creating Data:

// create a single db record
const createdUser = await db.user.create({
  data: {
    name: 'John',
    email: 'johndoe@gmail.com',
  },
});

// create multiple records at once:
const creationCount = await db.user.createMany({
  data: [
    { name: 'John', email: 'johndoe@gmail.com' },
    { name: 'Sam', email: 'sam.johnson@outlook.com' },
    // ..., ...,
  ],
});

// create a user, and create a relational blog for them
const createdUser = await db.user.create({
  data: {
    name: 'John',
    email: 'johndoe@gmail.com',
    blogs: {
      create: {
        title: 'My first blog',
        body: 'And that was my first blog, it was a short one..',
      },
    },
  },
});

Reading Data:

// all records within a table:
const users = await db.user.findMany();

// all records that satisfy a where clause:
const users = await db.user.findMany({ where: { profileIsPublic: true } });

// get the first record that satisfies a where clause:
const user = await db.user.findFirst({ where: { email: 'johndoe@gmail.com' } });

// enforce that only one record could ever exist (must pass a unique column id):
const user = await db.user.findUnique({ where: { id: 'some-id' } });

// do the same thing, but throw an error if the data is missing
const user = await db.user.findUniqueOrThrow({ where: { id: 'some-id' } });

Including Relational Data:

// all users and a list of all their blogs:
const users = await db.user.findMany({ include: { blogs: true } });

// where clauses can be applied to relational data:
const users = await db.user.findMany({
  include: {
    blogs: { where: { published: true } },
  },
});

// nest all blogs, and all comments on blogs. its just relations all the way down.
const users = await db.user.findMany({
  include: {
    blogs: {
      include: { comments: true },
    },
  },
});

For more details on advanced querying, filtering and sorting, check out this page from the Prisma docs.

Updating data:

// update a single record
const updatedData = await db.blog.update({
  where: { id: 'some-id' }, // must use a unique db key to use .update
  data: { title: 'New Blog title' },
});

// update many records
const updateCount = await db.blog.updateMany({
  where: { authorId: userId },
  data: { isPublished: true },
});

Deleting data:

// delete a single record.  must use a unique db key to use .delete
const deletedBlog = await db.blog.delete({ where: { id: 'some-id' } });

// delete many records
const deleteCount = await db.blog.deleteMany({ where: { isPublished: false } });

Database Rules

If you want to ignore this during development, you can set your database rules to the following to allow all requests (this is not secure):

export const rules: DbRules = { default: true };

Because your database is now available on the frontend, that means anyone who can access your website will have access to your database. Fortunately, we can create custom rules to prevent our queries from being used nefariously 🥷.

Your rules could look something like the following:

export const rules: DbRules = {
  user: {
    find: { profileIsPublic: true }, // only allow reads on public profiles
    update: (uid, data) => ({ id: uid }), // update only if its being done by the user
    create: (uid, data) => {
      // prevent the user from starting themself at level 99
      if (data.level !== 1) return false;
      return true; // otherwise allow creation
    },
    delete: false, // never authorize any calls to delete users
  },
  // table2: {},
  // table3: {},...
  blog: {
    // model default, used if model.method not provided
    default: true,
  }
  // global default, used if model.method and model.default aren't provided
  // defaults to 'false' if not provided. set to 'true' only in development
  default: false,
};

As you can see, your rules will basically look like:

{
    tableName: {
        find: validator,
        delete: validator,
    }
}

NOTE: If you don't provide a rule for a property, it will default to preventing those requests.

In the above example, all update and create requests will fail. Since they weren't provided, they default to false.

The properties available to create rules for are:

  • find: authorizes reading data (.findMany, .findFirst, .findFirstOrThrow, .findUnique, .findUniqueOrThrow, .aggregate, .count, .groupBy)
  • update: authorizes updates (.update, .updateMany, .upsert)
  • create: authorizes creating data (.create, .createMany, .upsert)
  • delete: authorizes deleting data (.delete, .deleteMany)

Note: .upsert uses update rules, if no data is updated, it will use create rules for creation

What is a validator?

Validators control whether a particular request will be allowed to execute or not.

They can be provided in three ways:

  1. boolean - use a boolean when you always know whether a certain request should go through or be blocked

    tableName {
        find: false, // blocks all reads on a model
        create: true // allows any creation for a model
    }

2) where clause - You can also apply a Prisma where clause for the given model. This clause will be required to be true, along with whatever input is passed from the client request.

note: create does not accept where clauses

blog {
    // allow reads only on blogs where isPublished = true
    find: { isPublished: true }
}

3) callback function - The most powerful option is a callback function. This will allow you to dynamically authorize requests based on the context of the request. You can also pass an async function, and make as many async calls as you want in the validator.

args:

uid: the id of the user making the request

data: the body data from the request (only available on update, create)

return value: boolean | where clause

Your callback function should either return a Prisma Where object for the corresponding table, or a boolean indicating whether the request should resolve or not.

Example use of callbacks:

const rules = {
  blog: {
    // where clause: allow reads if the blog is published OR if the user authored the blog
    find: (uid) => ({ OR: [{ isPublished: true }, { authorId: uid }] }),

    // prevent the user from setting their own vote count
    create: (uid, data) => (data.voteCount === 0 ? true : false),

    // make an async call to determine if request should resolve
    // note: this should USUALLY be done via a relational query,
    // which only takes 1 trip to the db, but they are not always practical
    delete: async (uid) => {
      const userMakingRequest = await db.user.findFirst({ where: { id: uid } });
      return userMakingRequest.isAdmin ? true : false;
    },

    // you can run literally any javascript you want, anything..
    update: async (uid) => {
      const isTheSunShining = await someWeatherApi.sunIsOut();
      const philliesWinWorldSeries = Math.random() < 0.000001;
      return isTheSunShining && philliesWinWorldSeries;
    },
  },
};

Rules stress testing

There may be undiscovered edgecases where an attacker could circumvent our database rules and access data that they shouldn't be allowed to. Here's a previous example for reference.

If you stumble upon something like this, please 🙏 create an issue with a detailed example, so it can be fixed.

1.1.13

1 day ago

1.1.12

1 month ago

1.1.11

1 month ago

1.1.9

2 months ago

1.1.8

2 months ago

1.1.7

2 months ago

1.1.6

2 months ago

1.1.10

2 months ago

1.1.6-alpha.1

2 months ago

1.1.5

2 months ago

1.1.4

2 months ago

1.1.4-alpha.1

2 months ago

1.1.3-alpha.2

2 months ago

1.1.3-alpha.1

2 months ago

1.1.3-beta.1

4 months ago

1.1.3-beta.3

4 months ago

1.1.3-beta.2

4 months ago

1.1.3

4 months ago

1.1.2

5 months ago

1.1.1-alpha.6

6 months ago

1.0.30-beta.1

9 months ago

1.0.30-beta.2

9 months ago

1.0.30-beta.3

9 months ago

1.0.30-beta.4

9 months ago

1.0.30-beta.5

9 months ago

1.1.1-alpha.4

6 months ago

1.1.1-alpha.5

6 months ago

1.1.1-alpha.2

6 months ago

1.1.1-alpha.3

6 months ago

1.0.30

9 months ago

1.1.1-alpha.0

6 months ago

1.1.1-alpha.1

6 months ago

1.1.1

6 months ago

1.1.0

9 months ago

1.0.30-beta.101

9 months ago

1.0.30-beta.100

9 months ago

1.0.30-alpha.1

9 months ago

1.0.30-alpha.2

9 months ago

1.0.30-alpha.3

9 months ago

1.0.30-alpha.30

9 months ago

1.0.26

12 months ago

1.0.25

12 months ago

1.0.24

12 months ago

1.0.29

12 months ago

1.0.28

12 months ago

1.0.27

12 months ago

1.0.19

1 year ago

1.0.18

1 year ago

1.0.17

1 year ago

1.0.16

1 year ago

1.0.22

1 year ago

1.0.21

1 year ago

1.0.20

1 year ago

1.0.23

1 year ago

1.0.15

1 year ago

1.0.11

1 year ago

1.0.14

1 year ago

1.0.13

1 year ago

1.0.12

1 year ago

1.0.10

1 year ago

1.0.9

1 year ago

1.0.8

1 year ago

1.0.7

1 year ago

1.0.6

1 year ago

1.0.5

1 year ago

1.0.4

1 year ago

1.0.3

1 year ago

1.0.2

1 year ago

1.0.0

1 year ago