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向け実装


2024年5月30日木曜日

サンプル・アプリSample Treesのapex_util.prepare_urlをapex_page.get_urlに置き換える

ツリー・リージョンの実装サンプルとしてSample Treesが提供されています。このアプリケーションのツリー・リージョンですが、ソースとなるSELECT文にapex_util.prepare_urlが含まれています。

現行のAPEXでは、APEX_UTIL.PREPARE_URLの使用は推奨されていません。代わりにAPEX_PAGE.GET_URLを使うように案内されています。


APEX_UTIL.PREPARE_URLは引数としてURLを受け取り、それにチェックサムを加えます。一般にAPEX_UTIL.PREPARE_URLには、f?p URL構文によるURLを与えます。

Oracle APEX App Builder User's Guide

APEX_PAGE.GET_URLの呼び出しでは、アプリケーションID、ページ番号、URLに含めるアイテム名、その値などを、それぞれ引数として指定します。そのため、URL構文について考える必要はありません。

Sample Treesのツリー・リージョンのソースは、APEX_PAGE.GET_URLを使って以下のように置き換えることができます。

Sample Treesでは、ノードのタイプに依存して開く編集フォームを切り替えるために、ソースのSELECT文でAPEX_PAGE.GET_URLを呼び出しています。

ITEM_TYPEPであればcreate-edit-projectのフォーム、Tであればcreate-edit-tasksのフォーム、Sであればmodify-subtask-informationに遷移します。

一般的にAPEX_PAGE.GET_URLの引数と、ターゲットをクリックすると開くリンク・ビルダーの設定項目は1対1で対応しています。

ツリー・リージョンの場合、属性設定リンクでノードをクリックしたときに遷移する宛先を設定できます。


リンク・ビルダーの設定項目は、APEX_PAGE.GET_URLの引数に以下のように対応します。アンカーは、APEX_PAGE.GET_URLに対応する引数はありません(コードからは、生成されたURLに文字列としてアンカーを追加できるため)。


対話モード・レポートなどのレポートの場合、アイテム(引数p_valuesに与える)にレポートの列の値を割り当てることができます。Sample Treesのソースがレポートのソースだったと仮定すると、検索結果に含まれる列IDSTATUS#ID##STATUS#として参照することができます。

しかし、ツリー・リージョンの場合はレポートの表示とは異なり、レポートとして参照できる列はありません。そのため、p_valuesに渡す値は、ソースとなるSELECT文で指定する必要があります。

また、P、T、SといったITEM_TYPEの種類によって、ターゲットとなるURLだけではなく、p_valuesの値も変わっています。

Pの場合は、P3_SELECTED_NODEidP7_PROJ_IDidが渡されています。Tの場合は、P3_SELECTED_NODEidP9_PROJ_IDproj_idP9_TASK_IDtask_idSの場合はP3_SELECTED_NODEidP10_PROJ_IDproj_idP10_ROWIDROWIDが渡されています。

このような切り替えは、宣言的なターゲットの設定ではできません。

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

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

表に保存したTIFF画像を表示する

表に保存されているTIFF画像を表示する方法を考えてみました。一般的なブラウザ(Apple Safariを除く)はTIFF画像に対応していません


テストに使用するアプリケーションを作成します。

クイックSQLの以下のモデルから表TEST_IMAGESを作成します。
# prefix: test
images
    name    vc40 /nn
    content file
レビューおよび実行をクリックし、表の作成まで作業を進めます。


表が作成されたらアプリケーションの作成をクリックし、アプリケーションを自動生成します。


TEST_IMAGESフォーム付き対話モード・レポートのページが、作成されるアプリケーションに含まれています。

アプリケーションの名前TIFF Imageとします。

アプリケーションの作成を実行します。


アプリケーションが作成されます。

ページ・デザイナで、ページ番号の対話モード・レポートのページを開きます。

