2024年7月19日金曜日

unpkg.comからESモジュールを取得しOracle Database 23aiのMLEモジュールを作成する

以前にjsDelivrよりESモジュールを取得し、それを元にOracle Database 23aiのMLEモジュールを作成するAPEXアプリケーションを作っています。

NPMパッケージからOracle Datase 23aiのMLEモジュールを作成する
https://apexugj.blogspot.com/2024/05/simple-mle-module-manager.html

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 Providerunpkgを選択します。Initをクリックし、ESモジュールの情報を保持するAPEXコレクションを初期化します。

Module Nameにインポート対象としてcjstoesmを設定し、MLE EnvTESTENVを設定します。


Add Es Moduleをクリックします。

UNPKGからcjstoesmのモジュールのソースを含む情報を、APEXコレクションに取り込みます。


Resolve Once
をクリックします。

ロードしたモジュール(StatusLOADED)のソースを解析し、参照しているモジュールをAPEXコレクションに追加します。解析が終わったモジュールのStatusRESOLVEDに変わります。新たにロードされたモジュールのStatusはLOADEDです。


ロードされたモジュールがすべて解析される(StatusRESOLVED)まで、Resolve Onceを繰り返します。

バージョン番号が取れないモジュールは、モジュールのソースの取得に失敗しています。その場合、StatusはLOADEDのままでRESOLVEDに変わりません。

Create Mle Modulesをクリックします。


それぞれのモジュールのStatusが、VALIDまたはINVALIDに変わります。INVALIDの場合は、大抵、UNPKGが「Package XXX does not contain an ES module」というレスポンスを返しています。そのレスポンスが、MLE Moduleのソースとして扱われていることがINVALIDの原因です。


モジュールのStatusVALIDであっても、そのモジュールの中で存在しないモジュールを参照していると、モジュールのインポート時(await importでの呼び出し時)にエラーが発生します。


依存関係のない単体で利用可能なESモジュールであれば、利用可能なMLEモジュールを作ることができます。実際には、そのようなESモジュールは稀でしょう。

Add Importsをクリックすると、StatusVALIDのモジュールをMLE環境にインポート可能なモジュールとして追加します。


ナビゲーション・メニューよりImportsを開き、MLE環境にインポート可能なモジュールとして追加された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と同一)

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;
/


パッケージ本体

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;
/