# DocuWorld 2019 SDK Workshop - Code Guide Dominik Peukert - Director Research & Development - DocuWare GmbH sdk@docuware.com ## Content description of projects The workshop was held with DocuWare Version 7.1. Both services also compatible with older versions except basic authentication part, this is only available since 7.1. You can start a new project or download the project skeleton here: [http://go.docuware.com/docuworldsdk2019](http://go.docuware.com/docuworldsdk2019) ### Node.js Valdiation Service - REST This project will be a simple example how to work with [DocuWare Validation](https://developer.docuware.com/Extension_Services/extension_services.html#document-validation). The service will contain a POST method for validating if the a single invoice is already available in an external data source. ### Node.js Enhancement Service - SOAP This project contains the logic of enhancing the external datasource with the DWDocid of a stored DocuWare document. This will be done by a Workflow containing an [Web Serice Activity](https://developer.docuware.com/Extension_Services/extension_services.html#workflow-manager-web-service-activity). ### DocuWare System Layout - A file cabinet with following layout is needed: | Name | Type | | ------------- | ------------- | | InvoiceNumber | Number | | Company | Text | | Amount | Decimal | | Comment | Text | - Variables in Workflow: | Name | Type | | ------------- | ------------- | | DWDocid | Text | | InvoiceNumber | Number | | Result | Text | | Result_Message | Text | Also don't forget to assign DWDocid and Invoice number in the beginning of your DocuWare Workflow. The 2 result variables has to be filled by the Workflow Web Service Activity. ## Base setup (IFNEWPROJECT) 1. Go to project folder and choose _{ProjectFolder}_ ``` -FakeDatabase -REST_ValidationService -SOAP_EnhancmentService ``` 2. ```"npm init"``` 1. Description: ```"DocuWorld Sample: {ProjectName}"``` 2. Main: ```"index.js"``` 3. Add ```"private":true``` 4. Add ```"start": "node ./dist/index.js",``` to ```"scripts"``` section 3. Add typescript: ```"npm i typescript"``` (local installation) 4. Add typescript support: ```"npm i -D typescript @types/node"``` 5. Init tsconfig.json: ```"npx tsc --init"``` (Package runner - available since npm 5.2.0) 1. Add ```"outDir" :"dist"``` and ```"sourceMap" : "true"``` 6. Add ```"index.ts"```: ```ts export function hello(): string{ return 'Hello World!'; } hello(); ``` 7. Enable file watcher: ```"npx tsc -w"``` _Alternativly you can use "strg + shift + B" in VS Code to choose tsconfig_ ## Validation Service #### Snapshot 0: REST Initial State 1. (IFNEWPROJECT) Get express for building up service: ```"npm i express --save"``` (--save sets it as dependency to the package.json) 2. (IFNEWPROJECT) To start the application use ```"npm start" ``` (Firewall YES!) 3. Add typescript support for express: ```"npm i -D typescript @types/express"``` 4. Init the express in ```"index.ts"```: ```ts //express parts import express from "express"; import bodyParser from 'body-parser'; const app = express(); const port = 1337; //enable json bodyparsing app.use(bodyParser.json()); //start server listening app.listen(port, () => { console.log(`Server is running on port: ${port}`); }); ``` #### Snapshot 1: REST Express Base Ready 5. Add GET request for returning dummy first: ```ts //define GET for testing http://localhost:1337/status app.get("/status", (req: any, res: any, next: any) => { res.json({"status":"Up and running"}); }); ``` 6. Try with http://localhost:1337/status you'll get ```json {"status":"Service is up and running!"} ``` #### Snapshot 2: REST Express Test Ready ### Prepare the dummy JSON database 1. (IFNEWPROJECT) Go to root directory ``` -FakeDatabase --db.json -REST_ValidationService -SOAP_EnhancmentService ``` and create the _db.json_: ```json { "invoices": [ { "InvoiceNumber": 1, "Company": "John Doe Inc", "Amount": 30, "DWDOCID": null, "ValidtionSuccess": false, "EnhancementSuccess": false }, { "InvoiceNumber": 2, "Company": "Peters Engineering", "Amount": 5, "DWDOCID": null, "ValidtionSuccess": false, "EnhancementSuccess": false }, { "InvoiceNumber": 3, "Company": "DocuWorld EMEA GmbH", "Amount": 1337, "DWDOCID": null, "ValidtionSuccess": false, "EnhancementSuccess": false } ] } ``` 1. (IFNEWPROJECT) Install lowdb: ```"npm i lowdb --save"``` (https://github.com/typicode/lowdb) 2. (IFNEWPROJECT) Add typescript support for lowdb: ```"npm i -D typescript @types/lowdb"``` 3. (IFNEWPROJECT) Init lowdb: ```ts //lowdb parts import low from 'lowdb'; import FileAsync from 'lowdb/adapters/FileAsync'; let lowdbFileAdapter: AdapterAsync = new FileAsync("../FakeDatabase/db.json"); ``` 4. Start db and refactor express: ```ts //Create database instance lowDb(lowdbFileAdapter) .then((db: LowdbAsync) => { //define GET for status http://localhost:1337/status app.get("/status", (req: any, res: any, next: any) => { res.json({"status":"Service is up and running!"}); }); }) .then(() => { //start server listening app.listen(port, () => { console.log(`Server is running on port: ${port}`); }); }) ``` #### Snapshot 3: REST LowDb Base Ready ### Prepare for Validation DocuWare Validation Schemas available here: https://github.com/DocuWare/Validation-NodeJS-samples 1. (IFNEWPROJECT) Add typescript support for json-schema: ``` "npm i -D typescript @types/json-schema" ``` 2. Create class and method to build validation response json, see ```DocuWareValidationContract.d.ts```: ```ts function CreateValidationFeedback(type: DocuWareValidation.FeedbackType, reason: string): DocuWareValidation.IValidationFeedback { return new DWValidationFeedback(type, reason); } class DWValidationFeedback implements DocuWareValidation.IValidationFeedback { constructor(public Status: DocuWareValidation.FeedbackType, public Reason: string) { } } ``` 3. Add validation method to POST and rename it: ```ts //define POST for validation http://localhost:1337/validate app.post("/validate", (req: any, res: any, next: any) => { }); ``` 4. Use newly created method and call it in GET: ```ts //define POST for validation http://localhost:1337/validate app.post("/validate", (req: any, res: any, next: any) => { let result = CreateValidationFeedback(DocuWareValidation.FeedbackType.OK, "Alright!"); res.json(result); }); ``` #### Snapshot 4: REST Validation Method Prepared Ready ### Call fake database and do the validation 1. Parse the body of validate POST method: ```ts //define POST for validation http://localhost:1337/validate app.post("/validate", (req: any, res: any, next: any) => { try{ //Get body from request let validationBody: DocuWareValidation.IValidationBody = req.body; } catch(error){ console.log(error); } ``` 2. Do simple checks on body object: ```ts function validateJsonBody(jsonBody: jsonSchema.JSONSchema4):boolean{ //Check if any values exist if (!validationBody.Values || validationBody.Values.length === 0) { throw new Error("No index values found!"); } //and e.g. proof you getting called by the right organization (in case you have more than one) if (validationBody.OrganizationName !== "Peters Engineering") { throw new Error(`The validation for organization ${validationBody.OrganizationName} is not supported!`); } } ``` ```ts try { //Get body from request let validationBody: DocuWareValidation.IValidationBody = req.body; //Check if layout looks like desired validateJsonBody(validationBody); ``` 3. Get index value from the request body: ```ts function getFieldFromBodyByName(validationBody: DocuWareValidation.IValidationBody, dwFieldName: string): DocuWareValidation.IValidationField { let indexField = validationBody.Values.find((x: DocuWareValidation.IValidationField) => { return x.FieldName.toLocaleLowerCase() === dwFieldName.toLocaleLowerCase(); }); if (!indexField) { throw new Error(`${dwFieldName} not available`); } return indexField; } ``` 4. Call new method with ```"INVOICENUMBER"``` (naming of your DocuWare DB Field): ```ts //Get the invoice number let invoiceField = getFieldFromBodyByName(validationBody, "invoicenumber"); if (invoiceField.Item !== null) { } else { throw new Error("No invoice number provided!"); } ``` 5. Get database entry and check if it exists: ```ts function getEntryFromDatabase(db: LowdbAsync, invoiceNumber: number): DocuWareWorkshop.IDbInvoiceEntry{ let results: LoDashExplicitAsyncWrapper = db.get('invoices'); let databaseInvoiceEntry = results .find({ InvoiceNumber: invoiceNumber }) .value(); return databaseInvoiceEntry; } ``` ```ts if (invoiceField.Item !== null) { //lockup database by validation field invoice number let databaseInvoiceEntry: DocuWareWorkshop.IDbInvoiceEntry = getEntryFromDatabase(db, invoiceField.Item as number); //check if there is something in the database if (databaseInvoiceEntry) { } else { throw new Error("Invoice entry not foudn in external in database!"); } ``` 6. In case everything is fine, set validationsuccess to true in database and return positive feedback result: ```ts function updateDataBase(db: LowdbAsync, invoiceNumber: number): Promise { return db.get('invoices') .find({ InvoiceNumber: invoiceNumber }) .assign({ ValidtionSuccess: true }) .write(); } ``` ```ts //check if there is something in the database if (databaseInvoiceEntry) { //update database and set validation boolean to true let promise: Promise = updateDataBase(db, invoiceField.Item as number) promise .then((updatedDatabaseInvoiceEntry: DocuWareWorkshop.IDbInvoiceEntry) => { console.info(updatedDatabaseInvoiceEntry); return res.json(CreateValidationFeedback(DocuWareValidation.FeedbackType.OK, "Invoice found in database. OK!")); }); ``` 7. Add better catch handling: ```ts } catch (error) { return res.json(CreateValidationFeedback(DocuWareValidation.FeedbackType.Failed, `${error.message}`)); } ``` #### Snapshot 5: REST Validation Without Auth Ready ## Build executable 1. (IFNEWPROJECT) Install lowdb: ```"npm i pkg --save"``` (https://github.com/zeit/pkg) 2. (IFNEWPROJECT) Add to ```package.json```: ```ts "bin": "./dist/index.js", . . . "pkg": { "scripts": "dist/**/*.js", "targets": [ "node6" ] } ``` 3. Run ```npx pkg package.json``` 4. You'll find a _rest_validationservice.exe_ in your root directory ## Add basic authentication to REST (Supported with DocuWare 7.1) 1. (IFNEWPROJECT) Install basic authentication for express: ```"npm i express-basic-auth --save"``` 2. (IFNEWPROJECT) Add basicAuthentication: ```ts import basicAuth from 'express-basic-auth'; ``` 3. "Register" it: ```ts //enable basic auth app.use(basicAuth({ users: { "admin":"secret" } })); ``` #### Snapshot 6: REST Validation Including Auth Ready ## SOAP Service #### Snapshot 0: SOAP inital state (index.ts) _Testing of the SOAP functionality can be done easily with the WCF Test Client or POSTMan_ 1. (IFNEWPROJECT) Get express for building up service: ```"npm i express --save"``` (--save sets it as dependency to the package.json) 2. (IFNEWPROJECT) To start the application use ```"npm start" ``` 3. Init the express in ```index.ts```: ```ts //express imports import express, { Application, NextFunction, Request, Response } from "express"; import bodyParser from "body-parser"; import basicAuth from "express-basic-auth"; //express setup let app: Application = express(); let port: number = 1338; app.use(bodyParser.raw({ type: () => { return true; }, limit: "5mb" })); //In case something goes completely wrong app.use((err: Error, req: Request, res: Response, next: NextFunction) => { console.error(err.stack); res.status(500).send(`Something broke: ${err.message}`); }); ``` 4. (IFNEWPROJECT) Add typescript support for express: ```"npm i -D typescript @types/express"``` #### Snapshot 1: SOAP Express Base Ready 5. (IFNEWPROJECT) Install soap: ```"npm i soap --save" ``` + dependency ```"npm i bluebird --save"``` 6. (IFNEWPROJECT) Install types for bluebird, types of soap already there: ```"npm i -D @types/bluebird"``` 7. (IFNEWPROJECT) Add _soap_service.wsdl_: ```xml ``` 8. Setup service, check also DocuWareWorkshopSampleContracts.d.ts: ```ts function buildService(db: LowdbAsync): IServices { return { EnhancementService: { BasicHttpBinding_EnhancementService: { SyncDocid: (args: DocuWareWorkshop.ISoapArguments, callback: any) => { return { Success: true, Message: "test" }; } } } }; } ``` 9. Read out the .wsdl file: ```ts import fs from "fs"; ``` ```ts //Read wsdl definition var wsdlXML = fs.readFileSync("soap_service.wsdl", "utf8"); ``` 10. Get service, define listening GET + POST: ```ts //SOAP parts import soap from "soap"; ``` ```ts //Get the service definition let soapService : IServices = buildService(db); //GET /EnhancementService returns wsdl file app.get("/EnhancementService", (req: any, res: any, next: any) => { res.set("Content-Type", "text/xml"); res.send(wsdlXML); }); //Start listening app.listen(1338, () => { // POST /EnhancementService route will be handled by soap module let theService = soap.listen(app, "/EnhancementService", soapService, wsdlXML); }); ``` 11. (IFNEWPROJECT) Install lowdb: ```"npm i lowdb --save"``` (https://github.com/typicode/lowdb) 12. (IFNEWPROJECT) Add typescript support for lowdb: ```"npm i -D typescript @types/lowdb"``` 13. Init lowdb: ```ts //init database lowDb(lowdbFileAdapter) .then((db: LowdbAsync) => { try{ //Read wsdl definition var wsdlXML = fs.readFileSync("soap_service.wsdl", "utf8"); //Get the service definition let soapService : IServices = buildService(db); //GET /EnhancementService returns wsdl file app.get("/EnhancementService", (req: any, res: any, next: any) => { res.set("Content-Type", "text/xml"); res.send(xml); }); //Start listening app.listen(1338, () => { // POST /EnhancementService route will be handled by soap module let theService = soap.listen(app, "/EnhancementService", soapService, xml); //do logging theService.log = (type: string, data: any) => { console.log(`${type} || ${data}`); }; }); }catch(error){ console.error(error); } }); ``` 14. Create SoapResult class and build it by method: ```ts class EnhancmentSoapResult implements DocuWareWorkshop.ISoapResult{ constructor(public Success: boolean, public Message: string) { } } function buildResponse(success: boolean, message: string): DocuWareWorkshop.ISoapResult { //Success depending logging (success) ? console.info(`${success}: ${message}`) : console.error(`${success}: ${message}`); return new EnhancmentSoapResult(success, message); } ``` ```ts SyncDocid: (args: DocuWareWorkshop.ISoapArguments, callback: any) => { callback(buildResponse(true, "OK")); } ``` #### Snapshot 2: SOAP Service Base Ready 15. Write validation of soap arguments function and use it: ```ts function validateArguments(soapArguments: DocuWareWorkshop.ISoapArguments){ if(!soapArguments){ throw new Error(`No data provided!`); }else if(soapArguments.invoiceNumber === null || soapArguments.dwdocid === null){ throw new Error(`Incomplete data provided!`) } } ``` ```ts SyncDocid: (args: DocuWareWorkshop.ISoapArguments, callback: any) => { try { //validate the arguments validateArguments(args); ``` 16. Get entry from database: ```ts function getEntryFromDatabase(db: LowdbAsync, invoiceNumber: number): DocuWareWorkshop.IDbInvoiceEntry{ let results: LoDashExplicitAsyncWrapper = db.get("invoices"); let databaseInvoiceEntry = results .find({ InvoiceNumber: invoiceNumber }) .value(); return databaseInvoiceEntry; } ``` ```ts SyncDocid: (args: DocuWareWorkshop.ISoapArguments, callback: any) => { try { . . //lockup database arguments invoice number let databaseInvoiceEntry: DocuWareWorkshop.IDbInvoiceEntry = getEntryFromDatabase(db, args.invoiceNumber); ``` 17. Do real enhancement: Get database entry and check if it was not syncronized earlier: ```ts SyncDocid: (args: DocuWareWorkshop.ISoapArguments, callback: any) => { try { . . //check if there is something in the database if (databaseInvoiceEntry) { //Deny sync in case it is already flagged 'true' if (databaseInvoiceEntry.EnhancementSuccess === true) { throw new Error("DWDOCID already syncronized. Syncronized ID: " + databaseInvoiceEntry.DWDOCID); } else { ``` 18. In case everything is fine, set _EnhancementSuccess_ to true, and write DWDOCID to external database and return positive feedback response: ```ts function updateDataBase(db: LowdbAsync, soapArguments: DocuWareWorkshop.ISoapArguments): Promise { return db.get("invoices") .find({ InvoiceNumber: soapArguments.invoiceNumber}) .assign({ EnhancementSuccess: true, DWDOCID: soapArguments.dwdocid }) .write(); } ``` ```ts SyncDocid: (args: DocuWareWorkshop.ISoapArguments, callback: any) => { try { . . . if (databaseInvoiceEntry.EnhancementSuccess === true) { throw new Error("DWDOCID already syncronized. Syncronized ID: " + databaseInvoiceEntry.DWDOCID); } else { //syncronize dwdocid and set enhancement to true let promise: Promise = updateDataBase(db, args); promise .then((updatedEntry: DocuWareWorkshop.IDbInvoiceEntry) => { console.info(updatedEntry); callback(buildResponse(true, "DWDOCID syncronized!")); }); } } else { throw new Error(`Invoice ${args.invoiceNumber} not found in external database!`); } ``` 19. Finish try catch: ```ts SyncDocid: (args: DocuWareWorkshop.ISoapArguments, callback: any) => { try { . . . } catch (error) { callback(buildResponse(false, error.message)); } ``` #### Snapshot 3: SOAP Service Without Auth Ready ## Build executable 1. Install lowdb: ```"npm i pkg --save"``` (https://github.com/zeit/pkg) 2. Add to ```package.json```: ```ts "bin": "./dist/index.js", . . . "pkg": { "scripts": "dist/**/*.js", "targets": [ "node6" ] } ``` 3. Run ```npx pkg package.json``` 4. You'll find a _soap_enhancementservice.exe_ in your root directory ## Add basic authentication to SOAP (Supported with DocuWare 7.1) _We could use same like on rest, just for demo we build our own_ 1. Create custom error response: ```ts function sendNotAuthorizedResponse(req: Request, res: Response){ console.error(`'${req.url}' Request blocked, no or wrong authentication provided!`); res.set("WWW-Authenticate", "Basic realm=Authorization Required"); res.sendStatus(401); } ``` 2. Add custom auth function: ```ts //custom authentication let customAuth = function(req: Request, res: Response, next: NextFunction){ //get auth header value let authorizationHeaderValue = req.headers["authorization"]; if(!authorizationHeaderValue){ sendNotAuthorizedResponse(req, res); return; } //match with regex: 'Basic {base64encodedCredentials}' let regex: RegExp = /[Bb]asic ([\s\S]+)/i; let result : RegExpMatchArray | null = authorizationHeaderValue.match(regex); //regex successfully executed if(result){ //get result string let base64EncodiedCredentials = result[1]; //Explanation: Authorization value could be 'Basic SGVsbG86VGhlcmU=' // so it splits like this Basic -> result[0], SGVsbG86VGhlcmU= -> result[1] //Decode from base64 to string let buffer = Buffer.from(base64EncodiedCredentials, "base64"); let decodedCredentials = buffer.toString("utf8"); //Format is like: {username}:{password} //Get single credentials let credentialsArray : Array = decodedCredentials.split(":"); let userName : string = credentialsArray[0]; let userPassword : string = credentialsArray[1]; if(userName && userPassword && userName === "admin" && userPassword === "secret"){ console.log("Authentication successful"); next(); //You have to call NextFunction otherwise no other middleware will do stuff return; } } sendNotAuthorizedResponse(req, res); return; } ``` 3. Register after: ```ts app.use(customAuth); ``` #### Snapshot 4: SOAP Service Including Auth Ready