diff --git a/README.md b/README.md
index e50f5c1..eefea28 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,7 @@ An official Learnosity open-source project.
* [Requirements](#requirements)
* [Installation](#installation)
* [Quick start guide](#quick-start-guide)
+* [Data API Routing Layer](#data-api-routing-layer)
* [Next steps: additional documentation](#next-steps-additional-documentation)
* [Contributing to this project](#contributing-to-this-project)
* [License](#license)
@@ -29,7 +30,7 @@ The Learnosity Node.js SDK makes it simple to interact with Learnosity APIs.
It provides a number of convenience features for developers, that make it simple to do the following essential tasks:
* Creating signed security requests for API initialization, and
-* Interacting with the Data API.
+* Interacting with the Data API (now includes a routing layer for making HTTP requests).
For example, the SDK helps with creating a signed request for Learnosity:
@@ -133,20 +134,20 @@ Let's take a look at a simple example of the SDK in action. In this example, we'
### **Start up your web server and view the standalone assessment example**
To start up your Node.js web server, first find the following folder location under the SDK. Change directory ('cd') to this location on the command line.
- .../learnosity-sdk-nodejs/docs/quickstart/assessment/
+ .../learnosity-sdk-nodejs/docs/quickstart/
To start, run this command from that folder:
```
-npm run start-standalone-assessment
+npm start
```
-From this point on, we'll assume that your web server is available at this local address (it will report the port being used when you launch it, by default it is port 3000):
+From this point on, we'll assume that your web server is available at this local address (it will report the port being used when you launch it, by default it is port 8000):
-http://localhost:3000/
+http://localhost:8000/
-When you open this URL with your browser, the page will load. This is a basic example of an assessment loaded into a web page with Learnosity's assessment player. You can interact with this demo assessment to try out the various Question types.
+When you open this URL with your browser, you'll see a menu of interactive examples demonstrating different Learnosity APIs. Click on "Items API" to view the standalone assessment example, which loads an assessment into a web page with Learnosity's assessment player. You can interact with this demo assessment to try out the various Question types.
@@ -292,6 +293,51 @@ Take a look at some more in-depth options and tutorials on using Learnosity asse
[(Back to top)](#table-of-contents)
+## Data API Routing Layer
+
+The SDK now includes a routing layer for the Data API, making it easy to interact with Learnosity's Data API without manually handling HTTP requests.
+
+### Quick Example
+
+```javascript
+const DataApi = require('learnosity-sdk-nodejs/lib/DataApi');
+
+const dataApi = new DataApi({
+ consumerKey: 'your_consumer_key',
+ consumerSecret: 'your_consumer_secret',
+ domain: 'yourdomain.com'
+});
+
+// Make a request
+const response = await dataApi.request(
+ 'https://data.learnosity.com/v2023.1.LTS/itembank/items',
+ { consumer_key: 'xxx', domain: 'example.com' },
+ 'secret',
+ { limit: 10 },
+ 'get'
+);
+
+const data = await response.json();
+console.log(data);
+
+// Or iterate through all pages automatically
+for await (const page of dataApi.requestIter(endpoint, security, secret, request, 'get')) {
+ console.log(`Page has ${page.data.length} items`);
+}
+```
+
+### Features
+
+- ✅ Automatic request signing
+- ✅ Built-in HTTP client
+- ✅ Pagination support with iterators
+- ✅ Routing metadata headers
+- ✅ Custom HTTP adapter support
+
+See the [Data API documentation](docs/DataApi.md) for complete details and examples.
+
+[(Back to top)](#table-of-contents)
+
## Next steps: additional documentation
### **SDK reference**
diff --git a/docs/DataApi.md b/docs/DataApi.md
new file mode 100644
index 0000000..e6fe565
--- /dev/null
+++ b/docs/DataApi.md
@@ -0,0 +1,192 @@
+# Data API Routing Layer
+
+The Node.js SDK now includes a routing layer for the Data API, making it easier to interact with Learnosity's Data API without manually handling HTTP requests and signatures.
+
+## Features
+
+- ✅ **Automatic request signing** - No need to manually sign requests
+- ✅ **Built-in HTTP client** - Makes actual HTTP requests to Data API
+- ✅ **Pagination support** - Automatically handles paginated responses
+- ✅ **Iterator methods** - Easy iteration through pages and individual results
+- ✅ **Routing metadata** - Automatically adds ALB routing headers
+- ✅ **Custom HTTP adapter** - Use your own HTTP library (axios, node-fetch, etc.)
+
+## Installation
+
+The DataApi class is included with the SDK:
+
+```javascript
+const LearnositySDK = require('learnosity-sdk-nodejs');
+const DataApi = LearnositySDK.DataApi;
+```
+
+Or import directly:
+
+```javascript
+const DataApi = require('learnosity-sdk-nodejs/lib/DataApi');
+```
+
+## Basic Usage
+
+### Simple Request
+
+```javascript
+const DataApi = require('learnosity-sdk-nodejs/lib/DataApi');
+
+const dataApi = new DataApi({
+ consumerKey: 'your_consumer_key',
+ consumerSecret: 'your_consumer_secret',
+ domain: 'yourdomain.com'
+});
+
+// Make a request
+const response = await dataApi.request(
+ 'https://data.learnosity.com/v2023.1.LTS/itembank/items',
+ {
+ consumer_key: 'your_consumer_key',
+ domain: 'yourdomain.com'
+ },
+ 'your_consumer_secret',
+ {
+ limit: 10,
+ references: ['item_1', 'item_2']
+ },
+ 'get'
+);
+
+const data = await response.json();
+console.log(data);
+```
+
+### Paginated Requests
+
+Automatically iterate through all pages:
+
+```javascript
+for await (const page of dataApi.requestIter(
+ 'https://data.learnosity.com/v2023.1.LTS/itembank/items',
+ { consumer_key: 'xxx', domain: 'example.com' },
+ 'secret',
+ { limit: 100 },
+ 'get'
+)) {
+ console.log(`Page has ${page.data.length} items`);
+ console.log(`Total records: ${page.meta.records}`);
+}
+```
+
+### Individual Results Iterator
+
+Iterate through individual results across all pages:
+
+```javascript
+for await (const item of dataApi.resultsIter(
+ 'https://data.learnosity.com/v2023.1.LTS/itembank/items',
+ { consumer_key: 'xxx', domain: 'example.com' },
+ 'secret',
+ { limit: 100 },
+ 'get'
+)) {
+ console.log(`Item: ${item.reference}`);
+}
+```
+
+## API Reference
+
+### Constructor
+
+```javascript
+new DataApi(options)
+```
+
+**Options:**
+- `consumerKey` (string, optional) - Your Learnosity consumer key
+- `consumerSecret` (string, optional) - Your Learnosity consumer secret
+- `domain` (string, optional) - Your domain for security packet
+- `httpAdapter` (function, optional) - Custom HTTP adapter function
+
+### Methods
+
+#### `request(endpoint, securityPacket, secret, requestPacket, action)`
+
+Makes a single HTTP request to the Data API.
+
+**Parameters:**
+- `endpoint` (string) - Full URL to the Data API endpoint
+- `securityPacket` (object) - Security object with `consumer_key` and `domain`
+- `secret` (string) - Consumer secret for signing
+- `requestPacket` (object, optional) - Request parameters
+- `action` (string, optional) - Action type: `'get'`, `'set'`, `'update'`, `'delete'` (default: `'get'`)
+
+**Returns:** Promise
+
+#### `requestIter(endpoint, securityPacket, secret, requestPacket, action)`
+
+Async generator that yields pages of results, automatically handling pagination.
+
+**Parameters:** Same as `request()`
+
+**Yields:** Page objects with `meta` and `data` properties
+
+#### `resultsIter(endpoint, securityPacket, secret, requestPacket, action)`
+
+Async generator that yields individual results from the `data` array, automatically handling pagination.
+
+**Parameters:** Same as `request()`
+
+**Yields:** Individual result objects
+
+## Advanced Usage
+
+### Custom HTTP Adapter
+
+Use your own HTTP library (e.g., axios):
+
+```javascript
+const axios = require('axios');
+
+const dataApi = new DataApi({
+ consumerKey: 'xxx',
+ consumerSecret: 'secret',
+ domain: 'example.com',
+ httpAdapter: async (url, options) => {
+ const response = await axios({
+ method: options.method,
+ url: url,
+ headers: options.headers,
+ data: options.body
+ });
+
+ return {
+ ok: response.status >= 200 && response.status < 300,
+ status: response.status,
+ statusText: response.statusText,
+ json: async () => response.data,
+ text: async () => JSON.stringify(response.data)
+ };
+ }
+});
+```
+
+## Routing Metadata
+
+The DataApi automatically adds routing metadata headers to all requests:
+
+- `X-Learnosity-Consumer` - Consumer key
+- `X-Learnosity-Action` - Derived action (e.g., `get_/itembank/items`)
+- `X-Learnosity-SDK` - SDK version (e.g., `Node.js:0.6.2`)
+
+These headers are used by Learnosity's Application Load Balancer for routing.
+
+## Examples
+
+See the [examples/data-api-example.js](../examples/data-api-example.js) file for complete working examples.
+
+## Comparison with Python SDK
+
+This implementation mirrors the Python SDK's DataApi class, providing:
+
+- `request()` - Single request (Python: `request()`)
+- `requestIter()` - Page iterator (Python: `request_iter()`)
+- `resultsIter()` - Results iterator (Python: `results_iter()`)
+
diff --git a/docs/quickstart/index.js b/docs/quickstart/index.js
new file mode 100644
index 0000000..f521bc4
--- /dev/null
+++ b/docs/quickstart/index.js
@@ -0,0 +1,313 @@
+// Copyright (c) 2024 Learnosity, Apache 2.0 License
+//
+// Unified quickstart server with all API examples
+'use strict';
+
+const Learnosity = require('../../index');
+const DataApi = require('../../lib/DataApi');
+const config = require('./config');
+const uuid = require('uuid');
+const express = require('express');
+const packageJson = require('../../package.json');
+const app = express();
+const port = 8000;
+const domain = 'localhost';
+
+app.set('view engine', 'ejs');
+
+// Serve static CSS files
+app.use('/css', express.static('./css'));
+
+// Home page - list all examples
+app.get('/', function (req, res) {
+ res.render('index', {
+ name: 'Learnosity SDK Quickstart Examples'
+ });
+});
+
+// Items API - Standalone Assessment
+app.get('/itemsapi', function (req, res) {
+ const learnositySdk = new Learnosity();
+ const user_id = uuid.v4();
+ const session_id = uuid.v4();
+
+ const request = learnositySdk.init(
+ 'items',
+ {
+ consumer_key: config.consumerKey,
+ domain: domain
+ },
+ config.consumerSecret,
+ {
+ user_id: user_id,
+ activity_template_id: 'quickstart_examples_activity_template_001',
+ session_id: session_id,
+ activity_id: 'quickstart_examples_activity_001',
+ rendering_type: 'assess',
+ type: 'submit_practice',
+ name: 'Items API Quickstart',
+ state: 'initial'
+ }
+ );
+
+ res.render('standalone-assessment', { request });
+});
+
+// Questions API
+app.get('/questionsapi', function (req, res) {
+ const learnositySdk = new Learnosity();
+
+ const request = learnositySdk.init(
+ 'questions',
+ {
+ consumer_key: config.consumerKey,
+ domain: domain
+ },
+ config.consumerSecret,
+ {
+ type: 'local_practice',
+ state: 'initial',
+ questions: [
+ {
+ response_id: '60005',
+ type: 'association',
+ stimulus: 'Match the cities to the parent nation.',
+ stimulus_list: ['London', 'Dublin', 'Paris', 'Sydney'],
+ possible_responses: ['Australia', 'France', 'Ireland', 'England'],
+ validation: {
+ valid_responses: [
+ ['England'], ['Ireland'], ['France'], ['Australia']
+ ]
+ }
+ }
+ ]
+ }
+ );
+
+ res.render('questions', { request });
+});
+
+// Author API - Item List
+app.get('/authorapi', function (req, res) {
+ const learnositySdk = new Learnosity();
+
+ const request = learnositySdk.init(
+ 'author',
+ {
+ consumer_key: config.consumerKey,
+ domain: domain
+ },
+ config.consumerSecret,
+ {
+ mode: 'item_list',
+ config: {
+ item_list: {
+ item: {
+ status: true
+ }
+ }
+ },
+ user: {
+ id: 'demos-site',
+ firstname: 'Demos',
+ lastname: 'User',
+ email: 'demos@learnosity.com'
+ }
+ }
+ );
+
+ res.render('item-list', { request });
+});
+
+// Reports API
+app.get('/reportsapi', function (req, res) {
+ const learnositySdk = new Learnosity();
+
+ const request = learnositySdk.init(
+ 'reports',
+ {
+ consumer_key: config.consumerKey,
+ domain: domain
+ },
+ config.consumerSecret,
+ {
+ reports: [
+ {
+ id: 'report-1',
+ type: 'sessions-summary',
+ user_id: '$ANONYMIZED_USER_ID'
+ }
+ ]
+ }
+ );
+
+ res.render('reports', { request });
+});
+
+// Author Aide API
+app.get('/authoraide', function (req, res) {
+ const learnositySdk = new Learnosity();
+
+ const request = learnositySdk.init(
+ 'authoraide',
+ {
+ consumer_key: config.consumerKey,
+ domain: domain
+ },
+ config.consumerSecret,
+ {
+ user: {
+ id: 'demos-site',
+ firstname: 'Demos',
+ lastname: 'User',
+ email: 'demos@learnosity.com'
+ }
+ }
+ );
+
+ res.render('main', { request });
+});
+
+// Data API
+app.get('/dataapi', async function (req, res) {
+ const itembank_uri = 'https://data.learnosity.com/latest-lts/itembank/items';
+ const security_packet = {
+ consumer_key: config.consumerKey,
+ domain: domain
+ };
+
+ const dataApi = new DataApi({
+ consumerKey: config.consumerKey,
+ consumerSecret: config.consumerSecret,
+ domain: domain
+ });
+
+ // Initialize request metadata (will be populated from first successful request)
+ const sdk_version = packageJson.version;
+ const request_metadata = {
+ endpoint: itembank_uri,
+ action: 'get',
+ statusCode: null,
+ headers: {
+ 'X-Learnosity-Consumer': dataApi._extractConsumer(security_packet),
+ 'X-Learnosity-Action': dataApi._deriveAction(itembank_uri, 'get'),
+ 'X-Learnosity-SDK': `Node.js:${sdk_version}`
+ }
+ };
+
+ // Demo 1: Manual iteration (5 items)
+ const demo1_output = [];
+ let demo1_error = null;
+
+ try {
+ let data_request = { limit: 1 };
+
+ for (let i = 0; i < 5; i++) {
+ const result = await dataApi.request(itembank_uri, security_packet,
+ config.consumerSecret, data_request, 'get');
+
+ // Capture status code from the first request
+ if (i === 0 && result) {
+ request_metadata.statusCode = result.status;
+ }
+
+ const response = await result.json();
+
+ if (response.data && response.data.length > 0) {
+ const item = response.data[0];
+
+ demo1_output.push({
+ number: i + 1,
+ reference: item.reference || 'N/A',
+ status: item.status || 'N/A'
+ });
+ }
+
+ if (response.meta && response.meta.next) {
+ data_request = { next: response.meta.next };
+ } else {
+ break;
+ }
+ }
+ } catch (error) {
+ demo1_error = error.message;
+ }
+
+ // Demo 2: Page iteration (5 pages)
+ const demo2_output = [];
+ let demo2_error = null;
+
+ try {
+ const data_request = { limit: 1 };
+ let page_count = 0;
+
+ for await (const page of dataApi.requestIter(itembank_uri, security_packet,
+ config.consumerSecret, data_request, 'get')) {
+ page_count++;
+ const pageData = {
+ pageNumber: page_count,
+ itemCount: page.data ? page.data.length : 0,
+ items: []
+ };
+
+ if (page.data) {
+ for (const item of page.data) {
+ pageData.items.push({
+ reference: item.reference || 'N/A',
+ status: item.status || 'N/A'
+ });
+ }
+ }
+
+ demo2_output.push(pageData);
+
+ if (page_count >= 5) {
+ break;
+ }
+ }
+ } catch (error) {
+ demo2_error = error.message;
+ }
+
+ // Demo 3: Results iteration (5 items)
+ const demo3_output = [];
+ let demo3_error = null;
+
+ try {
+ const data_request = { limit: 1 };
+ let result_count = 0;
+
+ for await (const item of dataApi.resultsIter(itembank_uri, security_packet,
+ config.consumerSecret, data_request, 'get')) {
+ result_count++;
+ demo3_output.push({
+ number: result_count,
+ reference: item.reference || 'N/A',
+ status: item.status || 'N/A',
+ json: JSON.stringify(item, null, 2).substring(0, 500)
+ });
+
+ if (result_count >= 5) {
+ break;
+ }
+ }
+ } catch (error) {
+ demo3_error = error.message;
+ }
+
+ res.render('data-api', {
+ name: 'Data API Example - With Metadata Headers',
+ request_metadata: request_metadata,
+ demo1_output: demo1_output,
+ demo1_error: demo1_error,
+ demo2_output: demo2_output,
+ demo2_error: demo2_error,
+ demo3_output: demo3_output,
+ demo3_error: demo3_error
+ });
+});
+
+app.listen(port, function () {
+ console.log(`Server started http://${domain}:${port}. Press Ctrl-c to quit.`);
+});
+
diff --git a/docs/quickstart/package.json b/docs/quickstart/package.json
index 6ecbd9b..15482fe 100644
--- a/docs/quickstart/package.json
+++ b/docs/quickstart/package.json
@@ -10,11 +10,7 @@
},
"devDependencies": {},
"scripts": {
- "start-standalone-assessment": "npm install && node ./assessment/standalone-assessment.js",
- "start-reports": "npm install && node ./analytics/reports.js",
- "start-item-list": "npm install && node ./authoring/item-list.js",
- "start-questions": "npm install && node ./questions/questions.js",
- "start-authoraide": "npm install && node ./authoraide/main.js"
+ "start": "npm install && node ./index.js"
},
"author": "",
"license": "ISC"
diff --git a/docs/quickstart/views/data-api.ejs b/docs/quickstart/views/data-api.ejs
new file mode 100644
index 0000000..ff1309b
--- /dev/null
+++ b/docs/quickstart/views/data-api.ejs
@@ -0,0 +1,266 @@
+
+
+
+
+
+
+
+
<%= name %>
+
+
+ Note: This example demonstrates the Data API with automatic metadata headers. Every request includes consumer, action, and SDK language-version information.
+
+
+ <% if (request_metadata) { %>
+
+
API Responses
+
+
Request Information
+
+
+
Endpoint
+
<%= request_metadata.endpoint %>
+
+
+
Action
+
<%= request_metadata.action %>
+
+
+
Status Code
+
<%= request_metadata.statusCode || 'N/A' %>
+
+
+
+
Metadata Headers (Sent Automatically)
+
+ These headers are added automatically by the SDK and are invisible to customers:
+