CONTENTを選択し、タイプイメージの表示に変更します。BLOB属性MIMEタイプ列CONTENT_MIMETYPEファイル名列CONTENT_FILENAME最終更新列CONTENT_LASTUPDを選択します。


対話モード・レポートを選択し、プロパティ・エディタの属性タブを開きます。

パフォーマンス遅延ロードオンにします。この後に動的アクションを作成しますが、タイミングとしてリフレッシュ後を選択します。遅延ロードオンにしていないと、ページ・ロード時にレポートの表示が遅延ロードにはならないため、リフレッシュ後のイベントが発生しません。

変更を保存します。


ページ・デザイナでページ番号のフォームのページを開きます。

ページ・アイテムP3_CONTENTを選択し、ストレージMIMEタイプ列CONTENT_MIMETYPEファイル名列CONTENT_FILENAME文字セット列CONTENT_CHARSETBLOB最終更新列CONTENT_LASTUPDを設定します。

変更を保存します。


以上で画像のアップロードと表示を行う、簡単なアプリケーションが作成できました。

表示される画像が画面に収まるように制限するため、ページ・プロパティのCSSインラインに以下を記述します。
img {
    width: 80%;
}

アプリケーションを実行し、画像の対話モード・レポートのページを開きます。

作成をクリックし、画像をアップロードします。


最初はJPEGの画像をアップロードしてみます。


アップロードした画像が表示されます。


画像をTIFF画像に入れ替えると、その画像は表示されません。


TIFF画像をサポートしているSafariであれば、画像が表示されます。


Safariでいいじゃん、とはならないため、Chromeなどでも表示されるように機能を追加します。

今回はUTIF.jsを使って、ブラウザ上でTIFF画像を表示できる形式に変換します。GitHub上の以下のリンクより、UTIF.jsをダウンロードします。



共有コンポーネント静的アプリケーション・ファイルを開きます。


ファイルの作成を実行します。


コンテンツに先ほどダウンロードしたUTIF.jsを選択し、作成をクリックします。


静的アプリケーション・ファイルとしてUTIF.jsが作成されます。

この時点ではまだミニファイされたファイルが作成されていないため、変更の保存をクリックします。


変更を保存するとミニファイされたファイルが作成されますが、参照がミニファイルされたファイルを指していません。一旦ページをリロードすると、参照がミニファイされたファイルを指すようになります。

#APP_FILES#UTIF#MIN#.js

これをクリップボードにコピーしておきます。


静的アプリケーション・ファイルの準備は以上です。

ページ・デザイナでページ番号の対話モード・レポートのページを開きます。

ページ・プロパティJavaScriptファイルURLに、先ほどの静的アプリケーション・ファイルUTIF.jsへの参照を設定します。

#APP_FILES#UTIF#MIN#.js


対話モード・レポートに動的アクションを作成します。

識別名前リフレッシュ後とします。タイミングイベントリフレッシュ後選択タイプリージョンは、対象としている対話モード・レポートを指定します。


TRUEアクションとしてJavaScriptコードの実行を選択し、設定コードに以下を記述します。



以上でアプリケーションは完成です。

TIFF画像の変換はレポートに含まれるすべてのIMG要素を対象としているため、TIFF以外の場合はエラーが発生します。querySelectorAllでの選択条件を調整してTIFF画像に限定する必要があります。

セレクタの指定を簡単にするためには、列CONTENTタイプイメージの表示を使う代わりに、対話モード・レポートのソースであるSELECT文でAPEX_UTIL.GET_BLOB_FILE_SRCを呼び出すようにすると、IMG要素にCONTENT_MIMETYPEに応じた属性を付加することができます。

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

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

2024年5月28日火曜日

Oracle Database 23aiのJavaScriptでのモジュールの呼び出し方を調べる

Oracle Database 23aiのMLE - Multilingual Engineで、JavaScriptのモジュールを呼び出す方法について確認してみました。

以下の資料を主に参照しています。

