Node.js - Group home

Managing files in a partitioned dataset with the zoau Node.js module

  
A partitioned data set, or PDS for short, is a data set containing multiple members. This can be thought of as being similar to having files in a directory. When working on z/OS, it is often necessary to be able to read, write, or execute files contained in a PDS.  The following video will demonstrate how you can use IBM Open Enterprise SDK for Node.js to manage files in a PDS using Node.js by leveraging the zoau node module:


In the above video, we create a web application that can edit, build, and run C source code residing in a PDS, and can be used by developers who are unfamiliar with z/OS.

We start by creating a project directory:

$ mkdir dpp-node-zoau-demo
$ cd dpp-node-zoau-demo

We then initialize our working directory as a new "npm package", and install the express and zoau node modules as dependencies:

$ npm init --yes
$ npm install express
$ npm install zoau

This will create a package.json file which describes our application, and contains information about our application's dependencies. 

We'll then create server.js, which leverages the express node module to set up a basic web server:

const path = require('path');
const express = require('express');

const routes = require('./routes');
const app = express();

const middlewares = [
  // Serves static .html, .js, .css file, etc.
  express.static(path.join(__dirname, 'public')),
  // Parse requests containing json.
  express.json(),
  // Parse requests containing urlencoded bodies.
  express.urlencoded({ extended: true }),
];
app.use(middlewares);

app.use('/', routes);

// Page Not Found handler.
app.use((req, res, next) => {
  res.status(404).json('Sorry, page not found!');
});

// Server Error handler.
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json(err.message);
});

// Using port 3000 for this example; can be changed to a port of your choice.
app.listen(3000, () => {
  console.log(`App running at http://localhost:3000`);
});

We'll also need to create a routes.js, where we will write the code for our web application API end points:

const express = require('express');
const router = express.Router();

const zoau = require('zoau');

module.exports = router;

To create an end point that can list all the members within a PDS, we'll add the following code to routes.js:

// List members of a PDS.
router.get('/api/:pds', (req, res, next) => {
  zoau.datasets.list_members(req.params.pds)
    .then(members => res.json(members))
    .catch(err => next(err));
});

This can be performed by sending a HTTP GET request to http://localhost:3000/api/<name of pds>.

To create an end point that can read the contents of a PDS member, we'll add the following code to routes.js:

// Get content of a member.
router.get('/api/:pds/:member', (req, res, next) => {
  zoau.datasets.read(`${req.params.pds}(${req.params.member})`)
    .then(content => res.json(content))
    .catch(err => next(err));
});

This can be performed by sending a HTTP GET request to http://localhost:3000/api/<name of pds>/<name of member>.

To create an end point that can write to a PDS member, we'll add the following code to routes.js:

// Write to the member of a PDS.
router.put('/api/:pds/:member', (req, res, next) => {
  let dataset = `${req.params.pds}(${req.params.member})`;

  let content = req.body.content;
  if (content === undefined || content === null) {
    return next(new Error('Content must be specified'));
  }

  // Correctly format newlines, whitespace, etc.
  content = content.replace(/(\r\n)/gm, "\n");
  content = content.replace(/\s+\n/g, '\n');
content = content.replace(/\\/g, "\\\\");

  // Write content to PDS member.
  zoau.datasets.write(dataset, content)
    .then(rc => res.json(rc))
    .catch(err => next(err));
});

This can be performed by sending a HTTP PUT request to http://localhost:3000/api/<name of pds>/<name of member> with the data to write to the member specified in the request body.

Finally, to build and run the source code contained within a PDS member, we'll add the following code to routes.js:

const util = require('util');
const exec = util.promisify(require('child_process').exec);

// Build and run the member of a PDS.
router.post('/api/:pds/:member', (req, res) => {
let dataset = `${req.params.pds}(${req.params.member})`;

  // Random binary name to prevent naming conflict.
  let binary = `demo_${Math.random().toString(36).substring(7)}`;

  // Compile and run.
  exec(`cd /tmp/ && xlc -+ -o ${binary} "//'${dataset}'" && ./${binary}`)
    .then(({ stdout, stderr }) => res.json(stdout + stderr))
    .catch(err => res.json(err.message));
});

This can be performed by sending a HTTP POST request to http://localhost:3000/api/<name of pds>/<name of member>.

Putting it all together, our routes.js now contains the following:

const util = require('util');
const exec = util.promisify(require('child_process').exec);
const express = require('express');
const router = express.Router();

const zoau = require('zoau');

// REST API
// List members of a PDS.
router.get('/api/:pds', (req, res, next) => {
  zoau.datasets.list_members(req.params.pds)
    .then(members => res.json(members))
    .catch(err => next(err));
});

// Get content of a member.
router.get('/api/:pds/:member', (req, res, next) => {
  zoau.datasets.read(`${req.params.pds}(${req.params.member})`)
    .then(content => res.json(content))
    .catch(err => next(err));
});

// Write to the member of a PDS.
router.put('/api/:pds/:member', (req, res, next) => {
  let dataset = `${req.params.pds}(${req.params.member})`;

  let content = req.body.content;
  if (content === undefined || content === null) {
    return next(new Error('Content must be specified'));
  }

  // Correctly format newlines, whitespace, etc.
  content = content.replace(/(\r\n)/gm, "\n");
  content = content.replace(/\s+\n/g, '\n');
  content = content.replace(/\\/g, "\\\\");

  // Write content to PDS member.
  zoau.datasets.write(dataset, content)
    .then(rc => res.json(rc))
    .catch(err => next(err));
});

