Continuous Integration (CI) is the process of automatically building and testing your application’s code each time a developer pushes a commit to a remote source-code control repository like GitLab, Jenkins, or GitHub. The use of CI is an integral part of DevOps and can be used to great effect: lead times (e.g. the time it takes from idea to shipping) can be reduced and thanks to automated unit and integration testing, error rates typically drop as well.

In most cases a CI server coordinates the execution of tests using a so-called CI Pipeline. CI Pipelines are typically defined as code, and checked into the same Git repository as the application’s code. Most CI servers accept YAML syntax to define jobs and associate them to stages such as linting, build, test, and deploy.

Ideally CI Pipelines never “break”, e.g. hit defects in the code. You should therefore try to push code to your repository with a high degree of confidence that it won’t break the pipeline. Using local test tools can help with that.

Local Tests

Testing code locally before committing/pushing it to the remote Git repository is very important to avoid problems with the CI Pipeline’s execution. This article concerns itself with local tests of JavaScript modules used with Oracle Database 26ai on Linux. JavaScript Modules are an interesting new database feature allowing developers to process data where they live using one of the most popular programming languages. With the addition of JavaScript Oracle Database features 3 programming languages (PL/SQL and Java being the other 2). The feature enabling JavaScript support on Linux x86-64 and aarch64 in Oracle AI Database 26ai is known as Multilingual Engine (MLE).

Testing your code typically involves the following steps:

  • checking for syntactical correctness
  • linting
  • unit testing
  • others

The easiest way to generate good code right from the beginning is to include support for these right from the get-go. Instead of focusing on JavaScript as in the previous posts, this one is about Typescript. Just as with node, you cannot use Typescript directly with Oracle AI Database, you need to transpile it to JavaScript first. The developer experience with Typescript is potentially better since Typescript can warn you of unexpected things before they happen as you can see in this article.

The MLE team has provided the TypeScript declarations used by all MLE built-in modules, documentation can be found on GitHub. These allow you to use code completion, mouse-over information and type checking in your code.

In addition Biome will be used in this article as linter and formatter.

NOTE: The use of biome is merely an example, not an endorsement for the tool. There are many more linters available to developers. Always ensure you are comfortable working with a given piece of software, and that you are (license, …) compliant.

Versions Used

The following versions were used in the creation of this article:

$ jq '{
  dependencies: .dependencies,
  devDependencies: .devDependencies
}' package.json
{
  "dependencies": null,
  "devDependencies": {
    "@biomejs/biome": "2.3.12",
    "typescript": "^5.9.3",
    "@types/mle-js": "github:oracle-samples/mle-modules#main"
  }
}

$ node --version
v24.11.0

With that out of the way let’s look at some code.

Initial JavaScript Example

For the sake of demonstration let’s assume someone created the following MLE JavaScript module in their favourite editor, saved as src/badfile.js. It’s deliberately riddled with issues, don’t use it as an example except for how not to do it.

/**
 * Update the "lastUpdated" field in a purchase order, adding it if it does not 
 * yet exist. Uses the example defined in the JSON Developer's Guide, chapter 4 
 * Examples 4-1 and 4-3
 * @param {object} purchaseOrder - the PO to update 
 * @param {string} lastUpdate - a string representation of a date (YYYY-MM-DDThh:mm:ss)
 * @returns {object} the updated purchaseOrder
 */
function setLastUpdatedDate(purchaseOrder, lastUpdate) {
    if (purchaseOrder === undefined) {
        throw Error("unknown purchase order");
    }
    if (lastUpdate = undefined) {
        lastUpdate = new Date().toISOString();
    }
    console.log(`last update set to ${lastUpdate}`);
    purchaseOrder.lastUpdate = lastUpdate;
    return purchaseOrder;
}
/**
 * Use vanilla JavaScript to validate a PurchaseOrder. This could have been
 * done with JSON schema validation as well
 * @param {object} purchaseOrder - the PO to validate
 * @returns {boolean} true if the PO could be successfully validated, false if not
 */
