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:
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:
Altering Downlinks in one Item
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.
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.
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:
Let's retrieve an existing TC Item and do some experiments. We've also got "OR" labels "ORANGE" and "APPLE" in our Label configuration.
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];
server.setComment("Changing labels");
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:
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:
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.
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:
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:
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.
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:
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:
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.
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:
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:
We hope you enjoy using the SDK to solve problems. Beyond this guide, the FAQ and Reference documentation may be helpful.