// Build and run the member of a PDS.
router.post('/api/:pds/:member', (req, res) => {
  let dataset = `${req.params.pds}(${req.params.member})`;

  // Random binary name to prevent naming conflict.
  let binary = `demo_${Math.random().toString(36).substring(7)}`;

  // Compile and run.
  exec(`cd /tmp/ && xlc -+ -o ${binary} "//'${dataset}'" && ./${binary}`)
    .then(({ stdout, stderr }) => res.json(stdout + stderr))
    .catch(err => res.json(err.message));
});

module.exports = router;

Next, we will need a user interface that will make requests to the API end points. There are many approaches to creating a user interface. One possible approach is to use HTML and JavaScript to create a web UI that can be accessed at http://localhost:3000.

We'll need to create a directory for our web UI:

$ mkdir public
$ cd public

We'll create index.html which contains the HTML code for our web UI:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Managing files in a PDS using Node.js with zoau</title>
  <!-- CSS. -->
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-F3w7mX95PdgyTmZZMECAngseQB83DfGTowi0iMjiWaeVhAn4FJkqJByhZMI3AhiU" crossorigin="anonymous">
</head>
<body>

<div class="container-sm" style="max-width: 540px;">

<div class=" g-3 mb-3">
  <div class="row">
    <div class="col-12 text-center"><h2>Managing files in a PDS using Node.js with zoau</h2></div>
  </div>

  <div id="results" class="row" style="display: none;">
    <div class="col-12"><h4>Results:</h4></div>
    <div class="col-12">
      <pre id="output" style="border: 5px solid grey;"></pre>
    </div>
  </div>
</div>

<form class="row g-3 mb-3 bg-light border">
  <div class="col-12">
    <label for="pds" class="form-label">PDS</label>
    <input type="text" name="pds" id="pds" value="DPP.CSRC" class="form-control">
  </div>
  <div class="col-12 d-grid gap-2">
    <button type="button" onclick="listMembers()" class="btn btn-primary mb-3">List PDS Members</button>
  </div>
</form>

<form class="row g-3 mb-3 bg-light border">
  <div class="col-12">
    <label for="members" class="form-label">Members</label>
    <select name="members" id="members" class="form-select"></select>
  </div>
  <div class="col-12 d-grid gap-2">
    <button type="button" onclick="getContent()" class="btn btn-primary mb-3">Read Member Content</button>
  </div>
</form>

<form class="row g-3 mb-3 bg-light border">
  <div class="col-12">
    <label for="content" id="content-label" class="form-label">Content</label>
    <textarea name="content" id="content" rows="20" class="form-control"></textarea>
  </div>
  <div class="col-12 d-grid gap-2">
    <button type="button" onclick="writeContent()" class="btn btn-primary mb-3">Save</button>
    <button type="button" onclick="buildAndRun()" class="btn btn-success mb-3">Build and Run</button>
  </div>
</form>

</div>

<!-- Scripts. -->
<script src="script.js" type="text/javascript"></script>
</body>
</html>

We'll also create script.js which contains the JavaScript code to make requests to our API end points:

// List members of a PDS.
function listMembers() {
  var pds = document.getElementById('pds').value;

  fetch(`/api/${pds}`)
    .then(response => response.json())
    .then(members => {
      var select = document.getElementById('members');

      // Clear existing options.
      for (var i = select.options.length - 1; i >= 0; i--) {
        select.remove(i);
      }

      // Populate with new options.
      for (member of members){
        var option = document.createElement('option');
        option.value = member;
        option.innerHTML = member;
        select.appendChild(option);
      }
      printOutput('Success');
    })
    .catch(err => printOutput(err));
}

// Get content of a member.
function getContent() {
  var pds = document.getElementById('pds').value;
  var member = document.getElementById('members').value;

  fetch(`/api/${pds}/${member}`)
    .then(response => response.json())
    .then(content => {
      document.getElementById('content-label').innerText = `${pds}(${member})`;
      document.getElementById('content').value = content;
      printOutput('Success');
    })
    .catch(err => printOutput(err));
}

// Write to the member of a PDS.
function writeContent() {
  var pds = document.getElementById('pds').value;
  var member = document.getElementById('members').value;
  var content = document.getElementById('content').value;

  fetch(`/api/${pds}/${member}`, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ content })
  })
  .then(response => response.json())
  .then(rc => printOutput(`Return Code: ${rc}`))
  .catch(err => printOutput(err));
}

// Build and run the member of a PDS.
function buildAndRun() {
  var pds = document.getElementById('pds').value;
  var member = document.getElementById('members').value;

  fetch(`/api/${pds}/${member}`, {
    method: 'POST',
  })
  .then(response => response.json())
  .then(output => printOutput(output))
  .catch(err => printOutput(err));
}

// Show the #results div and print the output to the #output preformatted block.
function printOutput(output) {
  console.log(output);
  document.getElementById('results').style.display = 'initial';
  document.getElementById('output').innerText = output.toString();
  // Scroll to top of the screen.
  window.scrollTo(0, 0);
}

Finally, to run our web application, we'll use the following command:

$ node server.js

Congratulations, you have now created a web application that can manage files in a PDS using Node.js with zoau!