Oracle Database Release 23
JavaScript Developer's Guide
4 Overview of Importing MLE JavaScript Modules

実装のサンプルとしては、以下のLiveLabsも参考にしています。
APEX + Server-Side JavaScript (MLE)
Lab 4: Using External Modules

JavaScriptの実行には、Always FreeのOracle Autonomous Database 23aiのOracle APEX 23.2を使います。

Example 4-2のFunction Export using Named Exportsに記載されているMLEモジュールnamed_exports_moduleを作成します。モジュールに含まれるファンクションsumとdifferenceがエクスポートされます。
CREATE OR REPLACE MLE MODULE named_exports_module LANGUAGE JAVASCRIPT AS

function sum(a, b) {
    return a + b;
}

function difference(a, b) {
    return a - b;
}

export {sum, difference};
/
SQLワークショップオブジェクト・ブラウザより、MLEモジュールを作成します。


名前NAMED_EXPORTS_MODULEです。APEXからMLEモジュールを作成する際は名前が大文字になりますが、コードで作成してもダブル・クォーテーションで囲まなければ、ディクショナリには大文字で登録されます。

ソース・タイプソース・コードを選択し、ソース・コード(JAVASCRIPT AS 以下)を記述します。

MLEモジュールの作成をクリックします。


MLEモジュールとしてNAMED_EXPORTS_MODULEが作成されます。


作成したMLEモジュールに含まれる、エクスポートされたファンクションsumおよびdiffrenceを呼び出してみます。

MLE環境としてNAMED_EXPORTS_ENVを作成します。
CREATE MLE ENV named_exports_env;

環境名NAMED_EXPORTS_ENVとします。

MLE環境の作成をクリックします。


MLE環境としてNAMED_EXPORTS_ENVが作成されます。インポート・タブを開き、インポートの追加をクリックします。

先ほど作成したMLEモジュールNAMED_EXPORTS_MODULEを、このMLE環境NAMED_EXPORTS_ENVにインポート可能なモジュールとして登録します。


内部的には以下のスクリプトが実行されます。
alter MLE ENV named_exports_env
    add imports('namedExports' module named_exports_module);
モジュール所有者APEXのワークスペース・スキーマモジュール名NAMED_EXPORTS_MODULEです。インポート名namedExportsとします。インポート名は大文字小文字が区別されます。

作成をクリックします。


MLE環境NAMED_EXPORTS_ENVにインポート可能なMLEモジュールとして、NAMED_EXPORTS_MODULEが追加されます。JavaScriptのコードから、このモジュールをインポートする際には、インポート名namedExportsを指定します。


SQLワークショップSQLコマンドから、モジュールNAMED_EXPORTS_MODULEでエクスポートされているファンクションsumdifferenceを呼び出してみます。

言語としてJavaScript(MLE)を選択し、環境としてNAMED_EXPORTS_ENVを選択します。MLEモジュールnamedExportsのインポートは、以下の文で実行します。

const { sum, difference } = await import("namedExports");

以下のスクリプトを実行します。
const { sum, difference } = await import("namedExports");
console.log(sum(2,1));
console.log(difference(2,1));
sumとdifferenceが呼び出せます。


以下のスクリプトでも確認します。

const e = await import("namedExports");

結果は同じになります。
const e = await import("namedExports");
console.log(e.sum(2,1));
console.log(e.difference(2,1));

MLEモジュールNAMED_EXPORTS_MODULEsumdifferenceを呼び出すために、このモジュールをインポートするMLEモジュールNAMED_IMPORTS_MODULEを作成します。

Example 4-7 Named Imports Using Specified Identifiersとして記載されているコードを実行します。
CREATE OR REPLACE MLE MODULE named_imports_module
LANGUAGE JAVASCRIPT AS

import {sum, difference} from "namedExports";

function mySum(){
    const result = sum(4, 2);
    console.log(`the sum of 4 and 2 is ${result}`);
}

