2024年5月31日金曜日

NPMパッケージからOracle Datase 23aiのMLEモジュールを作成する

Oracle Database 23aiに追加されたMLE(Multilingual Engine - GraalVMのこと)により、データベースでJavaScriptを実行できるようになりました。Oracle APEXは以前からコードを記述する言語は選択可能で、データベースの対応待ちでした。

さて、JavaScriptでコードを記述しようと思っても、Node.jsのnpmで配布されているようなパッケージが無い、となると相当に大変な作業になってしまいます。

Oracle Database 23aiのMLEモジュールは、基本的にESモジュールです。npmのパッケージをESモジュールとして取得すれば、(原則的に)それをもとにMLEモジュールを作成できます。

CDNのjsDelivrでは、NPMパッケージを取得するURLの末尾に+esmを付加することにより、そのパッケージをESモジュールの形式で返してくれます。

今回はJavaScriptを清書するパッケージpretty-jsをデータベースにロードしてみます。

pretty-jsをESモジュールとして取得するURLは以下になります。


CDNからESモジュールを取得し、MLEモジュールとして作成するAPEXアプリケーションを作成しています。相当にやっつけ仕事ですが、何もないよりは遥かに良いです。エクスポートは以下に置きました。

https://github.com/ujnak/apexapps/blob/master/exports/mle-module-manager.zip

以下のGIF動画のように操作します。
  1. Module Namepretty-jsを指定し、Add Moduleをクリックします。jsDelivrからpretty-jsをESモジュールとして取得し、MLEモジュールの作成対象として追加します。
  2. Resolve Onceをクリックします。pretty-jsのESモジュールのコードを読んで、importされているESモジュールを、MLEモジュールの作成対象として追加します。
  3. 追加されたESモジュールを再帰的に解析するようにはしていないので、ESモジュールがすべて解析されるまで、Resolve Onceを複数回クリックします。
  4. Create Mle Modulesをクリックし、jsDelivrから取得したESモジュールを元にMLEモジュールを作成します。
  5. MLE Envに指定されているMLE環境(なければ新規作成)に、作成したMLEモジュールのインポートを追加します。
動作確認のために、以下のコードを実行しています。
const { default:prettyJs } = await import("/npm/pretty-js@0.2.2/+esm");
const { fetch } = await import("mle-js-fetch");

fetch('https://cdn.jsdelivr.net/npm/pretty-js@0.2.2/+esm')
.then(response => {
    return response.text();
})
.then(text => {
    console.log(prettyJs(text));
});


ほとんどの処理は、記事の末尾に添付したパッケージUTL_MLE_NPMに実装しています。APEXアプリケーションのボタンをクリックすると、対応したUTL_MLE_NPMのプロシージャを呼び出します(プロセスのタイプAPI呼出し)。
  • ボタンINITだけはUTL_MLE_NPMではなく、APEX_COLLECTION.CREATE_OR_TRUNCATE_COLLECTIONを呼び出し、APEXコレクションMLE_MODULESを初期化します。
  • ボタンADD_ES_MODULEは、UTL_MLE_NPM.ADD_MODULEを呼び出します。
  • ボタンRESOLVE_ONCEは、UTL_MLE_NPM.RESOLVE_ONCEを呼び出します。
  • ボタンCREATE_MLE_MODULESは、UTL_MLE_NPM.CREATE_MLE_MODULESを呼び出します。
  • ボタンADD_IMPORTSは、UTL_MLE_NPM.ADD_IMPORTSを呼び出します。
  • ボタンDROP_MLE_ENVは、drop mle env MLE環境名を実行します。
  • ボタンDROP_MLE_MODULESは、UTL_MLE_NPM.DROP_MLE_MODULES_ESMを呼び出します。
すべての処理はホーム・ページに実装しています。


ボタンに対応したプロセスが作成されています。


上記の例ではpretty-jsをMLEモジュールとして作成しましたが、元々はjimpをロードするために、このアプリケーションを作成しました。

Module Namejimpを入力し、Add Es Moduleをクリックします。ESモジュールは以下のURLから取得しています。

https://cdn.jsdelivr.net/npm/jimp/+esm

バージョンを指定していないため、ESモジュールの内容からバージョンを取り出しています。

レポートの列C001モジュール名C002バージョンです。列C007MLEモジュール名です。ESモジュールの名前は記号(例えば)で始まることもあるため、MLEモジュール名は必ずESM_で開始するようにしています。また、MLEモジュールにはバージョン情報を付けることができますが、同じモジュール名でバージョンが異なるモジュールは作成できないようです。同じモジュールでもバージョンが異なるものを別のMLEモジュールとして作成できるように、MLEモジュール名にはバージョンを付加しています。

