Skip to content

Latest commit

 

History

History
854 lines (655 loc) · 31.6 KB

README.md

File metadata and controls

854 lines (655 loc) · 31.6 KB

graphql-arangodb

An experimental library for 'translating' GraphQL operations into ArangoDB AQL queries which are designed to fetch all requested data in as few queries as possible. Flexibility is another objective; I want to empower the developer to define exactly how they want their GraphQL schema without being forced into a particular schema shape due to their database structure.

Maintainers Welcome

I've moved on from the side project which drove me to create this library and I don't actively use it anymore. If there's a feature or bugfix you need, I encourage you to open a PR. If you'd like to take this project in a new direction, you're welcome to fork it!

Example

type Query {
  user(id: ID!): User @aqlDocument(collection: "users", key: "$args.id")
}

type User {
  friends: [FriendOfEdge!]!
    @aqlEdge(
      collection: "friendOf"
      direction: ANY
      sort: { property: "name", sortOn: "$field_node" }
    )
}

type FriendOfEdge {
  strength: Int
  user: User! @aqlEdgeNode
}

type Mutation {
  createPost(input: PostCreateInput!): Post!
    @aqlSubquery(
      query: """
      INSERT { title: $args.input.title, body: $args.input.body } INTO posts
      """
      return: "NEW"
    )
}

Code Examples

For a simple and small example, see the example directory.

For a larger scale app that uses this library, check out my now-defunct startup idea Toast. The toast-core microservice (linked) drove the entire development of this library and almost every feature is utilized in that codebase.

Known Limitations

  • This library is not designed to run on the Foxx framework. Because Foxx code runs with direct database memory access, the concern of batching up queries is probably not as important, and designing for Foxx posed too many complexities to getting the library working.
  • Experimental features, like Relay support, have known shortcomings. See the documentation for those sections.
  • Writing database queries inside a GraphQL schema can be error prone! That's just a fact of life for the way this library is implemented.

Table of Contents

Setup

Installing

Start by installing the library

npm i --save graphql-arangodb

You may also need to install peer dependencies if you don't have them:

npm i --save graphql arangojs

Directive type definitions

To use the directives in this library, you need to add type definitions for them. The library exports pre-built type definitions for all directives, you just need to include them in your type definitions.

import { directiveTypeDefs } from 'graphql-arangodb';

const typeDefs = [directiveTypeDefs, ...allYourAppsOtherTypeDefs];

makeExecutableSchema({ typeDefs });

Adding a Database instance

The easiest way to connect graphql-arangodb to your ArangoDB database is to instantiate a Database class from arangojs and assign it to the arangoDb field of your GraphQL context:

const arangoDb = new Database({
  url: 'http://localhost:8529',
});
arangoDb.useDatabase('mydb');
arangoDb.useBasicAuth('mysecretuser', 'mysecretpassword');

const context = {
  arangoDb,
};

// pass the context into your GraphQL server according to documentation of the server

Resolvers

To start resolving queries using AQL, you need to set up resolvers for fields which will be resolved using those queries. For most use cases, this means all of the top-level fields in the root query and mutation types.

For most people, adding the default aqlResolver from graphql-arangodb should be enough:

import aqlResolver from 'graphql-arangodb';

const resolvers = {
  Query: {
    user: aqlResolver,
    users: aqlResolver,
    // ...
  },
};

Customizing the resolver

However, there are some advanced scenarios where you may want to customize how the resolver works. To do this, you can import createResolver and create your own version of the default resolver. All config properties are optional.

import { createResolver, plugins as defaultPlugins } from 'graphql-arangodb';

const resolver = createResolver({
  // argument resolvers are called like regular resolvers, but they are used only by
  // graphql-arangodb to apply custom transformations to field arguments before
  // adding them to the AQL query. They are separated from normal resolvers for
  // technical reasons related to how queries are extracted and built by the library.
  // Whenver possible, prefer to put this logic inside the AQL query itself.
  argumentResolvers: {
    Query: {
      searchUsers: args => ({
        ...args,
        // apply Lucene fuzzy indicator to user's match string before passing it to AQL
        match: `${args.match}~`,
      }),
    },
  },

  // customize the key in your context which stores data which will be passed down
  // into AQL queries via the $context interpolation
  contextKey: 'arango_context',

  // customize the context property which is used to get your Database instance
  contextDbKey: 'arango_db',

  // advanced: you can reassign the names of the default directive plugins, or
  // create your own plugin here. Plugins aren't documented yet, see source.
  plugins: {
    ...defaultPlugin,
    custom: myCustomPlugin,
  },

  // you can specify a static database instance instead of passing one through context
  db: new Database(),
});