function myDifference(){
    const result = difference(4, 2);
    console.log(`the difference between 4 and 2 is ${result}`);
}

export {mySum, myDifference};
/
先ほどと同じ手順でMLEモジュールの作成を呼び出します。

名前NAMED_IMPORTS_MODULEソース・タイプソース・コードです。MLEモジュールNAMED_EXPORTS_MODULEのインポートは、以下の文で実行します。

import {sum, difference} from "namedExports";


作成したMLEモジュールをMLE環境NAMED_EXPORTS_ENVのインポートに追加します。インポート名namedImportsとします。


MLE環境NAMED_EXPORTS_ENVにインポート名namedImportsが追加されます。


SQLコマンドよりインポート名namedImportsを指定してインポートし、ファンクションmySummyDifferenceを呼び出してみます。
const { mySum, myDifference } = await import("namedImports");
mySum();
myDifference();
namedImportsとしてインポートされたMLEモジュールNAMED_IMPORTS_MODULEから、MLEモジュールNAMED_EXPORTS_MODULEのファンクションが参照できていることが確認できます。


Example 4-6 Module Object Definitionに記載されているモジュール本文は以下です。この内容でモジュールNAMED_IMPORTS_MODULEを置き換えても、動作は変わりません。
import * as myMath from "namedExports"

function mySum(){
    const result = myMath.sum(4, 2);
    console.log(`the sum of 4 and 2 is ${result}`);
}

function myDifference(){
    const result = myMath.difference(4, 2);
    console.log(`the difference between 4 and 2 is ${result}`);
}

export {mySum, myDifference};
Example 4-8 Named Imports with Aliasesの本文も同様です。
import {sum as theSum, difference as theDifference} from "namedExports";

function mySum(){
    const result = theSum(4, 2);
    console.log(`the sum of 4 and 2 is ${result}`);
}

function myDifference(){
    const result = theDifference(4, 2);
    console.log(`the difference between 4 and 2 is ${result}`);
}

export {mySum, myDifference};
実際にモジュールNAMED_IMPORTS_MODULEの本文を置き変え、保存してコンパイルを行います


コンパイル後にオブジェクト・ブラウザをリロードすると、MLE環境NAMED_EXPORTS_ENVが無効になっていることが確認できます。


現在のところ、オブジェクト・ブラウザにはMLE環境をバリッドに戻す方法が提供されていません。

SQLコマンドを開き、言語SQLにして以下のALTER文を実行し、MLE環境をコンパイルします。

alter mle env named_exports_env compile;


今までは簡単なコードを書いてMLEモジュールを作成していました。

実際はESモジュールからMLEモジュールを作成することができます。

文字列の検証を行うvalidator.jsをMLEモジュールとして作成してみます。MLEモジュールのソースとして以下のURLを指定します。

https://cdn.jsdelivr.net/npm/validator@13.12.0/+esm

MLEモジュールの作成画面を開きます。

名前VALIDATORソース・タイプとしてURLを選択し、URLにESモジュールのソースを指すURLを指定します。


作成したMLEモジュールVALIDATORをMLE環境NAMED_EXPORTS_ENVインポート名validatorとして追加します。


SQLコマンドから、validator.jsに含まれているファンクションisEmailを呼び出してみます。

以下のコードを実行します。
const validator = await import("validator");
console.log(validator.default.isEmail("yuji"));
console.log(validator.default.isEmail("yuji@oracle.com"));

isEmailの最初の呼び出しはfalse、次の呼び出しはtrueが返されることが確認できます。


Oracle DatabaseのMLEではモジュールがDatabaseに保存されているため、Node.jsやブラウザとは扱い方が若干異なります。そのため、Oracle Database 23aiのMLE上でJavaScriptを実行するにあたって、モジュールがどのように扱われるかを確認してみました。

ちなみにOracle APEXのアプリケーションで参照するMLE環境は、アプリケーション定義セキュリティMLE環境で設定します。


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