Skip to content

Commit

Permalink
feat(collection)!: Improve updating logic
Browse files Browse the repository at this point in the history
BREAKING CHANGE: `update()` and `updateMany()` now require an ObjectId string as the first argument and the updated object as the second.
  • Loading branch information
aerotoad committed Jul 28, 2023
1 parent 7416fe0 commit a3b2fc3
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 43 deletions.
96 changes: 66 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" style="max-width: 100%;">
</a>

&nbsp;
&nbsp;

<p align="center" dir="auto">
<b>:sparkles: Type-Safe</b> |
Expand All @@ -32,71 +32,78 @@

Nebra is a powerful, type-safe NoSQL database library for Node.js. It offers seamless data management with compile-time type-checking for enhanced reliability. Leveraging SQLite, Nebra ensures efficient and lightweight performance, making it perfect for Electron and other platforms. With its unique approach, Nebra empowers you to use a relational database as a NoSQL, providing the flexibility to harness the best of both worlds. Effortlessly manage your data and embrace a misty-smooth database experience.

Under the hood, Nebra makes use of [Knex.js](https://knexjs.org/), a flexible SQL query builder, and [better-sqlite3](https://github.com/WiseLibs/better-sqlite3), a high-performance SQLite wrapper. These powerful tools allow Nebra to deliver optimal performance and ensure smooth interactions with the database. Whether you're building a desktop application with Electron or a web-based project, Nebra's reliable and efficient architecture has got you covered.
Under the hood, Nebra makes use of [Knex.js](https://knexjs.org/), a flexible SQL query builder, and [better-sqlite3](https://github.com/WiseLibs/better-sqlite3), a high-performance SQLite wrapper. These powerful tools allow Nebra to deliver optimal performance and ensure smooth interactions with the database. Whether you're building a desktop application with Electron or a web-based project, Nebra's reliable and efficient architecture has got you covered.

## Getting Started

### Installation

To get started with Nebra, you can install it via npm. Open your terminal or command prompt and run the following command:

```bash
npm install nebra
```

### Example usage

Now that you have Nebra installed, you can start using it to manage your data with ease. Below is a basic example of how to set up and interact with the database using Nebra:

```typescript
import { nebra } from 'nebra'; // Or const {nebra} = require('nebra') for CommonJs
import { nebra } from "nebra"; // Or const {nebra} = require('nebra') for CommonJs

// Create a new Nebra instance and initialize the database
const db = await nebra('path/to/database.db') // Or :memory: for in-memory sqlite
const db = await nebra("path/to/database.db"); // Or :memory: for in-memory sqlite

// Add a collection to the tabase
const Users = await db.collection('users');
const Users = await db.collection("users");

// Insert a new json document to the collection
const user = await Users.insert({
name: 'Darth Vader',
email: 'i_am_your_father@deathstar.net'
name: "Darth Vader",
email: "i_am_your_father@deathstar.net",
});

// Create a new query for the collection
const query = Users.query();
// Add constraints
query.equalTo('name', 'Darth Vader');
query.equalTo("name", "Darth Vader");
// Execute the query
const results = await query.exec();
console.log(results[0]); // { name: 'Darth Vader', email: 'i_am_your_father@deathstar.net'}
```

This example demonstrates how to create a new Nebra instance, create a new collection and perform basic insert and query operations.

## Documentation

### Connecting to a database

To connect to a database, you must create a new instance of Nebra. This can be done by importing the nebra function and then passing to it a string representing the path for the location of the database:

```typescript
import { nebra } from 'nebra';
import { nebra } from "nebra";

const db = nebra('path/to/database.db')
const db = nebra("path/to/database.db");
```

Alternatively you can pass `:memory:` to the instance to get an in-memory database.

#### Testing the connection

You can use the `.authenticate()` method to test the if the connection was successful:

```typescript
try {
await db.authenticate();
console.log('Connection has been established successfully.');
} catch(error) {
console.error('Unable to connect to the database:', error);
console.log("Connection has been established successfully.");
} catch (error) {
console.error("Unable to connect to the database:", error);
}
```

#### Closing the connection

The connection will be kept open by default and will be used by all queries.
The connection will be kept open by default and will be used by all queries.
If you need to close it you can call the `.close()` method, which will return a promise.

> Note: Once a connection is closed it cannot be reopened, you will need to create a new instance of nebra to access the database again.
Expand All @@ -107,38 +114,40 @@ Collections are the primary way to organise your data in Nebra, each collection
A collection can be accessed by calling the `.collection()` function of the nebra instance:

```typescript
import { nebra } from 'nebra';
import { nebra } from "nebra";

const db = await nebra('path/to/database.db');
const Users = await db.collection('users');
const db = await nebra("path/to/database.db");
const Users = await db.collection("users");
```

This will create a new table called users if it doesn't exist or a reference to it if it does, allowing you to perform queries to it.

You can also specify the type of the documents in the collection easily by passing the type to the collection method:

```typescript
import { nebra } from 'nebra';
import { nebra } from "nebra";

const db = await nebra('path/to/database.db');
const db = await nebra("path/to/database.db");

interface User {
username: string;
password: string;
}

const Users = await db.collection<User>('users');
const Users = await db.collection<User>("users");
```
Querying the collection now will return an array of `Document<User>`, the `Document<T>` type wraps your interface and adds the required _id and timestamp fields added by Nebra.
In the example above the `Document<User>` will have the follwing structure:

Querying the collection now will return an array of `Document<User>`, the `Document<T>` type wraps your interface and adds the required \_id and timestamp fields added by Nebra.
In the example above the `Document<User>` will have the follwing structure:

```typescript
type Document<User> = {
_id: string; // Autogenerated ObjectId string
username: string; // Username key from the User type
password: string; // Password key from the User type
_id: string; // Autogenerated ObjectId string
username: string; // Username key from the User type
password: string; // Password key from the User type
createdAt: string; // ISO date string managed by Nebra
updatedAt: string; // ISO date string managed by Nebra
}
};
```

### Inserting documents
Expand All @@ -147,12 +156,39 @@ After we have our collection created, we can start inserting documents into them
To insert a new document into our collection we can use the `.insert()` method:

```typescript
const Users = await db.collection('users');
const Users = await db.collection("users");
const user = await user.insert({
username: 'NeoTheOne',
email: 'neo@matrix.com',
password: 'redpillbluepill'
username: "NeoTheOne",
email: "neo@matrix.com",
password: "redpillbluepill",
});
```

The `.insert()` method will return a promise that resolves to the inserted document, with the \_id and timestamp fields added by Nebra.

You can also insert multiple documents at once by using the `.insertMany()` method, which takes an array of documents and returns a promise that resolves to an array of the inserted documents.

```typescript
const Users = await db.collection("users");
const newUsers = await user.insertMany([
{
username: "GandalfTheGrey",
email: "gandalf@middleearth.com",
password: "youshallnotpass",
},
{
username: "PrincessLeia",
email: "leia@rebelalliance.com",
password: "helpmeobiwankenobi",
},
]);
```

### Updating documents

To update a document in a collection you can use the `.update()` method, which takes a document that belongs to the collection with the updated values and returns a promise that resolves to the updated document with the timestamp fields updated by Nebra.

```typescript

```

Expand Down
8 changes: 5 additions & 3 deletions src/__tests__/classes/collection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,7 @@ describe('Collection class', () => {
// Wait 100ms to ensure updatedAt is different
await new Promise(resolve => setTimeout(resolve, 100));

const updated = await collection.update({
...inserted,
const updated = await collection.update(inserted._id, {
name: 'test2'
});

Expand All @@ -79,6 +78,9 @@ describe('Collection class', () => {
const knex = nebra.knex();
const result = await knex('test').where('id', inserted._id);

console.log(inserted);
console.log(JSON.parse(result[0].data));

expect(result).toBeDefined();
expect(result.length).toBe(1);
expect(result[0].id).toBe(inserted._id);
Expand All @@ -104,7 +106,7 @@ describe('Collection class', () => {
// Wait 100ms to ensure updatedAt is different
await new Promise(resolve => setTimeout(resolve, 100));

const updated = await collection.updateMany([
const updated = await collection.updateMany([inserted[0]._id, inserted[1]._id], [
{
...inserted[0],
name: 'test3'
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/classes/query.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ async function generateUsers(number: number, usernames: string[], Users: any, Po
const post = await Posts.insert(newPost);
user.postIds.push(post._id);
}));
await Users.update(user);
await Users.update(user._id, user);
});
}

Expand All @@ -262,7 +262,7 @@ async function generateUsersWithCar(number: number, usernames: string[], Users:
const post = await Posts.insert(newPost);
user.postIds.push(post._id);
}))
await Users.update(user);
await Users.update(user._id, user);
});
}

Expand Down
63 changes: 55 additions & 8 deletions src/classes/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,21 +66,49 @@ export class Collection<T = {}> {
* @param document Document to update
* @returns A promise that resolves to the updated document
*/
async update(document: Document<T>): Promise<Document<T>> {
document.updatedAt = new Date().toISOString();
const result = await this._knex(this._name).where('id', document._id).update({
data: JSON.stringify(document)
}).returning('data');
async update(objectId: string, document: T): Promise<Document<T>> {

try {
const newDocument = this.newDocument(document);

// Create a new transaction
const trx = await this._knex.transaction();

// Get the existing document
const existingDocument = await trx(this._name).where('id', objectId).first();

if (!existingDocument) throw new Error('Document not found');

const existingDocumentData = JSON.parse(existingDocument.data);
const updatedDocument = {
...existingDocumentData,
...newDocument,
updatedAt: new Date().toISOString(),
_id: existingDocument.id, // Use the existing id to prevent user overwrites
createdAt: existingDocumentData.createdAt // Use the existing createdAt to prevent user overwrites
};

// Update the document
await trx(this._name).where('id', objectId).update({
data: JSON.stringify(updatedDocument)
});
// Commit the transaction
await trx.commit();

// Return the updated document
return updatedDocument;
} catch (error) {
throw error;
}

return JSON.parse(result[0]?.data);
}

/**
* Updates multiple documents in the collection
* @param documents Array of documents to update
* @returns A promise that resolves to an array of updated documents
*/
async updateMany(documents: Document<T>[]): Promise<Document<T>[]> {
async updateMany(objectIds: string[], documents: Document<T>[]): Promise<Document<T>[]> {
try {
const newDocuments = documents.map(document => {
document.updatedAt = new Date().toISOString();
Expand All @@ -90,8 +118,27 @@ export class Collection<T = {}> {
// Create a new transaction
const trx = await this._knex.transaction();

// Get the existing documents
const existingDocuments = await trx(this._name).whereIn('id', objectIds);

if (!existingDocuments) throw new Error('Documents not found');
if (existingDocuments.length !== newDocuments.length) throw new Error('Document mismatch');

// Update each document
const updatedDocuments: Document<T>[] = existingDocuments.map((existingDocument, index) => {
const existingDocumentData = JSON.parse(existingDocument.data);
const updatedDocument = {
...existingDocumentData,
...newDocuments[index],
updatedAt: new Date().toISOString(),
_id: existingDocument.id, // Use the existing id to prevent user overwrites
createdAt: existingDocumentData.createdAt // Use the existing createdAt to prevent user overwrites
};
return updatedDocument;
});

// Update each document
const result = await Promise.all(newDocuments.map(document => {
const result = await Promise.all(updatedDocuments.map(document => {
return trx(this._name).where('id', document._id).update({
data: JSON.stringify(document)
}).returning('data');
Expand Down

0 comments on commit a3b2fc3

Please sign in to comment.