N0010の場合は、MLEモジュールとして未作成1の場合は作成済み、列N0020の場合はソースコードは未解析の場合は解析済み(インポートしているESモジュールがAPEXコレクションに追加済み)です。


ボタンResolve Onceをクリックします。

C001jimpの列N0021になり(つまり解析済み)、@jimp/custom@jimp/types@jimp/pluginsが行として追加されます。これらの列N0020で、まだ解析されていません。


再度Resolve Onceをクリックします。@jimp/custom@jimp/types@jimp/pluginsの内容が解析され(列N0021になります)、インポートされていたESモジュールがAPEXコレクションに追加されます。


同じ操作(Resolve Onceのクリック)を、列N002の値がすべて1(ESモジュールの取得に失敗しているものを除く)になるまで繰り返します。

C002(バージョン)が空白のESモジュールが2つあります。@jimp/coregifwrapです。


バージョンが取れないのは、jsDelivrの側でESモジュールのコード生成に失敗しているためです。生成されたコードを確認します。

https://cdn.jsdelivr.net/npm/@jimp/core/+esm
https://cdn.jsdelivr.net/npm/gifwrap/+esm

両方ともに以下のような出力になっています。どのようなNPMでも、ESモジュールにできるわけではないようです。
/**
 * Failed to bundle using Rollup v2.79.1: the file imports a not supported node.js built-in module "fs".
 * If you believe this to be an issue with jsDelivr, and not with the package itself, please open an issue at https://github.com/jsdelivr/jsdelivr
 */

 throw new Error('Failed to bundle using Rollup v2.79.1: the file imports a not supported node.js built-in module "fs". If you believe this to be an issue with jsDelivr, and not with the package itself, please open an issue at https://github.com/jsdelivr/jsdelivr');
また、MLEモジュールとして作成できていても、インポートして使おうとするとエラーが発生する場合もあります。

MLE環境に追加されたインポート名より、MLEモジュールをインポートする文を生成します。MLE環境はMYENVとしてます。
select '// const e' || rownum || ' = await import("' || import_name || '");' from user_mle_env_imports where env_name = 'MYENV'

以下のインポートを実行してみます。

const e5 = await import("/npm/@jimp/custom@0.22.12/+esm");

以下のエラーが発生します。モジュール@jimp/coreはESモジュールの生成に失敗しているため、MLEモジュールとして作成できていません。@jimp/coreのコードを修正する以外に対応方法は無いと思われます。

ORA-04161: Error: Cannot load ES module: /npm/@jimp/core@0.22.12/+esm

以下のインポートを実行してみます。

const e49 = await import("/npm/xml-parse-from-string@1.0.1/+esm");

以下のエラーが発生しました。

ORA-04161: ReferenceError: self is not defined 
ORA-06512: "APEX_230200.WWV_FLOW_CODE_EXEC_MLE", 行728 
ORA-04171: 場所:module:eval (WKSP_APEXDEV.ESM_XML_PARSE_FROM_STRING_1_0_1:7:16)

ESモジュールのコードを確認してみます。

https://cdn.jsdelivr.net/npm/xml-parse-from-string@1.0.1/+esm

コード中でMicrosoft.XMLDOMを参照している模様です。Oracle Database上ではエラーが発生するのは仕方がなさそうです。
/**
 * Bundled by jsDelivr using Rollup v2.79.1 and Terser v5.19.2.
 * Original file: /npm/xml-parse-from-string@1.0.1/index.js
 *
 * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
 */
var e=void 0!==self.DOMParser?function(e){return(new self.DOMParser).parseFromString(e,"application/xml")}:void 0!==self.ActiveXObject&&new self.ActiveXObject("Microsoft.XMLDOM")?function(e){var r=new self.ActiveXObject("Microsoft.XMLDOM");return r.async="false",r.loadXML(e),r}:function(e){var r=document.createElement("div");return r.innerHTML=e,r};export{e as default};
//# sourceMappingURL=/sm/be5cb35a93829eb0b811add8f6983796069c4d7086c8acebcc12b0d69a3be01d.map
最終的にjimpはロードできなかったのは残念ですが、依存関係のあるNPMモジュールからMLEモジュールを作成する作業は、それなりに効率的にできるようになりました。

今回の記事は以上になります。

Oracle APEXのアプリケーション作成の参考になれば幸いです。


UTL_MLE_NPMパッケージ


jsdelivr向け実装