blob: a226bb14a7201b9d118797bd30e1bf542b303c1f [file]
////
Copyright (c) 2016 NumberFour AG.
All rights reserved. This program and the accompanying materials
are made available under the terms of the Eclipse Public License v1.0
which accompanies this distribution, and is available at
http://www.eclipse.org/legal/epl-v10.html
Contributors:
NumberFour AG - Initial API and implementation
////
:toc: right
:toc-title: Jump to topic:
:commandkey: ⌘
:bib-file: ../../res/n4js.bib
.N4JS Tutorial
= N4JS Tutorial
*The flexibility of JavaScript with the type safety of Java*
N4JS is a language and IDE designed for developers who need to build scalable projects whose code should be
modular, reusable and easily maintainable over time. N4JS bridges the strengths of ECMAScript
(also known as JavaScript) and Java. The result is a typed JavaScript superset that is dynamic,
flexible and type-safe.
*Experience with JavaScript or Java?*
Developers who are experienced with JavaScript can easily apply their knowledge in N4JS. As N4JS is a
typed superset of ECMAScript, it is possible to use plain ECMAScript in N4JS. Most of the important
features from ECMAScript 2015 are supported, such as *modules* with import and export,
*classes*, *iterables* and *destructuring* of arrays
and objects. Developers with Java experience will be familiar with features from Java 8
such as *nominal typing*, *interfaces* and *generic classes
and methods*; even dependency injection and testing look similar.
*Development Environment*
N4JS has an IDE based on Eclipse. It comes with its own workspace, libraries and plugins. The
IDE has a transpiler (i.e. a compiler that translates from one language to another) which validates
N4JS code and then transforms it to JavaScript. The transpiler also performs imports from ECMAScript
to N4JS. The benefit of type safety means that errors in code are noticed while editing in the
IDE, allowing for problem-solving before code is ever deployed.
Developers who have previous experience using Eclipse will be familiar with most of the common UI features
such as Editor, Project Outline, Project Explorer and Console views. The arrangement of views
can be easily selected using the perspectives icons as highlighted in the upper-right of the window.
== Using N4JS to Implement a Node.js Application
The following screenshot shows
the most important views of the IDE:
image::images/ide.png[N4JS IDE, title="N4JS IDE"]
In order to illustrate some of the features of N4JS, we shall look at building a Node.js-based
server of a Task Manager which serves as a reminder or to-do list application. As we build this
basic example, we shall add improvements step-by-step, introducing new features with each
increment in order to demonstrate some of the strengths of the language and IDE through a
simple practical model.
Before we dive in and start creating our example, it is worth briefly describing the architecture
of our application in order to have a considered approach to the concept. In this particular
example, we can think of a multi-tiered client-server structure. The idea here is that when we
have the application separated, each tier can be reused for different purposes.
image::images/architecture.svg[Architecture]
We will start with modeling and implementing the domain model before implementing a first
version of the storage tier which will enable our first tests. The controller is then built
using a test-driven approach. We later improve the storage tier using a real database
before adding a REST API with http://expressjs.com/[Express] and, finally, a simple Web UI.
== Creating a new N4JS Project
Let's begin by creating a new N4JS Project that contains our whole application. In case of
larger applications, we might want to create projects for each component (or tier) of the
application. In order to keep this example small, we will use only one project.
The "New N4JS Project" wizard can be accessed at menu:File[New, N4JS Project] (Or using the
keyboard shortcuts kbd:[{commandkey} + n] for Mac, kbd:[Alt+Shift+n] on Windows)
image::images/newprojectwizard.png[New Project Wizard, 700]
When we select kbd:[Finish], a new project folder is created with the following content:
* folder *src*: contains the .n4js source files for the project.
* folder *src-gen*: contains the transpiled JavaScript that is generated after compiling n4js
files. The IDE automatically transpiles n4js files on save.
* *manifest.n4mf*: project description file, containing description of dependencies.
=== The Domain Model
Before we start implementing the domain model, we shall take a look at a UML diagram of it:
image::images/domainmodel.svg[]
The UML diagram illustrates how the entities of our domain model are related
and which features the classes of the model will have.
We will begin by defining Task which is extended by Appointment and Todo.
=== Creating new classes and modules
In our new N4JS project, we can begin creating our entities. The easiest way to
do this is to utilize the New N4JS Class wizard, accessed via menu:File[New,N4JS Class]
image::images/newclasswizard.png[, 700]
N4JS supports modules introduced by ECMAScript 2015. A module contains functions,
classes and other declarations and code. The declared elements can be exported
and imported by other modules. We will use a single module for all classes of our
domain model. In larger projects, one might follow the Java convention to create
a single file per class.
The module specifier is the full path name of the module file, relative to the source
folder. We use Unix-like path separators, that is forward slashes "/". In the Class
Wizard (and later in import statements) we will omit the file extension.
In the above figure, we are creating a new class named Task which will be saved in
the module *model* in the project source folder.
==== Implement the entity classes
The class wizard has already created a file and the empty class "Task". We will
manually enhance this class as follows:
[source, n4js]
----
export abstract class Task { // <1>
public id: string?;
public label: string?;
}
----
<1> In the first line of code, we have defined an *abstract class* named Task. Classes in
N4JS are similar to classes in ECMAScript 2015 or Java. The concept of an abstract
class is borrowed from Java. This means that we cannot have a _direct instance_
of Task in our model, but we may have _subclasses_ of Task.
We are populating the class with fields which are simple data fields of the class. This
is also borrowed from Java. The transpiler will move the field (with possible
initializers) into the constructor. The two data fields of Task are +id+ and +label+
which we have annotated with types. The N4JS transpiler will later remove these type annotations.
The type annotations declare the type of the data field. The type checker will issue
errors if we later assign values of a non-compatible type to the variables. The question
mark +?+ is a *type modifier* declaring the value as optional. That means that a new Task
may or may not have values assigned for their +id+ and +label+.
N4JS also provides the concept of *access modifiers* similar to Java with the modifiers
+public+, +protected+, +project+ and +private+. Access modifiers constrain the visibility
of elements, that is, they restrict from where an element can be accessed.
* `public` means that the element can be accessed from everywhere,
* `protected` may only be accessed from subclasses,
* `project` only from within the project (this is the default visibility)
* `private` accessed only from within the same module.
We now manually add two classes to the same file:
[source, n4js]
.model.n4js
----
/** An appointment. */ // <1>
export class Appointment extends Task {   // <2>
public time: Date;   // <3>
public place: string;   // <3>
}
export class Todo extends Task {  
public dueDate: Date?;   
public done = false;   // <4>
}  
----
<1> Appointment is prefaced with a *JSDoc* annotation (a comment beginning with pass:[/**] and
closed with pass:[*/] which documents that it is a single Appointment task. JSDoc comments are
used to provide markup annotation of your code. The content of these comments is displayed when
you hover over a reference to that element.
<2> The class Appointment is a *subclass* of Task. This is achieved by using the keyword
+extends+ and demonstrates *inheritance*. N4JS supports single class inheritance similar
to ECMAScript 2015 or Java. The subclass Appointment will inherit the members
+id+ and +label+ with types from Task.
<3> We add the members +time+ and +place+ with *type annotations*. The
type annotations of the members in Appointment are more strict than before, since
the optional modifier (denoted by the question mark) is missing. We will see the effects
of these different modifiers later on.
<4> Class Todo has a data field without type annotation but with an *initializer*. The N4JS
type checker can infer the type of the initializer, in this case a boolean literal,
and implicitly sets the type of the field to boolean.
In all classes we have defined above, we are using the ECMAScript 2015 +export+
keyword so that Appointment and Task can be imported and reused in other modules.
As the project grows, the benefits of having individual, reusable *modules* become
more and more useful in that they can be imported into other modules or even other projects.
=== Add an Enumeration
We also want to add a priority field to the `Todo` class. We will modify the
`Todo` class and add a new type `Priority` as follows:
[source, n4js]
.model.n4js (cntd.)
----
export class Todo extends Task {
public dueDate: Date?;
public priority = Priority.NORMAL;
public done = false;
}
@StringBased // <1>
export enum Priority {
LOW, NORMAL, HIGH
}
----
<1> Use string-based `enum` to simplify (de-)serialization
Enumerations allow us to represent a fixed set of constants: `LOW`, `NORMAL` and `HIGH`.
The reason we use an enumeration here is because we know all possible values for Priority
at compile-time so we may limit it to these constants. N4JS provides two kinds of
enumerations: "ordinary" and "string-based". The former
will be translated to objects, enabling extended reflection (for example to get the type of
the enumeration or get all literals). The latter will be translated to strings. Literals of
string-based enumerations are, in fact, represented as plain strings in the JavaScript
output code. As a result, they offer less reflection capabilities.
=== The Storage Layer
Let's first have a look at the UML diagram describing out storage tier:
image::images/storage.svg[]
The storage tier is responsible for persisting our entities. We will create two
different kinds of "persistence". We will start with a simple in-memory peristence,
which can be used for testing. Later on, we will add a proper persistence layer using MongoDB.
==== Defining an Interface
We can create the storage module with the New Class wizard.
We call it "Storage.n4js", following Java's convention of naming the module similar
to the contained class or interface. We will manually edit the file as follows:
[source, n4js]
.Storage.n4js
----
import { Task } from "model"
export public interface Storage {
size(): int
clear()
getTasks(): Array<Task>
storeTask(task: Task): string
isEmpty(): boolean {
return (this.size()) === 0;
}
}
----
The very first line of code displays ECMAScript 2015's import statement.
It is a so-called "named import": We import the element "Task", in
our case a class, from the module with the specifier "model".
This time, we do not define a class but an interface "Storage". N4JS
supports *interfaces* which operate similar to those in Java 8. Interfaces
are similar to classes, but they cannot be instantiated. In N4JS it is
however possible to use the "instanceof" operator with interfaces. Usually
interfaces contain abstract methods, but they can contain data fields,
getters and setters as well. Similar to Java 8, interface methods can
provide a default implementation. Here, we provide a default implementation for the method +isEmpty+.
Classes implementing the interface can either rely on the default
implementation or provide a more efficient one. As in Java, a class
can implement multiple interfaces, and also interfaces can extend multiple interfaces.
=== Implement the Interface
Since we cannot instantiate an interface, we need a class implementing the
interface. We will create a new module for a class called "StorageInMemory.n4js".
This module will simply keep all entities in memory. If you copy-paste the
following code snippet in your IDE, you will see a few errors. Do not fret,
it is expected and we will deal with them shortly.
[source, n4js]
.StorageInMemory.n4js
----
import { Storage } from "Storage"
import { Task } from "model"
export class StorageInMemory implements Storage { // <2>
private lowestUnusedId = 1;
@Final
private tasks = new Map<string,Task>(); // <1>
@Override
public size(): int {
return this.tasks.size;
}
@Override
public storeTask(task: Task): string {
let id = 'id' + this.lowestUnusedId++;
this.tasks.set(id, task);
task.id = id;
return id;
}
@Override
public clear() {
this.lowestUnusedId = 1;
this.tasks.clear();
}
@Override
public getTasks(): Array<Task> {
return Array.from(this.tasks.values());
}
}
----
<1> The IDE will show an error here!
<2> We use the keyword +implements+ (known from Java) to define that this class implements
the interface. We have to provide specific implementation of the methods of the Storage
interface by using the `@Override` annotation to define `size`, `clear`, `getTasks`
and `storeTasks` (not all methods are shown here). This annotation is similar to
the annotation used in Java. It ensures that whenever a method in the interface is
changed, the type checker can issue a warning. This can be a lifesaver when larger
projects are to be maintained over time or across several development teams.
The above code will raise a compile error because type `Map` is not available
in ECMAScript Version 5. We'll have to tell N4JS that our example is intended
to run as ECMAScript 2015. Before doing this in the following section, let's
first look at the other parts of the above class declaration in more detail.
We use a data field `tasks` to store all the tasks in a map. The type Map stems from
ECMAScript 2015. It is a generic type similar to Array, which the observant reader may
have already seen in the Storage interface. N4JS provides support for generic types and methods
is similar to Java 8.
=== Edit the Manifest
By default, N4JS provides all the types known by ECMAScript 5. In order to use elements
(types, functions or variables) defined by a newer JavaScript version, we have to add
a corresponding runtime library as project dependency to the manifest. This has no direct
effect on the compiled code, it simply tells the type checker to assume that certain
types of a newer JavaScript version will be available at runtime (provided by the JavaScript
engine the code is intended for).
Such meta information about an N4JS project is kept in a so-called *manifest file*.
We need to open the `manifest.n4mf` file and edit a dependency. The default manifest
files created by the New Project wizard look like the following:
[source,n4mf]
.manifest.n4mf
----
ProjectId: n4js.example.tasks
ProjectType: library
ProjectVersion: 0.0.1
VendorId: eu.mycompany
VendorName: "MyCompany AG"
Output: "src-gen"
Sources {
source {
"src"
}
}
----
We need to add the following section at the end. Note that the manifest editor supports content
assist similar to the N4JS editor.
[source,n4mf]
----
RequiredRuntimeLibraries {
n4js-runtime-es2015
}
----
This will add all additionally defined types of ECMAScript 2015. It will also add new methods
to types already defined in ECMAScript 5.
== Running a Module
Having created the first version of our domain model and storage tier, we are ready to try it out.
For that, we create a module "Runner.n4js" with the following code:
[source, n4js]
----
import { StorageInMemory } from "StorageInMemory"
import { Todo } from "model"
let sim = new StorageInMemory();
let todo = new Todo();
todo.done = false;
todo.dueDate = new Date();
todo.label = "Test TODO";
sim.storeTask(todo);
console.log(sim.getTasks());
----
We then launch this module with Node.js. The easiest way to do that is with the context menu
(accessed by right-clicking in the editor) and selecting "Launch in Node.js". as shown in the
following screenshot:
image::images/firstlaunch.png[]
This will run the module currently opened in the editor. The output will be printed to the console
view, for example
image::images/firstlaunchconsole.png[]
[.language-n4js]
== Extend Entities with Spec-Constructor
When we look at the runner code, creating a new task is quite annoying: It has to be created with
a new expression, and then every data field has to be set separately. To simplify this, we add
a constructor to our base entity class Task as follows:
[source, n4js]
.model.n4js (cntd.)
----
export abstract class Task {
// ...
constructor(@Spec spec: ~i~this=undefined) {  
// code for initialization will be generated due to @Spec annotation  
}  
}  
...
----
The concept of constructors is taken from ECMAScript 2015. However, the parameter is very special
to N4JS. We briefly describe the type expression `pass:[~i~this=undefined]` used here. The part `=undefined` declares
the default initializer of the parameter `spec` (c.f. ECMAScript 2015 cite:[ECMA11a]). Parameters which have default
initializers are optional, hence N4JS allows for omitting them in function calls (or in case of
constructors, in the `NewExpression`).
`this` is a known keyword in ECMAScript, it usually refers to the receiver of a property or, in case of classes,
method call. But here we use it as a type expression, referring to the type of the `this`
keyword. This is usually the class in which the method or constructor is defined. That is,
in case of Task it will be `Task`. However, we have two subclasses of `Task`. We do not
define a new constructor in these classes, instead we let these classes inherit `this`
constructor. In case of `Todo`, the `this` type will become `Todo` and in case of `Appointment`,
`Appointment`. Simply referring to the `this` type wouldn't make any sense in the constructor,
since we would need a first instance in order to create another one - but how could we
create the first one? The solution comes with the `pass:[~i~]` prefix.
As discussed in the feature sheet, N4JS supports nominal and structural typing. Structural typing
is activated in N4JS with the tilde `pass:[~]`. Two structural types are compatible, if they provide
the same properties, or in case of classes, public members. In the constructor, we only need to
set the fields. In N4JS, we can use `pass:[~~]` to refer to the so-called **field structural type**. Two
field structural types are compatible, if they provide the same fields - methods are
ignored in these cases. Actually, optional fields are also ignored. This explains why we marked
some of the fields with the optional modifier. Note that fields with an initializer are also
treated as optional (since the initializer provides a default value). Actually, N4JS can do even
more. There are several modifiers to further filter the properties or members to be considered:
`pass:[~r~]` only considers getters or data fields, `pass:[~w~]` only setters and data fields. `pass:[~i~]` is used
for initializer parameters: For every setter or (non-optional) data field in the type, the
`pass:[~i~]`-type needs to provide at least a getter (or a readable data field). Optional fields
are also treated as optional in the field structural types.
For the concrete class `Todo`, the `pass:[~i~]`-type is not required to contain any property since all
its fields are either optional or have an initializer. It contains the optional fields `id`,
`label`, `dueDate`, `priority` and `done`. `pass:[~i~]Appointment` contains the required properties `time` and
`place`, and the optional fields `id` and `label`.
In most cases, we need this information in the constructor to set the fields accordingly.
For Task we would write:
[source, n4js]
----
constructor(spec: ~i~this=undefined) {
this.id = spec.id;
this.label = spec.label;
}
----
In order to simplify the code, the annotation `@Spec` tells the transpiler to add exactly this
code automatically. Even better: Since `Appointment` and `Todo` inherit the constructor, the
transpiler will add constructor code in these classes to set the additional fields also. That
is, with this single constructor, the `@Spec` annotation and the `pass:[~i~]this` type expression,
we have solved the problem of initialization for all our entity classes with a single stroke!
Using this `@Spec` constructor would then look similar to this:
[source, n4js]
----
let todo = new Todo({dueDate: new Date(), label: "Test TODO"});
sim.storeTask(todo);
----
*Short summary*
The main concepts demonstrated so far by our example are:
* *Modules* with import and export
* *Classes* with inheritance and constructors
* *Interfaces* with default methods
* *Enumeration*
* Special strategies for structural types
* *Manifest.n4mf* file and runtime library dependencies
We can now proceed to implement a Task Manager.
== Creating a Task Manager
We will now create the controller tier. This tier uses the entity and storage classes to provide
functionality that is actually useful for the user of the application.
Since we eventually want to implement a REST API (and use a real database), we need to introduce
asynchronous functions. So before we actually implement any controller class, we have to adjust our storage
tier to support asynchronous functions.
=== Make the Storage Asynchronous
If we would like to use a real database, all calls to the database will be asynchronous. Asynchronous
programming is a typical task in ECMAScript and there are several solutions to do this.
ECMAScript 2015 introduced a new class Promise which is supposed to be used with in these cases. Its methods
accept callback functions which are called once the asynchronous event has been triggered. Since these
callback functions tend to call other asynchronous functions, ECMAScript programmers easily end up in the so
called 'callback hell'. There is a proposal for upcoming ECMAScript versions to use special constructs in
the language to get rid of this callback hell. The idea is to mark asynchronous functions as "async" and,
when these functions are called, the program can "await" the result. This async/await feature is already
supported by several JavaScript frameworks and it is also built-in to N4JS including validation.
First we have to change the Storage interface and mark all methods which are supposed to be asynchronous as
`async`:
[source, n4js]
.Storage.n4js (cntd.)
------
import { Task } from "model"
export public interface Storage {
async size(): int // <1>
async clear()
async getTasks(): Array<Task>
async storeTask(task: Task): string
async isEmpty(): boolean {
return (await this.size()) === 0;
}
}
------
<1> Adding "async" to the size method and without adding the "await" keyword, you will get a
warning in method `isEmpty` similar to the following:
image::images/awaitwarning.png[]
You will also get a lot of other errors in other files:
image::images/asyncerrors.png[]
Without an async/await and type aware IDE you probably would have missed one or the other of these errors.
We can easily fix that by simply adding `async` to all the indicated methods.
If you still have the runner module, you probably will get a warning there as well. If you ignore that
warning and run it again, you will get the following
[source, n4js]
----
Promise { <pending> }
----
instead of the expected output. We are not going to fix this problem now as we will introduce a better way
of testing the code after the next step.
*Create TaskManager*
[source, n4js]
.TaskManager.n4js
----
import { Todo } from "model"
import { Task } from "model"
import { Storage } from "Storage"
import { StorageInMemory } from "StorageInMemory"
export public class TaskManager {
private storage: Storage = new StorageInMemory();
public async getTasks(): Array<Task> {
return await this.storage.getTasks();
}
public async createTodo(label: string): string {
let newTodo = new Todo({label: label});
let newId = await this.storage.storeTask(newTodo);
return newId;
}
}
----
This class does not reveal any new concepts, but how do we test it? For that, we are going to use the N4JS
test framework.
By utilizing the built-in test suite, classes and modules will not become polluted with superfluous test-
code. In addition, it is possible to overcome some access modifiers restrictions so there's no need to
restructure or rewrite your code specifically to run tests.
Since we use a tier architecture, it is quite easy to add a test: We simply replace one tier with
appropriate tests:
image::images/controllertest.svg[]
=== Testing
Since we do not want to mix up the application with the tests, we create a new project. We use the new
project wizard
image::images/createtestproject.png[]
We adjust the `manifest.n4mf` accordingly:
* Define which project we test in the *TestedProjects* section.
* Change the *source* folder to *test* folder. This way the IDE knows where to look for tests later on.
* Add project dependencies to the built-in test framework "Mangelhaft", which also provides a comprehensive
collection of assert methods.
After adding these changes, the manifest of the test project will look as follows:
[source,n4mf]
.manifest.n4mf (in project n4js.example.tasks.tests)
----
TestedProjects {
n4js.example.tasks
}
Output: "src-gen"
Sources {
test {
"src"
}
}
ProjectDependencies {
org.eclipse.n4js.mangelhaft,
org.eclipse.n4js.mangelhaft.assert
}
----
We can now write our first test. Again, we use the class wizard to create a module "TaskManagerTest"
containing a class with the same name. The first test should look like that:
[source, n4js]
.TaskManagerTest.n4js
----
import { TaskManager } from "TaskManager"
import { Assert } from "n4/mangel/assert/Assert"
export public class TaskManagerTest {
mgr: TaskManager = new TaskManager();
@Test
async testCreateTodo() {
await this.mgr.createTodo("test todo");
Assert.equal("test todo", (await this.mgr.getTasks())[0].label);
}
}
----
Mangelhaft is an xUnit-like test framework. For the sake of simplicity, N4JS uses the same annotations as
the popular Java test framework JUnit. In our case, we have a single test method which needs to be annotated
with `@Test`.
Since we are testing asynchronous code, the test method needs to be asynchronous as well and we need to
"await" the results of the methods we call. Mangelhaft supports asynchronous code so we do not have to
bother about that any further. This is the nice thing about using `async`/`await` and N4JS: asynchronous
programming becomes as simple as synchronous programming!
We can run the test via the IDE. This works similar to launching the code with Node.js by simply using the
context menu. The IDE will detect a test and it will automatically add the correct menu entry to the context
menu:
image::images/testinnodejs.png[]
This will run the test and the test view will show the result of the first test:
image::images/firsttestresults.png[]
== Storage using MongoDB
Instead of "storing" the entities in memory, we want to use a real database. In this example, we are going
to use https://www.mongodb.com/[MongoDB]. To follow along this section on your own computer,
you must have MongoDB installed and start a data base server instance via the command line as follows:
[source,bash]
mongod --dbpath /db
In order to use MongoDB from N4JS, we need the appropriate npm package which allows MongoDB access from
ECMAScript. Adding this npm and making it available in N4JS is as simple as adding any project dependency.
We have to open the manifest editor (of the tasks project) and add the following project dependency:
[source, n4js]
----
ProjectDependencies {
mongodb
}
----
image::images/quickfixnpminstall.png[]
The quick-fix will automatically download all required npm packages, that is mongodb and all its
dependencies. We can now use mongodb from our N4JS code.
To let N4JS know about the types a particular npm package provides, an N4JS definition file with extension `.
n4jsd` is required (the same applies if you use a plain JavaScript file from N4JS). For some npm packages,
definition files are provided at https://github.com/NumberFour/n4jsd[github.com/NumberFour/n4jsd].
Let's assume for the moment there are no `.n4jsd` file available for MongoDB; we import MongoDB using
a *dynamic import* as follows:
[source, n4js]
----
import * as mongodb+ from "mongodb"
mongodb.MongoClient.connect('mongodb://localhost:27017/tasks', function (err: any+, db: any+) {
if (!err) {
// ... use data base ...
db.close();
}
});
----
However, since we do have an `.n4jsd` file available, we can import types such as `MongoDB`, `Collection`,
or `ObjectID` provided by MongoDB using an ordinary ECMAScript2015 named import, just as if we were
importing from an N4JS module:
[source, n4js]
.StorageMongoDB.n4js
----
import { Storage } from "Storage";
import { Task, Appointment, Todo } from "model";
import { Collection, Db, MongoClient, ObjectID } from "mongodb";
/**
* Persistence for task lists using a mongodb instance.
*/
export class StorageMongoDB implements Storage {
cachedDb: Db = null;
private async getTasksCollection(): Collection {
if (!this.cachedDb) {
this.cachedDb = await MongoClient.connect('mongodb://localhost:27017/tasks');
}
return this.cachedDb.collection('tasks');
}
public async shutdown() {
this.cachedDb.close(true);
this.cachedDb = null;
}
}
----
In the above code section, we are implementing StorageMongoDB from the Storage interface and then calling
some of the standard MongoDB collection methods.
The next step is to retrieve the information about our Tasks and to store them in our MongoDB database. In
the storeTask method, we are then retrieving the inserted item id's from MongoDB and returning them as a
`task.id`.
[source, n4js]
.StorageMongoDB.n4js (cntd.)
----
export class StorageMongoDB implements Storage {
// ...
@Override
public async size(): int {
let coll = await this.getTasksCollection();
return await coll.count({});
}
@Override
public async clear() {
let coll = await this.getTasksCollection();
await coll.deleteMany({});
}
@Override
public async getTasks(): Array<Task> {
let coll = await this.getTasksCollection();
let resultRaw = await coll.find({}).toArray();
let result = resultRaw.map((data): Task => fromData(data)); // <1>
return result;
}
@Override
public async storeTask(task: Task): string {
let coll = await this.getTasksCollection();
let result = await coll.insertOne(toData(task));
if (result.insertedCount === 1) {
task.id = result.insertedId.toHexString();
return task.id;
}
throw new Error("insert document failed");
}
}
----
<1> The use of the `=&gt;` arrow function, derived from ES6. Arrow functions have implicit lexical
binding and are less verbose than traditional function expressions.
The above code uses two helper functions, `toData()` and `fromData()`. Those illustrate two techniques available
in N4JS: reflection and so-called `@Spec` constructors, respectively. Reflection is known from many
languages and allows for retrieving information of a type and its members at runtime. It is used in
`fromData()` as follows:
[source,n4js]
.StorageMongoDB.n4js (cntd.)
----
function toData(task: Task): ~~Task {
let metaClass = N4Type.of(task),
data: any+ = {};
// note: would have to set data._id, here, if we
// wanted support for updating existing tasks
data._type = metaClass.name;
let taskAsObject: Object = task; // up-cast to object to allow index access
for (let field of metaClass.dataFields(true,true)) {
data[field.name] = taskAsObject[field.name];
}
return data;
}
----
Conversely, `@Spec` constructors are special constructors that allow us to create a new instance of a class and
initialize it with values provided by a plain data object in properties that correspond to the type's fields.
[source,n4js]
.StorageMongoDB.n4js (cntd.)
----
function fromData(data: any+): Task {
let ctor = typeToCtor.get(data._type as string);
if (!ctor) {
throw new Error('Unsupported type of data model entity: ' + data._type);
}
let task = new ctor(data);
task.id = (data._id as ObjectID).toHexString();
return task;
}
const typeToCtor = new Map<string,constructor{Task}>([
['Todo', Todo] as Iterable2<string, constructor{Task}>,
['Appointment', Appointment] as Iterable2<string, constructor{Task}>
]);
----
Note that both functions `toData` and `fromData` should be declared outside of the `StorageMongoDB` class but in the same file `StorageMongoDB.n4js`.
By using the above two helper functions, we avoid sending our data model instances directly to the MongoDB
driver. Note that the entire implementation is intended for illustration purposes and in a real-world
systems many details would be handled differently, depending on the actual requirements.
[.language-n4js]
== Dependency Injection
We now have two implementations of the interface Storage. For testing, the in-memory solution is adequate,
but for the application we want to use the MongoDB solution of course. Since we are using the Storage in our
TaskManager class, we would need to change the TaskManager depending on the storage solution. This is
inconvenient and error prone. It would be much better if we could configure which storage class to use from
outside the TaskManager at some central location. This is possible with dependency injection.
To learn more about how dependency injection works, we have written an
<<../features/dependency-injection#dependency-injection, extended feature description>> that describes the benefits of this technique.
In short, N4JS provides built-in support for dependency injection using the same annotations as known from JSR-330/
Google Guice. Instead of using initializers calling the constructor for certain fields, we just mark them
with `@Inject`. We will do that with the storage field in the TaskManager class:
[source,n4js]
.TaskManger.n4js (cntd.)
----
export public class TaskManager {
@Inject
private storage: Storage;
// ...
}
----
NOTE: After removing the constructor, the IDE will create a warning that one of the imports is unused. You
can easily fix that by using the "Organize Import" feature, either from the context menu (or via
kbd:[{commandkey}+Shift+O] on Mac OS, kbd:[Ctrl+Shift+O] on Windows).
How does N4JS now create the instance of storage? For that, we need an injector. An injector is responsible
for creating all variables annotated with `@Inject`. The injector is configured with a so-called "binder".
The binder is more or less a translation table telling the injector which type it should use to create a
concrete instance when a certain type is given. In our case, we need to tell the injector whether we want an
instance of StorageInMemory or StorageMongoDB. We are going to adjust the test accordingly.
For that, we first add a binder to the test module "TaskManagerTest.n4js":
[source,n4js]
.TaskMangerTest.n4js (cntd.)
----
@Binder
@Bind(Storage,StorageInMemory)
class InMemoryBinder {}
----
The annotation `@Binder` marks the class InMemoryBinder to become a binder. For each mapping we need to add
an annotation `@Bind`, which takes the requested type as the first argument and the actual type as the
second one. We only actually need to define bindings for interfaces. If the requested type is a class and if
no binding is defined for it, the injector will simply create an instance of that very type.
The next step is to create an injector. Fortunately, we do not have to do that manually. The dependency
injection framework of N4JS introduces the notion of dependency injection components (DIC). A DIC is
associated with an injector; this is done by using the annotation `@GenerateInjector`. Additionally we need
to tell the framework which configuration it should use for the injector, this is done via the annotation
`@UseBinder`, which expects the type name of a binder class.
[source,n4js]
.TaskMangerTest.n4js (cntd.)
----
@GenerateInjector @UseBinder(InMemoryBinder)
export public class TaskManagerTest {
}
----
The IDE helps us in finding problems: since the TaskManager class uses injection to get the storage field,
it needs to be injected itself. The IDE warns us in the TaskManagerTest class:
image::images/injectionwarning.png[]
This is hard to find but easy to fix: We just have to replace the initalizer with an `@Inject` annotation.
[source,n4js]
.TaskMangerTest.n4js (cntd.)
----
export public class TaskManagerTest {
@Inject
mgr: TaskManager;
}
----
With these little changes, we can now configure the storage solution from outside the TaskManager class.
Running this test will behave as before but we have removed the hard-coded dependency from TaskManager to
StorageInMemory.
== Web UI module
The final step in building functionality into our model is to create a simple web user interface using
http://expressjs.com/[Express] as a dependency to create a web server. We will then pass our tasks from MongoDB into a small
HTML page to display the results. We will create the web server using http://expressjs.com/[Express].
In order to use Express, we need the appropriate npm module. Adding this npm and making it available in
N4JS is as simple as adding any project dependency, as already shown for MongoDB, above. We have to open the
manifest editor (of the tasks project) and add the following project dependency (along with the existing
dependencies):
[source,n4mf]
.manifest.n4mf (cntd.)
----
ProjectDependencies {
mongodb,
express,
n4js.lang
}
----
The dependency `n4js.lang` contains `N4Injector` which is needed later, so make sure it is declared as a dependency.
The quickfix will automatically download all required npm packages, that is, Express and all its
dependencies. We can now use Express in our N4JS code (for more details, esp. N4JS definition files, see
above description on MongoDB).
[source,n4js]
.WebUI.n4js
----
//Creating a simple Web User Interface in HTML
import { TaskManager } from "TaskManager"
import {Application, Response } from "express";
import express from "express";
import { Todo } from "model"
export class WebUI {
private app: Application;
@Inject
private manager: TaskManager;
public start() {
this.app = express();
this.app.get('/', async (req, res) => {
let page = await this.renderHomePage();
res.send(page);
});
}
}
----
Express is a web framework that provides (among other things) HTTP helpers for web routing. In the above
example, we are importing the classes `Application` and `Response` from Express and creating a home page
that we can render some HTML to. Next we will add a method for creating new tasks:
[source,n4js,subs=macros]
.WebUI.n4js
----
public start() {
// ... code shown above ...
this.app.get("/create", async (req, res) => {
let values = req.query as pass:[~]Object with {type: string, label: string};
if (values && values.type === 'Todo' && values.label && values.label.length > 0) {
await this.manager.createTodo(values.label);
}
redirect(res, '/');
});
}
----
Have Express listen on port 4000 at localhost:
[source,n4js]
.WebUI.n4js
----
public start() {
// ... code shown above ...
this.app.listen(4000, '0.0.0.0', 511, function() {
console.log("HTTP server listening on http://localhost:4000/");
});
}
----
Finally, we add a helper method for rendering a simple HTML page so we can view our Todos and edit them:
[source,n4js]
.WebUI.n4js
----
export class WebUI {
// ... methods shown above ...
protected async renderHomePage(): string {
let tasks = await this.manager.getTasks();
let todos = tasks.filter((task) => task instanceof Todo);
return `
<html>
<body>
Your to-do's:
<ul>
${
todos.length === 0 ? '<li><em>none</em></li>\n'
: todos.map((todo) =>
'<li>'+todo.label+' <small>(id: '+ todo.id +')</small></li>'
).join('\n')
}
</ul>
<hr/>
<form action="/create" method="get">
<input type="hidden" name="type" value="Todo">
Label: <input type="text" name="label"><br>
<input type="submit" value="Create Todo">
</form>
<hr/>
<a href="/clear">[Clear All]</a>
</body>
</html>
`;
}
}
function redirect(res: Response, url: string) {
res.header('Cache-Control', 'no-cache');
res.redirect(301, url);
}
----
//`
At this point, we have to launch our web server. For this purpose, we create a small launch script that
configures the dependency injection (as shown in the section on dependency injection, above), creates an
instance of class `WebUI`, and invokes method `start()`:
[source,n4js]
.launch.n4js
----
import { Storage } from "Storage";
import { StorageMongoDB } from "StorageMongoDB";
import { WebUI } from "WebUI";
import { N4Injector } from "n4js/lang/N4Injector";
@Binder
@Bind(Storage, StorageMongoDB)
class Binding { }
@GenerateInjector
@UseBinder(Binding)
class Root { }
N4Injector.of(Root).create(WebUI).start();
----
You can now use the HTML page by going to http://localhost:4000/[http://localhost:4000/],
allowing you to interact with all of the logic we have built so far in order to read Todos, create new Todos
and clear the storage.
All of the fundamental elements of our model are completed and we have a functioning Task Manager with a
simple Web UI.
//////////////////////////////////////////
Testing Our Finished Model
TODO - Revise this section
We can use the annotation `@Before` to delegate the order in which certain tests take place. If we wanted to
mark a method to be executed once before each test in a given test class, we use `@Before` test annotation.
If we want a method to be executed once before all tests, such as clearing a database, we can use the
keyword `@BeforeAll`.
[source, n4js,subs=macros]
----
import { Priority, Appointment, Todo } from "model";
import { Storage } from "Storage";
import { Assert } from "n4/mangel/assert/Assert";
/**
* Contains the actual test cases for testing implementations of Storage.
* Subclasses will choose which concrete implementation to test.
*/
export public abstract class AbstractStorageTest {
/** A storage intended for testing. Created in pass:[#]prepare() method. */
private storage: Storage;
protected abstract createStorage(): Storage;
@BeforeAll
async prepareStorage() {
this.storage = this.createStorage();
}
@Before
async clearStorage() {
// make sure test storage is empty before each test starts
await this.storage.clear();
}
}
----
Using the test annotation `@BeforeAll`, we are creating the storage space a single time before running all
of our tests. The next section with the annotation `@Before`, ensures that the storage we created is cleared
before each test. After all tests are complete, we run a 'garbage collector' method telling MongoDB to
`shutdown` which cleans up all database resources and terminates the process.
[source, n4js]
----
@Test
async testStoreAppointment() {
let s = this.storage;
let appointment = new Appointment();
Assert.equal(0, await s.size());
let id = await s.storeTask(appointment);
Assert.equal(1, await s.size());
Assert.equal(id, appointment.id);
}
@Test
async testGetTasks() {
let s = this.storage;
let todo = new Todo({
label: 'test',
priority: Priority.HIGH,
dueDate: new Date(2016, 3, 13, 14, 30, 0)
});
await s.storeTask(todo);
let restoredTasks = await s.getTasks();
Assert.equal(1, restoredTasks.length);
Assert.isTrue(restoredTasks[0] instanceof Todo);
let restoredTodo = restoredTasks[0] as Todo;
Assert.equal(todo.label, restoredTodo.label);
Assert.equal(todo.priority, restoredTodo.priority);
Assert.equal(todo.dueDate.getTime(), restoredTodo.dueDate.getTime());
}
----
Now that our AbstractStorageTest module is completed, we can create further test modules for other sections
of our project:
[source, n4js]
----
// Creating StorageInMemoryTest to extend AbstractStorageTest
import { AbstractStorageTest } from "AbstractStorageTest"
import { Storage } from "Storage"
import { StorageInMemory } from "StorageInMemory"
export public class StorageInMemoryTest extends AbstractStorageTest {
@Override
protected createStorage(): Storage {
return new StorageInMemory();
}
}
----
In this instance, we are importing AbstractStorageTest and overriding the `createStorage` method and
adapting it for use in testing our StorageInMemory module.
The same can be done to test our MongoDB module:
[source, n4js]
----
//Creating StorageMongoDBTest to extend AbstractStorageTest
import { AbstractStorageTest } from "AbstractStorageTest"
import { Storage } from "Storage"
import { StorageMongoDB } from "StorageMongoDB"
export public class StorageMongoDBTest extends AbstractStorageTest {
@Override
protected createStorage(): Storage {
return new StorageMongoDB();
}
}
----
By importing our test module and using method overriding, we can adapt and reuse the same test module for a
number of practical purposes, saving time and building an efficient testing system.
//////////////////////////////////////////
[.language-n4js]
== Export as npm
Finally, we can export our project as an npm package to integrate it into other Node.js projects or to
launch from command line.
* You can export one or multiple projects by selecting them in the Project Explorer and opening the
"Export ..." wizard by right-clicking on them. On the first page select "N4JS Exports / N4JS npm Export". For
the purpose of this example, only export project `n4js.example.tasks`.
* On the second page you have to choose a target folder to export to.
IMPORTANT: Export to a folder outside your Eclipse workspace!
* By default, the exporter exports as a directory. Optionally, you can export as a versioned tarball by
checking the option "Compress the contents of the file".
* The last page of the wizard shows a preview of the package.json file that will be created in the
exported npm package, together with a comparison to an existing package.json file in your N4JS project, if
present. You can place such a package.json file next to your `manifest.n4mf` file (optional), in case you
need
to define more https://docs.npmjs.com/files/package.json[specifics in the package.json], that
are not covered by the N4JS export wizard.
image::images/npmexport.png[]
== Run from command line with Node.js
Once you've exported your project, you can either publish it to
https://www.npmjs.com/[npmjs.com] or install directly from your hard disk and call the launch
module, for example
[source,bash]
----
$ npm install -g n4js.example.tasks
$ node -r n4js.example.tasks/launch
HTTP server listening on http://localhost:4000/
----
In case you want to install dependencies and run it right away in the exported npm folder, you have to
manually set up the `NODE_PATH` to the folder hosting the exported npms, otherwise node cannot resolve the
project/npm IDs.
[source,bash]
----
$ export NODE_PATH=`pwd`
$ pushd n4js.example.tasks; npm install; popd
n4js.example.tasks@0.0.1 /Users/me/prj/n4js.example.tasks
├─┬ express@4.13.4
...
$ node n4js.example.tasks/launch.js
HTML server listening on http://localhost:4000/
----
So far we've been loading all code via node's `require()` function.
In case you want to load your modules via https://github.com/systemjs/systemjs[SystemJS],
which has more support to resolve cyclic dependencies across modules, you could use a handy starter
executable called `n4js`. The `n4js` starter is part of the `n4js-node` runtime environment npm (a default
dependency of any exported npm) and is therefore already installed:
[source,bash]
----
$ export NODE_PATH=`pwd`
$ cd n4js.example.tasks; npm install
n4js.example.tasks@0.0.1 /Users/me/prj/n4js.example.tasks
├─┬ express@4.13.4
...
$ ./node_modules/.bin/n4js n4js.example.tasks/launch
HTTP server listening on http://localhost:4000/
----
[bibliography]
== Bibliography
bibliography::[]