Skip to content

Simple JSON "database" for Deno with type-safety! ⚡️

License

Notifications You must be signed in to change notification settings

campvanilla/casualdb

Repository files navigation

Simple JSON "database" for Deno with type-safety! ⚡️

WARNING: This project is still in beta phase. We are actively working on enhancing the API and ironing out kinks. If you find a bug or have a feature request, feel free to create an issue or contribute. 🙂

GitHub release (latest SemVer including pre-releases) All Contributors

Contents

Quick Usage

// create an interface to describe the structure of your JSON
interface Schema {
  posts: Array<{
    id: number;
    title: string;
    views: number;
  }>;
  user: {
    name: string;
  };
}

const db = new CasualDB<Schema>(); // instantiate the db, casually 🤓
await db.connect("./test-db.json"); // "connect" to the db (JSON file)

// (optional) seed it with data, if starting with an empty db
await db.seed({
  posts: [
    { id: 1, title: "Post 1", views: 99 },
    { id: 2, title: "Post 2", views: 30 },
  ],
  user: { name: "Camp Vanilla" },
});

const posts = await db.get<Schema['posts']>('posts'); // pass the interface key in order for type-checking to work

const postTitlesByViews = (
  posts
    .sort(['views']) // sort by views (ascending)
    .pick(['title']) // pick the title of every post
    .value() // => ['Post 2', 'Post 1']
);

Installation

import { CasualDB } from "https://deno.land/x/[email protected]/mod.ts";

// create an interface to describe the structure of your JSON
interface Schema {
  posts: Array<{
    id: number;
    title: string;
    views: number;
  }>;
  user: {
    name: string;
  };
}

const db = new CasualDB<Schema>();

Note: When running via deno, this module will require you to pass the following flags (all flags are mandatory):-

  • --allow-read : in order to be able to read the JSON files
  • --allow-write: in order to be able to write to the JSON files
  • --unstable : this module uses the experimental Worker API in deno, and hence requires this flag
  • --allow-net : this is to enable to download of the Worker file.

If you want to always run the latest code (from the master branch) of this module, install via:

import { CasualDB } from "https://deno.land/x/casualdb/mod.ts";

API

new CasualDB()

Returns an instance of the CasualDB. Passing in a interface describing your JSON data ensures that type checking works correctly. The following are the methods available on this class instance

.connect(pathToJsonFile: string, options?: ConnectOptions)

Creates a connection to a json file passed as parameter. Returns a promise.

ConnectOptions:

  • bailIfNotPresent : Controls whether you would like an error to be thrown if the file being connected to does not exist. Default = false .
await db.connect("./test-db.json");

// or with options

await db.connect("./test-db.json", {
  bailIfNotPresent: true,
});

.get(jsonPath: string)

Fetches value from connected JSON file. Takes an object path as parameter. Returns a Promise<CollectionOperator | PrimitiveOperator> . Important: For type checking to work, ensure that the Template Type is provided to .get<T>() . If this is not provided, typescript cannot decide a CollectionOperator or PrimitiveOperator has been returned and hence you'd have to manually narrow it down for TS.

interface Schema {
  posts: Array<{
    id: number;
    title: string;
    views: number;
  }>;
  user: {
    name: string;
  };
}

await db.get<Schema["posts"]>('posts'); // Returns a Promise<CollectionOperator>

// or

await db.get<Schema["posts"][number]["id"]>('posts.0.id'); // Returns a Promise<PrimitiveOperator>

.seed(data: Schema)

Overrides the contents of the connected JSON file. This is beneficial for when you don't already have data in the file or you want to add some defaults. Returns a promise.

interface Schema {
  posts: Array<{
    id: number;
    title: string;
    views: number;
  }>;
  user: {
    name: string;
  };
}

await db.seed({
  posts: [
    { id: 1, title: "Post 1", views: 99 },
    { id: 2, title: "Post 2", views: 30 },
  ],
  user: { name: "Camp Vanilla" },
});

.write(jsonPath: string, data: any)

Writes the provided value to the Object path provided. Returns a promise.

await db.write('posts', [
  { id: 1, title: "Post 1", views: 99 },
  { id: 2, title: "Post 2", views: 30 },
]);

// or

await db.write('posts.0.title', 'Post 1');

PrimitiveOperator

When performing a db.get() on a path that returns a non-array value, the Promise resolves to an instance of PrimitiveOperator . The PrimitiveOperator class encapsulates functions that allow you work with any non-array-like data in javascript (eg. object , string , number , boolean ). All functions that are a part of PrimitiveOperator allow function chaining.

