DEV Community

Cover image for Introducing Desolid: Single file backend as a service๐ŸŒŸ
Sajjad Shirazy for Desolid

Posted on

Introducing Desolid: Single file backend as a service๐ŸŒŸ

Have you ever wanted to have a direct dive from your business plan analysis to a simple ready to use backend service?

Desolid is an opinionated backend as a service. the project aimed to cut the distance between the ideas and reality as much as possible. It's currently still under development but the first alpha release is now available. although this version of the code is an early alpha release and should not be used for production implementations.

The core imagination is to have a human-readable single file that describes both of the data model and logic and Desolid will serve the desired API based on that single file. It shouldn't ask (by default) any configuration for handling files or accessing any database, all must be satisfied by the Desolid itself.

Desolid provides a GraphQL API and an admin panel based on the schema file. it creates and uses an SQLite database and local file storage for storing the data and files.

It also supports other kinds of SQL databases like Postgres, MySQL, and MS SQL Server instead of SQLite or any S3 services instead of local file storage.

Let's see how is it, suppose we wanna make a simple blog service, we have Users, Posts, Categories as entities of the system.

Create the schema file

As explained above we just need a single file to describe and run the backend so let go ahead and make that in an empty:

touch schema.graphql

Create the Categories model

Models define using GraphQL type objects, so properties will represent as fields and logics will define using directives. so let's edit the schema file, I'm using VS Code (you can choose anyone you want). it open with this:

code .

Then copy & paste this and save the file:

type Category @model {
  id: ID!
  createdAt: DateTime!
  updatedAt: DateTime!
  name: String!
}

Start Desolid

So far we just defined a model on the schema file, we can start Desolid to see what happens:

npx desolid

