appy-one / acebase

A fast, low memory, transactional, index & query enabled NoSQL database engine and server for node.js and browser with realtime data change notifications
MIT License
491 stars 27 forks source link

Generating unique keys for child objects #43

Closed Devwulf closed 3 years ago

Devwulf commented 3 years ago

Hello! If I have this schema:

schema.set("users/$userId", {
    name: "string",
    "pets?": {
        "$petId": {
            name: "string",
            "awards?": {
                "$awardId": {
                    name: "string"
                }
            }
        }
    }
});

which are mapped to these models:

class User {
    public name: string;
    public pets?: {
        [key: string]: Pet;
    }
}

class Pet {
    public name: string;
    public awards?: {
        [key: string]: Award;
    }
}

class Award {
    public name: string;
}

Is there a way to auto-generate all the child keys simply by pushing a filled in top-most node into the database, like so?

const award = new Award();
award.name = "Some Award";

const pet = new Pet();
pet.name = "Some Pet";
pet.awards = {
    "*": award
};

const user = new User();
user.name = "Some User";
user.pets = {
    "*": pet
};

await db.ref("users").push(user);

Currently, the only way I could see to do this is to have this monstrosity of a code just to give the child nodes their own auto-generated keys:

const award = new Award();
award.name = "Some Award";

const pet = new Pet();
pet.name = "Some Pet";

const user = new User();
user.name = "Some User";

db.ref("users")
    .push(user)
    .then(ref => {
        db.ref(`users/${ref.key}/pets`)
            .push(pet)
            .then(ref2 => {
                db.ref(`users/${ref.key}/pets/${ref2.key}/awards`)
                    .push(award);
            });
    });

Is there a better way of achieving this? Thanks!


Edit: Seems I was too tired last night to realized this is also possible:

const award = new Award();
award.name = "Some Award";

const pet = new Pet();
pet.name = "Some Pet";

const user = new User();
user.name = "Some User";

const userRef = await db.ref("users").push(user);
const petRef = await db.ref(`users/${userRef.key}/pets`).push(pet);
const awardRef = await db.ref(`users/${userRef.key}/pets/${petRef.key}/awards`).push(award);

But the problem is still there. This solution really doesn't scale well, once nodes get deeper further down. It would be great if there's a way to automatically generate unique keys for child nodes in object collections, preferably a feature in this library by default.

appy-one commented 3 years ago

The best way to do this is using ID.generate() instead of "*":

const { ID } = require('acebase');

const award = new Award();
award.name = "Some Award";

const pet = new Pet();
pet.name = "Some Pet";
pet.awards = {
    [ID.generate()]: award
};
const user = new User();
user.name = "Some User";
user.pets = {
    [ID.generate()]: pet
};

const userRef = await db.ref("users").push(user);

Or, using backward compatible js:

user.pets = {};
user.pets[ID.generate()] = pet;
Devwulf commented 3 years ago

I have tried this, but it doesn't seem to work either with LocalStorage or IndexedDB. Only the user node is created, but everything else is missing. Is this not supported for the browser?

appy-one commented 3 years ago

This should work just fine in the browser

Devwulf commented 3 years ago

I have created a test project implementing this, and it works as expected. However, it does not seem to work when I start binding the classes with custom creators and serializers. I'll try to recreate my main project into a minimal working test project to see if that really is the case.

Devwulf commented 3 years ago

I have created a minimal working example that could be found here: test-acebase

That project uses the React Typescript template and only the App.tsx file has been edited. The same project can be created using these commands:

  1. npx create-react-app test-acebase --template typescript
  2. cd ./test-acebase
  3. npm install
  4. Copy everything from the App.tsx from the project above to the new project's App.tsx

This test project exhibits the problem I'm experiencing with my main project: only the users node is created in the IndexedDB. Perhaps I'm doing something wrong with the binding?

appy-one commented 3 years ago

Thanks, I'll take a look!

appy-one commented 3 years ago

The problem is in your serializer methods, you are only returning the object's name properties.

In your User type mapping, change this:

    serializer: (ref, obj) => {
      return {
        name: obj.name
      };
    }

To this:

    serializer: (ref, obj) => {
      return {
        name: obj.name,
        pets: obj.pets
      };
    }

Do the same for your Pet mapping with awards, and everything should work as expected.

Devwulf commented 3 years ago

I have confirmed that it works as expected now. Thanks!

I have always thought that serializing child nodes using the serializer will create redundant copies of the child nodes in the IndexedDB key-values, so I never thought to do this. It would be greatly appreciated if both ID.generate() and serializing child nodes in parent nodes are documented as well!

appy-one commented 3 years ago

I'll take a look at the docs to see what I can improve 👍🏼