Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/campvanilla/casualdb
Simple JSON "database" for Deno with type-safety! ⚡️
https://github.com/campvanilla/casualdb
database deno json prototyping
Last synced: 3 months ago
JSON representation
Simple JSON "database" for Deno with type-safety! ⚡️
- Host: GitHub
- URL: https://github.com/campvanilla/casualdb
- Owner: campvanilla
- License: mit
- Created: 2020-05-16T12:32:01.000Z (over 4 years ago)
- Default Branch: main
- Last Pushed: 2021-07-19T06:33:40.000Z (over 3 years ago)
- Last Synced: 2024-10-16T16:16:15.695Z (3 months ago)
- Topics: database, deno, json, prototyping
- Language: TypeScript
- Homepage:
- Size: 96.7 KB
- Stars: 34
- Watchers: 5
- Forks: 5
- Open Issues: 5
-
Metadata Files:
- Readme: README.md
- Contributing: .github/contributing.md
- License: LICENSE
Awesome Lists containing this project
README
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)](https://img.shields.io/github/v/release/campvanilla/casualdb?color=%232ecc71&include_prereleases&style=flat-square)
[![All Contributors](https://img.shields.io/badge/all_contributors-5-orange.svg?style=flat-square)](#contributors-)## Contents
* [Quick Usage](#quick-usage)
* [Installation](#installation)
* [API](#api)
* [Inspiration](#inspiration)
* [Disclaimer](#disclaimer) ⚠️
* [Contributing](#contributing)## Quick Usage
``` 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(); // 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('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
``` ts
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();
```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:
```ts
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()](#casual-db-connect)
* [.get()](#casual-db-get)
* [.seed()](#casual-db-seed)
* [.write()](#casual-db-write)
.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` .
``` ts
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` .
**Important**: For type checking to work, ensure that the Template Type is provided to `.get()` . 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.``` ts
interface Schema {
posts: Array<{
id: number;
title: string;
views: number;
}>;
user: {
name: string;
};
}await db.get('posts'); // Returns a Promise
// or
await db.get('posts.0.id'); // Returns a Promise
```
.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.
``` ts
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.
``` ts
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.
``` ts
interface Schema {
posts: Array<{
id: number;
title: string;
views: number;
}>;
user: {
name: string;
};
}const data = await db.get('posts'); // ❌ Not a PrimitiveOperator as the value is going to be an array
const data = await db.get('posts.0'); // ✅ PrimitiveOperator as the value is a non-array.
```Instances of this class have the following methods:
* [.value()](#primitive-operator-value)
* [.update()](#primitive-operator-update)
* [.pick()](#primitive-operator-pick)
.value()
Returns the value of the data.
``` ts
const data = await db.get('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.
``` ts
const data = await db.get('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.
``` ts
const data = await db.get('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.
``` ts
interface Schema {
posts: Array<{
id: number;
title: string;
views: number;
}>;
user: {
name: string;
};
}const data = await db.get('posts'); // ✅ CollectionOperator as the value is an array.
const data = await db.get('posts.0'); // ❌ PrimitiveOperator as the value is a non-array.
```Instances of this class contain the following methods. All methods are chainable:
* [.value()](#collection-operator-value)
* [.size()](#collection-operator-size)
* [.findOne()](#collection-operator-findOne)
* [.findAllAndUpdate()](#collection-operator-findAllAndUpdate)
* [.findAllAndRemove()](#collection-operator-findAllAndRemove)
* [.findById()](#collection-operator-findById)
* [.findByIdAndRemove()](#collection-operator-findByIdAndRemove)
* [.findByIdAndUpdate()](#collection-operator-findByIdAndUpdate)
* [.sort()](#collection-operator-sort)
* [.page()](#collection-operator-page)
* [.pick()](#collection-operator-pick)
.value()
Returns the value of the data.
``` ts
const data = await db.get('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.
``` ts
const data = await db.get('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` . 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.
``` ts
const data = await db.get('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.
``` ts
const data = await db.get('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.
``` ts
const data = await db.get('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.
``` ts
const data = await db.get('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.
``` ts
const data = await db.get('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` ](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Parameters)'s `compareFunction` .``` ts
const posts = await db.get('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.
``` ts
const posts = await db.get('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.
``` ts
const posts = await db.get('posts');posts
.pick(['title'])
.value() // [{ title: "Post 1" }, { title: "Post 2" }]
```## Inspiration
This project has taken inspiration from [lowdb](https://github.com/typicode/lowdb) for the concept and [mongoose](https://mongoosejs.com/) 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](./.github/CONTRIBUTING.md) page a read. 🤓
## Contributors ✨
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
Abinav Seelan
💻 📖 🤔 ⚠️
Aditi Mohanty
💻 📖 🤔 ⚠️
William Terry
🐛
Keith Yao
🐛 💻
Jacek Fiszer
💻
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!