We should see:

  ____                         _   _       _
 |  _ \    ___   ___    ___   | | (_)   __| |
 | | | |  / _ \ / __|  / _ \  | | | |  / _` |
 | |_| | |  __/ \__ \ | (_) | | | | | | (_| |
 |____/   \___| |___/  \___/  |_| |_|  \__,_|

๐Ÿค– [Desolid](https://desolid.netlify.app/): Single file self-hosted backend as a service
๐Ÿ”ฅ v0.4.0 running in "win32" on "E:\opensource\desolid\desolid\test"

[2020-06-27 02:17:24]  INFO  Compiling Schema ...
[2020-06-27 02:17:25]  WARN  Authentication Secret value didn't set into the configuration file. the generated JWT tokens will expire on every restart.
[2020-06-27 02:17:25]  INFO  Connecting to database ...
[2020-06-27 02:17:25]  INFO  Connected to "sqlite://./databse.sqlite"
[2020-06-27 02:17:25]  INFO  Starting server ...
[2020-06-27 02:17:25]  INFO  Server is running on http://localhost:3000
[2020-06-27 02:17:25]  INFO  Amin panel is available on http://localhost:3000/admin
[2020-06-27 02:17:25]  INFO  ๐Ÿš€ in 474ms

Also on the root directory, we have two new generated files:

  • database.sqlite: the database
  • desolid.log: persists the logs in JSON format

Now open http://localhost:3000 in the browser, as you see we have a GraphQL playground to try the API:

[Desolid](https://desolid.netlify.app/) playground

Desolid generates a standard CRUD for all models, including handling complex queries, various mutations, standard aggregations, and file uploads. we are planning to reach all features of Open CRUD specifications but it's still under development. Also, CRUDs will consider authorization logics which are described in the schema file.

Now open http://localhost:3000/admin in the browser, it's the builtin simple admin panel of Desolid. Here You can view, create, edit & remove records:

Alt Text

As you see the admin panel redirects us to the signup page automatically. it's because there is no admin user-defined so far in the system, so let create one and sign in to the dashboard:

Alt Text

Also, you can create a category using the admin panel:

Alt Text

Protect Categories model

Now we have to make some constraints on our category model, for example, only Editors have permission to create, update or delete a category, let's see how we can.

First we need to take a look to the builtin User model of Desolid (builtin type are the types which Desolid prepends them to our schema file):

type User
  @model
  @authorization(
    CREATE: [Admin]
    READ: [Admin, "$user.id == {{id}}"]
    UPDATE: [Admin, "$user.id == {{id}}"]
    DELETE: [Admin]
  ) {
  id: ID!
  createdAt: DateTime!
  updatedAt: DateTime!
  email: EmailAddress! @unique
  password: Password!
  group: UserGroup!
  name: String!
}

Just note the group field, for now, it defines the UserGroup that the user belongs to, so What are available UserGroups? by default Desolid has Admin and User as the available user groups:

enum UserGroup {
  """
  System Administrator
  """
  Admin
  """
  Authenticated User
  """
  User
}

Now let's add our custom required UserGroups, Editor, and Author. it will be by extending the UserGroup, add these lines of codes to the top of your schema.graphql file:

extend enum UserGroup {
  Editor
  Author
}

So our file will be like:

extend enum UserGroup {
  Editor
  Author
}

type Category @model {
  id: ID!
  createdAt: DateTime!
  updatedAt: DateTime!
  name: String!
}

Now we have to authorize requests to our Category model using the @authorization directive:

type Category
  @model
  @authorization(
    CREATE: [Admin, Editor]
    UPDATE: [Admin, Editor]
    DELETE: [Admin, Editor] #
  ) {
  id: ID!
  createdAt: DateTime!
  updatedAt: DateTime!
  name: String!
}

It says that only members of Admins & Editors groups have permission to create, update or delete a Category.

Relations

Let's continue with creating the Post model:

type Post
  @model
  @authorization(
    READ: [Admin, Editor, "{{published}} || $user.id == {{author.id}}"]
    CREATE: [Admin, Editor, "$user.group == 'Author' && !$input.published"]
    UPDATE: [Admin, Editor, "$user.id == {{author.id}} && !$input.published"]
    DELETE: [Admin, Editor, "!{{published}} && $user.id == {{author.id}}"] #
  ) {
  id: ID!
  createdAt: DateTime!
  updatedAt: DateTime!
  author: User!
  title: String!
  content: String!
  published: Boolean!
  categories: [Category]
}

Wait, wait, don't fear, I'm here ๐Ÿค“, the authorization logic is a bit complex for Posts, let's check it one by one:



  • READ: If you are a member of Admins or Editors you are passed, also if you are not but the post is published or not published but you're the author you are passed too.

  • CREATE: If you are a member of Admins or Editors you are passed, also if you are not but you are a member of Authors and the post you wanna create is not set as published, (because of course an Editor must revise and approve your post before putting in the front) then you are welcome too.

  • UPDATE: If you are a member of Admins or Editors you are passed, also if you are not but you are a member of Authors and the you are the owner of the post then you are passed too. just don't try to update published, because you not allowed for.

  • DELETE: If you are a member of Admins or Editors you are passed, also if you are not but you are a member of the Authors and you are the owner of the post and the post didn't publish yet, then you are passed too.

So let's return to relations, look at the type of categories field, It's a list of Categories. Desolid handles relations automatically on the data layer based on the model's structures. so for helping Desolid to realize this relation better, let's update Category model too:

type Category
  @model
  @authorization(
    CREATE: [Admin, Editor]
    UPDATE: [Admin, Editor]
    DELETE: [Admin, Editor] #
  ) {
  id: ID!
  createdAt: DateTime!
  updatedAt: DateTime!
  name: String!
  posts: [Post]
}

Now Desolid knows that this relation is a many-to-many one and handles it in the right way.

Extending built-in types

Suppose now we need to have an avatar per each user, the builtin User model doesn't have suche a field as you saw before. so we have to extent the User model on our schema file. Maybe not bad to update the authorization rulls to allow Editors to create new users too:

extend type User
  @model
  @authorization(
    CREATE: [Admin, Editor]
    READ: [Admin, "$user.id == {{id}}"]
    UPDATE: [Admin, "$user.id == {{id}}"]
    DELETE: [Admin] #
  ) {
  avatar: File @upload(accept: ["image/jpeg"], size: { max: 5, min: 0.1 })
}

As you see we added a new field to the User model. the type of avatar is declared as File. File is the other builtin type which will include by default. You may guess that here we have another relation type. Desolid considers relations to the File model as a many-to-one relation.

Using the @upload directive we indicated some extra constraints too, an avatar should be image/jpeg, and the size must be between 0.1 to 5 MB.

So far our schema file should be like this:

extend enum UserGroup {
  Editor
  Author
}

extend type User
  @model
  @authorization(
    CREATE: [Admin, Editor]
    READ: [Admin, "$user.id == {{id}}"]
    UPDATE: [Admin, "$user.id == {{id}}"]
    DELETE: [Admin] #
  ) {
  avatar: File @upload(accept: ["image/jpeg"], size: { max: 5, min: 0.1 })
}

type Post
  @model
  @authorization(
    READ: [Admin, Editor, "{{published}} || $user.id == {{author.id}}"]
    CREATE: [Admin, Editor, "$user.group == 'Author' && !$input.published"]
    UPDATE: [Admin, Editor, "$user.id == {{author.id}} && !$input.published"]
    DELETE: [Admin, Editor, "!{{published}} && $user.id == {{author.id}}"] #
  ) {
  id: ID!
  createdAt: DateTime!
  updatedAt: DateTime!
  author: User!
  title: String!
  content: String!
  published: Boolean!
  categories: [Category]
}

type Category
  @model
  @authorization(
    CREATE: [Admin, Editor]
    UPDATE: [Admin, Editor]
    DELETE: [Admin, Editor] #
  ) {
  id: ID!
  createdAt: DateTime!
  updatedAt: DateTime!
  name: String!
  posts: [Post]
}

Configuration

I was trying to keep Desolid executable with just a single file by considering some light default configurations with no dependencies. but the configuration is customizable, you can connect Desolid to an external database or S3 file storage service. it's possible using the desolid.yaml file next to the schema file. like:

database:
  dialect: "sqlite"
  storage: "/db/databse.sqlite"
  logging: false
  forceSync: false

api:
  port: 3000
  authentication:
    secret: "secret"
    expiration: 48 # hours
  upload:
    maxFileSize: 64 # MB

storage:
  pattern: "/${YYYY}/${MM}/${DD}/${NAME}-${EPOCH}.${EXT}"
  driver: "s3"
  config:
    # AWS
    key: "AWS_S3_KEY"
    secret: "AWS_S3_SECRET"
    region: "AWS_S3_REGION"
    bucket: "AWS_S3_BUCKET"

Contribution

Any contribution to Desolid is more than welcome!

A great way to contribute to the project is to send a detailed report when you encounter an issue. We'd love to see your pull requests, even if it's just to fix a typo!

Road map

  • Database Migrations
  • Embeded Desolid and Extentions
  • Docker image & Cloud Native behaviors
  • Developing Admin panel missed functionalities

Top comments (0)