function validatePO(purchaseOrder) {
    // a PO must contain line items
    if (purchaseOrder.LineItems.length <= 0) {
        return false;
    }
    // a PO must contain shipping instructions
    if (purchaseOrder.ShippingInstructions === undefined) {
        return false;
    }
    return true;
}
/**
 * Fetch a PurchaseOrder from the database and process it. Store the last modification
 * timestamp alongside
 * @param {number} poNumber - the PONumber as stored in j_purchaseorder.po_document.PONumber 
 */
export function processPurchaseOrder(poNumber) {
    const result = session.execute(
        `SELECT
            po.po_document as PO
        FROM
            j_purchaseorder po
        WHERE
            po.po_document.ponumber = :1`,
        [poNumber],
        " thisIsAnIncorrectParameter "
    );
    // ensure the PO exists
    if (result.rows === undefined) {
        throw Error(`could not find a PO for PO Number ${poNumber}`);
    } else {
        const myPO = result.rows[0].PO;
    }
    // make sure the PO is valid
    if (!validatePO(myPO)) {
        throw Error(`Purchase Order ${poNumber} is not a valid PO`);
    }
    // do some fancy processing with the PO
    // indicate when the last operation happened
    myPO = setLastUpdatedDate(myPO, undefined);
    result = session.execute(
        `UPDATE j_purchaseorder po
        SET
            po.po_document = :myPO
        WHERE
            po.po_document.PONumber = :poNumber`,
        {
            myPO: {
                dir: oracledb.BIND_IN,
                type: oracledb.DB_TYPE_JSON,
                val: myPO
            },
            poNumber: {
                dir: oracledb.BIND_IN,
                type: oracledb.NUMBER,
                val: poNumber
            }
        }
    );
    if (result.rowsAffected != 1) {
        throw Error(`unable to persist purchase order ${poNumber}`);
    }
}

Before submitting the code to the database for testing, the developer should ensure the code doesn’t have any obvious and easy-to-spot errors. The example has been chosen to put emphasis on the fact that it’s very hard to ensure code quality by merely eyeballing the text.

Improving Code Quality

Converting the above snippet to TypeScript provides lots of benefits to developers:

  • Type declarations, available for all MLE modules, add a safety net to the code.
  • Linting TypeScript adds even more checks to ensure the transpiled code is good to go.

The remainder of this article is concerned with the transition to Typescript and correction of the errors encountered.

Installing MLE JavaScript Type Declarations

As per the MLE module documentation the first step is to install the mle-js module. This module contains the type declarations for all built-in modules used later. The following instructions are executed relative to the project’s root directory.

npm install --save-dev typescript
npm install --save-dev "https://github.com/oracle-samples/mle-modules#main"

Installing the linter

In the next step you need to decide which linter to use, this post focuses on biome as per the introduction.

$ npm install --save-dev --save-exact @biomejs/biome

Creating a basic configuration

The first step working with biome is to create a configuration. You create it using npx biome init, it’s a sane default and good starting point. Adjust as needed for your project, it’s safe to leave as is in the scope of this article.

JavaScript to TypeScript

The first step in the conversion from JavaScript to Typescript is to rename the source file to src/badFile.ts.

Next you need to create a tsconfig.json file, needed for the Typescript compiler. The following is a minimal file, instructing TypeScript to transpile JavaScript code with the latest version of JavaScript supported by Oracle AI Database 26ai (at the time of writing):

{
	"compilerOptions": {
		"target": "es2023",
		"module": "es2022",
		"rootDir": "src/",
		"outDir": "dist",
		"noEmitOnError": true,
		"esModuleInterop": true,
		"forceConsistentCasingInFileNames": true,
		"strict": true,
		"skipLibCheck": true
	}
}

