talyssonoc/structure

View on GitHub
docs/schema-concept/circular-references-and-dynamic-types.md

Summary

Maintainability
Test Coverage
# Circular references and dynamic types

Sometimes we may need to have a type reference itself in its attributes, or have two types that reference eachother in separate files, it can be a complication because it's not possible to do this using the type definitions like we did before. For cases like this we can use a feature called "dynamic types".

When using dynamic types you can pass a string instead of the type itself in the type definition, this string will contain the type **identifier**, and then pass an object as the _second_ parameter of the `attributes` function with a key `dynamics` where the concrete type for each identifier is declared:

```js
/*
 * User.js
 */
const User = attributes(
  {
    name: String,
    friends: {
      type: Array,
      itemType: 'User', // << identifier
    },
    favoriteBook: {
      type: 'BookStructure', // << identifier
      required: true,
    },
    books: {
      type: 'BooksCollection', // << identifier
      itemType: String,
    },
  },
  {
    dynamics: {
      /* dynamic types for each identifier */
      User: () => User,
      BookStructure: () => require('./Book'),
      BooksCollection: () => require('./BooksCollection'),
    },
  }
)(class User {});

module.exports = User;

/*
 * Book.js
 */
const Book = attributes(
  {
    name: String,
    owner: 'User', // << dynamic type with inferred identifier
    nextBook: 'BookStructure', // << dynamic type with custom identifier
  },
  {
    identifier: 'BookStructure', // << custom identifier
    dynamics: {
      /* dynamic types for each identifier */
      User: () => require('./User'),
      BookStructure: () => Book,
    },
  }
)(class Book {});

module.exports = Book;
```

If you're using `import` instead of `require` (thus having to import the dynamic value in the top-level of the file), go to the [With ES Modules](#with-es-modules) section of this page.

## Dynamic type identifier

The type's identifier has to be same everywhere it's used, and can be defined in two ways:

### Inferred identifier

The identifier can be inferred based on the class that is wrapped by the `attributes` function. In backend scenarios this will be the most common case:

```js
const User = attributes(
  {
    name: String,
    bestFriend: 'User', // [A] type with inferred identifier
  },
  {
    dynamics: {
      User: () => User, // [B] inferred identifier concrete type
    },
  }
)(
  class User {
    //   ⬑-- the name of this class is the identifier
    // so if we change this name to UserEntity, we'll have to change
    // both [A] and [B] to use the string 'UserEntity' instead of 'User'
  }
);
```

### Custom identifier

If for some reason you can't rely on the class name, be it because you're using a compiler that strips class names or creates a dynamic one, you can explicitly set an indentifier.

To do that, in the second argument of the `attributes` function (e.g. the options) you should add a `identifier` key and set it to be the string with the type's identifier and then use that custom value everywhere this type is dynamically needed:

```js
const User = attributes(
  {
    name: String,
    bestFriend: 'UserEntity', // << type with custom identifier
  },
  {
    identifier: 'UserEntity', // << custom identifier
    dynamics: {
      // ⬐--- custom identifier concrete type
      UserEntity: () => User,
    },
  }
)(class User {});
```

## Concrete type definition inside `dynamics`

For the cases where the dynamic type is in a different file, it's important that the actual needs type to be resolved **inside** the function with the identifier. Let's break it down in two cases:

### With CommonJS modules

When using CommonJS modules you have two possibilities:

1. Putting the `require` call directly inside the concrete type resolution function:

```js
const Book = attributes(
  {
    name: String,
    owner: 'User',
    nextBook: 'BookStructure',
  },
  {
    identifier: 'BookStructure',
    dynamics: {
      User: () => require('./User'), // << like this
      BookStructure: () => Book,
    },
  }
)(class Book {});

module.exports = Book;
```

2. Exporting an **object** containing your other structure (instead of exporting the structure itself) and only access it inside the concrete type resolution function:

```js
/*
 * User.js
 */
const BookModule = require('./Book');

const User = attributes(
  {
    name: String,
    favoriteBook: {
      type: 'Book',
      required: true,
    },
  },
  {
    dynamics: {
      User: () => User,
      Book: () => BookModule.Book,
    },
  }
)(class User {});

exports.User = User;

/*
 * Book.js
 */
const UserModule = require('./User');

const Book = attributes(
  {
    name: String,
    owner: 'User',
  },
  {
    dynamics: {
      User: () => UserModule.User,
      Book: () => Book,
    },
  }
)(class Book {});

exports.Book = Book;
```

### With ES Modules

When using ES Modules you have a single possibility, which is importing the other structure from the top-level of your file and then returning it from the concrete type resolution function. It's important to note that this **only** works with ES Modules, if you're using CommonJS, check the [With CommonJS modules](#with-commonjs-modules) section.

```javascript
/*
 * User.js
 */
import Book from './Book';

const User = attributes(
  {
    name: String,
    favoriteBook: {
      type: 'Book',
      required: true,
    },
  },
  {
    dynamics: {
      User: () => User,
      Book: () => Book,
    },
  }
)(class User {});

export default User;

/*
 * Book.js
 */
import User from './User';

const Book = attributes(
  {
    name: String,
    owner: 'User',
  },
  {
    dynamics: {
      User: () => User,
      Book: () => Book,
    },
  }
)(class Book {});

export default Book;
```