interface Schema {
  posts: Array<{
    id: number;
    title: string;
    views: number;
  }>;
  user: {
    name: string;
  };
}

const data = await db.get<Schema["posts"]>('posts'); // ❌ Not a PrimitiveOperator as the value is going to be an array

const data = await db.get<Schema["posts"][number]>('posts.0'); // ✅ PrimitiveOperator as the value is a non-array.

Instances of this class have the following methods:

.value()

Returns the value of the data.

const data = await db.get<Schema["posts"][number]>('posts.0');

data.value(); // { id: 1, title: "Post 1", views: 99 }

.update(updateMethod: (currentValue) => T)

Method to update the data. Method takes an updater-function as parameter. The updater-function will receive the value you want to update and expects a return value. The type of the updated data is inferred by the ReturnType of the updater-function.

const data = await db.get<Schema["posts"][number]>('posts.0');

data
  .update((value) => ({
    title: "Modified Post",
  }))
  .value(); // { id: 1, title: "Modified Post" }

.pick(keys: string[])

Picks and returns a subset of keys from the data. Method allows only keys present on data. If the data is not an object, method returns the data as is.

const data = await db.get<Schema["posts"][number]>('posts.0');

data
  .pick(["id", "title"])
  .value(); // { id: 1, title: "Post 1" }

CollectionOperator

When performing a db.get() on a path that returns an array, the Promise resolves to a instance of CollectionOperator . The CollectionOperator class encapsulates functions that allow you work with array-like data (collection of items). All functions that are a part of CollectionOperator allow function chaining.

interface Schema {
  posts: Array<{
    id: number;
    title: string;
    views: number;
  }>;
  user: {
    name: string;
  };
}

const data = await db.get<Schema["posts"]>('posts'); // ✅ CollectionOperator as the value is an array.

const data = await db.get<Schema["posts"][number]>('posts.0'); // ❌ PrimitiveOperator as the value is a non-array.

Instances of this class contain the following methods. All methods are chainable:

.value()

Returns the value of the data.

const data = await db.get<Schema["posts"]>('posts');

console.log(data.value()); // [ { id: 1, title: "Post 1", views: 99 }, { id: 2, title: "Post 2", views: 30 }, ]

.size()

Returns the length of the data.

const data = await db.get<Schema["posts"]>('posts');

console.log(data.size()); // 2

.findOne(predicate: Object | Function => boolean)

Searches through the collection items and returns an item if found, else returns an instance of PrimitiveOperator<null> . The predicate can be of two forms:

  1. An object with keys that you would like to match. The keys of the object should be a subset of the keys available on the items of the collection.
  2. A search-function where you can provide your custom logic and return true for the condition you are looking for.

Returns a PrimitiveOperator or CollectionOperator based on type of the found element.

const data = await db.get<Schema["posts"]>('posts');

data
  .findOne({ id: 1 })
  .value();// { id: 1, title: "Post 1", views: 99 }

// or

data
  .findOne((value) => {
    return value.id === 1
  })
  .value(); // { id: 1, title: "Post 1", views: 99 }

.push(value)

Push a new value into the collection. Returns a CollectionOperator with the updated items.

const data = await db.get<Schema["posts"]>('posts');

data
  .push({ id: 3, post: 'Post 3', views: 45 })
  .value(); // [ { id: 1, title: "Post 1", views: 99 }, { id: 2, title: "Post 2", views: 30 }, { id: 3, title: "Post 3", views: 45 } ]

.findAll(predicate: Object | Function => boolean)

Searches through the items of the collection and returns a CollectionOperator of all occurrences that satisfy the predicate. The predicate can be of two forms:

  1. An object with keys that you would like to match. The keys of the object should be a subset of the keys available on the items of the collection.
  2. A search-function where you can provide your custom logic and return true for the condition you are looking for.

Returns a CollectionOperator with the subset of items.

const data = await db.get<Schema["posts"]>('posts');

data
  .findAll({ title: 'Post 1' })
  .value();// [{ id: 1, title: "Post 1", views: 99 }]

// or

data
  .findAll((value) => {
    return value.views > 40;
  })
  .value(); // [{ id: 1, title: "Post 1", views: 99 },{ id: 3, title: "Post 3", views: 45 }];

.findAllAndUpdate(predicate: Object | Function => boolean, updateMethod: (value) => T)

