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パッケージ

create or replace package utl_mle_npm
as
/**
* ESモジュールのロードからMLEモジュールの作成までに取り得る状態。
*/
C_MODULE_NA constant varchar2(8) := 'NA';
C_MODULE_LOADED constant varchar2(8) := 'LOADED';
C_MODULE_RESOLVED constant varchar2(8) := 'RESOLVED';
C_MODULE_VALID constant varchar2(8) := 'VALID';
C_MODULE_INVALID constant varchar2(8) := 'INVALID';
/**
* MLEモジュールとして作成する予定のESモジュールを、APEXコレクションに追加する。
*/
procedure add_module(
p_collection_name in varchar2
,p_module_name in varchar2
,p_module_version in varchar2 default null
,p_module_path in varchar2 default null
,p_parent_module_name in varchar2 default null
,p_parent_module_version in varchar2 default null
,p_parent_module_path in varchar2 default null
);
/**
* APEXコレクションに含まれているESモジュールで、そのモジュールがインポートしている
* ESモジュールの解析が終わっていないものを対象にして解析する。
*/
procedure resolve_once(
p_collection_name in varchar2
);
/**
* APEXコレクションに追加されているESモジュールより、MLEモジュールを作成する。
*/
procedure create_mle_modules(
p_collection_name in varchar2
,p_replace in boolean
);
/**
* MLE環境にインポートを追加する。
*/
procedure add_imports_to_mle_env(
p_collection_name in varchar2
,p_mle_env in varchar2
);
/**
* drop all MLE modules if the name start with 'ESM__'
*/
procedure drop_mle_modules_esm;
end utl_mle_npm;
/
view raw utl_mle_npm.sql hosted with ❤ by GitHub

jsdelivr向け実装

