├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── README.md ├── package.json ├── src ├── index.ts └── test │ ├── index.test.ts │ └── workflows │ ├── signals-queries.ts │ └── timer.ts └── tsconfig.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | jobs: 8 | test: 9 | runs-on: ${{ matrix.os }} 10 | 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest] 14 | node: [16] 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Setup node 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: ${{ matrix.node }} 22 | - name: Install deps 23 | run: npm install 24 | - name: Build 25 | run: npm run build 26 | - run: npm test 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | lib -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | .scripts 3 | .github -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # temporal-rest 2 | 3 | Creates an [Express](http://expressjs.com/) middleware router that automatically exposes endpoints for [Temporal](https://temporal.io/) Workflows, Signals, and Queries. 4 | 5 | ## Usage 6 | 7 | Suppose you have some Temporal Workflows, Queries, and Signals in a `workflows.js` file: 8 | 9 | ```javascript 10 | 'use strict'; 11 | 12 | const wf = require('@temporalio/workflow'); 13 | 14 | exports.unblockSignal = wf.defineSignal('unblock'); 15 | exports.isBlockedQuery = wf.defineQuery('isBlocked'); 16 | 17 | exports.unblockOrCancel = async function unblockOrCancel() { 18 | let isBlocked = true; 19 | wf.setHandler(exports.unblockSignal, () => void (isBlocked = false)); 20 | wf.setHandler(exports.isBlockedQuery, () => isBlocked); 21 | console.log('Blocked'); 22 | try { 23 | await wf.condition(() => !isBlocked); 24 | console.log('Unblocked'); 25 | } catch (err) { 26 | if (err instanceof wf.CancelledFailure) { 27 | console.log('Cancelled'); 28 | } 29 | throw err; 30 | } 31 | } 32 | ``` 33 | 34 | Temporal-rest exports a function that returns an Express router with an endpoint for every Workflow, Signal, and Query. 35 | 36 | ```javascript 37 | const { WorkflowClient } = require('@temporalio/client'); 38 | const workflows = require('./workflows'); 39 | 40 | const createExpressMiddleware = require('temporal-rest'); 41 | const express = require('express'); 42 | 43 | const app = express(); 44 | 45 | // Router has the below endpoints: 46 | // - POST /workflow/unblockOrCancel 47 | // - PUT /signal/unblock 48 | // - GET /query/isBlocked 49 | const router = createExpressMiddleware(workflows, new WorkflowClient(), 'my-task-queue'); 50 | app.use(router); 51 | ``` 52 | 53 | Note that temporal-rest _only_ registers endpoints for exported Signals and Queries. 54 | If you want to register an endpoint for a Signal or Query, make sure you export it from `workflows.ts` / `workflows.js`: 55 | 56 | ```javascript 57 | // Temporal-rest will create a `PUT /signal/unblock/:workflowId` endpoint 58 | exports.unblockSignal = wf.defineSignal('unblock'); 59 | 60 | // Temporal-rest will NOT create a `PUT /signal/otherSignal/:workflowId` endpoint, 61 | // because this Signal isn't exported. 62 | const otherSignal = wf.defineSignal('otherSignal'); 63 | ``` 64 | 65 | temporal-rest adds the below endpoints for every exported Workflow: 66 | 67 | - `POST /workflow/`: create a new instance of the given Workflow. Use [uuid](https://npmjs.com/package/uuid) to generate the Workflow id 68 | - `POST /workflow//:workflowId`: create a new instance of the given Workflow with the given Workflow id 69 | 70 | temporal-rest adds the below endpoints for every exported Query: 71 | 72 | - `GET /query//:workflowId`: execute the Query with the given name against the given Workflow id 73 | 74 | temporal-rest adds the below endpoints for every exported Signal: 75 | 76 | - `PUT /signal//:workflowId`: execute the Signal with the given name against the given Workflow id 77 | 78 | ## Passing Arguments 79 | 80 | For Signals and Workflows, temporal-rest passes the HTTP request body as the first parameter to the Signal or Workflow. 81 | For example, suppose you have the below `workflows.js` file. 82 | 83 | ```javascript 84 | 'use strict'; 85 | 86 | const { defineSignal, defineQuery, setHandler, condition } = require('@temporalio/workflow'); 87 | 88 | exports.setDeadlineSignal = defineSignal('setDeadline'); 89 | exports.timeLeftQuery = defineQuery('timeLeft'); 90 | 91 | exports.countdownWorkflow = async function countdownWorkflow({ delay }) { 92 | delay = delay == null ? 1500 : delay; 93 | let deadline = Date.now() + delay; 94 | 95 | setHandler(exports.setDeadlineSignal, (data) => { 96 | // send in new deadlines via Signal 97 | deadline = data.deadline; 98 | }); 99 | setHandler(exports.timeLeftQuery, (data) => { 100 | if (data.seconds === 'true') { 101 | return Math.floor((deadline - Date.now()) / 1000); 102 | } 103 | return deadline - Date.now(); 104 | }); 105 | 106 | await condition(() => (deadline - Date.now()) < 0); 107 | } 108 | ``` 109 | 110 | To pass a `delay` argument to `countdownWorkflow()`, you should send a `POST /workflow/countdownWorkflow` request with `{"delay": 3000}` as the request body. 111 | Temporal-rest currently assumes the request body is JSON, and passes the parsed request body as the first argument to the Workflow. 112 | For example, you can use the below CURL command. 113 | 114 | ``` 115 | curl -X POST http://localhost:3000/workflow/countdownWorkflow -d '{"delay": 3000}' 116 | ``` 117 | 118 | Similarly, you can pass arguments to Signals. 119 | The below CURL command sets `deadline` to 3000 in `setDeadlineSignal`: 120 | 121 | ``` 122 | curl -X PUT http://localhost:3000/signal/setDeadline -d '{"deadline": 3000}' 123 | ``` 124 | 125 | For Queries, temporal-rest passes `req.query` as the first argument. 126 | For example, the below CURL command calls `timeLeftQuery({ seconds: 'true' })`: 127 | 128 | ``` 129 | curl http://localhost:3000/query/timeLeft?seconds=true 130 | ``` 131 | 132 | # For Development 133 | 134 | ## Running Tests 135 | 136 | 1. Make sure [Temporal server is running](https://github.com/temporalio/docker-compose) 137 | 2. Run `npm install` 138 | 3. Run `npm test` 139 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "temporal-rest", 3 | "version": "0.5.1", 4 | "private": false, 5 | "main": "lib/index.js", 6 | "bugs": { 7 | "url": "https://github.com/vkarpov15/temporal-rest/issues/new" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/vkarpov15/temporal-rest.git" 12 | }, 13 | "homepage": "https://github.com/vkarpov15/temporal-rest", 14 | "scripts": { 15 | "build": "tsc --build", 16 | "build.watch": "tsc --build --watch", 17 | "prepublishOnly": "npm run build", 18 | "test": "mocha lib/test/*.test.js" 19 | }, 20 | "dependencies": { 21 | "@types/uuid": "8.x", 22 | "uuid": "8.x" 23 | }, 24 | "peerDependencies": { 25 | "@types/express": "4.x", 26 | "express": "4.x", 27 | "temporalio": ">= 0.17.0 || 1.0.0-rc.0" 28 | }, 29 | "devDependencies": { 30 | "@temporalio/testing": "0.23.x", 31 | "@tsconfig/node16": "^1.0.0", 32 | "@types/express": "4.17.2", 33 | "@types/mocha": "9.1.0", 34 | "axios": "0.24.0", 35 | "express": "4.17.2", 36 | "mocha": "9.1.4", 37 | "sinon": "12.0.1", 38 | "temporalio": "1.0.0-rc.0", 39 | "typescript": "4.5.5" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { WorkflowClient } from '@temporalio/client'; 2 | import { QueryDefinition, SignalDefinition, Workflow } from '@temporalio/common'; 3 | import express, { Router } from 'express'; 4 | import { v4 } from 'uuid'; 5 | 6 | const signalValidators = new WeakMap(); 7 | 8 | export function useValidator(signal: SignalDefinition, fn: Function): void { 9 | signalValidators.set(signal, fn); 10 | } 11 | 12 | export function createExpressMiddleware(workflows: any, client: WorkflowClient, taskQueue: string, router?: Router) { 13 | if (router === undefined) { 14 | router = Router(); 15 | } 16 | 17 | for (const key of Object.keys(workflows)) { 18 | const value: any = workflows[key]; 19 | if (typeof value === 'function') { 20 | // Workflow 21 | createWorkflowEndpoint(router, client, key, value as Workflow, taskQueue); 22 | } else if (typeof value === 'object' && value != null) { 23 | if (value['type'] === 'signal') { 24 | // Signal 25 | createSignalEndpoint(router, client, value as SignalDefinition); 26 | } else if (value['type'] === 'query') { 27 | // Query 28 | createQueryEndpoint(router, client, value as QueryDefinition); 29 | } 30 | } 31 | } 32 | 33 | return router; 34 | } 35 | 36 | function createWorkflowEndpoint(router: Router, client: WorkflowClient, name: string, fn: Workflow, taskQueue: string) { 37 | router.post(`/workflow/${name}`, express.json(), function(req: express.Request, res: express.Response) { 38 | const workflowId = v4(); 39 | const opts = { 40 | taskQueue, 41 | workflowId, 42 | args: [req.body] 43 | }; 44 | client.start(fn, opts).then(() => res.json({ workflowId })); 45 | }); 46 | 47 | router.post(`/workflow/${name}/:workflowId`, express.json(), function(req: express.Request, res: express.Response) { 48 | const workflowId = req.params.workflowId; 49 | const opts = { 50 | taskQueue, 51 | workflowId, 52 | args: [req.body] 53 | }; 54 | client.start(fn, opts).then(() => res.json({ workflowId })); 55 | }); 56 | } 57 | 58 | function createSignalEndpoint(router: Router, client: WorkflowClient, signal: SignalDefinition) { 59 | router.put(`/signal/${signal.name}/:workflowId`, express.json(), function(req: express.Request, res: express.Response) { 60 | let data = req.body; 61 | 62 | let fn: Function | undefined = signalValidators.get(signal); 63 | if (fn != null) { 64 | data = fn(data); 65 | } 66 | 67 | const handle = client.getHandle(req.params.workflowId); 68 | handle.signal(signal, req.body). 69 | then(() => res.json({ received: true })). 70 | catch(err => res.status(500).json({ message: err.message })); 71 | }); 72 | } 73 | 74 | function createQueryEndpoint(router: Router, client: WorkflowClient, query: QueryDefinition) { 75 | router.get(`/query/${query.name}/:workflowId`, function(req, res) { 76 | const handle = client.getHandle(req.params.workflowId); 77 | 78 | handle.query(query, req.query). 79 | then(result => res.json({ result })). 80 | catch(err => res.status(500).json({ message: err.message })); 81 | }); 82 | } -------------------------------------------------------------------------------- /src/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { Runtime, DefaultLogger, Worker } from '@temporalio/worker'; 2 | import { Server } from 'http'; 3 | import { TestWorkflowEnvironment } from '@temporalio/testing'; 4 | import { WorkflowClient } from '@temporalio/client'; 5 | import assert from 'assert'; 6 | import axios, { Axios } from 'axios'; 7 | import { before, describe, it } from 'mocha'; 8 | import { createExpressMiddleware } from '../'; 9 | import express from 'express'; 10 | 11 | import * as signalsQueries from './workflows/signals-queries'; 12 | import * as timer from './workflows/timer'; 13 | 14 | describe('createExpressMiddleware', function() { 15 | let server: Server; 16 | let worker: Worker; 17 | let client: WorkflowClient; 18 | let apiClient: Axios; 19 | let runPromise: Promise; 20 | let workflows: any; 21 | let env: TestWorkflowEnvironment; 22 | const taskQueue = 'temporal-rest-test'; 23 | 24 | describe('using signals-queries', function() { 25 | before(async function() { 26 | this.timeout(10000); 27 | workflows = signalsQueries; 28 | 29 | // Suppress default log output to avoid logger polluting test output 30 | Runtime.install({ logger: new DefaultLogger('WARN') }); 31 | 32 | env = await TestWorkflowEnvironment.create(); 33 | 34 | worker = await Worker.create({ 35 | connection: env.nativeConnection, 36 | workflowsPath: require.resolve('./workflows/signals-queries'), 37 | taskQueue 38 | }); 39 | 40 | runPromise = worker.run(); 41 | 42 | client = env.workflowClient; 43 | 44 | const app = express(); 45 | app.use(createExpressMiddleware(workflows, client, taskQueue)); 46 | server = app.listen(3001); 47 | 48 | apiClient = axios.create({ baseURL: 'http://localhost:3001' }); 49 | }); 50 | 51 | after(async function() { 52 | worker.shutdown(); 53 | await runPromise; 54 | await server.close(); 55 | 56 | await env.nativeConnection.close(); 57 | await env.teardown(); 58 | }); 59 | 60 | it('allows creating workflows', async function() { 61 | const res = await apiClient.post('/workflow/unblockOrCancel'); 62 | assert.ok(res.data.workflowId); 63 | 64 | const handle = await client.getHandle(res.data.workflowId); 65 | const isBlocked = await handle.query(workflows.isBlockedQuery); 66 | assert.strictEqual(isBlocked, true); 67 | }); 68 | 69 | it('can query and signal the workflow', async function() { 70 | let res = await apiClient.post('/workflow/unblockOrCancel'); 71 | const { workflowId } = res.data; 72 | 73 | res = await apiClient.get(`/query/isBlocked/${workflowId}`); 74 | assert.ok(res.data.result); 75 | 76 | await apiClient.put(`/signal/unblock/${workflowId}`); 77 | 78 | res = await apiClient.get(`/query/isBlocked/${workflowId}`); 79 | assert.strictEqual(res.data.result, false); 80 | }); 81 | }); 82 | 83 | describe('using custom router', function() { 84 | let router: express.Router; 85 | let app: express.Application; 86 | 87 | before(async function() { 88 | this.timeout(10000); 89 | workflows = signalsQueries; 90 | 91 | // Suppress default log output to avoid logger polluting test output 92 | Runtime.install({ logger: new DefaultLogger('WARN') }); 93 | 94 | env = await TestWorkflowEnvironment.create(); 95 | 96 | worker = await Worker.create({ 97 | connection: env.nativeConnection, 98 | workflowsPath: require.resolve('./workflows/timer'), 99 | taskQueue 100 | }); 101 | 102 | runPromise = worker.run(); 103 | 104 | client = env.workflowClient; 105 | 106 | apiClient = axios.create({ baseURL: 'http://localhost:3001' }); 107 | }); 108 | 109 | after(async function() { 110 | worker.shutdown(); 111 | await runPromise; 112 | await server.close(); 113 | 114 | await env.nativeConnection.close(); 115 | await env.teardown(); 116 | }); 117 | 118 | it('allows registering middleware', async function() { 119 | this.timeout(10000); 120 | 121 | app = express(); 122 | let count = 0; 123 | router = express.Router(); 124 | router.use('/workflow/unblockOrCancel', (_req, _res, next) => { 125 | ++count; 126 | next(); 127 | }); 128 | 129 | app.use(createExpressMiddleware(workflows, client, taskQueue, router)); 130 | server = await app.listen(3001); 131 | 132 | await apiClient.post('/workflow/unblockOrCancel'); 133 | assert.strictEqual(count, 1); 134 | }); 135 | }); 136 | 137 | describe('using timer', function() { 138 | before(async function() { 139 | this.timeout(10000); 140 | workflows = timer; 141 | 142 | // Suppress default log output to avoid logger polluting test output 143 | Runtime.install({ logger: new DefaultLogger('WARN') }); 144 | 145 | env = await TestWorkflowEnvironment.create(); 146 | 147 | worker = await Worker.create({ 148 | connection: env.nativeConnection, 149 | workflowsPath: require.resolve('./workflows/timer'), 150 | taskQueue 151 | }); 152 | 153 | runPromise = worker.run(); 154 | 155 | client = env.workflowClient; 156 | 157 | const app = express(); 158 | app.use(createExpressMiddleware(workflows, client, taskQueue)); 159 | server = app.listen(3001); 160 | 161 | apiClient = axios.create({ baseURL: 'http://localhost:3001' }); 162 | }); 163 | 164 | after(async function() { 165 | worker.shutdown(); 166 | await runPromise; 167 | await server.close(); 168 | 169 | await env.nativeConnection.close(); 170 | await env.teardown(); 171 | }); 172 | 173 | it('can pass args to signals in request body', async function() { 174 | let res = await apiClient.post('/workflow/countdownWorkflow'); 175 | const { workflowId } = res.data; 176 | 177 | assert.ok(workflowId); 178 | 179 | res = await apiClient.get(`/query/timeLeft/${workflowId}`); 180 | assert.ok(res.data.result >= 1000 && res.data.result <= 1500, res.data.result); 181 | 182 | res = await apiClient.put(`/signal/setDeadline/${workflowId}`, { deadline: Date.now() + 3000 }); 183 | assert.ok(res.data.received); 184 | 185 | res = await apiClient.get(`/query/timeLeft/${workflowId}`); 186 | assert.equal(typeof res.data.result, 'number'); 187 | assert.ok(res.data.result >= 1500 && res.data.result <= 3000, res.data.result); 188 | }); 189 | 190 | it('can create workflow with a custom id', async function() { 191 | const customWorkflowId = 'test' + Date.now(); 192 | let res = await apiClient.post('/workflow/countdownWorkflow/' + customWorkflowId); 193 | const { workflowId } = res.data; 194 | 195 | assert.equal(workflowId, customWorkflowId); 196 | }); 197 | 198 | it('can pass args to workflows in request body', async function() { 199 | let res = await apiClient.post('/workflow/countdownWorkflow', { delay: 3000 }); 200 | const { workflowId } = res.data; 201 | 202 | assert.ok(workflowId); 203 | 204 | res = await apiClient.get(`/query/timeLeft/${workflowId}`); 205 | assert.ok(res.data.result >= 2500 && res.data.result <= 3000); 206 | }); 207 | 208 | it('can pass args to queries in query string', async function() { 209 | let res = await apiClient.post('/workflow/countdownWorkflow'); 210 | const { workflowId } = res.data; 211 | 212 | assert.ok(workflowId); 213 | 214 | res = await apiClient.get(`/query/timeLeft/${workflowId}?seconds=true`); 215 | assert.strictEqual(res.data.result, 1); 216 | }); 217 | }); 218 | }); -------------------------------------------------------------------------------- /src/test/workflows/signals-queries.ts: -------------------------------------------------------------------------------- 1 | import * as wf from '@temporalio/workflow'; 2 | 3 | exports.unblockSignal = wf.defineSignal('unblock'); 4 | exports.isBlockedQuery = wf.defineQuery('isBlocked'); 5 | 6 | exports.unblockOrCancel = async function unblockOrCancel() { 7 | let isBlocked = true; 8 | wf.setHandler(exports.unblockSignal, () => void (isBlocked = false)); 9 | wf.setHandler(exports.isBlockedQuery, () => isBlocked); 10 | console.log('Blocked'); 11 | try { 12 | await wf.condition(() => !isBlocked); 13 | console.log('Unblocked'); 14 | } catch (err) { 15 | if (err instanceof wf.CancelledFailure) { 16 | console.log('Cancelled'); 17 | } 18 | throw err; 19 | } 20 | } -------------------------------------------------------------------------------- /src/test/workflows/timer.ts: -------------------------------------------------------------------------------- 1 | import { defineSignal, defineQuery, setHandler, condition } from '@temporalio/workflow'; 2 | 3 | exports.setDeadlineSignal = defineSignal('setDeadline'); 4 | exports.timeLeftQuery = defineQuery('timeLeft'); 5 | 6 | exports.countdownWorkflow = async function countdownWorkflow({ delay }: { delay: number }) { 7 | delay = delay == null ? 1500 : delay; 8 | let deadline = Date.now() + delay; 9 | 10 | setHandler(exports.setDeadlineSignal, (data) => { 11 | // send in new deadlines via Signal 12 | deadline = data.deadline; 13 | }); 14 | setHandler(exports.timeLeftQuery, (data) => { 15 | if (data.seconds) { 16 | return Math.floor((deadline - Date.now()) / 1000); 17 | } 18 | return deadline - Date.now(); 19 | }); 20 | 21 | await condition(() => (deadline - Date.now()) < 0); 22 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node16/tsconfig.json", 3 | "version": "4.4.2", 4 | "compilerOptions": { 5 | "target": "es2020", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "esModuleInterop": true, 9 | "moduleResolution": "node", 10 | "sourceMap": true, 11 | "rootDir": "./src", 12 | "outDir": "./lib" 13 | }, 14 | "include": ["src/**/*.ts"], 15 | "exclude": ["node_modules"] 16 | } 17 | --------------------------------------------------------------------------------