TypeScript code no longer needs to be told about the type definitions you just installed. The format changed, and they now live in node_modules/@types/mle-js.

With the setup complete you will almost immediately see things light up in red in your IDE. If not, you can use the command line to check for problems. Let’s start by transpiling the code to JavaScript:

$ npx tsc --pretty false
src/goodFile.ts(9,29): error TS7006: Parameter 'purchaseOrder' implicitly has an 'any' type.
src/goodFile.ts(9,44): error TS7006: Parameter 'lastUpdate' implicitly has an 'any' type.
src/goodFile.ts(26,21): error TS7006: Parameter 'purchaseOrder' implicitly has an 'any' type.
src/goodFile.ts(42,38): error TS7006: Parameter 'poNumber' implicitly has an 'any' type.
src/goodFile.ts(51,9): error TS2559: Type '" thisIsAnIncorrectParameter "' has no properties in common with type 'IExecuteOptions'.
src/goodFile.ts(60,21): error TS2304: Cannot find name 'myPO'.
src/goodFile.ts(65,5): error TS2304: Cannot find name 'myPO'.
src/goodFile.ts(65,31): error TS2304: Cannot find name 'myPO'.
src/goodFile.ts(66,5): error TS2588: Cannot assign to 'result' because it is a constant.
src/goodFile.ts(76,22): error TS2304: Cannot find name 'myPO'.

Since Typescript is based on types, the absence of these is reported (TS7006 error). This can and should be fixed in the next iteration.

One of the prime benefits of TypeScript – type checking – is clearly demonstrated by error 2559. An unknown and therefore incorrect option is passed to the execute() statement in line 51. This is an obvious one, but there are definitely cases where you mix. the order of parameters up without noticing until you hit a runtime exception.

Another classic mistake, trying to change the value of a constant, is also detected (line 66).

Let’s fix these errors; the resulting file is shown here for reference. Please note that the types are merely stubs to drive the point home, a proper implementation would add all the other required fields from the PO as well, in their respective files.

/**
 * Stub interfaces, for demonstration purpose only, should be fleshed out to 
 * reflect all data as per Example 4-3 in chapter 4 of the JSON Developer's
 * Guide
 */
interface ILineItem {
    ItemNumber: number,
    Part: string,
    Quantity: number
}
interface IShippingInstructions {
    name: string,
    address: string,
    phone: string
}
interface IPurchaseOrder {
    PONumber: number,
    lastUpdate: string,
    LineItems: ILineItem[],
    ShippingInstructions: IShippingInstructions[]
}
/**
 * Update the "lastUpdated" field in a purchase order, adding it if it does not 
 * yet exist. Uses the example defined in the JSON Developer's Guide, chapter 4 
 * Examples 4-1 and 4-3
 * @param {object} purchaseOrder - the PO to update 
 * @param {string} lastUpdate - a string representation of a date (YYYY-MM-DDThh:mm:ss)
 * @returns {object} the updated purchaseOrder
 */
function setLastUpdatedDate(purchaseOrder: IPurchaseOrder, lastUpdate: string): IPurchaseOrder {
    if (purchaseOrder === undefined) {
        throw Error("unknown purchase order");
    }
    if (lastUpdate == undefined) {
        lastUpdate = new Date().toISOString();
    }
    console.log(`last update set to ${lastUpdate}`);
    purchaseOrder.lastUpdate = lastUpdate;
    return purchaseOrder;
}
/**
 * Use vanilla JavaScript to validate a PurchaseOrder. This could have been
 * done with JSON schema validation as well 
 * @param {object} purchaseOrder - the PO to validate
 * @returns {boolean} true if the PO could be successfully validated, false if not
 */