create or replace package body utl_mle_npm
as
C_PROVIDER_URL constant varchar2(80) := 'https://cdn.jsdelivr.net/npm/%s/+esm';
C_IMPORT_PRE constant varchar2(20) := '/npm/';
C_IMPORT_POST constant varchar2(8) := '/+esm';
C_ORIGINAL_FILE_LINE constant varchar2(20) := 'Original file: /npm/';
C_MLE_ENV_FOR_PRETTY_PRINT constant varchar2(30) := 'MLE_ENV_FOR_PRETTY_PRINT';
/**
* Pretty print the moudle source.
*
* Prerequisites:
* - An MLE module named '/npm/pretty-js@0.2.2/+esm' must exist.
* - An MLE Environment containing this module must be defined in the
* Application Setting 'MLE_ENV_FOR_PRETTY_PRINT'.
*
* Note: This functionality is currently only applicable to jsdelivr.
*/
function make_pretty(
p_source in clob
,p_module in varchar2 default '/npm/pretty-js@0.2.2/+esm'
)
return clob
as
l_ctx dbms_mle.context_handle_t;
C_CODE constant clob := q'~
(async() => {
const bindings = await import("mle-js-bindings");
const { default:prettyJs } = await import("#MODULE#");
const source = bindings.importValue("source");
const pretty = prettyJs(source.getData());
bindings.exportValue("pretty", pretty);
})();
~';
l_code clob;
l_pretty clob;
l_environment varchar2(80);
begin
l_environment := apex_app_setting.get_value(
p_name => C_MLE_ENV_FOR_PRETTY_PRINT
);
-- apex_debug.info('MLE Env for Pretty Print, %s', l_environment);
/*
* Do nothing without Application Setting.
*/
if l_environment is null then
return p_source;
end if;
/*
* Do pretty print by calling pretty-js.
*/
l_ctx := dbms_mle.create_context(
environment => l_environment
);
l_code := replace(C_CODE, '#MODULE#', p_module);
-- apex_debug.info('Code for Pretty Print, %s', l_code);
dbms_mle.export_to_mle(l_ctx, 'source', p_source);
dbms_mle.eval(l_ctx, 'JAVASCRIPT', l_code);
dbms_mle.import_from_mle(l_ctx, 'pretty', l_pretty);
dbms_mle.drop_context(l_ctx);
return l_pretty;
exception
when others then
/* preserve the orignal source when pretty-print is failed.  */
return p_source;
end make_pretty;
/**
* Retrieve module version from the source code of ES module.
* If version could not be retrieved, mainly because jsdelivr failed to generate ES module.
*/
function get_version_from_source(
p_source clob
)
return varchar2
as
l_start integer;
l_end integer;
l_file varchar2(80);
l_version varchar2(80);
begin
l_start := dbms_lob.instr(
lob_loc => p_source
,pattern => C_ORIGINAL_FILE_LINE
);
l_end := dbms_lob.instr(
lob_loc => p_source
,pattern => apex_application.LF
,offset => l_start
);
l_file := dbms_lob.substr(
lob_loc => p_source
,amount => (l_end - l_start - length(C_ORIGINAL_FILE_LINE))
,offset => (l_start + length(C_ORIGINAL_FILE_LINE))
);
-- dbms_output.put_line(apex_string.format('start %s end %s file %s', l_start, l_end, l_file));
/*
* separator between module name and version is '@' but the module name
* could start with '@'.
* When module name is stared with '@', it is not treated as a separator.
*/
if l_file like '@%' then
l_start := instr(l_file, '@', 1, 2);
else
l_start := instr(l_file, '@', 1, 1);
end if;
l_end := instr(l_file, '/', l_start, 1);
l_version := substr(l_file, (l_start + 1), (l_end - l_start - 1));
return l_version;
end get_version_from_source;
/**
* Extracts module names, versions, and paths from import statements within ES module source code.
* Valid only with ES modules generated by jsDelivr.
*/
procedure get_name_and_version_from_import(
p_import in varchar2
,p_module_name out varchar2
,p_module_version out varchar2
,p_module_path out varchar2
)
as
l_start integer;
l_end integer;
l_line varchar2(32767);
begin
l_start := instr(p_import, C_IMPORT_PRE);
if l_start = 0 then
p_module_name := null;
p_module_version := null;
return;
end if;
l_line := substr(p_import, (l_start + length(C_IMPORT_PRE)));
if l_line like '@%' then
l_end := instr(l_line, '@', 1, 2);
else
l_end := instr(l_line, '@', 1, 1);
end if;
if l_end = 0 then
p_module_name := null;
p_module_version := null;
return;
end if;
p_module_name := substr(l_line, 1, (l_end - 1));
l_line := substr(l_line, (l_end + 1));
l_end := instr(l_line, C_IMPORT_POST);
if l_end = 0 then
p_module_name := null;
p_module_version := null;
return;
end if;
l_line := substr(l_line, 1, (l_end - 1));
l_start := instr(l_line, '/');
if l_start > 0 then
/* version and path exists */
p_module_version := substr(l_line, 1, (l_start - 1));
p_module_path := substr(l_line, (l_start + 1));
else
/* version only */
p_module_version := l_line;
p_module_path := null;
end if;
end get_name_and_version_from_import;
/**
* Generate the name for MLE module based on the ES module name, version, and path.
*/
function generate_mle_module_name(
p_module_name in varchar2
,p_module_version in varchar2
,p_module_path in varchar2
)
return varchar2
as
l_module_name varchar2(128);
begin
/*
* ES module name might start with '@' but MLE module name should begin with A-Z.
* Prepend ESM_ to all MLE module name to ensure the name begin with A-Z.
*/
l_module_name := 'ESM_' || p_module_name || '@' || p_module_version;
if p_module_path is not null then
l_module_name := l_module_name || '_' || p_module_path;
end if;
l_module_name := upper(translate(l_module_name, '@./-', '____'));
return l_module_name;
end generate_mle_module_name;
/**
* Generates the name of an MLE module based on the ES module name, version, and path.
*/
procedure add_module(
p_collection_name in varchar2
,p_module_name in varchar2
,p_module_version in varchar2 default null
,p_module_path in varchar2 default null
,p_parent_module_name in varchar2 default null
,p_parent_module_version in varchar2 default null
,p_parent_module_path in varchar2 default null
)
as
l_provider_url varchar2(200);
l_source clob;
l_module_version varchar2(80);
l_mle_module_name varchar2(128);
l_module_status varchar2(8) := C_MODULE_NA;
l_seq_id number;
begin
/*
* If no version is specified, the latest version is fetched. The version number is extracted from the source code.
*/
if p_module_version is null then
l_provider_url := apex_string.format(C_PROVIDER_URL, p_module_name);
elsif p_module_path is null then
l_provider_url := apex_string.format(C_PROVIDER_URL, p_module_name || '@' || p_module_version);
else
l_provider_url := apex_string.format(C_PROVIDER_URL, p_module_name || '@' || p_module_version || '/' || p_module_path);
end if;
-- dbms_output.put_line(l_jsdelivr_url);
/*
* Get module source as ES moudle by calling jsDelivr.
*/
apex_web_service.clear_request_headers();
apex_debug.info('Request to get source: %s', l_provider_url);
l_source := apex_web_service.make_rest_request(
p_url => l_provider_url
,p_http_method => 'GET'
);
apex_debug.info('Response: %s', apex_web_service.g_status_code);
if apex_web_service.g_status_code = 200 then
l_module_version := get_version_from_source(
p_source => l_source
);
l_mle_module_name := generate_mle_module_name(
p_module_name => p_module_name
,p_module_version => l_module_version
,p_module_path => p_module_path
);
if l_module_version is not null then
l_module_status := C_MODULE_LOADED;
end if;
end if;
if l_module_status = C_MODULE_LOADED then
begin
select status into l_module_status from user_objects
where object_type = 'MLE MODULE' and object_name = l_mle_module_name;
exception
when no_data_found then
null;
end;
end if;
-- update mle module status if module is in the apex collection.
begin
select seq_id into l_seq_id from apex_collections
where 1=1
and collection_name = p_collection_name
and c001 = p_module_name
and c002 = l_module_version
and (
(p_module_path is null and c003 is null)
or
(c003 = p_module_path)
);
exception
when no_data_found then
l_seq_id := -1;
end;
if l_seq_id >= 0 then
apex_collection.update_member(
p_collection_name => p_collection_name
,p_seq => l_seq_id
,p_c001 => p_module_name
,p_c002 => l_module_version
,p_c003 => p_module_path
,p_c004 => p_parent_module_name
,p_c005 => p_parent_module_version
,p_c006 => p_parent_module_path
,p_c007 => l_mle_module_name
,p_c008 => l_module_status
,p_clob001 => l_source
);
else
apex_collection.add_member(
p_collection_name => p_collection_name
,p_c001 => p_module_name
,p_c002 => l_module_version
,p_c003 => p_module_path
,p_c004 => p_parent_module_name
,p_c005 => p_parent_module_version
,p_c006 => p_parent_module_path
,p_c007 => l_mle_module_name
,p_c008 => l_module_status
,p_clob001 => l_source
);
end if;
end add_module;
/**
* Extracts imported ES modules from ES module source code and adds them to an APEX collection.
*/
procedure resolve_imported_modules(
p_collection_name in varchar2
,p_module_name in varchar2
,p_module_version in varchar2
,p_module_path in varchar2
)
as
l_source clob;
l_start integer;
l_end integer;
l_line varchar2(32767);
l_module_name varchar2(80);
l_module_version varchar2(80);
l_module_path varchar2(80);
begin
if p_collection_name is null then
/* work without APEX collection, debugging purpose only. */
apex_web_service.clear_request_headers();
l_source := apex_web_service.make_rest_request(
p_url => (
case when p_module_path is null then
apex_string.format(C_PROVIDER_URL, p_module_name || '@' || p_module_version)
else
apex_string.format(C_PROVIDER_URL, p_module_name || '@' || p_module_version || '/' || p_module_path)
end)
,p_http_method => 'GET'
);
else
select clob001 into l_source
from apex_collections
where 1=1
and collection_name = p_collection_name
and c001 = p_module_name
and c002 = p_module_version
and (
(p_module_path is null and c003 is null)
or
(p_module_path = c003)
)
and c008 = C_MODULE_LOADED;
end if;
l_end := 1;
loop
l_start := dbms_lob.instr(
lob_loc => l_source
,pattern => 'import'
,offset => l_end
);
exit when l_start = 0;
l_end := dbms_lob.instr(
lob_loc => l_source
,pattern => ';'
,offset => l_start
);
l_line := dbms_lob.substr(
lob_loc => l_source
,amount => (l_end - l_start)
,offset => l_start
);
/*
* Extract module name, version and path from the import statement.
*/
get_name_and_version_from_import(
p_import => l_line
,p_module_name => l_module_name
,p_module_version => l_module_version
,p_module_path => l_module_path
);
/*
* Add extracted module to APEX collection if both module name and version are successfully extracted.
*/
if l_module_name is not null and l_module_version is not null then
add_module(
p_collection_name => p_collection_name
,p_module_name => l_module_name
,p_module_version => l_module_version
,p_module_path => l_module_path
,p_parent_module_name => p_module_name
,p_parent_module_version => p_module_version
,p_parent_module_path => p_module_path
);
end if;
l_end := l_start + 1;
end loop;
end resolve_imported_modules;
/**
* Resolve ES modules included in the APEX collection that have not yet been analyzed for their imported ES modules.
*/
procedure resolve_once(
p_collection_name in varchar2
)
as
begin
for r in (
select seq_id,c001,c002,c003,c004,c005,c006,c007,c008,clob001
from apex_collections
where collection_name = p_collection_name
and c008 = C_MODULE_LOADED
)
loop
resolve_imported_modules(
p_collection_name => p_collection_name
,p_module_name => r.c001
,p_module_version => r.c002
,p_module_path => r.c003
);
apex_collection.update_member(
p_collection_name => p_collection_name
,p_seq => r.seq_id
,p_c001 => r.c001
,p_c002 => r.c002
,p_c003 => r.c003
,p_c004 => r.c004
,p_c005 => r.c005
,p_c006 => r.c006
,p_c007 => r.c007
,p_c008 => C_MODULE_RESOLVED
,p_clob001 => r.clob001
);
end loop;
end resolve_once;
/**
* Create MLE Module from ES module stored in APEX Collection.
*/
procedure create_mle_modules(
p_collection_name in varchar2
,p_replace in boolean
)
as
l_sql clob;
l_source clob;
l_module_status varchar2(8);
begin
for r in (
select seq_id,c001,c002,c003,c004,c005,c006,c007,c008,clob001
from apex_collections
where collection_name = p_collection_name
and c008 in (C_MODULE_RESOLVED,C_MODULE_VALID,C_MODULE_INVALID)
)
loop
l_source := r.clob001;
/*
* ES module from jsdelivr is minified, not human readable.
* if APEX application setting MLE_ENV_FOR_PRETTY_PRINT is set, do pretty print.
*/
l_source := make_pretty(l_source);
/*
* Create MLE module from ES module.
*/
l_sql := 'create or replace mle module ' || r.c007 || ' language javascript version ''' || r.c002 || ''' as ' || l_source || '/';
-- apex_debug.info(l_sql);
if r.c008 = C_MODULE_RESOLVED or p_replace then -- create only if MLE module is not exist or force flag is set to true.
begin
execute immediate l_sql;
exception
when others then
-- module status is set from user_objects
null;
end;
end if;
/*
* Verify if MLE module exists and VALID.
*/
begin
select status into l_module_status from user_objects
where object_name = r.c007 and object_type = 'MLE MODULE';
exception
when no_data_found then
l_module_status := C_MODULE_NA;
end;
/*
* Update collection status.
*/
apex_collection.update_member(
p_collection_name => p_collection_name
,p_seq => r.seq_id
,p_c001 => r.c001
,p_c002 => r.c002
,p_c003 => r.c003
,p_c004 => r.c004
,p_c005 => r.c005
,p_c006 => r.c006
,p_c007 => r.c007
,p_c008 => l_module_status
,p_clob001 => l_source
);
end loop;
end create_mle_modules;
/**
* drop all MLE modules if the name start with 'ESM__'
*/
procedure drop_mle_modules_esm
as
l_sql varchar2(4000);
begin
for r in (
select module_name from user_mle_modules where substr(module_name, 1, 4) = 'ESM_'
)
loop
l_sql := 'drop mle module ' || r.module_name;
dbms_output.put_line(l_sql);
execute immediate l_sql;
end loop;
end drop_mle_modules_esm;
/**
* Add imports to MLE Environment.
*/
procedure add_imports_to_mle_env(
p_collection_name in varchar2
,p_mle_env in varchar2
)
as
l_sql varchar2(4000);
l_exist number;
begin
l_sql := 'create mle env if not exists ' || p_mle_env;
execute immediate l_sql;
for r in (
select
case when c003 is null then
C_IMPORT_PRE || c001 || '@' || c002 || C_IMPORT_POST
else
C_IMPORT_PRE || c001 || '@' || c002 || '/' || c003 || C_IMPORT_POST
end import_name
,c007 module_name
from apex_collections
where 1=1
and collection_name = p_collection_name
and c008 = 'VALID'
and c002 is not null
)
loop
begin
select 1 into l_exist from user_mle_env_imports
where 1=1
and import_name = r.import_name
and module_name = r.module_name;
exception
when no_data_found then
l_sql := 'alter mle env ' || p_mle_env || ' add imports(''' || r.import_name || ''' module ' || r.module_name || ')';
execute immediate l_sql;
end;
end loop;
end add_imports_to_mle_env;
end utl_mle_npm;
/

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を使って以下のように置き換えることができます。

select case when connect_by_isleaf = 1 then 0
when level = 1 then 1
else -1
end as status,
level,
label||': '||name as title,
case when item_type = 'P' then 'fa-file-text-o'
when item_type = 'S' then 'fa-caret-square-o-right'
when item_type = 'T' then 'fa-minus-square-o'
else null
end as icon,
id as value,
case when tooltip is not null then name||' - '||tooltip||'% complete'
else name
end as tooltip,
/*
* apex_util.prepare_urlをapex_page.get_urlに置き換えます。
*/
case when item_type = 'P' then
apex_page.get_url(
p_page => 'create-edit-project'
,p_request => 'T'
,p_items => 'P3_SELECTED_NODE,P7_PROJ_ID'
,p_values => id||','||id
)
-- apex_util.prepare_url('f?p='||:app_id||':7:'||:app_session||':T:::P3_SELECTED_NODE,P7_PROJ_ID:'||id||','||id)
when item_type = 'T' then
apex_page.get_url(
p_page => 'create-edit-tasks'
,p_request => 'T'
,p_items => 'P3_SELECTED_NODE,P9_PROJ_ID,P9_TASK_ID'
,p_values => id||','||link
)
-- apex_util.prepare_url('f?p='||:app_id||':9:'||:app_session||':T:::P3_SELECTED_NODE,P9_PROJ_ID,P9_TASK_ID:'||id||','||link)
when item_type = 'S' then
apex_page.get_url(
p_page => 'modify-subtask-information'
,p_request => 'T'
,p_items => 'P3_SELECTED_NODE,P10_PROJ_ID,P10_ROWID'
,p_values => id||','||link
)
-- apex_util.prepare_url('f?p='||:app_id||':10:'||:app_session||':T:::P3_SELECTED_NODE,P10_PROJ_ID,P10_ROWID:'||id||','||link)
end as link
from (
select 'P' item_type,
t.label label,
to_char(a.PROJ_ID) id,
null parent,
a.project_name name,
a.status tooltip,
null link
from eba_demo_tree_projects a, (select wwv_flow_lang.system_message('PROJECT') label from dual) t
union all
select 'T' item_type,
u.label label,
to_char(b.proj_id)||'-'||to_char(b.task_id) id,
to_char(b.proj_id) parent,
b.task_name name,
null tooltip,
b.proj_id||','||b.task_id link
from eba_demo_tree_task b, (select wwv_flow_lang.system_message('TASK') label from dual) u
union all
select 'S' item_type,
v.label label,
to_char(c.proj_id)||'-'||to_char(c.task_id)||'-'||to_char(c.sub_id) id,
to_char(c.proj_id)||'-'||to_char(c.task_id) parent,
c.sub_name name,
null tooltip,
c.proj_id||','||c.rowid link
from eba_demo_tree_subtask c, (select wwv_flow_lang.system_message('SUBTASK') label from dual) v
)
start with parent is null
connect by prior id = parent
order siblings by name
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コードの実行を選択し、設定コードに以下を記述します。

const ir = this.triggeringElement;
// console.log(ir);
// imgがTIFFイメージだけになるように、Query Selectorは限定する必要あり。
// gifやjpegがあると、そのイメージではエラーが発生する。
const imgs = ir.querySelectorAll("img");
for (var i=0; i<imgs.length; i++)
{
var img=imgs[i], src=img.getAttribute("src"); if(src==null) continue;
var xhr = new XMLHttpRequest(); UTIF._xhrs.push(xhr); UTIF._imgs.push(img);
xhr.open("GET", src); xhr.responseType = "arraybuffer";
xhr.onload = UTIF._imgLoaded; xhr.send();
}
view raw replaceTiff.js hosted with ❤ by GitHub


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

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環境で設定します。


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