Skip to content

Commit

Permalink
✨ Offline mode and document paging, also move to ESM
Browse files Browse the repository at this point in the history
  • Loading branch information
BetaHuhn committed Nov 4, 2021
1 parent 1356f5e commit f8da2a7
Show file tree
Hide file tree
Showing 13 changed files with 462 additions and 57 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
node_modules/
.env
dev.ts
dev/
41 changes: 39 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ This project is under heavy development and not yet suitable for production use!

## 👋 Introduction

Deta Base ORM is a JavaScript/TypeScript library which lets your work with [Deta Base](https://docs.deta.sh/docs/base/about) in a object relational model. Define a schema for your Base, cross reference items between Bases and interact with your data in a more functional way. It is heavily inspired by [mongoose](https://mongoosejs.com/docs/index.html) and helps you write high quality, maintainable applications in a more productive way then with the standard [Deta Base SDK](https://docs.deta.sh/docs/base/sdk/) (it uses it under the hood).
Deta Base ORM is a JavaScript/TypeScript library which lets your work with [Deta Base](https://docs.deta.sh/docs/base/about) in a object relational model. Define a schema for your Base, cross reference items between Bases and interact with your data in a more functional way. It is heavily inspired by [mongoose](https://mongoosejs.com/docs/index.html) and helps you write high quality, maintainable applications in a more productive way then with the standard [Deta Base SDK](https://docs.deta.sh/docs/base/sdk/) (it uses it under the hood). Additionally it features a offline mode which mocks the remote Deta Base and replaces it with a local JSON file for better access during development.

## 🚀 Get started

Expand Down Expand Up @@ -90,6 +90,8 @@ See below for a more detailed guide.

## 📚 Usage

*Deta Base ORM is a pure ESM package. If you're having trouble importing it in your project, please [read this](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c).*

### Defining a schema

Before you can properly use the other methods you need to define a schema for your Base. It specifies what values to expect and in what format. You can set a property to required (`false` by default) or specify a default value:
Expand Down Expand Up @@ -305,6 +307,30 @@ await Base.find([

> Here: The name must be `Hello` OR the cuteness greater than `8`
### Paging

Deta Base ORM supports the same paging as the normal Base SDK. Just specify a limit and optionally the key of the last item as parameters: `.find(query, limit, last)`

Example:

```js
const items = await Base.find({ age: 24 }, 5) // Limit number of items to 5

const nextFive = await Base.find({ age: 24 }, 5, last: items[4].key) // Use the key of the last item
```

### Offline Mode

You can optionally enable the offline mode which uses locally stored JSON files instead of the live remote Deta Base service. This is very helpful during development as it allows you to run your queries against a test database and then use the live one in production. And of course, it works offline so you can develop your application with all of the benefits of Base without internet access.

```ts
const OfflineBase = new DetaOrm.Base('name', schema, {
offline: true
})
```

> It will store the JSON files in a `.deta-base-orm` folder by default
## ⚙️ Options

You can optionally pass a options object to the the Base contructor:
Expand All @@ -324,6 +350,18 @@ In ascending mode it will use the Unix timestamp and in descending order `maxDat

> Because the timestamp is in ms, the key is only sequential until a certain point i.e two keys generated in the same ms may not be in the right order
### Offline Mode

Set `offline` to true to enable the offline mode. In offline mode it will use a locally stored JSON files instead of the live remote Deta Base service.

The JSON files will by default be stored in a `.deta-base-orm` directory but this can be changed with the `storagePath` option. For each Base you create, a JSON file with the same name will be created in that folder. E.g the data for a Base with the name Kittens will be stored in `Kittens.json`.

**Important Note:** The offline mode is currently still in development and doesn't yet support everything that is available with the normal Base service. Here are all the things that are missing/don't fully work yet:

- Advanced [query operators](#filtering): The query currently only supports direct matching of values i.e. `{ name: 'Maxi', age: 18 }`
- Limits/paging: Currently all items are returned that match the query
- Cross referencing: Running `.populate()` to cross reference items across Bases doesn't work and returns an error

### Timestamp

This library can optionally add a `createdAt` field to each new document containing the timestamp of when it was created. To enabled this, set `timestamp` to true.
Expand All @@ -341,7 +379,6 @@ const Base = new DetaOrm.Base(name, schema, { db })

## 💡 Planned features

- Offline mode
- Add custom methods/actions to the Base

## 💻 Development
Expand Down
52 changes: 52 additions & 0 deletions example/offline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import * as DetaOrm from '../src/index' // import * as DetaOrm from 'deta-base-orm'

const run = async () => {

// ✨ Define a schema for the kittens
type KittenSchema = {
name: string,
cuteness?: number
}

const schema = new DetaOrm.Schema<KittenSchema>({
name: 'string',
cuteness: {
type: 'number',
default: 0
}
})

// 🛌 Create our Kitten base
const Kitten = new DetaOrm.Base('Kitten', schema, {
offline: true // Enable offline mode
})

// 🐱 Create a new kitten
const line = Kitten.create({
name: 'Line',
cuteness: 8
})

// 🔖 Access the kittens name
console.log(line.name) // 'Line'

// 💾 Save our kitten to the Deta Base
await line.save()

// 🔍 Find all kittens
const kittens = await Kitten.find()
console.log(kittens) // [{name: 'Line', cuteness: 8}, ...]

// 🔑 Find a kitten by its key
const sameKitten = await Kitten.findByKey(line.key)
console.log(sameKitten) // {name: 'Line', cuteness: 8}

// 🧵 Find a kitten by its cuteness
const cutest = await Kitten.find({ cuteness: 8 })
console.log(cutest) // [{name: 'Line', cuteness: 8}]

// 💘 Delete a kitten
await sameKitten?.delete()
}

run()
26 changes: 9 additions & 17 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
Expand All @@ -8,20 +7,15 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Base = exports.Schema = void 0;
/* eslint-disable valid-jsdoc */
const deta_1 = require("deta");
const dotenv_1 = __importDefault(require("dotenv"));
const random_1 = require("./random");
dotenv_1.default.config();
import { Deta } from 'deta';
import dotenv from 'dotenv';
import { generateKey } from './random';
dotenv.config();
/**
* Create a schema for your Base
*/
class Schema {
export class Schema {
constructor(schema) {
this.schema = this.parse(schema);
}
Expand Down Expand Up @@ -139,11 +133,10 @@ class Schema {
return { errors, result: result };
}
}
exports.Schema = Schema;
/**
* Create and interact with a Deta Base
*/
class Base {
export class Base {
/**
* Create a new Base with the provided name, schema and options
* @param {string} name Name of the Base
Expand All @@ -168,7 +161,7 @@ class Base {
return;
}
// Create new Deta Base instance
const deta = deta_1.Deta();
const deta = Deta();
this._db = deta.Base(name);
}
/**
Expand Down Expand Up @@ -369,7 +362,6 @@ class Base {
});
}
}
exports.Base = Base;
/**
* Represents a Document with all of its data and methods
* @internal
Expand All @@ -383,7 +375,7 @@ class Document {
*/
constructor(data, _baseSchema) {
Object.assign(this, data);
this.key = this.key || random_1.generateKey(Document._opts.ascending);
this.key = this.key || generateKey(Document._opts.ascending);
// Add timestamp to document
if (Document._opts.timestamp && this.createdAt === undefined) {
this.createdAt = Date.now();
Expand Down Expand Up @@ -424,7 +416,7 @@ class Document {
if (!baseName)
throw new Error(`Can't populate this path because it doesn't have a baseName defined`);
// Create new Deta Base instance
const deta = deta_1.Deta();
const deta = Deta();
const db = deta.Base(baseName);
const key = this[path];
const rawData = yield db.get(key);
Expand Down
18 changes: 18 additions & 0 deletions lib/offline.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Low, Adapter } from 'lowdb';
import lodash from 'lodash';
declare class LowDash<T> extends Low<T> {
chain?: lodash.CollectionChain<any> & lodash.FunctionChain<any> & lodash.ObjectChain<any> & lodash.PrimitiveChain<any> & lodash.StringChain;
constructor(adapter: Adapter<T>);
}
export declare class OfflineDB {
db: LowDash<any>;
constructor(storagePath?: string);
static create(storagePath?: string): Promise<OfflineDB>;
init(): Promise<void>;
put(data: any): void;
list(): any[] | undefined;
get(key: string): any;
fetch(query: any): any;
delete(key: string): void;
}
export {};
63 changes: 63 additions & 0 deletions lib/offline.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { join } from 'path';
import { Low, JSONFile } from 'lowdb';
import lodash from 'lodash';
class LowDash extends Low {
constructor(adapter) {
super(adapter);
}
}
export class OfflineDB {
constructor(storagePath = '') {
// const __dirname = dirname(fileURLToPath(import.meta.url))
const file = join(storagePath, 'db.json');
const adapter = new JSONFile(file);
this.db = new LowDash(adapter);
}
static create(storagePath = '') {
return __awaiter(this, void 0, void 0, function* () {
const db = new OfflineDB(storagePath);
yield db.init();
return db;
});
}
init() {
return __awaiter(this, void 0, void 0, function* () {
yield this.db.read();
if (this.db.data === null) {
this.db.data = [];
}
this.db.chain = lodash.chain(this.db.data);
});
}
put(data) {
this.db.data.push(data);
this.db.write();
}
list() {
var _a;
return (_a = this.db.chain) === null || _a === void 0 ? void 0 : _a.value();
}
get(key) {
var _a;
return (_a = this.db.chain) === null || _a === void 0 ? void 0 : _a.find({ key }).value();
}
fetch(query) {
var _a;
return (_a = this.db.chain) === null || _a === void 0 ? void 0 : _a.find(query).value();
}
delete(key) {
var _a;
(_a = this.db.chain) === null || _a === void 0 ? void 0 : _a.remove({ key }).value();
this.db.write();
}
}
10 changes: 3 additions & 7 deletions lib/random.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateKey = void 0;
const nanoid_1 = require("nanoid");
import { nanoid } from 'nanoid';
// Used as large number to make sure keys are generated in descending order
const maxDateNowValue = 8.64e15; // Fun fact: This will only work until the year 275760
/**
Expand All @@ -15,8 +12,7 @@ const maxDateNowValue = 8.64e15; // Fun fact: This will only work until the year
* @param {boolean} ascending
* @returns {string} randomKey
*/
const generateKey = (ascending) => {
export const generateKey = (ascending) => {
const timestamp = ascending ? Date.now() : maxDateNowValue - Date.now();
return `${timestamp.toString(16)}${nanoid_1.nanoid(5)}`;
return `${timestamp.toString(16)}${nanoid(5)}`;
};
exports.generateKey = generateKey;
Loading

0 comments on commit f8da2a7

Please sign in to comment.