Searches through the collection and returns a CollectionOperator with all occurrences that satisfy the predicate updated with the return value of the updateMethod. The predicate can be of two forms:

  1. An object with keys that you would like to match. The keys of the object should be a subset of the keys available on the items of the collection.
  2. A search-function where you can provide your custom logic and return true for the condition you are looking for.

Returns a CollectionOperator with the updated array.

const data = await db.get<Schema["posts"]>('posts');

data
  .findAllAndUpdate({ title: 'Post 1' }, (value) => ({ ...value, title: 'Modified Post' }))
  .value(); // [{ id: 1, title: "Modified Post", views: 99 },{ id: 2, title: "Post 2", views: 30 }, { id: 3, title: "Post 3", views: 45 }]

// or

data
  .findAllAndUpdate((value) => {
    return value.views > 40;
  }, (value) => ({
    ...value,
    title: 'Trending Post'
  }))
  .value(); // [{ id: 1, title: "Trending Post", views: 99 }, { id: 2, title: "Post 2", views: 30 }, { id: 3, title: "Trending Post", views: 45 }];

.findAllAndRemove(predicate: Object | Function => boolean, updateMethod: (value) => T)

Searches through the collection and returns a new CollectionOperator where all occurrences that satisfy the predicate are omitted. The predicate can be of two forms:

  1. An object with keys that you would like to match. The keys of the object should be a subset of the keys available on the items of the collection.
  2. A search-function where you can provide your custom logic and return true for the condition you are looking for.

Returns a CollectionOperator with the updated array.

const data = await db.get<Schema["posts"]>('posts');

data
  .findAllAndRemove({ title: 'Post 1' })
  .value(); // [{ id: 2, title: "Post 2", views: 30 }, { id: 3, title: "Post 3", views: 45 }]

// or

data
  .findAllAndRemove((value) => value.views > 40)
  .value(); // [{ id: 2, title: "Post 2", views: 30 }];

.findById(id: string)

Syntactical sugar for .findOne({ id }) .

.findByIdAndRemove(id: string)

Syntactical sugar for .findAllAndRemove({ id }) .

.findByIdAndUpdate(id: string, updateMethod: (value) => T)

Syntactical sugar for .findAllAndUpdate({ id }, updateMethod) .

.sort(predicate: string[] | Function => boolean)

Sorts and returns a new sorted CollectionOperator instance. The comparison predicate can be one of two types:

  • an array of keys to select for sorting the items in the collection (priority is left-right).
    For example, when the predicate is ['views','id'] , the method will first sort posts in ascending order of views that each post has. Any posts which have the same number of views, will then be sorted by id .
  • a compare function similar to Array.prototype.sort 's compareFunction .
const posts = await db.get<Schema["posts"]>('posts');

posts
  .sort(['views'])
  .value() // [{ id: 2, title: "Post 2", views: 30 }, { id: 1, title: "Post 1", views: 99 }]

// or

posts
  .sort((a,b) => a.views - b.views)
  .value() // [{ id: 2, title: "Post 2", views: 30 }, { id: 1, title: "Post 1", views: 99 }]

.page(page: number, pageSize: number)

Returns a paginated subset of the collection.

const posts = await db.get<Schema["posts"]>('posts');

posts
  .page(1, 1)
  .value() // [{ id: 1, title: "Post 1", views: 99 }]

.pick(keys: string[])

Returns a CollectionOperator of items with each item having only the picked keys. Only keys present on the type of the items in the collection are allowed. If the item is not an object, this method returns an empty object ( {} ) for it.

const posts = await db.get<Schema["posts"]>('posts');

posts
  .pick(['title'])
  .value() // [{ title: "Post 1" }, { title: "Post 2" }]

Inspiration

This project has taken inspiration from lowdb for the concept and mongoose for certain parts of the CollectionOperator API.

It aims to simplify the process of setting up a full-fledged db when building prototypes or small-scale applications like CLI tools or toy apps for Deno.

🚧 ⚠️ Disclaimer ⚠️ 🚧

Disclaimer : As mentioned above, this module is best used for small-scale apps and should not be used in a large production application and you may face issues like:

  • concurrency management (for writes)
  • storing and parsing large amounts of JSON data.

Contributing

Want to raise an issue or pull request? Do give our Contribution Guidelines page a read. 🤓

Contributors ✨

Thanks goes to these wonderful people (emoji key):


Abinav Seelan

💻 📖 🤔 ⚠️

Aditi Mohanty

💻 📖 🤔 ⚠️

William Terry

🐛

Keith Yao

🐛 💻

Jacek Fiszer

💻

This project follows the all-contributors specification. Contributions of any kind welcome!