Usage

Now that the library is configured, you can start adding directives to indicate how to query for your data.

Usage of these directives is fairly similar to writing subqueries directly in AQL. The main thing to know is that you never write the RETURN statement. This library automatically constructs the correct RETURN projections based on the selected fields in the GraphQL query.

Enums

Before we begin with the directives, this library also ships some enums which will be used in directive parameters. To use an enum, just supply its literal value to the parameter (don't enclose it in " marks).

  • AqlEdgeDirection: OUTBOUND | INBOUND | ANY
  • AqlSortOrder: DESC | ASC
  • AqlRelayConnectionSource: Default | FullText

Inputs

Some directives take complex inputs:

input AqlSortInput {
  """
  The property to sort on
  """
  property: String!
  """
  The order to sort in. Defaults ASC
  """
  order: AqlSortOrder = ASC
  """
  Change the object being sorted. Defaults to $field
  """
  sortOn: String
}

input AqlLimitInput {
  """
  The upper limit of documents to return
  """
  count: String!
  """
  The number of documents to skip
  """
  skip: String
}

"""
These are the same as the OPTIONS for a regular edge traversal in AQL
"""
input AqlTraversalOptionsInput {
  bfs: Boolean
  uniqueVertices: String
  uniqueEdges: String
}

Interpolations

All directives support the following interpolations in their parameter values:

  • $parent: Reference the parent document. If there is no parent (this is a root field in the query), references the parent from GraphQL, if that exists.
  • $field: Reference the field itself. In @aql directives, you must assign something to this binding to be returned as the value of the field. For all other purposes, you can use this to reference the current value (for instance, if you want to do a filter on $field.name or some other property).
  • $args: Reference the field args of the GraphQL query. You can use nested arg values. Usages of $args get turned into bind variables when the query is executed, and all field args are passed in as values.
  • $context: Reference values from the arangoContext key in your GraphQL context. Use this for global values across all queries, like the authenticated user ID.

Directives

@aqlDocument

Selects a single or multiple documents (depending on whether the return type of the field is a list) from a specified collection. If a single document is selected, you can supply an key parameter to select it directly. This key parameter may be an argument interpolation ($args.id, etc), or a concrete value. It is passed directly into the DOCUMENT AQL function as the second parameter. If you do not specify an key parameter, the first item from the collection will be returned. To select a single item with a filter, use @aql.

Parameters

  • collection: String!: The name of the collection of documents
  • key: String: A string value or interpolation that indicates the database key of the document.
  • filter: String: Adds a filter expression. Applies to key-based single document fetching (the first document will be taken after filter is applied).
  • sort: AqlSortInput: Adds a sort expression. Applies to key-based single document fetching (the first document will be taken after sort is applied).
  • limit: AqlLimitInput: Adds a limit expression. Only works when key is not provided.

Example

type Query {
  user(id: ID!): User @aqlDocument(collection: "users", key: "$args.id")
}

@aqlNode

Traverses a relationship from the parent document to another document across an edge. @aqlNode skips over the edge and returns the related document as the field value. If you want to utilize properties from the edge, use @aqlEdge/@aqlEdgeNode instead.

Parameters

  • edgeCollection: String!: The name of the collection which the edge belongs to
  • direction: AqlEdgeDirection!: The direction to traverse. Can be ANY.
  • filter: String: Adds a filter expression.
  • sort: AqlSortInput: Adds a sort expression.
  • limit: AqlLimitInput: Adds a limit expression.
  • options: AqlTraverseOptionsInput: Modify OPTIONS parameters on the traversal.

Example

type User {
  posts: [Post!]! @aqlNode(edgeCollection: "posted", direction: OUTBOUND)
}

@aqlEdge/@aqlEdgeNode

@aqlEdge traverses an edge from the parent document, returning the edge itself as the field value. @aqlEdgeNode can be used on the type which represents the edge to reference the document at the other end of it. @aqlEdgeNode should only be used on a field within a type represented by an edge. It has no directive parameters.

Parameters

Only @aqlEdge takes parameters:

  • collection: String!: The name of the collection for the edge
  • direction: AqlEdgeDirection!: The direction to traverse. Can be ANY.
  • filter: String: Adds a filter expression. To filter on the node, you can use $field_node as an interpolation. Defaults sortOn to $field.
  • sort: AqlSortInput Adds a sort expression.
  • limit: AqlLimitInput: Adds a limit expression.
  • options: AqlTraverseOptionsInput: Modify OPTIONS parameters on the traversal.

@aqlEdgeNode has no parameters.

Example

type User {
  friends: [FriendOfEdge!]!
    @aqlEdge(
      collection: "friendOf"
      direction: ANY
      sort: { property: "name", sortOn: "$field_node" }
    )
}

type FriendOfEdge {
  strength: Int
  user: User! @aqlEdgeNode
}

@aqlSubquery

Construct a free-form subquery to resolve a field. There are important rules for your subquery:

  • Important: You must assign the value you wish to resolve to the $field binding. This can be done for a single value using LET $field = value, or for a list by ending the subquery with FOR $field IN list. See the examples.
  • Do not wrap in (). This is done by the library.
  • Do not include a RETURN statement. All RETURN projections are constructed by the library for you to match the GraphQL query.

Parameters

  • query: String!: Your subquery string, following the rules listed above.
  • return: String: An optional way to specify the name of a binding to return. By default, in a subquery, you must follow the important rule marked above and assign to $field. However, if you prefer, you may specify which variable binding you want to return within your subquery, and we will do this for you.

Examples

Resolving a single value

type Query {
  userCount: Int!
    @aqlSubquery(
      query: """
      LET $field = LENGTH(users)
      """
    )
}

Resolving multiple values

type Query {
  """
  Merges the list of public posts with the list of posts the user has posted (even
  private) to create a master list of all posts accessible by the user.
  """
  authorizedPosts: [Post!]!
    @aqlSubquery(
      query: """
      LET authenticatedUser = DOCUMENT('users', $context.userId)
      LET allAuthorizedPosts = UNION_DISTINCT(
        (FOR post IN posts FILTER post.public == true RETURN post),
        (FOR post in OUTBOUND authenticatedUser posted RETURN post)
      )
      FOR $field in allAuthorizedPosts
      """
    )
}

In the above example, instead of the final line, you could also pass "allAuthorizedPosts" to the return parameter:

type Query {
  """
  Merges the list of public posts with the list of posts the user has posted (even
  private) to create a master list of all posts accessible by the user.
  """
  authorizedPosts: [Post!]!
    @aqlSubquery(
      query: """
      LET authenticatedUser = DOCUMENT('users', $context.userId)
      LET allAuthorizedPosts = UNION_DISTINCT(
        (FOR post IN posts FILTER post.public == true RETURN post),
        (FOR post in OUTBOUND authenticatedUser posted RETURN post)
      )
      """
      return: "allAuthorizedPosts"
    )
}

@aql

Free-form AQL for resolving individual fields using parent data or arbitrary expressions. Unlike @aqlSubquery, this should not be used for a full query structure, only for a simple expression.

Parameters

  • expression: String!: The expression to evaluate. Use interpolations to access in-scope information, like the $parent.

Example

type User {
  fullName: String!
    @aql(expression: "CONCAT($parent.firstName, \" \", $parent.lastName)")
}

@aqlKey/@aqlId

Resolve the annotated field with the _key or _id of the parent document, respectively. You can just attach these to any field which indicates the type's ID if you want your GraphQL IDs to be based on the underlying ArangoDB keys or full IDs.

Example

type User {
  id: ID @aqlKey # will be "2301" or similar
}

type Post {
  id: ID @aqlId # will be "posts/1234" or similar (depending on your collection name)
}

Relay Directives (Experimental)

Known limitations

The current Relay directives don't conform entirely to the Relay spec. They only support first/after paging; no reverse paging. pageInfo does not include hasPreviousPage. They work for basic, forward-looking pagination use cases, but have not been tested with the official Relay client library.

The usage of these directives may change a bit over time, so be sure to check when upgrading the library!

You must use all of the provided directives to properly construct a Relay connection, according to the rules below. The following example provides a full picture of how to create a Relay Connection:

Basic Relay Example

type User {
  postsConnection(first: Int = 10, after: String): UserPostsConnection!
    @aqlRelayConnection(
      edgeCollection: "posted"
      edgeDirection: OUTBOUND
      cursorExpression: "$node.title"
    )
}

type UserPostsConnection {
  edges: [UserPostEdge!]! @aqlRelayEdges
  pageInfo: UserPostsPageInfo! @aqlRelayPageInfo
}

type UserPostEdge {
  cursor: String!
  node: Post! @aqlRelayNode
}

type UserPostsPageInfo {
  hasNextPage: Boolean!
}

type Post {
  id: ID!
  title: String!
  body: String!
  publishedAt: String!
}

Relay Example with filtering

type User {
  postsConnection(
    first: Int = 10
    after: String
    filter: PostsFilterInput
  ): UserPostsConnection!
    @aqlRelayConnection(
      edgeCollection: "posted"
      edgeDirection: OUTBOUND
      cursorExpression: "$node.title"
      filter: """
      ($args['filter'] && (
        $args['filter'].titleLike == null || LIKE($node.title, CONCAT("%", $args['filter'].titleLike, "%"))
      ) && (
        $args['filter'].publishedAfter == null || $node.publishedAt > $args['filter'].publishedAfter
      ))
      """
    )
}

input PostsFilterInput {
  titleLike: String
  publishedAfter: String
}

About filtering

  • The filter parameter must be evaluated as a single boolean expression. Outer parameters should be used to enclose multiple computations.
  • If your filter parameter is optional, you should guard against it being null within your filter statement.
  • The word filter is interpreted in AQL as a new FILTER statement, so if you use that as a parameter name, you must access it via bracket syntax (['filter']), not dot syntax (.filter)
  • Test that the user has supplied a filterable value before filtering on that value (this is the reason the above example tests that $args['filter'].titleLike is not null before asserting that the node title is LIKE that value)
  • You may use $node and $edge to represent the current node and edge you are filtering against. $edge is only valid in a true edge connection from a parent node.

All directives can be applied to either the field which is resolved, or the type it resolves to. Applying the directive to the type might be useful if you reuse the connection in multiple places and don't want to apply the directive to each one. However, doing so may make your schema harder to read.

@aqlRelayConnection

Add this directive to a field or type definition to indicate that it should be resolved as a Relay Connection. The resolved value will have the standard edges and pageInfo parameters.

Note: Currently this only supports forward pagination using after.

Parameters

  • edgeCollection: String: The name of the collection of edges to traverse
  • edgeDirection: AqlEdgeDirection: The direction to traverse edges. Can be ANY.
  • cursorExpression: String: An expression used to compute a cursor from a node or edge. Using $node will refer to the node, $edge refers to the edge. If omitted, entries will be sorted by _key.
  • filter: String: Supply a filter statement to further reduce the edges which will be matched in the connection. $node, $edge, and $path may be used in addition to all standard interpolations, and will correspond to the first, second and third positional bindings in a FOR ... IN edge traversal statement.
  • source: String: (Advanced) Supply your own custom FOR expression to source documents from. For example, FOR $node IN FULLTEXT(Posts, "title", $args.searchTerm) would create a fulltext search connection. Use $node and $edge as bindings when traversing documents so that the rest of the query works properly. It's also possible to use subqueries to traverse more advanced collections, like FOR $node IN (FOR foo IN ...). Using a subquery in this way is valid AQL, so you can place any complex traversal logic within it if you wish. Also, if you use $edge or $path in your filter or cursorExpression arg, you should be sure to bind them in your source arg!

@aqlRelayEdges

Add this directive to a field or type definition to indicate that it should be resolved as a Relay Edge list. Must be used as a child field of a type resolved by @aqlRelayConnection.

@aqlRelayPageInfo

Add this directive to a field or type definition to indicate that it should be resolved as a Relay Page Info object. Must be used as a child field of a type resolved by @aqlRelayConnection.

@aqlRelayNode

Add this directive to a field or type definition to indicate that it should be resolved as the Node of a Relay Edge. Must be used as a child field of a type resolved by @aqlRelayEdge.

Running Custom Queries (Experimental)

In addition to adding directives to your schema to resolve fields, you can also utilize a function called runCustomQuery to imperatively execute AQL queries like you would using the standard arangojs client, but with added support for projected return values based on the GraphQL selection!

If that doesn't make sense, imagine a scenario where you are writing a query to do a full text search and you want to pre-process the user's input to work with Lucene. There's not currently a great place to put that processing logic; all the @aql directives assume you're just passing in the user's arguments verbatim.

Instead, you can write your own resolver like so:

import aqlResolver from 'graphql-arangodb';
import aql from 'arangojs';

const searchResolver = async (parent, args, context, info) => {
  const fullTextSearchString = processSearchString(args.searchString);

  return aqlResolver.runCustomQuery({
    query: aql`
    FOR matchedPost IN FULLTEXT(posts, "title", ${fullTextSearchString})
      RETURN matchedPost
    `,
    parent,
    args,
    context,
    info,
  });
};

Here we're using the aqlResolver.runCustomQuery function, which accepts a custom query string and bind variables. Write your own AQL however you'd like and return the data to resolve the current field (but be aware that your AQL will be run inside a larger query!).

The magic comes in when the result is returned. Because you passed in the parent, context, and info, graphql-arangodb can extend your query to return the rest of the data the user needs for their GraphQL operation. In other words, if the user made the query:

query Search($searchString: "good") {
  search(searchString: $searchString) {
    id
    title
    body

    tags {
      id
      name
    }

    author {
      id
      name
    }
  }
}

... they would still get tags and author resolved by your existing @aql directives on your schema, at no cost to you.

runCustomQuery is a tool to give you as much power as possible to craft root queries and mutations, while still getting the benefits of your declarative directives to resolve deeply nested data in a single database round-trip.

Using the built-in query builders

In addition to crafting your own queries with a literal string, you can still use this library's built-in 'query builders' which power the directives to create your custom query. This enables you to either opt out of using directives entirely (if you prefer not to clutter your schema document) or conditionally trigger different built-in behaviors.

import aqlResolver, { builders } from 'graphql-arangodb';

const conditionalResolver = async (parent, args, context, info) => {
  if (args.searchTerm) {
    return aqlResolver.runCustomQuery({
      queryBuilder: builders.aqlRelayConnection({
        // this sets up the relay connection to draw from a search view using the requested search term
        source: `FOR $node IN PostSearchView SEARCH PHRASE($node.name, $args.searchTerm, 'text_en')`,
        // our 'cursor' will actually be the weight value of the result, allowing proper sorting of results by weight.
        cursorExpression: `BM25($node)`,
        // because we order by weight, we actually want to start at higher values and go down
        sortOrder: 'DESC',
      }),
      parent,
      args,
      context,
      info,
    });
  } else {
    return aqlResolver.runCustomQuery({
      queryBuilder: builders.aqlRelayConnection({
        source: `FOR $node IN posts`,
        cursorExpression: '$node.createdAt',
      }),
      parent,
      args,
      context,
      info,
    });
  }
};

With the custom resolver above, for example, we construct our Relay-style connection based on a search view if the user has supplied a search term argument, or else we simply list all documents in the collection.

Mutations (Experimental)

Simple mutations are essentially made possible using the same tools as queries, especially @aqlSubquery:

type Mutation {
  createPost(input: PostCreateInput!): Post!
    @aqlSubquery(
      query: """
      INSERT { title: $args.input.title, body: $args.input.body } INTO posts
      """
      return: "NEW"
    )
}

The user can, of course, make selections on the returned Post, which will be properly converted into projections and subqueries just like a query operation.

However, there are some limitations to how complex things can get before you want a proper resolver. If there is logic to be done before writing to the database, you can defer calling graphql-arangodb's resolver until you have done it:

import { resolver } from 'graphql-arangodb';

const resolvers = {
  Mutation: {
    createPost: async (parent, args, ctx, info) => {
      const canCreatePost = await doSomethingElse(args, ctx);

      if (!canCreatePost) {
        throw new ForbiddenError("Hey, you can't do that!");
      }

      return resolver(parent, args, ctx, info);
    },
  },
};

You could also use the same trick to do some logic after.

If you want to modify the arguments before passing them on, or do even more advanced logic, see the section on runCustomQuery above.

Splitting Up Queries (Experimental)

There are notable use cases where you may want to specifically split the overall GraphQL operation into multiple AQL queries. For instance, if you do a write mutation, ArangoDB will not allow you to read from that collection again in the same query. However, it's possible (depending on what you return from your mutation) for the user to create a selection set which re-traverses collections which were affected by the original write. In such a case, you may want to split the initial write AQL query from the subsequent read queries in the remainder of the operation.

You can use the experimental @aqlNewQuery directive to do this. Simply add it to any field, and that field will start a brand new AQL query, as if it had been a root field.

Important: you must attach the library resolver to any field you annotate with @aqlNewQuery, so that it can process that field and any sub-selections into the new AQL query.

Important: if you are using this directive to accomplish a read-after-write scenario, you should add the waitForSync option to your write queries to ensure the data is consistent before the second query is run.

Example:

type Post {
  id: ID! @aqlKey
  title: String!
  body: String!
  publishedAt: String!
  author: User! @aqlNode(edgeCollection: "posted", direction: INBOUND)
}

type CreatePostPayload {
  post: Post!
    @aqlNewQuery
    @aqlSubquery(
      query: """
      LET $field = DOCUMENT(posts, $parent._key)
      """
    )
}

type Mutation {
  createPost: CreatePostPayload!
    @aqlSubquery(
      query: """
      INSERT { title: "Fake post", body: "foo", publishedAt: "2019-05-03" }
      INTO posts
      OPTIONS { waitForSync: true }
      LET $field = {
        post: NEW
      }
      """
    )
}

The example above allows a user to make a query like this:

mutation CreatePost {
  createPost {
    post {
      id
      title
      author {
        id
      }
    }
  }
}

without triggering an "access after data-modification by traversal" error from AQL.

Splitting up queries may also be useful for tuning performance and balancing the overall size of queries.

Splitting queries on relationships

One interesting property of AQL is that it will interpret a binding parameter which is shaped like a document as a document. This enables you to seamlessly split up fields which traverse edges using @aqlNewQuery without any further modifications, because the node from the previous query will be passed into the new query as a @parent bind parameter, and all built-in traversal queries are designed to utilize this. In other words, you can add @aqlNewQuery to @aqlNode, @aqlEdge, and @aqlRelayConnection without any further changes, and they will function correctly (while splitting into new queries themselves).

In detail: while a typical @aqlNode query, for instance, might look like this when generated (much of this is scaffolding from the library, but pay attention to the simplePosts field subquery):

LET query = FIRST(
  LET createUser = FIRST(
    INSERT {_key: @userId, role: @role, name: @name} INTO users
    RETURN NEW
  )
  RETURN {
    _id: createUser._id,
    _key: createUser._key,
    _rev: createUser._rev,
    name: createUser.name,
    id: createUser._key,
    simplePosts: (
      FOR createUser_simplePosts IN OUTBOUND createUser posted
      RETURN {
        _id: createUser_simplePosts._id,
        _key: createUser_simplePosts._key,
        _rev: createUser_simplePosts._rev,
        title: createUser_simplePosts.title,
        id: createUser_simplePosts._key
      }
    )
  }
)
RETURN query

... if you were to add @aqlNewQuery to the simplePosts field, it would generate two queries:

LET query = FIRST(
  LET createUser = FIRST(
    INSERT {_key: @userId, role: @role, name: @name} INTO users
    RETURN NEW
  )
  RETURN {
    _id: createUser._id,
    _key: createUser._key,
    _rev: createUser._rev,
    name: createUser.name,
    id: createUser._key,
  }
)
RETURN query

for the rest of the fields, and then:

LET query = FIRST(
  FOR createUser_simplePosts IN OUTBOUND @parent posted
    RETURN {
      _id: createUser_simplePosts._id,
      _key: createUser_simplePosts._key,
      _rev: createUser_simplePosts._rev,
      title: createUser_simplePosts.title,
      id: createUser_simplePosts._key
    }
)
RETURN query

for the simplePosts field.

The @parent bind parameter of the second query will be populated with the returned value from the first query, which includes the needed _id field (the library ensures this is always present) for AQL to evaluate the @parent bind variable as a document reference.

If you want to expriment with this behavior on your own, try running an AQL query in your database and passing an object with a valid _id field as a bind parameter, then traversing edges from it.


Development

This project was bootstrapped with TSDX.

Local Development

Below is a list of commands you will probably find useful.

npm start or yarn start

Runs the project in development/watch mode. Your project will be rebuilt upon changes. TSDX has a special logger for you convenience. Error messages are pretty printed and formatted for compatibility VS Code's Problems tab.

Your library will be rebuilt if you make edits.

npm run build or yarn build

Bundles the package to the dist folder. The package is optimized and bundled with Rollup into multiple formats (CommonJS, UMD, and ES Module).

npm test or yarn test

Runs the test watcher (Jest) in an interactive mode. By default, runs tests related to files changed since the last commit.