Skip to content

Links, Labels and Item Creation

Matrix is designed such that items of a particular Category may link to other items of some particular Categories. For example, a Requirement should link to a Specification. A Specification to a Test, and a Test to an Executed Test. We call this Traceability in the documentation.

You can explore the traceability rules for a project with the ItemConfiguration class. Let's focus on which categories link to each other, and create/edit some links between them. Here I've compiled the information into a drawing showing those categories which have downlinks:

relationships.js
const lib = require("./lib.js");

async function run() {
    const [server, wheely] = await lib.getServerAndProject();
    const wheelyConfig = wheely.getItemConfig();
    const data = wheelyConfig.getCategories().map((c) => { 
      return { cat: c, downLinks: wheelyConfig.getItemConfiguration(c).downLinksRequired }; })
      .filter((o) => o.downLinks.length > 0)
      .map((o) => `  ${o.cat} -> ${o.downLinks.join(",")}`);
    console.log(`digraph {\n${data.join('\n')}\n rankdir="LR";\n}`);
}

run().then(() => process.exit(0));

You'll see I also added a method to our library to return the server and project, lib.getServerAndProject(connection, projectName), with appropriate default values for my server.

The output, processed with GraphViz:

%3REQREQSPECSPECREQ->SPECTCTCSPEC->TC

Let's write a program that finds a random SPEC item which links to a TC. We'll remove the TC link and ensure that our now-changed item doesn't show up in the original query for SPECs which link to TCs. Then we'll put things back as we found them. This demonstrates making changes that affect the server.

change-downlink.js
const assert = require("assert");
const lib = require("./lib.js");

async function run() {
    const [server, wheely] = await lib.getServerAndProject();
    server.setComment("Changing for a test");
    const mask = wheely.constructSearchFieldMask({ includeDownlinks: true });
    const query = "mrql:category=SPEC and downLink=TC";
    const specsWithTCs = await wheely.searchForItems(query, "", false, mask);
    const itemIndex = Math.floor(Math.random() * specsWithTCs.length);
    let spec = specsWithTCs[itemIndex];
    console.log(
        `Found ${specsWithTCs.length} SPEC Items that have TC downlinks. Choosing ${spec.getId()} at random.`);

    const oldDownlinks = [...spec.getDownlinks()];  // Ensure we copy the array, as we need it later.
    assert(oldDownlinks.length > 0);
    console.log(`${spec.getId()} linked to ${oldDownlinks[0].to}`);
    spec.removeDownlink(oldDownlinks[0].to);

    spec = await wheely.updateItem(spec);

    console.log(`Item updated...re-running search query..`);
    const query2 = await wheely.searchForItems(query, "", false, mask);
    // Ensure spec is not present.
    assert(query2.filter((item) => item.getId() == spec.getId()).length == 0);

    // set things right again.
    spec.addDownlink(oldDownlinks[0].to);
    spec = await wheely.updateItem(spec);

    const query3 = await wheely.searchForItems(query, "", false, mask);
    // Ensure spec is present.
    assert(query3.filter((item) => item.getId() == spec.getId()).length == 1);
    console.log(`Success changing and restoring ${spec.getId()}.`);
 }

run().then(() => process.exit(0));

Note line 21 where we save our changes to the server. Project.updateItem() returns a fresh copy of the item. Running the application gives the following output:

mstanton@darkstar:~/work/matrix-sdk-docs/codes (main)$ node change-downlink
Debugger attached.
Found 4 SPEC Items that have TC downlinks. Choosing SPEC-11 at random.
SPEC-11 linked to TC-4
Item updated...re-running search query..
Success changing and restoring SPEC-11.
Waiting for the debugger to disconnect...
mstanton@darkstar:~/work/matrix-sdk-docs/codes (main)$

Labels

Labels in Matrix are quite sophisticated. In the WHEELY_CLIENT_TESTS project, there is an "XOR" label with two values:

  • DAYTIME
  • NIGHTTIME

If one of these labels is set on an Item, then the other is set, the first label will be removed. The client enforces the label rules. Additionally, labels can be limited to particular categories. Let's try and set the DAYTIME label on a REQ.

bad-set-label.js
const lib = require("./lib.js");

async function run() {
    const [server, wheely] = await lib.getServerAndProject();
    const reqs = await wheely.searchForItems("mrql:category=REQ");
    const itemIndex = Math.floor(Math.random() * reqs.length);
    let req = reqs[itemIndex];

    req.setLabel("DAYTIME");
 }

run().then(() => process.exit(0));

Looking at the output, we've been rebuked!

mstanton@darkstar:~/work/matrix-sdk-docs/codes (main)$ node bad-set-label
/Users/mstanton/work/matrix-sdk-docs/codes/node_modules/matrix-requirements-sdk/server/index.js:8535
                throw new Error(`Category ${this.type} doesn't allow labels`);
                      ^

