NPMパッケージからOracle Datase 23aiのMLEモジュールを作成する
https://apexugj.blogspot.com/2024/05/simple-mle-module-manager.html
Resolve Onceをクリックします。
UNPKGも?moduleを付けるとESモジュールを取得できるみたいなので、jsDelivrと同じ作業ができるようにPL/SQLパッケージを作成しました。コードは本記事に末尾に添付します。
ただし、結果としてはUNPKGに記載のある通り、very experimental な感じで、あまり使い道はなかったです。 UNPKGの記載です。
Query Parameters
?meta
- Return metadata about any file in a package as JSON (e.g.
/any/file?meta
) ?module
- Expands all “bare”
import
specifiers in JavaScript modules to unpkg URLs. This feature is very experimental
ESモジュールの提供元として、jsDelivrとUNPKGの切り替えを実装したAPEXアプリケーションのエスクポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/mle-module-manager%2Bords-handler.zip
APEXアプリケーションを実行し、UNPKGからパッケージcjstoesmをロードしています。
操作手順はjsDelivrと同じです。
作業を始めるにあたってNPM Providerにunpkgを選択します。Initをクリックし、ESモジュールの情報を保持するAPEXコレクションを初期化します。
Module Nameにインポート対象としてcjstoesmを設定し、MLE EnvにTESTENVを設定します。
Add Es Moduleをクリックします。
UNPKGからcjstoesmのモジュールのソースを含む情報を、APEXコレクションに取り込みます。
ロードしたモジュール(StatusがLOADED)のソースを解析し、参照しているモジュールをAPEXコレクションに追加します。解析が終わったモジュールのStatusはRESOLVEDに変わります。新たにロードされたモジュールのStatusはLOADEDです。
モジュールのStatusがVALIDであっても、そのモジュールの中で存在しないモジュールを参照していると、モジュールのインポート時(await importでの呼び出し時)にエラーが発生します。
ナビゲーション・メニューよりImportsを開き、MLE環境にインポート可能なモジュールとして追加されたMLEモジュールを確認します。
ロードされたモジュールがすべて解析される(StatusがRESOLVED)まで、Resolve Onceを繰り返します。
バージョン番号が取れないモジュールは、モジュールのソースの取得に失敗しています。その場合、StatusはLOADEDのままでRESOLVEDに変わりません。
Create Mle Modulesをクリックします。
それぞれのモジュールのStatusが、VALIDまたはINVALIDに変わります。INVALIDの場合は、大抵、UNPKGが「Package XXX does not contain an ES module」というレスポンスを返しています。そのレスポンスが、MLE Moduleのソースとして扱われていることがINVALIDの原因です。
依存関係のない単体で利用可能なESモジュールであれば、利用可能なMLEモジュールを作ることができます。実際には、そのようなESモジュールは稀でしょう。
Add Importsをクリックすると、StatusがVALIDのモジュールをMLE環境にインポート可能なモジュールとして追加します。
ほとんどのケース(おそらくjsDelivrで失敗してUNPKGで成功するケースは無い)でjsDelivrの方がUNPKGより、動作するMLEモジュールを生成します。これは、jsDelivrではRollupやTerserを呼び出し、参照しているファイルをバンドルしているためです。
MLEモジュールの作成には失敗しますが、UNPKGにも良い点があります。
jsDelivrはRollupなどを呼び出してソースを生成しているため、ソースの可読性はとても低いです。UNPKGでは元のソースのimport文の置き換えを実施している程度なので、ソースは比較的読みやすいです。コードを改変することで、MLEモジュールとして動作させるつもりであれば、UNPKGから作成したMLE Moduleの方が扱いやすいでしょう。
今回の記事は以上です。
完
パッケージUTL_MLE_NPM2の定義部(UTL_MLE_NPMと同一)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
create or replace package utl_mle_npm2 | |
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_npm2; | |
/ |
パッケージ本体
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
create or replace package body utl_mle_npm2 | |
as | |
C_PROVIDER_URL constant varchar2(80) := 'https://unpkg.com/%s?module'; | |
C_IMPORT_PRE constant varchar2(20) := 'https://unpkg.com/'; | |
C_IMPORT_POST constant varchar2(8) := '?module'; | |
/** | |
* Extracts module names, versions, and paths from import statements within ES module source code. | |
* Valid only with ES modules generated by unpkg. | |
*/ | |
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 | |
/* ignore first @ that is a part of module name */ | |
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; | |
if p_module_version like '^%' then | |
p_module_version := substr(p_module_version, 2); | |
end if; | |
apex_debug.info('module = %s, version = %s, path = %s', p_module_name, p_module_version, p_module_path); | |
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; | |
/** | |
* Add ES module which is planned to be created as MLE module, to APEX collection. | |
*/ | |
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; | |
l_start integer; | |
l_end integer; | |
l_file varchar2(80); | |
begin | |
/* | |
* Steps to get ES module from unpkg.com | |
* Request without version or latest. (ex. jimp) | |
* First request. | |
* https://unpkg.com/jimp@latest?module | |
* - Note: if latest is omitted, unpkg respond with HTML. | |
* To handle the response as TEXT, always latest for version if p_module_version is null. | |
* unpkg respond with 302 to get the latest version. | |
* https://unpkg.com/jimp@0.22.12?module | |
* then unpkg respond with 302 again to get the source of ES module. | |
* https://unpkg.com/jimp@0.22.12/es/index.js?module | |
*/ | |
if p_module_version is null or p_module_version = 'latest' or not regexp_like(p_module_version,'^[0-9|\.]+$') then | |
if p_module_version is null or p_module_version = 'latest' then | |
l_provider_url := apex_string.format(C_PROVIDER_URL, p_module_name || '@latest'); | |
else | |
/* for the version such as '>=3.2.x || >= 4.x' */ | |
l_provider_url := apex_string.format(C_PROVIDER_URL, p_module_name || '@' || p_module_version); | |
end if; | |
/* | |
* call unpkg.com to get the latest version number of the ES module. | |
*/ | |
utl_http.set_follow_redirect( | |
max_redirects => 0 -- force not to follow redirect. | |
); | |
apex_web_service.clear_request_headers(); | |
apex_debug.info('Request to get version: %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, %s', apex_web_service.g_status_code, l_source); | |
if apex_web_service.g_status_code = 302 then | |
l_start := dbms_lob.instr( | |
lob_loc => l_source | |
,pattern => '/' | |
); | |
l_end := dbms_lob.instr( | |
lob_loc => l_source | |
,pattern => C_IMPORT_POST | |
,offset => l_start | |
); | |
l_file := dbms_lob.substr( | |
lob_loc => l_source | |
,amount => (l_end - l_start - 1) | |
,offset => (l_start + 1) | |
); | |
/* update unpkg.com URL including module name and VERSION NUMBER */ | |
l_provider_url := apex_string.format(C_PROVIDER_URL, l_file); | |
if l_file like '@%' then | |
l_start := instr(l_file, '@', 1, 2); | |
else | |
l_start := instr(l_file, '@', 1, 1); | |
end if; | |
l_module_version := substr(l_file, (l_start + 1)); | |
end if; | |
else | |
if 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; | |
l_module_version := p_module_version; | |
end if; | |
/* | |
* retrieve actual source | |
*/ | |
utl_http.set_follow_redirect( | |
max_redirects => 1 -- enable follow redirects | |
); | |
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_status := C_MODULE_LOADED; | |
end if; | |
/* | |
* Generate MLE module name | |
*/ | |
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 | |
); | |
/* | |
* check if MLE Module is exist. | |
*/ | |
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; | |
-- 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 ES 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 1=1 | |
and collection_name = p_collection_name | |
and c008 in (C_MODULE_RESOLVED,C_MODULE_VALID,C_MODULE_INVALID) | |
) | |
loop | |
l_source := r.clob001; | |
/* | |
* 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 => r.clob001 | |
); | |
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' -- add if MLE MODULE is created and VALID. | |
and c002 is not null | |
) | |
loop | |
begin | |
select 1 into l_exist from user_mle_modules | |
where module_name = r.module_name; | |
exception | |
when no_data_found then | |
continue; | |
end; | |
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_npm2; | |
/ |