function validatePO(purchaseOrder: IPurchaseOrder): boolean {
    // a PO must contain line items
    if (purchaseOrder.LineItems.length <= 0) {
        return false;
    }
    // a PO must contain shipping instructions
    if (purchaseOrder.ShippingInstructions === undefined) {
        return false;
    }
    return true;
}
/**
 * Fetch a PurchaseOrder from the database and process it. Store the last modification
 * timestamp alongside
 * @param {number} poNumber - the PONumber as stored in j_purchaseorder.po_document.PONumber 
 */
export function processPurchaseOrder(poNumber: IPurchaseOrder["PONumber"]): void {
    let result = session.execute(
        `SELECT
            po.po_document as PO
        FROM
            j_purchaseorder po
        WHERE
            po.po_document.PONumber = :1`,
        [ poNumber ]
    );
    // ensure the PO exists
    if (result.rows === undefined || result.rows.length === 0) {
        throw new Error(`could not find Purchase Order ${poNumber}`);
    }
    let myPO = result.rows[0].PO as IPurchaseOrder;
    // make sure the PO is valid
    if (! validatePO(myPO)) {
        throw new Error(`Purchase Order ${poNumber} failed validation`);
    }
    // do some (imaginary) fancy processing with the PO ... 
    // ... then: indicate when the last operation happened
    myPO = setLastUpdatedDate(myPO, "");
    result = session.execute(
        `UPDATE j_purchaseorder po
        SET
            po.po_document = :myPO
        WHERE
            po.po_document.PONumber = :poNumber`,
        {
            myPO: {
                dir: oracledb.BIND_IN,
                type: oracledb.DB_TYPE_JSON,
                val: myPO
            },
            poNumber: {
                dir: oracledb.BIND_IN,
                type: oracledb.DB_TYPE_NUMBER,
                val: poNumber
            }
        }
    );
    if (result.rowsAffected != 1) {
        throw new Error(`unable to persist purchase order ${poNumber}`);
    }
}

At this stage, the TypeScript transpiler stops complaining

$ npx tsc --pretty false && echo "well done!"
well done!

Next up: linting

The above code looks all right on first inspection, doesn’t it? Well, you’re in for a surprise. The linter has uncovered some interesting details:

$ npx biome ci ./src
src/badfile.ts:37:20 lint/suspicious/noDoubleEquals  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━

  ✖ Using == may be unsafe if you are relying on type coercion.
  
    35 │         throw Error("unknown purchase order");
    36 │     }
  > 37 │     if (lastUpdate == undefined) {
       │                    ^^
    38 │         lastUpdate = new Date().toISOString();
    39 │     }
  
  ℹ == is only allowed when comparing against null.
  
  ℹ Unsafe fix: Use === instead.
  
    37 │ ····if·(lastUpdate·===·undefined)·{
       │                      +             

src/badfile.ts:109:29 lint/suspicious/noDoubleEquals  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━

  ✖ Using != may be unsafe if you are relying on type coercion.
  
    107 │         },
    108 │     );
  > 109 │     if (result.rowsAffected != 1) {
        │                             ^^
    110 │         throw new Error(`unable to persist purchase order ${poNumber}`);
    111 │     }
  
  ℹ != is only allowed when comparing against null.
  
  ℹ Unsafe fix: Use !== instead.
  
    109 │ ····if·(result.rowsAffected·!==·1)·{
        │                               +     

Checked 1 file in 3ms. No fixes applied.
Found 2 errors.
ci ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ✖ Some errors were emitted while running checks.

Well, that’s quite a bit of text! Thankfully it’s not all that bad, only 2 errors have been reported. Time to fix the code some more. Since it’s a bit tedious switching from command line to editor, many IDEs offer support for linting. Using IDE support for linting and syntax checking is the most convenient way moving forward. If your list of problems is zero, chances are high that your code will pass the CI Pipeline, too.

Don’t forget to add linting and syntax checking to your CI Pipeline

With the hard part completed you can now add your linter and formatter to your toolchain. Git Hooks are pretty good for this, but more importantly, add support for linting, syntax checking and your unit tests into your CI Pipeline.

Happy coding!