Error: Category REQ doesn't allow labels
    at Item.verifyLabelsAllowed (/Users/mstanton/work/matrix-sdk-docs/codes/node_modules/matrix-requirements-sdk/server/index.js:8535:23)
    at Item.setLabel (/Users/mstanton/work/matrix-sdk-docs/codes/node_modules/matrix-requirements-sdk/server/index.js:8559:14)
    at run (/Users/mstanton/work/matrix-sdk-docs/codes/bad-set-label.js:9:9)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)

Node.js v18.11.0
mstanton@darkstar:~/work/matrix-sdk-docs/codes (main)$ 

Note that the exception was thrown on the call to Item.setLabel(), and no trip to the server was made. This is because the SDK has the label configuration locally, and can enforce those rules. Looking at the Label settings, we see that this label only works on TC Items:

Screenshot

Let's retrieve an existing TC Item and do some experiments. We've also got "OR" labels "ORANGE" and "APPLE" in our Label configuration.

set-labels.js
const lib = require("./lib.js");

function printLabels(item, msg) {
    const labels = item.getLabels();
    console.log(`${item.getId()}: ${msg}`);
    console.log(`  Labels: ${labels.join(", ")}`);
}

async function run() {
    const [server, wheely] = await lib.getServerAndProject();
    const tcs = await wheely.searchForItems("mrql:category=TC");
    const itemIndex = Math.floor(Math.random() * tcs.length);
    let tc = tcs[itemIndex];

    const oldLabels = [...tc.getLabels()];
    printLabels(tc, "Initial state");
    tc.setLabel("DAYTIME");
    printLabels(tc, "After set of DAYTIME (xor label)");
    tc.setLabel("NIGHTTIME");
    printLabels(tc, "After set of NIGHTTIME (xor label)");
    tc.setLabel("APPLE").setLabel("ORANGE");
    printLabels(tc, "After set of APPLE and ORANGE (or labels)");
    tc.unsetLabel("DAYTIME");
    printLabels(tc, "After unset of DAYTIME (should have no effect)");
    tc.setLabels([]);
    printLabels(tc, "Labels set to empty");
    if (tc.needsSave()) {
        console.log(`${tc.getId()} is dirty, not saving to the server`);
    }
 }

run().then(() => process.exit(0));

Running the code above, we get:

mstanton@darkstar:~/work/matrix-sdk-docs/codes (main)$ node set-labels
TC-1: Initial state
  Labels: ORANGE, NIGHTTIME, FORPRINTING
TC-1: After set of DAYTIME (xor label)
  Labels: ORANGE, FORPRINTING, DAYTIME
TC-1: After set of NIGHTTIME (xor label)
  Labels: ORANGE, FORPRINTING, NIGHTTIME
TC-1: After set of APPLE and ORANGE (or labels)
  Labels: ORANGE, FORPRINTING, NIGHTTIME, APPLE
TC-1: After unset of DAYTIME (should have no effect)
  Labels: ORANGE, FORPRINTING, NIGHTTIME, APPLE
TC-1: Labels set to empty
  Labels: 
TC-1 is dirty, not saving to the server
mstanton@darkstar:~/work/matrix-sdk-docs/codes (main)$ 

Note that the DAYTIME label was removed when we set NIGHTTIME. Whereas the APPLE and ORANGE labels should not toggle on or off as we set one or the other. They are "OR" labels, not "XOR" labels.

Item creation

The SDK is designed to support you in creating new Items. Generally, no trip to the server is necessary during construction of an Item. Once the Project configuration is locally available, Fields and Labels can be set in a validated way, as demonstrated above with Labels.

The important methods for creating and saving Items are on the Project.

  • Project.createItem(category: string): Item - No trip to the server is made. An Item with no Id is returned.
  • Project.putItem(folderId: string, item: Item): Promise<Item> - Save an Item with no Id to the server.
  • Project.updateItem(item: Item): Promise<Item> - update an existing Item.
  • Project.deleteItem(itemId: string, force?: boolean) - Force must be true if the Item is a FOLDER with children.

Let's create a UC (Use Case) object. There is a steplist Field type called "Use Case Steps". It will be interesting to see how you add rows to a table:

create-uc.js
const lib = require("./lib.js");

