A simple ORM written in TypeScript, backed by JSON files with some other spicy features. 🔥
- Serialization powered by superjson 🚀 allowing type-safe storage of:
undefined
bigint
Date
RegExp
Set
Map
Error
URL
Note:
JSON.stringify()
stores Date objects as strings, and whenJSON.parse()
is called, the type of Date is lost - it will be a string.
-
Thin lodash wrapper
-
Active record-like pattern
-
Support for Expo, Node, web, and memory
-
Super easy and fun to use
Nano Records supports the following adapters:
- JSON files saved to the mobile applications document directory
npm install --save https://github.com/mphill/nano-record/tree/main/packages/core
npm install --save https://github.com/mphill/nano-record/tree/main/packages/expo
npm install --save https://github.com/mphill/nano-record/tree/main/packages/core
npm install --save https://github.com/mphill/nano-record/tree/main/packages/node
- localStorage based
npm install --save https://github.com/mphill/nano-record/tree/main/packages/core
npm install --save https://github.com/mphill/nano-record/tree/main/packages/web
- No presistence
npm install --save https://github.com/mphill/nano-record/tree/main/packages/core
npm install --save https://github.com/mphill/nano-record/tree/main/packages/memory
interface person {
id: string,
name: string,
age: number
}
const adapter = new NodeAdapter("/path/to/store");
const nano = new NanoRecord(adapter);
const people = nano.collecion<person>("people");
Adapters are responsible for reading, writing and deleting data. There are two main data storage mechanisms, collections
and items
. You can think of a collection and as array of items.
nano.items(); // list all items in store ['item-key1', 'item-key2']
nano.collections(); // list all collections in store ['collection-key1', 'collection-key2']
Custom adapters can be created by implementing the Adapter
interface in @nano-record/core
.
Keys can only contain a-z, 0-9 or - and they must be lowercase.
const people = nano.collecion<person>("people"); // ✅
const people = nano.collecion<person>("people-item"); // ✅
const people = nano.collecion<person>("People"); // ❌
const people = nano.collecion<person>("people/item"); // ❌
const people = nano.collecion<person>("people.item"); // ❌
💣 Note: all mutations are async while all queries as sync
await people.create({
id: store.makeId(),
name: "John",
age: 21
}); // create a user
await people.createMany([{
id: store.makeId(),
name: "John",
age: 21
},
{
id: store.makeId(),
name: "Jane",
age: 22
}]); // create multiple users at once
people.findFirst(t => t.id = "33cfb41f-bbe1-4b52-a8ed-b5b0096e134f"); // find first matching record
people.findMany(t => t.age >= 21); // find all matching records
await people.updateFirst(t => t.age >= 21, person => { person.age = 22 }); // update first matching records
await people.updateMany(t => t.age >= 21, person => { person.age = 22 }) // update all matching records
const success = await people.deleteFirst(t => t.id == "33cfb41f-bbe1-4b52-a8ed-b5b0096e134f"); // return true if found and deleted
const recordsDeleted = await people.deleteMany(t => t.age >= 21); // returns number of deleted records
collection.makeId(); // generate a guid for id values, helpful for ids
collection.paginate(1, 20); // paginate collection results, start at page 1 and take 20
collection.first(); // get first item
collection.last(); // get last item
collection.query(); // lodash hook
const result = collection
.query()
.filter(t => t.age == 20)
.sortBy(t => t.name, "asc")
.first()
.value(); // using lodash to get the first name alphabetically
collection.truncate(); // clear all items in the collection
Nano Record supports storing items as key-values
// Store a counter
const count = await nano.item<number>("count");
await count.set(2)
console.log(count.get() == 2); // true
// Store user settings
const settings = await nano.item<{
email: string,
notifications: boolean
}>("settings");
await settings.set({
email: "[email protected]",
notifications: true
})
Your JSON model may change over time, Nano Record can help. Internally your model has a schemaVersion that will be 1 when you first create your store.
Let's say you want to convert date of birth stored as a unix timestamp to a Date object:
const adapter = new NodeAdapter("/path/to/store");
const nano = new NanoRecord(adapter);
interface person {
name: string,
dob : int,
dobDate: Date // <-- add a new field
}
const people = nano.collection<person>("person");
if(people.schemaVersion == 1) {
(await people.findMany()).forEach(p => {
p.dobDate = new Date(p.dob);
});
people.schemaVersion++;
people.sync();
}
// you can now remove the dob property in a future release - the old data will be automatically cleared, or add it back and map the data back to effectively rename the property
You can effortlessly create a REST server using zod. Just define your schema and map it to a route, NRS will automatically create GET, POST, PUT and DELETE endpoints for you. 🤩
NRS automatically handles your primary identifier by attaching an id to your object, transparently handing it for you.
import NodeAdapter from "@nano-record/node";
import z from 'zod'
import server from "@nano-record/server";
const adapter = new NodeAdapter("./");
const cards = z.object({
name: z.string(),
year: z.number(),
graded: z.boolean(),
grade: z.number().optional(),
value: z.number(),
league: z.enum(["mlb", "nba", "nfl", "nhl"]),
vintage: z.boolean(),
manufacturer: z.enum(["topps", "panini", "upperdeck", "donruss", "bowman", "fleer", "score", "prizm", "other"]),
});
server(adapter, 3000, {
"cards": {
schema: cards
}
});
When you start your server, it will display all the routes that were automatically created.
GET /cards
GET /cards/:id
POST /cards
DELETE /cards/:id
PUT /cards/:id
Nano Server listening on port 3000