////////////////////////////////////////////////////////// // lambda main. ////////////////////////////////////////////////////////// (function (_g) { 'use strict'; // events. const events = require("events"); // イベント11超えでメモリーリーク警告が出るのでこれを排除. // (モジュールロード時に1回だけ設定すれば十分なため、handler内から移動). events.EventEmitter.defaultMaxListeners = 0; // path. const pathLib = require("path"); // 実行対象拡張子(js). const _RUN_JS = ".mt.js"; // 実行対象拡張子(jhtml-js). const _RUN_JHTML = ".jhtml.js"; // jhtml(変換前)拡張子. const _JHTML_SRC_EXTENSION = ".mt.html"; // mime追加定義. const _MIME_CONF = "mime.json"; // etags.conf. const _ETAGS_CONF_FILE = "etags.json"; // mtpkでコンテンツがgzip化されてる拡張子. const _PUBLIC_CONTENTS_GZ = ".gz"; // icon. const _FAVICON_ICO = "favicon.ico"; // 初期化条件. let _event = null; // lambda handler(event) パラメータ. let _context = null; // lambda handler(context) パラメータ. let _c_request = null; // minto の requestオブジェクト. let _c_response = null; // minto の responseオブジェクト. let _c_mime = null; // minto の mimeオブジェクト(ウォームスタート対応). let _c_etag = null; // minto の etag情報(ウォームスタート対応). let _c_cache = null; // minto の 汎用cache機能(exports.handler単位で削除). // lambda main(3番目の引数 callbackはサポートしない). exports.handler = async function (event, context) { // 初期化処理. _event = event; _context = context; _c_request = null; _c_response = null; _c_cache = null; let rawPath = event.rawPath; // sqsから当該lambdaが呼び出された場合. if (rawPath == null && event['Records'] != null) { return await _responseSqsParams(event['Records']); } // rawPathが無く、event.targetが指定されている場合はテーブル管理 // コマンド(createTable/dropTable/alterTable/alterIndex/backupTable/ // restoreTable/listBackups/previewRestore/pruneBackups/ // restoreBackupAs/describeBackup)の実行と判定する. // AWSコンソールの「テスト実行」やtools/tableTool.jsから、Function URL // 形式ではないevent({ target, command, ... })が渡されるケース. if (rawPath == null && event.target != null) { return await _responseTableCommand(event); } // 不正なパスが設定されている場合はエラーにする. // AIメモ: 以前は throw new HttpError(...) としていたが、handler()の // 先頭でthrowすると_responseRunJs等が持つtry/catch(_errorRunJs経由での // ステータス復元)を経由せずに呼び出し元まで例外が伝播してしまい、 // HttpErrorが意図したステータス(400)が失われ500に丸められてしまう // 問題があった。_errorRunJs()を直接呼んで正しいステータスで返却する. if (_hasParentTraversal(rawPath)) { return _errorRunJs( new HttpError({ status: 400, message: "不正なパスを検知しました" }), ""); } // favicon.icoのアクセス. if (rawPath.endsWith("/" + _FAVICON_ICO)) { // favicon.ico はフィルタを介さないで返却. return faviconIco(); } // ファイル拡張子を取得. let ext = _extends(rawPath); // 指定実行対象の末尾が[/filter]パスの場合. // .mt.js や .jhtml.js も直接指定はエラー. if (isSqsFuncPath(rawPath) || isFilterPath(rawPath) || isMintoJs(rawPath)) { // 拡張子がjsの場合(mintoJs). if (ext === "js") { ext = ""; } // 直接パス実行出来ない: 403エラー. return _errorStaticResult(403, ext); } // filter実行. // フィルターパスが存在する場合. if (_existsSync(_FILTER_PATH())) { // フィルタ実行. const resultFilter = await _runFilter(ext); // filterで処理を終了返却する場合. if (resultFilter != true) { return resultFilter; } } // rawPathのファイル指定が空の場合. if (rawPath.endsWith("/")) { // 静的ファイル(html or htm など)が存在する場合. const staticPath = _PUBLIC_PATH() + rawPath; if (_existsSync(staticPath + _STATIC_HTML) || _existsSync(staticPath + _STATIC_HTML + _PUBLIC_CONTENTS_GZ)) { // index.html ファイルが存在する場合. rawPath = rawPath + _STATIC_HTML; ext = "html"; // 静的ファイルの返却. return await _responseStaticFile( rawPath, ext); } else if (_existsSync(staticPath + _STATIC_HTM) || _existsSync(staticPath + _STATIC_HTM + _PUBLIC_CONTENTS_GZ)) { // index.htm ファイルが存在する場合. rawPath = rawPath + _STATIC_HTM; ext = "htm"; // 静的ファイルの返却. return await _responseStaticFile( rawPath, ext); } else { // staticなファイルが存在しない場合. // 動的ファイルを対象とする. rawPath += "index"; ext = ""; // 動的ファイルの実行. return await _responseRunJs( rawPath, ext); } } // 動的ファイル処理. if (ext === "jhtml" || ext === "") { // 動的ファイルの実行. return await _responseRunJs( rawPath, ext); } // 静的ファイルの返却. return await _responseStaticFile( rawPath, ext); } // [default]Baseパス名. const _BASE_PATH = pathLib.resolve() + "/"; let _basePath = _BASE_PATH; // baseパスを設定. // basePath 対象のbaseパスを設定します. exports.setBasePath = function (basePath) { basePath = basePath.trim(); if (basePath.endsWith("/")) { _basePath = basePath; } else { _basePath = basePath + "/"; } } // publicパス名. const _PUBLIC_PATH = function () { return _basePath + "public/"; } // libraryパス名. const _LIBRARY_PATH = function () { return _basePath + "lib/"; } // confパス名. const _CONF_PATH = function () { return _basePath + "conf/"; } // sqs実行プログラム名と実行パス const _SQS_FUNC_NAME = "runSqs"; const _SQS_FUNC_FILE = _SQS_FUNC_NAME + _RUN_JS; const _SQS_FUNC_PATH = function () { return _PUBLIC_PATH() + _SQS_FUNC_FILE; } // filter名と実行パス. const _FILTER_NAME = "filter"; const _FILTER_FILE = _FILTER_NAME + _RUN_JS; const _FILTER_PATH = function () { return _PUBLIC_PATH() + _FILTER_FILE; } // jhtml変換実行Function. // jhtmlをこのindex.js 内で処理する場合に変換処理 // > jhtml.convert // を設定します. let _jhtmlConvFunc = null; exports.setJHTMLConvFunc = function (func) { if (typeof (func) === "function") { _jhtmlConvFunc = func; } } // キャッシュ関連情報をクリア(local実行専用). exports.clearCache = function () { _event = null; _context = null; _c_request = null; _c_response = null; _c_mime = null; _c_etag = null; _c_cache = null; // キャッシュ情報. _existsCache.clear(); _jsCache.clear(); } // _toString64の文字列. const _CODE64 = "+0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz~"; // 数字を _CODE64 で表現. const _toString64 = function (num) { let a; let ret = ""; while (num !== 0) { a = num & 0x03f; num = (num - a) / 64; ret = _CODE64[a] + ret; } // num == 0 の場合. if (ret === "") { ret = "+"; } return ret; } // 実行中のミリ秒 + ナノ秒を取得. _g.$getNow = function () { const d = _toString64(Date.now()); const b = _toString64(process.hrtime()[1]); return "++++++++".substring(d.length) + d + "+++++".substring(b.length) + b; } // Lambda実行時のユニークリクエストIDを取得. // 戻り値: ユニークなUUIDが返却されます. _g.$requestId = function () { return _context["awsRequestId"]; } // ライブラリをロード処理. // name: 対象のJSファイル等を設定します. // 戻り値: require結果が返却されます. _g.$loadLib = function (name) { name = ("" + name).trim(); //if (name[0] === "/") { if (name.charCodeAt(0) === 47) { name = name.substring(1) } // "/lib" 以下のファイルを require. if (_existsSync(_LIBRARY_PATH() + name)) { return require(_LIBRARY_PATH() + name) } // 取得できない場合はエラー. throw new Error("Failed to load lib: " + name); } // コンフィグJSONをロード処理. // name: 対象のjsonファイル等を設定します. // 戻り値: require結果が返却されます. _g.$loadConf = function (name) { name = ("" + name).trim(); //if (name[0] === "/") { if (name.charCodeAt(0) === 47) { name = name.substring(1) } if (_existsSync(_CONF_PATH() + name)) { // "/conf" 以下のファイルを require. return require(_CONF_PATH() + name) } // 取得できない場合は null. return null; } // requireの代替え対応. // 基本 mt.jsや jhtml.js の場合、require が利用できない. // そのための代替え手段として $require を利用する. // また利用方法として "fs" などの 標準ライブラリ利用か // lambda index.js が存在するパスをカレントパスとした // 位置から require 対象の js ファイルを設定します. // name: requireで設定する文字列を設定します. // 戻り値: require結果が返却されます. _g.$require = function (name) { name = ("" + name).trim(); // カレントパスからのアクセスを行なう. if (name.indexOf("/") !== -1) { //if (name[0] === "/") { if (name.charCodeAt(0) === 47) { name = name.substring(1); } // カレントパスを絶対パスとして取得. if (_existsSync(_BASE_PATH + name)) { return require(_BASE_PATH + name); } try { // 標準ライブラリとして呼び出す. // @aws-sdk/client-s3 などの / が入るケースがあるため. return require(name); } catch (e) { } // 取得できない場合はエラー. throw new Error("Failed to load require: " + name); } else { // 標準ライブラリ. return require(name); } } /////////////////////////////////////////////// // テーブル管理コマンド(createTable/dropTable/alterTable/alterIndex/ // backupTable/restoreTable/listBackups/previewRestore/pruneBackups/ // restoreBackupAs/describeBackup). // // AIメモ: masterとindexは同一実行内で同時に処理しない // (タイムアウト対策のため、対象は必ずどちらか一方のみ). 定義ファイルは // conf/table/{target}.json( { options: {bucket,prefix,region}, // tables: {テーブル名: schema} } )に「あるべき新しい定義」として置き、 // S3上の集約ファイル(table.json、s3MasterTable.js/s3IndexTable.js内部の // listTables()相当)を「現在有効な実際の定義」として比較する。 // 検証(フェーズ1)→適用(フェーズ2)の2段階とし、1つでも検証エラーが // あれば何も適用せず中断する。多重実行防止はs3Lock.jsによる単一の // メンテナンスロックで行う(タイムアウトは実質無し。異常終了時は手動で // ロック解除する運用). // backupTable/restoreTable/listBackups/previewRestore/pruneBackupsは // master/index両対応の機能で、conf/table/{target}.jsonの「あるべき定義」 // との差分比較は行わず、指定したテーブルの現在のS3上のデータ(行データ・ // スキーマ、target=indexの場合はインデックスも含む)をそのまま対象にする. // previewRestoreはrestoreTableのdry-run(行数比較のみ、実際の復元・削除は // 行わない)、pruneBackupsは古いバックアップ世代を削除し直近keep世代分だけ // 残す機能. restoreBackupAsはバックアップ内容を元とは別のテーブル名 // (destTableName)として新規復元する(クローン用途、destTableNameが // 既に存在する場合はエラー)。describeBackupは指定バックアップの // スキーマ・行数を復元せずに確認する機能. // 詳細はdocs/s3-row-store-design.md・docs/s3MasterTable.md参照. /////////////////////////////////////////////// // メンテナンスロックのキー名. const _TABLE_LOCK_KEY = "table-migration"; // メンテナンスロックのタイムアウト(実質無し扱い. 異常終了時は手動解除). const _TABLE_LOCK_TIMEOUT_MS = Number.MAX_SAFE_INTEGER; // カラム定義の差分(追加・削除)を検出する. // 戻り値: { addedNames, removedNames } (いずれもカラム名の配列). const _diffColumns = function (currentColumns, desiredColumns) { currentColumns = currentColumns || {}; desiredColumns = desiredColumns || {}; const addedNames = []; const removedNames = []; for (const colName in desiredColumns) { if (currentColumns[colName] == null) { addedNames.push(colName); } } for (const colName in currentColumns) { if (desiredColumns[colName] == null) { removedNames.push(colName); } } return { addedNames: addedNames, removedNames: removedNames }; }; // primaryKey/uniqueの定義が変更されていないか検証する. // 戻り値: 変更が無ければtrue. const _samePrimaryKeyAndUnique = function (currentColumns, desiredColumns) { currentColumns = currentColumns || {}; desiredColumns = desiredColumns || {}; const names = Object.assign({}, currentColumns, desiredColumns); for (const colName in names) { const c = currentColumns[colName]; const d = desiredColumns[colName]; const cPk = c != null && c.primaryKey === true; const dPk = d != null && d.primaryKey === true; const cUnique = c != null && c.unique === true; const dUnique = d != null && d.unique === true; if (cPk !== dPk || cUnique !== dUnique) { return false; } } return true; }; // 追加カラムがnotNullの場合、defaultが指定されているか検証する. // 戻り値: 問題があれば説明メッセージ、問題無ければnull. const _validateAddedColumns = function (desiredColumns, addedNames) { for (let i = 0; i < addedNames.length; i++) { const def = desiredColumns[addedNames[i]] || {}; if (def.notNull === true && def.default === undefined) { return "追加カラム '" + addedNames[i] + "' はnotNullですがdefaultが指定されていません。"; } } return null; }; // インデックス定義の差分(追加・削除)を検出する. const _diffIndexes = function (currentIndexes, desiredIndexes) { currentIndexes = currentIndexes || {}; desiredIndexes = desiredIndexes || {}; const addedNames = []; const removedNames = []; for (const idxName in desiredIndexes) { if (currentIndexes[idxName] == null) { addedNames.push(idxName); } } for (const idxName in currentIndexes) { if (desiredIndexes[idxName] == null) { removedNames.push(idxName); } } return { addedNames: addedNames, removedNames: removedNames }; }; // SQSパラメータ実行処理. const _responseSqsParams = async function(list) { let successCount = 0, jsonStr, json; const len = list.length; for(let i = 0; i < len; i ++) { jsonStr = null; try { // json文字列を取得. jsonStr = list[i]["body"]; // json変換. json = JSON.parse(jsonStr); // sqs実行処理を呼び出す. await _runSqsFunction(json); // 処理成功. successCount ++; } catch(e) { console.error("errorSqs: json: " + jsonStr, e); } } return {statusCode: 200, result: {success: successCount, all: len}}; } // テーブル管理コマンド実行本体. // event.target "master"|"index" を設定します. // event.command "createTable"|"dropTable"|"alterTable"|"alterIndex"| // "backupTable"|"restoreTable"|"listBackups"|"previewRestore"| // "pruneBackups"|"restoreBackupAs"|"describeBackup"|"exportCsv"| // "importCsv" を設定します. // event.tableName alterIndex/backupTable/restoreTable/listBackups/ // previewRestore/pruneBackups/restoreBackupAs/describeBackup/ // exportCsv/importCsvの場合に必須の対象テーブル名(restoreBackupAsでは // バックアップ取得元のテーブル名。alterIndexのみtarget="index"限定、 // exportCsv/importCsvのみtarget="master"限定、それ以外はmaster/index // 両対応). // event.backupId restoreTable/previewRestore/restoreBackupAs/ // describeBackupの場合のみ必須のバックアップ世代ID // (backupTableの実行結果で返るbackupId). // event.keep pruneBackupsの場合のみ必須の残す世代数(0以上の整数). // event.destTableName restoreBackupAsの場合のみ必須の複製先テーブル名 // (既に存在する場合はエラーになる). // event.csvBucket exportCsv/importCsvの場合のみ必須のCSV入出力先S3バケット名 // (テーブル自体が保存されているバケットとは無関係に指定可能). // event.csvPrefix exportCsv/importCsvの場合の任意のCSV入出力先prefix // (省略時は空文字列). // event.csvFileName exportCsv/importCsvの場合のみ必須のCSVファイル名 // (csvBucket+csvPrefix+csvFileNameがオブジェクトキーになる). const _responseTableCommand = async function (event) { const target = event.target; const command = event.command; if (target !== "master" && target !== "index") { return { error: "不正なtargetです: " + target }; } const _CSV_COMMANDS = ["exportCsv", "importCsv"]; const _TABLE_NAME_REQUIRED_COMMANDS = ["alterIndex", "backupTable", "restoreTable", "listBackups", "previewRestore", "pruneBackups", "restoreBackupAs", "describeBackup"] .concat(_CSV_COMMANDS); if (["createTable", "dropTable", "alterTable"].concat(_TABLE_NAME_REQUIRED_COMMANDS).indexOf(command) === -1) { return { error: "不正なcommandです: " + command }; } if (command === "alterIndex" && target !== "index") { return { error: "alterIndexはtarget=indexのみ対応しています。" }; } if (_CSV_COMMANDS.indexOf(command) !== -1 && target !== "master") { return { error: command + "はtarget=masterのみ対応しています。" }; } if (_TABLE_NAME_REQUIRED_COMMANDS.indexOf(command) !== -1) { if (event.tableName == null) { return { error: command + "にはtableNameの指定が必須です。" }; } if (["restoreTable", "previewRestore", "restoreBackupAs", "describeBackup"].indexOf(command) !== -1 && event.backupId == null) { return { error: command + "にはbackupIdの指定が必須です。" }; } if (command === "pruneBackups" && !(Number.isInteger(event.keep) && event.keep >= 0)) { return { error: "pruneBackupsにはkeep(0以上の整数)の指定が必須です。" }; } if (command === "restoreBackupAs" && event.destTableName == null) { return { error: "restoreBackupAsにはdestTableNameの指定が必須です。" }; } if (_CSV_COMMANDS.indexOf(command) !== -1) { if (event.csvBucket == null) { return { error: command + "にはcsvBucketの指定が必須です。" }; } if (event.csvFileName == null) { return { error: command + "にはcsvFileNameの指定が必須です。" }; } } } const confFile = _g.$loadConf("table/" + target + ".json"); if (confFile == null) { return { error: "定義ファイルが見つかりません: conf/table/" + target + ".json" }; } const options = confFile.options || {}; const desiredTables = confFile.tables || {}; const s3Lock = _g.$loadLib("s3Lock.js"); const lock = s3Lock.create({ bucket: options.bucket, prefix: options.prefix, region: options.region, credentials: options.credentials, timeoutMs: _TABLE_LOCK_TIMEOUT_MS }); if (!(await lock.acquire(_TABLE_LOCK_KEY))) { return { error: "他のテーブル管理コマンドが実行中です。しばらく待ってから再実行してください。" }; } try { const db = (target === "master") ? _g.$loadLib("s3MasterTable.js").create(options) : _g.$loadLib("s3IndexTable.js").create(options); const currentTables = await db.listTables(); if (command === "createTable") { return await _tableCommandCreate(db, currentTables, desiredTables, target); } else if (command === "dropTable") { return await _tableCommandDrop(db, currentTables, desiredTables, target); } else if (command === "alterTable") { return await _tableCommandAlter(db, currentTables, desiredTables, target); } else if (command === "alterIndex") { return await _tableCommandAlterIndex(db, currentTables, desiredTables, event.tableName); } else if (command === "backupTable") { return await _tableCommandBackup(db, target, event.tableName); } else if (command === "listBackups") { return await _tableCommandListBackups(db, target, event.tableName); } else if (command === "restoreTable") { return await _tableCommandRestore(db, target, event.tableName, event.backupId); } else if (command === "previewRestore") { return await _tableCommandPreviewRestore(db, target, event.tableName, event.backupId); } else if (command === "pruneBackups") { return await _tableCommandPruneBackups(db, target, event.tableName, event.keep); } else if (command === "restoreBackupAs") { return await _tableCommandRestoreBackupAs(db, target, event.tableName, event.backupId, event.destTableName); } else if (command === "exportCsv") { return await _tableCommandExportCsv(db, event.tableName, event.csvBucket, event.csvPrefix, event.csvFileName, options); } else if (command === "importCsv") { return await _tableCommandImportCsv(db, event.tableName, event.csvBucket, event.csvPrefix, event.csvFileName, options); } else { return await _tableCommandDescribeBackup(db, target, event.tableName, event.backupId); } } catch (e) { console.error("[error][" + $requestId() + "]tableCommand: ", e); return { error: "テーブル管理コマンドの実行に失敗しました: " + e.message }; } finally { await lock.release(_TABLE_LOCK_KEY); } }; // createTable: 定義済みで未作成のテーブルを作成する. const _tableCommandCreate = async function (db, currentTables, desiredTables, target) { const created = []; for (const tableName in desiredTables) { if (currentTables[tableName] == null) { await db.createTable(tableName, desiredTables[tableName]); created.push(tableName); } } return { command: "createTable", target: target, created: created }; }; // dropTable: 定義から消えたテーブルを実体ごと削除する. const _tableCommandDrop = async function (db, currentTables, desiredTables, target) { const dropped = []; for (const tableName in currentTables) { if (desiredTables[tableName] == null) { await db.dropTable(tableName); dropped.push(tableName); } } return { command: "dropTable", target: target, dropped: dropped }; }; // alterTable: 両方に存在するテーブルのカラム差分を検証→適用する. const _tableCommandAlter = async function (db, currentTables, desiredTables, target) { const targets = []; const errors = []; for (const tableName in desiredTables) { const current = currentTables[tableName]; if (current == null) { continue; } const desired = desiredTables[tableName]; if (!_samePrimaryKeyAndUnique(current.columns, desired.columns)) { errors.push(tableName + ": primaryKey/uniqueの変更は非対応です。" + "変更する場合はテーブルの再作成(dropTable→createTable)が必要です。"); continue; } const diff = _diffColumns(current.columns, desired.columns); const invalidMsg = _validateAddedColumns(desired.columns, diff.addedNames); if (invalidMsg != null) { errors.push(tableName + ": " + invalidMsg); continue; } if (diff.addedNames.length > 0 || diff.removedNames.length > 0) { targets.push({ tableName: tableName, columns: desired.columns, diff: diff }); } } // フェーズ1で1つでも検証エラーがあれば、何も適用せず中断する. if (errors.length > 0) { return { command: "alterTable", target: target, error: "検証エラーのため中断しました。", details: errors }; } // フェーズ2: 適用. const altered = []; for (let i = 0; i < targets.length; i++) { const t = targets[i]; await db.alterColumns(t.tableName, t.columns); altered.push({ tableName: t.tableName, addedColumns: t.diff.addedNames, removedColumns: t.diff.removedNames }); } return { command: "alterTable", target: target, altered: altered }; }; // alterIndex: 指定した1テーブルのインデックス差分を検証→適用する(target=indexのみ). const _tableCommandAlterIndex = async function (db, currentTables, desiredTables, tableName) { const current = currentTables[tableName]; const desired = desiredTables[tableName]; if (current == null || desired == null) { return { command: "alterIndex", target: "index", tableName: tableName, error: "テーブルが定義または現在のテーブル一覧に存在しません: " + tableName }; } const diff = _diffIndexes(current.indexes, desired.indexes); // フェーズ2: 適用(インデックス増減の検証は「未知のカラム参照」 // 「json型のインデックス化」等、createIndex/dropIndex自体が行う). for (let i = 0; i < diff.addedNames.length; i++) { const idxName = diff.addedNames[i]; await db.createIndex(tableName, idxName, desired.indexes[idxName]); } for (let i = 0; i < diff.removedNames.length; i++) { await db.dropIndex(tableName, diff.removedNames[i]); } return { command: "alterIndex", target: "index", tableName: tableName, addedIndexes: diff.addedNames, removedIndexes: diff.removedNames }; }; // backupTable: 指定テーブルの新しいバックアップ世代を作成する // (master/index両対応. indexEntryCountはtarget=indexの場合のみ含まれる). const _tableCommandBackup = async function (db, target, tableName) { const result = await db.backupTable(tableName); const ret = { command: "backupTable", target: target, tableName: tableName, backupId: result.backupId, rowCount: result.rowCount }; if (result.indexEntryCount !== undefined) { ret.indexEntryCount = result.indexEntryCount; } return ret; }; // listBackups: 指定テーブルの既存バックアップ世代一覧を返す(master/index両対応). const _tableCommandListBackups = async function (db, target, tableName) { const backupIds = await db.listBackups(tableName); return { command: "listBackups", target: target, tableName: tableName, backupIds: backupIds }; }; // restoreTable: 指定した世代の内容でテーブル(行データ・スキーマ、 // target=indexの場合はインデックスも含む)を全置換する(master/index両対応. // 差分マージはしない). const _tableCommandRestore = async function (db, target, tableName, backupId) { const result = await db.restoreTable(tableName, backupId); const ret = { command: "restoreTable", target: target, tableName: tableName, backupId: backupId, rowCount: result.rowCount }; if (result.indexEntryCount !== undefined) { ret.indexEntryCount = result.indexEntryCount; } return ret; }; // previewRestore: restoreTable実行前に、現在の行数とバックアップの // 行数を比較できるdry-run用の確認API(master/index両対応. 実際の // 復元・削除は一切行わない). const _tableCommandPreviewRestore = async function (db, target, tableName, backupId) { const result = await db.previewRestore(tableName, backupId); return { command: "previewRestore", target: target, tableName: tableName, backupId: backupId, currentRowCount: result.currentRowCount, backupRowCount: result.backupRowCount }; }; // pruneBackups: 古いバックアップ世代を削除し、直近keep世代分だけを // 残す(master/index両対応). const _tableCommandPruneBackups = async function (db, target, tableName, keep) { const result = await db.pruneBackups(tableName, keep); return { command: "pruneBackups", target: target, tableName: tableName, keep: keep, deleted: result.deleted }; }; // restoreBackupAs: バックアップの内容を、元とは別のテーブル名 // (destTableName)として新規復元する(クローン用途。master/index両対応. // destTableNameが既に存在する場合はdb側でエラーになる). const _tableCommandRestoreBackupAs = async function (db, target, tableName, backupId, destTableName) { const result = await db.restoreBackupAs(tableName, backupId, destTableName); const ret = { command: "restoreBackupAs", target: target, tableName: tableName, backupId: backupId, destTableName: destTableName, rowCount: result.rowCount }; if (result.indexEntryCount !== undefined) { ret.indexEntryCount = result.indexEntryCount; } return ret; }; // describeBackup: 指定したバックアップ世代の中身(スキーマ・行数、 // target=indexの場合はインデックスエントリ数も含む)を、復元せずに // 確認する(master/index両対応). const _tableCommandDescribeBackup = async function (db, target, tableName, backupId) { const result = await db.describeBackup(tableName, backupId); const ret = { command: "describeBackup", target: target, tableName: tableName, backupId: backupId, schema: result.schema, rowCount: result.rowCount }; if (result.indexEntryCount !== undefined) { ret.indexEntryCount = result.indexEntryCount; } return ret; }; // exportCsv: マスターテーブルの内容をCSV文字列化し、指定したS3の // csvBucket+csvPrefix+csvFileNameへアップロードする(target=masterのみ. // テーブル自体が保存されているbucketとは無関係の出力先を指定できる). const _tableCommandExportCsv = async function (db, tableName, csvBucket, csvPrefix, csvFileName, options) { const s3sdk = _g.$loadLib("s3sdk.js"); const csv = await db.exportCsv(tableName); const rows = await db.select(tableName, {}); await s3sdk.put(csvBucket, csvPrefix || "", csvFileName, csv, { region: options.region, credentials: options.credentials, noError: false }); return { command: "exportCsv", target: "master", tableName: tableName, csvBucket: csvBucket, csvPrefix: csvPrefix || "", csvFileName: csvFileName, rowCount: rows.length }; }; // importCsv: 指定したS3のcsvBucket+csvPrefix+csvFileNameからCSVを取得し、 // マスターテーブルの内容を丸ごと置き換える(target=masterのみ。既存の // 行データは全て破棄される。restoreTableと同様、置換系操作のため // db.transaction()は使わずこの関数単位でロック済みの状態のまま実行する). const _tableCommandImportCsv = async function (db, tableName, csvBucket, csvPrefix, csvFileName, options) { const s3sdk = _g.$loadLib("s3sdk.js"); const res = await s3sdk.get(csvBucket, csvPrefix || "", csvFileName, { region: options.region, credentials: options.credentials, noError: false }); const csvString = await res.Body.transformToString("utf-8"); const rowCount = await db.importCsv(tableName, csvString); await db.flush(tableName); return { command: "importCsv", target: "master", tableName: tableName, csvBucket: csvBucket, csvPrefix: csvPrefix || "", csvFileName: csvFileName, rowCount: rowCount }; }; // requestオブジェクトを取得. // 戻り値: request情報が返却されます. _g.$request = function () { if (_c_request === null) { _createRequest(_event); } return _c_request; } // responseオブジェクトを取得. // 戻り値: response情報が返却されます. _g.$response = function () { if (_c_response === null) { _createResponse(); } return _c_response; } // 汎用キャッシュオブジェクトを取得. // 戻り値: キャッシュを格納するオブジェクトが返却されます. _g.$cache = function() { if(_c_cache == null) { _c_cache = {}; } return _c_cache; } // 指定拡張子からmimeTypeを取得. // ext ファイルの拡張子を設定します. // all true の場合 mimeの定義全体を取得します. // 戻り値: mimeTypeおよびmime定義が返却されます. // all == true で存在しない場合は null 返却. _g.$mime = function (ext, all) { const ret = _getMime(ext); // mime定義全体を取得の場合. if (all == true) { if (ret === undefined) { return null; } return ret; } // mimeTypeのみ取得の場合. if (ret === undefined) { return _OCTET_STREAM; } return ret.type; } // local file i/o. const fs = require("fs"); // mime(最低限). // よく使うと思われるものをピックアップ. const _MIME = { /** プレーンテキスト. **/ txt: { type: "text/plain", gz: true } /** HTML. **/ , htm: { type: "text/html", gz: true } /** HTML. **/ , html: { type: "text/html", gz: true } /** XHTML. **/ , xhtml: { type: "application/xhtml+xml", gz: true } /** XML. **/ , xml: { type: "text/xml", gz: true } /** JSON. */ , json: { type: "application/json", gz: true } /** stylesheet. */ , css: { type: "text/css", gz: true } /** javascript. */ , js: { type: "text/javascript", gz: true } /** gif. */ , gif: { type: "image/gif", gz: false } /** jpeg. */ , jpg: { type: "image/jpeg", gz: false } /** jpeg. */ , jpeg: { type: "image/jpeg", gz: false } /** png. */ , png: { type: "image/png", gz: false } /** ico. */ , ico: { type: "image/vnd.microsoft.icon", gz: false } } // [mimeType]octet-stream. // ダウンロードファイルはこれを利用する. const _OCTET_STREAM = "application/octet-stream"; // 対象拡張子のMimeTypeを取得. const _getMime = function (ext) { let mime = _MIME[ext]; if (mime === undefined) { if (_c_mime === null) { _c_mime = _g.$loadConf(_MIME_CONF); if (_c_mime === null) { _c_mime = {}; } } return _c_mime[ext]; } return mime; } // Sqs実行パス(/public/runSqs)が設定されている場合. const isSqsFuncPath = function (path) { if (path.endsWith("/" + _SQS_FUNC_NAME)) { return true; } return false; } // フィルターパス(/public/filter)が設定されている場合. const isFilterPath = function (path) { if (path.endsWith("/" + _FILTER_NAME)) { return true; } return false; } // minto系実行プログラムが設定されている場合. const isMintoJs = function (path) { if (path.endsWith(_RUN_JS) || path.endsWith(_RUN_JHTML) || path.endsWith(_JHTML_SRC_EXTENSION)) { return true; } return false; } // sqsパラメータを処理する実行関数を呼び出す. const _runSqsFunction = async function(params) { // 実行jsを取得. let runJs = _loadJs(_SQS_FUNC_PATH()); // 実行jsを実行. await runJs.handler(params); } // filter実行ファイル(リクエスト単位で必ず実行される動的js). // ext 対象パスの拡張子を設定します. // 戻り値: true の場合処理を続行します. const _runFilter = async function (ext) { try { // 実行jsを取得. let runJs = _loadJs(_FILTER_PATH()); // 実行jsを実行. let ret = await runJs.handler(); runJs = undefined; // フィルター正常終了の場合. if (ret == true) { return true; } // $responseが利用されている場合. if (_c_response !== null) { // $response内容を取得. return _resultFilter(_c_response._$get(), ext); } // filter実行エラー返却. return _resultFilter(null, ext); } catch (e) { // エラーログ出力. console.error("[error][" + $requestId() + "]runFilter: ", e); return _errorRunJs(e, ""); } } // filter実行結果を生成. const _resultFilter = function (res, ext) { if (res === undefined || res === null) { // filter実行で処理中断の場合は // 通常403返却を行なう. return _errorStaticResult(403, ext); } // 指定条件が存在する場合のエラー返却. return _returnRunJsResponse(res, ext); } // 指定リクエストのetagが etags.json と一致するかチェック. const _httpRequestEtag = function (outEtag, path) { // キャッシュ条件が生成されていない場合. if (_c_etag === null) { // 対象パスのetag情報のファイルを取得. const etagConf = _g.$loadConf(_ETAGS_CONF_FILE); if (etagConf === null) { // 空生成. _c_etag = {}; // 存在しない場合. return false; } _c_etag = etagConf; } // パスの整形. if (!path.startsWith("/")) { path = "/" + path; } // 対象パスの定義etagを取得. const srcEtag = _c_etag[path]; if (srcEtag === undefined) { // 存在しない場合. return false; } // conf定義のetagをセット. outEtag[0] = srcEtag; // requestのetagキャッシュ確認と比較. // 一致しない場合はキャッシュ扱いしない. return srcEtag == _event.headers["if-none-match"]; } // icon情報を取得. const faviconIco = function () { const headers = { "expires": "-1" }; const srcEtag = [null]; // etagを取得. const etagCache = _httpRequestEtag(srcEtag, "/" + _FAVICON_ICO); // etagレスポンスが必要な場合. if (srcEtag[0] !== null) { // etagが存在する場合はresponseヘッダにセット. headers["etag"] = srcEtag[0]; } // iconのmimeセット. headers["content-type"] = "image/vnd.microsoft.icon"; // etagキャッシュが一致する場合. if (etagCache == true) { // キャッシュ扱いで返却する. return { statusCode: 304 , headers: headers , isBase64Encoded: false , body: "" }; } // ファイルパスをセット. const targetFile = _PUBLIC_PATH() + _FAVICON_ICO; // faviconIcoが存在しない. if (!_existsSync(targetFile)) { // 404エラー. return { statusCode: 404 , headers: headers , isBase64Encoded: false , body: "" }; } // iconを取得. const body = fs.readFileSync(targetFile); // 返却処理. return { statusCode: 200 , statusMessage: "ok" , headers: headers , isBase64Encoded: true , body: body.toString("base64") }; } // 静的 index.html ファイル名定義. const _STATIC_HTML = "index.html"; const _STATIC_HTM = "index.htm"; // 静的なローカルファイルをレスポンス返却. const _responseStaticFile = async function (path, ext) { try { // パスを取得. //if (path[0] === "/") { if (path.charCodeAt(0) === 47) { path = path.substring(1) } let targetFile = _PUBLIC_PATH() + path; // 終端が / でファイル名が設定されていない場合. if (targetFile.endsWith("/")) { // index.html or index.htm として処理する. // index.html.gz も同時にチェックする. if (_existsSync(targetFile + _STATIC_HTML) || _existsSync(targetFile + _STATIC_HTML + _PUBLIC_CONTENTS_GZ)) { // index.html を対象とする. targetFile += _STATIC_HTML; path = _STATIC_HTML; } else { // index.htm を対象とする. targetFile += _STATIC_HTM; path = _STATIC_HTM; } ext = "html"; } // etag内容の精査. const headers = { "expires": "-1" }; const srcEtag = [null]; const etagCache = _httpRequestEtag(srcEtag, path); // etagレスポンスが必要な場合. if (srcEtag[0] !== null) { // etagが存在する場合はresponseヘッダにセット. headers["etag"] = srcEtag[0]; } // mimeを取得. let gz = false; let mime = _getMime(ext); if (mime === undefined) { mime = _OCTET_STREAM; } else { gz = mime.gz; mime = mime.type; } // mimeをセット. headers["content-type"] = mime; // etagキャッシュが一致する場合. if (etagCache == true) { // キャッシュ扱いで返却する. return { statusCode: 304 , headers: headers , isBase64Encoded: false , body: "" }; } // gzip圧縮済みの静的コンテンツが存在する場合. if (_existsSync(targetFile + _PUBLIC_CONTENTS_GZ)) { // gzipのfileを取得. let body = fs.readFileSync(targetFile + _PUBLIC_CONTENTS_GZ); headers["content-encoding"] = "gzip"; // 返却処理. return { statusCode: 200 , statusMessage: "ok" , headers: headers , isBase64Encoded: true , body: body.toString("base64") }; } // 対象のファイルが存在しない場合. if (!_existsSync(targetFile)) { console.warn("[warning][" + $requestId() + "] not static file: " + targetFile); return _errorStaticResult(404, ext); } // fileを取得. let body = fs.readFileSync(targetFile); // 圧縮処理. if (gz) { // gzip処理. body = _convGZIP(body); headers["content-encoding"] = "gzip"; } // 返却処理. return { statusCode: 200 , statusMessage: "ok" , headers: headers , isBase64Encoded: true , body: body.toString("base64") }; } catch (e) { console.error("[error][" + $requestId() + "]staticFile: " + path, e); return _errorStaticResult(500, ext); } } // gzip圧縮. let _ZLIB = null; const _convGZIP = function (body) { _ZLIB = _ZLIB || require("zlib"); return _ZLIB.gzipSync(body); } // (静的ファイル)エラー返却. const _errorStaticResult = function (status, ext) { let mime = _getMime(ext); let headers = {}; let body = "" if (mime === undefined) { headers["content-type"] = "text/plain"; body = "error: " + status; } else { headers["content-type"] = mime.type; body = ""; } // ノーキャッシュヘッダをセット. _setResponseNoCacheHeaders(headers); return { statusCode: status | 0 , headers: headers , isBase64Encoded: false , body: body } } // 動的jsを実行. const _responseRunJs = async function (path, ext) { //if (path[0] === "/") { if (path.charCodeAt(0) === 47) { path = path.substring(1).trim(); } // publicディレクトリ. path = _PUBLIC_PATH() + path; // 拡張子が空の場合 // *.mt.js: js実行(こちらが優先). // *.jhtml.js: jhtml実行. // の解釈をする. if (ext === "") { // js実行でない場合. if (!_existsSync(path + _RUN_JS)) { // jhtml.js か mt.html の場合. if (_existsSync(path + _RUN_JHTML) || _existsSync(path + _JHTML_SRC_EXTENSION)) { ext = "jhtml"; // 拡張子を jhtmlにする. path += ".jhtml"; // パスに jhtml 拡張子をセット. } } } // 動的処理実行. try { let convFunc = null; if (ext === "jhtml") { // jhtml->js変換用のfunctionが設定されている場合. if (_jhtmlConvFunc !== null) { // jhtmlソースパス変換. path = path.substring(0, path.length - (ext.length + 1)) + _JHTML_SRC_EXTENSION; convFunc = _jhtmlConvFunc; // jhtml変換function. } else { // jhtml->js 変換済みの場合はjhtml実行パス変換. path = path.substring(0, path.length - (ext.length + 1)) + _RUN_JHTML; } } else { // js実行. path += _RUN_JS; } // 対象のファイルが存在しない場合. if (!_existsSync(path)) { console.warn("[warning][" + $requestId() + "] not RunJs file: " + path); return _errorStaticResult(404, (ext === "jhtml") ? "html" : "js"); } // 実行jsを取得. let runJs = _loadJs(path, convFunc); // 実行jsを実行. let body = await runJs.handler(); runJs = undefined; let response = null; // $responseが利用されている場合. if (_c_response !== null) { // $response内容を取得. response = _c_response._$get(); } else { // $responseが利用されていない場合. // 空の正常結果を対象とする. response = { status: 200, message: "ok", headers: {}, cookies: {}, body: "" } } // 実行jsから body が直接設定されている場合. if (body !== undefined && body !== null) { // 返却情報のBodyをセット. response["body"] = body; } // 戻り条件をセット. return _returnRunJsResponse(response, ext); } catch (e) { // エラーログ出力. console.error("[error][" + $requestId() + "]runJs: " + path, e); return _errorRunJs(e, ext); } } // レスポンスヘッダにキャッシュなしをセット. const _setResponseNoCacheHeaders = function (resHeader) { // レスポンスヘッダにキャッシュなしをセット. if (resHeader["last-modified"] !== undefined) { // キャッシュ返却は削除. delete resHeader["last-modified"]; } if (resHeader["etag"] !== undefined) { // キャッシュ返却は削除. delete resHeader["etag"]; } // キャッシュなし設定. resHeader["cache-control"] = "no-cache"; resHeader["pragma"] = "no-cache"; resHeader["expires"] = "-1"; } // jsやjhtmlなどの実行戻り条件をセット. const _returnRunJsResponse = function (response, ext) { let base64 = false; let contentType = response.headers["content-type"]; let body = response.body; const tof = typeof (body); // 文字列. if (tof === "string") { // コンテンツタイプが設定されていない場合. if (contentType === undefined) { if (ext === "jhtml") { response.headers["content-type"] = "text/html"; } else { response.headers["content-type"] = "application/json"; } } } else if (body instanceof Buffer) { // バイナリ返却(Buffer). body = body.toString("base64"); base64 = true; // コンテンツタイプが設定されていない場合. if (contentType === undefined) { response.headers["content-type"] = _OCTET_STREAM; } } else if (ArrayBuffer.isView(body) || body instanceof ArrayBuffer) { // バイナリ返却(typedArray or ArrayBuffer). body = Buffer.from(body).toString('base64') base64 = true; // コンテンツタイプが設定されていない場合. if (contentType === undefined) { response.headers["content-type"] = _OCTET_STREAM; } } else if (tof === "object") { // json返却. body = JSON.stringify(body); // コンテンツタイプが設定されていない場合. if (contentType === undefined) { response.headers["content-type"] = "application/json"; } // それ以外の場合. } else { // 空文字をセット. body = ""; // コンテンツタイプが設定されていない場合. if (contentType === undefined) { response.headers["content-type"] = "text/plain"; } } // cookie変換. let cookies = []; if (response.cookies !== undefined && response.cookies !== null) { // cookiesが1つでも存在する場合処理を行なう. for (let k in response.cookies) { cookies = _responseCookies(response.cookies) break; } } // キャッシュなしを設定. _setResponseNoCacheHeaders(response.headers); // status message が設定されていない場合. if (response.message === undefined || response.message === null || response.message === "") { // status message なしで返却. return { statusCode: response.status , headers: response.headers , cookies: cookies , isBase64Encoded: base64 , body: body }; } // response返却処理. return { statusCode: response.status , statusMessage: response.message , headers: response.headers , cookies: cookies , isBase64Encoded: base64 , body: body }; } // runJS結果のエラー処理. const _errorRunJs = function (e, ext) { // エラー返却. const headers = {} let body = null; let status = 500; let message = "Internal Server Error"; if (e instanceof HttpError) { // HttpErrorオブジェクトの場合. status = e.getStatus(); message = e.getMessage(); } // エラーレスポンス返却. if (ext === "jhtml") { headers["content-type"] = "text/html"; body = "" + message; } else { headers["content-type"] = "application/json"; body = JSON.stringify({ status: status, message: message }); } // ノーキャッシュヘッダをセット. _setResponseNoCacheHeaders(headers); return { statusCode: status , statusMessage: message , headers: headers , isBase64Encoded: false , body: body }; } // サーバーサイドで実行処理. // JS読み込み+実行 (キャッシュ対応). const _jsCache = new Map(); const _loadJs = function (path, convFunc) { // convFunc がある場合はキャッシュしない. if (!convFunc) { const cached = _jsCache.get(path); if (cached) return cached; } const jsBody = fs.readFileSync(path, "utf8"); const src = convFunc ? convFunc(jsBody) : jsBody; const exp = {}; Function("exports", "module", src)(exp, { exports: exp }); if (!convFunc) _jsCache.set(path, exp); return exp; }; // formパラメータ解析. const _analysisFormParams = function (n) { const list = n.split("&"); const len = list.length; const ret = {}; for (var i = 0; i < len; i++) { n = list[i].split("="); n[0] = decodeURIComponent(n[0]); if (n.length === 1) { ret[n[0]] = ""; } else { ret[n[0]] = decodeURIComponent(n[1]); } } return ret; } // パスに親ディレクトリ参照("..")セグメントが含まれるか判定する. // AIメモ: 以前は rawPath.indexOf("/../") != -1 という部分文字列一致の // みで判定していたが、パス末尾が"/.."で終わる(区切りスラッシュが // 後続に無い)場合に検知をすり抜ける抜け穴があった。セグメント単位で // 判定することでこの抜け穴を無くす. const _hasParentTraversal = function (path) { const segs = path.split("/"); const len = segs.length; for (let i = 0; i < len; i++) { if (segs[i] === "..") { return true; } } return false; } // 拡張子を取得. const _extends = function (path) { // 最後が / の場合は拡張子なし. if (path.endsWith("/")) { return undefined; } // 最後にある / の位置を取得. let p = path.lastIndexOf("/"); const ex = path.substring(p); p = ex.lastIndexOf("."); if (p === -1) { return ""; } return ex.substring(p + 1) .trim().toLowerCase(); } // existsSyncをstatSyncで代用(existsSync=Deprecated) const _existsCache = new Map(); const _existsSync = function (name) { let r = _existsCache.get(name); if (r !== undefined) return r; try { fs.statSync(name); r = true; } catch (_) { r = false; } _existsCache.set(name, r); return r; }; // 登録されたCookie情報をレスポンス用headerに設定. // 戻り値: cookieリストが返却されます. const _responseCookies = function (cookieList) { const ret = []; let em, value, len, sameSite; len = 0; sameSite = false; for (let k in cookieList) { em = cookieList[k]; // 最初の条件は key=value条件. // (cookieの名前・値はURIエンコードするが、path/domain/ // samesite等の属性値はcookie仕様上のトークン・日付等であり // URIエンコードしてはいけない(例: path="/" が "%2F" に化けて // ブラウザに無効なpath指定と解釈され、default-pathにフォール // バックしてしまう不具合があった)). value = encodeURIComponent(k) + "=" + encodeURIComponent(em.value); for (let n in em) { // valueのkey名は設定済みなので飛ばす. if (n === "value") { continue; } else if (n === "samesite") { sameSite = true; value += "; samesite=" + em[n]; // 単一設定[Secureなど]. } else if (em[n] == true) { value += "; " + n; // key=value. } else { value += "; " + n + "=" + em[n]; } } // samesiteが設定されていない場合. // samesite=laxを設定. if (!sameSite) { value += "; samesite=lax"; } ret[ret.length] = value; len++; } return ret; } // デフォルトのインデックスパス. const _DEF_INDEX_FILE = "index"; // 関数URLのリクエストを取得. // event: lambdaのeventパラメータを設定します. // 戻り値: リクエスト情報が返却されます. const _createRequest = function (event) { const o = {}; // URLパスを取得. let _path = null; o.path = function () { // cache. if (_path !== null) { return _path; } const path = event.rawPath; if (path.endsWith("/")) { _path = path + _DEF_INDEX_FILE; } else { _path = path; } return _path; }; // パスの拡張子を取得. let _ext = null; o.extends = function () { // cache. if (_ext !== null) { return _ext; } _ext = _extends(o.path()); return _ext; } // HTTPメソッドを取得. let _method = null; o.method = function () { // cache. if (_method !== null) { return _method; } _method = event.requestContext.http.method.toUpperCase(); return _method; } // httpヘッダ. let _headers = null; o.headers = function () { if (_headers !== null) { return _headers; } const ret = {}; const h = event.headers; if (h !== undefined && h !== null) { for (let k in h) { ret[k] = h[k]; // lambdaでは keyは全て小文字変換されてる. } } _headers = ret; return ret; } // 指定keyのheaderを取得. o.header = function (key) { const kv = o.headers(); if (kv === undefined) { return null; } const ret = kv[key]; if (ret === undefined) { return null; } return ret; } // httpヘッダ(cookies). let _cookies = null; o.cookies = function () { if (_cookies !== null) { return _cookies; } const ret = {}; const c = event.cookies; if (c !== undefined && c !== null) { let p, value; const len = c.length; for (let i = 0; i < len; i++) { value = decodeURIComponent(c[i]) p = value.indexOf("="); if (p === -1) { ret[value] = true; } else { ret[value.substring(0, p)] = value.substring(p + 1); } } } _cookies = ret; return ret; } // 指定keyのcookieを取得. o.cookie = function (key) { const kv = o.cookies(); if (kv === undefined) { return null; } const ret = kv[key]; if (ret === undefined) { return null; } return ret; } // protocol. o.protocol = function () { return event.requestContext.http.protocol; } // URLParams. o.urlParams = function () { const ret = event.queryStringParameters; if (ret === undefined || ret === null) { return {}; } return ret; } // パラメータを取得. let _params = null; o.params = function () { if (_params !== null) { return _params; } if (o.method() === "GET") { // urlParams. _params = o.urlParams(); return _params; } let body, isBinary; if (event.isBase64Encoded == true) { // [body]base64=binaryの場合. body = Buffer.from(event.body, 'base64'); isBinary = true; } else { // [body]stringの場合. body = event.body; isBinary = false; } const contentType = o.headers()["content-type"]; if (contentType === "application/json") { // json. if (isBinary) { body = body.toString(); isBinary = false; } _params = JSON.parse(body); } else if (contentType === "application/x-www-form-urlencoded") { // form-data. if (isBinary) { body = body.toString(); isBinary = false; } _params = _analysisFormParams(body); } else if (!isBinary) { // string(formData). _params = _analysisFormParams(body); } else { // binary. _params = {}; } return _params; } // body情報を取得. o.body = function () { if (o.method() === "GET") { return null; } if (event.isBase64Encoded == true) { // [body]base64=binaryの場合. return Buffer.from(event.body, 'base64'); } // [body]stringの場合-> binary. return Buffer.from(event.body); } _c_request = o; return o; } // レスポンス生成. const _createResponse = function () { const o = {} // ステータス設定. let _state = 200; let _state_msg = ""; o.status = function (status, message) { if (message === null || message === undefined) { message = null; } _state = status; _state_msg = message; } // header情報を設定. const _headers = {} o.header = function (key, value) { _headers[("" + key).trim().toLowerCase()] = value; } // ヘッダ取得や削除関連. o.headers = { get: function (key) { return _headers[("" + key).trim().toLowerCase()]; }, keys: function () { const ret = []; for (let k in _headers) { ret.push(k); } return ret; }, put(key, value) { o.header(key, value); }, remove: function (key) { key = ("" + key).trim().toLowerCase(); if (_headers[key] !== undefined) { delete _headers[key]; } } } // cookie情報. // key 対象のキー名を設定します. // value 対象のvalueを設定します. // value="value; Max-Age=2592000; Secure;" // ※必ず先頭文字は "value;" 必須. // や // value={value: value, "Max-Age": 2592000, Secure: true} // のような感じで設定します. const _cookies = {} o.cookie = function (key, value) { const vparams = {}; if (typeof (value) === "string") { // 文字列から {} に変換. let n; const list = value.split(";"); const len = list.length; for (let i = 0; i < len; i++) { n = list[i].trim(); if (i === 0) { vparams.value = n; } else { const p = n.indexOf("="); if (p === -1) { vparams[n] = true; } else { vparams[n.substring(0, p)] = n.substring(p + 1); } } } } else { // objectの変換. for (let k in value) { // Date => String変換. if (value[k] instanceof Date) { vparams[k] = value[k].toUTCString(); } else { vparams[k] = value[k]; } } } _cookies[("" + key).trim().toLowerCase()] = vparams; } // bodyを設定. let _body = undefined; o.body = function (body) { _body = body; } // contentType. o.contentType = function (mime, charset) { if (typeof (charset) === "string" && charset.length > 0) { mime += "; charset=" + charset; } _headers["content-type"] = mime; } // リダイレクト. o.redirect = function (url, params, status) { if (status === undefined) { status = 301; } else { const srcStatus = status; if (isNaN(status = parseInt(status))) { throw new Error("The set HTTP status " + srcStatus + " is not a number."); } } if (typeof (url) !== "string") { throw new Error("No redirect URL specified"); } if (params !== undefined) { // パラメータが存在する場合セット. if (typeof (params) !== "string") { let cnt = 0 let pms = ""; for (let k in params) { if (cnt !== 0) { pms += "&"; } pms += decodeURIComponent(k) + "=" + decodeURIComponent(params[k]); cnt++; } params = pms; } // パラメータを追加. if (url.indexOf("?") !== -1) { url += "&"; } else { url += "?"; } url += params; } _headers["location"] = url; o.status(status); o.body(""); } // レスポンスパラメータを取得. o._$get = function () { return { status: _state, message: _state_msg, headers: _headers, cookies: _cookies, body: _body } } _c_response = o; return _c_response; } // Http系のエラー返却. const HttpError = class extends Error { #status; #message; #error; // コンストラクタ. constructor(args) { if (args === undefined || args === null) { args = {}; } if (args.status === undefined) { args.status = 500; } if (args.message === undefined) { if (args.status === 500) { args.message = "Internal Server Error"; } } if (args.error === undefined) { args.error = undefined; } // Errorの派生クラスは、thisにアクセスする前に必ずsuper()を // 呼ぶ必要がある(呼ばないとReferenceErrorになる). super(args.message); this.#status = args.status; this.#message = args.message; this.#error = args.error; // Errorオブジェクト設定. Object.defineProperty(this, 'name', { configurable: true, enumerable: false, value: this.constructor.name, writable: true, }); if (Error.captureStackTrace) { Error.captureStackTrace(this, HttpError); } } // ステータスを取得. // 戻り値: httpステータスが返却されます. getStatus() { return this.#status; } // メッセージを取得. // 戻り値: メッセージが返却されます. getMessage() { return this.#message; } // 親エラーオブジェクトを取得. // 戻り値: 親エラーオブジェクトが返却されます. getError() { return this.#error; } } _g.HttpError = HttpError; // xor128ランダム関数を生成. const createRandom = function (seed) { // xor128用変数を設定. let _a = 123456789; let _b = 362436069; let _c = 521288629; let _d = 88675123; // seed条件が設定されていない場合は // ミリ秒+ナノ秒をセット. seed = seed || Date.now() + parseInt((process.hrtime()[0] * 10000000) + (process.hrtime()[1] / 1000)); // seed条件をxor128用変数に反映. if (!isNaN(parseInt(seed))) { let hs = ((seed / 1812433253) | 0) + 1; let ls = ((seed % 1812433253) | 0) - 1; if ((ls & 0x01) === 0) { hs = (~hs) | 0; } _a = hs = (((_a * (ls)) * hs) + 1) | 0; if ((_a & 0x01) === 1) { _c = (((_c * (hs)) * ls) - 1) | 0; } } // 乱数関数返却. return function () { let t = _a; let r = t; t = (t << 11); t = (t ^ r); r = t; r = (r >> 8); t = (t ^ r); r = _b; _a = r; r = _c; _b = r; r = _d; _c = r; t = (t ^ r); r = (r >> 19); r = (r ^ t); _d = r; return r; }; }; // グローバル化: createRandom. _g.createRandom = createRandom; // デフォルトランダム生成機を生成. _g.rand = createRandom(); })(global);