async function run() {
    const [server, wheely] = await lib.getServerAndProject();

    let uc = wheely.createItem("UC");
    const title = "Test " + performance.now().toString();
    uc.setTitle(title);
    uc.getFieldByName("Description")[0].getHandler().setHtml("This is a test");
    let tableHandler = uc.getFieldByName("Use Case Steps")[0].getHandler();
    tableHandler.insertRow(0, ["Open fridge", "Fridge door opens"]);
    tableHandler.insertRow(1, ["Get milk", "Milk is in hand"]);
    tableHandler.insertRow(2, ["Drink milk", "Milk is gone"]);

    const columnName = tableHandler.columnNumberToFieldId(0);
    tableHandler.setColumnData(2, columnName, "Throw milk away");

    server.setComment("Saving our first Item");
    uc = await wheely.putItem("F-UC-1", uc);
    console.log(`Created Item ${uc.getId()}`);
}

run().then(() => process.exit(0));

Running gives the output Created Item UC-4 on my server, with the following screenshot:

Screenshot

It turns out the milk is spoiled! We can change "Drink milk" in the 3rd row of the table like so:

    const columnName = tableHandler.columnNumberToFieldId(0);
    tableHandler.setColumnData(2, columnName, "Throw milk away");
    // Be sure to update the server!

Let's make a program that does all of that then deletes the Item, so we don't leave our database a mess.

create-uc-cleanup.js
const assert = require("assert");
const lib = require("./lib.js");

async function run() {
    const [server, wheely] = await lib.getServerAndProject();

    let uc = wheely.createItem("UC");
    const title = "Test " + performance.now().toString();
    uc.setTitle(title);
    uc.getFieldByName("Description")[0].getHandler().setHtml("This is a test");
    let tableHandler = uc.getFieldByName("Use Case Steps")[0].getHandler();
    tableHandler.insertRow(0, ["Open fridge", "Fridge door opens"]);
    tableHandler.insertRow(1, ["Get milk", "Milk is in hand"]);
    tableHandler.insertRow(2, ["Drink milk", "Milk is gone"]);

    server.setComment("Saving our first Item");
    uc = await wheely.putItem("F-UC-1", uc);
    console.log(`Created Item ${uc.getId()}`);

    // We should get the tableHandler again, because we have a new uc object.
    const columnName = tableHandler.columnNumberToFieldId(0);
    tableHandler = uc.getFieldByName("Use Case Steps")[0].getHandler();
    tableHandler.setColumnData(2, columnName, "Throw milk away");
    uc = await wheely.updateItem(uc);
    console.log(`Updated Item ${uc.getId()}`);

    // Verify the change in the new object.
    tableHandler = uc.getFieldByName("Use Case Steps")[0].getHandler();
    assert(tableHandler.getColumnData(2, columnName) == "Throw milk away");

    await wheely.deleteItem(uc.getId());
    console.log(`Deleted Item ${uc.getId()}`);
}

run().then(() => process.exit(0));

After running that, you can look at the changes made on your Matrix Project, and you'll see the delete mentioned at the top:

Screenshot

Uploading image attachments

A Richtext Field can display images. When we upload an attachment to the server, we're given back information we can turn into a URL to retrieve the image from the server again. Let's just try uploading an image and printing the URL returned by the server. We need the Axios library to do this. To load Axios into your project, run the following at the command prompt:

npm install axios

Below, we pass the Axios library to Project.uploadLocalFile() as the first parameter. We also need to create stream for the file on line 10.

upload-image.js
const fs = require("fs");
const lib = require("./lib.js");
const axios = require("axios");

async function run() {
    const [server, wheely] = await lib.getServerAndProject();
    server.setComment("Creating attachment");

    const filePath = __dirname + "/resources/typewriter.jpg";
    let file = fs.createReadStream(filePath);
    let result = await wheely.uploadLocalFile(axios, file, (p) => {
        console.log("Uploading...");
    });

    // Convert the result to a url
    const url = wheely.computeFileUrl(result);
    console.log(`Visit ${url} to get your file`);
}

run().then(() => process.exit(0));

You'll see output like this:

mstanton@darkstar:~/work/matrix-sdk-docs/codes (main)$ node upload-image
Uploading...
Uploading...
Visit https://clouds5.matrixreq.com/rest/1/WHEELY_OBSERVABLE/file/11075?key=key_elnjk70etp7p1qadc5vuqss634 to get your file
mstanton@darkstar:~/work/matrix-sdk-docs/codes (main)$ 

Note that we're getting the image with https, but it is possible to get it without authentication. Just change the protocol to http. You can alter this on your Matrix server, such that all images require authentication.

To add an uploaded image to a Richtext field, simply reference the URL in an html img tag, like so:

place-image.js
const fs = require("fs");
const lib = require("./lib.js");
const axios = require("axios");

async function run() {
    const [server, wheely] = await lib.getServerAndProject();
    server.setComment("Creating attachment");

    const filePath = __dirname + "/resources/typewriter.jpg";
    let file = fs.createReadStream(filePath);
    let result = await wheely.uploadLocalFile(axios, file, (p) => {
        console.log("Uploading...");
    });

    // Convert the result to a url
    const url = wheely.computeFileUrl(result);

    // Find a UC and add the image to the bottom of the description.
    wheely.getCategory("UC").create
    const ucIds = await wheely.searchForIds("mrql:category=UC");
    // Take the first one.
    let uc = await wheely.getItem(ucIds[0]);
    let handler = uc.getFieldByName("Description")[0].getHandler();
    let text = handler.getHtml();
    text += `Adding an image with the SDK: <img width="400" src="${url}"><br>`;
    handler.setHtml(text);
    uc = await wheely.updateItem(uc);
    console.log(`Added image to ${uc.getId()}`);
}

run().then(() => process.exit(0));

Running at the command prompt reveals:

mstanton@darkstar:~/work/matrix-sdk-docs/codes (main)$ node place-image
Uploading...
Uploading...
Added image to UC-1

Which looks like this in the Matrix application:

Screenshot

File Attachments

You can also upload attachments to be stored by fileManager fields. I've added a fileManager field to category SPEC, and attached a file. Our FieldHandler for the fileManager field type is bare-bones, only providing the getData() and setData(string) methods that every FieldHandler provides. We'll have to examine the data format in order to work with it correctly.

get-attachments.js
const lib = require("./lib.js");

async function run() {
    const [server, wheely] = await lib.getServerAndProject();
    let spec = await wheely.getItem("SPEC-2");
    let handler = spec.getFieldByName("Files")[0].getHandler();
    let fileData = JSON.parse(handler.getData());
    console.dir(fileData, { depth: null, colors: true });
}

run().then(() => process.exit(0));

The code above prints out the JSON object it expects in the fileManager field:

mstanton@darkstar:~/work/matrix-sdk-docs/codes (main)$ node get-attachments
[
  {
    fileName: 'cdt-overview.jpg',
    fileId: '11085?key=key_fb77i3bti07cku2dp1f7lcglib'
  }
]
mstanton@darkstar:~/work/matrix-sdk-docs/codes (main)$ 

The data format of the field is an array of {fileName, fileId} tuples, where fileId is a concatenation of two fields in the AddFileAck structure returned by Project.uploadLocalFile() and Project.uploadFile(), fileId and key.

Let's try our hand at uploading a second file and saving it:

upload-file-manager.js
const fs = require("fs");
const lib = require("./lib.js");
const axios = require("axios");

async function run() {
    const [server, wheely] = await lib.getServerAndProject();

    // Upload the file.
    server.setComment("Creating attachment");
    const filePath = __dirname + "/resources/typewriter.jpg";
    let file = fs.createReadStream(filePath);
    const addFileAck = await wheely.uploadLocalFile(axios, file);
    console.log(`Uploaded ${filePath} to ${wheely.computeFileUrl(addFileAck)}`);

    // Save to "Files" in SPEC-2.
    let spec = await wheely.getItem("SPEC-2");
    let handler = spec.getFieldByName("Files")[0].getHandler();
    // the field may have no data (undefined or empty string).
    let fileData = [];
    if (handler.getData() !== undefined && handler.getData() !== "") {
        fileData = JSON.parse(handler.getData());
    }
    fileData.push({
        fileName: "typewriter.jpg",
        fileId: `${addFileAck.fileId}?key=${addFileAck.key}`
    });
    console.dir(fileData, { depth: null, colors: true });
    handler.setData(JSON.stringify(fileData));
    spec = await wheely.updateItem(spec);
    console.log(`Updated ${spec.getId()}`);
}

run().then(() => process.exit(0));

Running at the command prompt, we get:

mstanton@darkstar:~/work/matrix-sdk-docs/codes (main)$ node upload-file-manager
Uploaded /Users/mstanton/work/matrix-sdk-docs/codes/resources/typewriter.jpg to
 https://clouds5.matrixreq.com/rest/1/WHEELY_OBSERVABLE/file/11088?key=key_r8ovlj5cf9d1o53t5qfhli9p9r
[
  {
    fileName: 'cdt-overview.jpg',
    fileId: '11085?key=key_fb77i3bti07cku2dp1f7lcglib'
  },
  {
    fileName: 'typewriter.jpg',
    fileId: '11088?key=key_r8ovlj5cf9d1o53t5qfhli9p9r'
  }
]
Updated SPEC-2
mstanton@darkstar:~/work/matrix-sdk-docs/codes (main)$ 

Looking at SPEC-2 in the Matrix application, we can see we've got two attachments that can be downloaded:

Screenshot

We hope you enjoy using the SDK to solve problems. Beyond this guide, the FAQ and Reference documentation may be helpful.