rex0220 アプリマイスター・レイで、アプリ情報を取得してみます。
関連記事
概要
rex0220 アプリマイスター・レイで、kintone アプリ情報をマークダウン形式で出力してみます。
取得するアプリ情報
- 基本情報
- アプリメモ (説明)
- フィールド一覧
- 一覧設定
- グラフ設定
- プロセス管理
- 通知設定
- プラグイン情報
- JavaScript / CSS カスタマイズ
アプリ情報の例
アプリ情報の一部
アプリ情報取得コード
対象アプリを開いた画面で、コードを実行します。
情報追加・変更は、アプリマイスター・レイに依頼しましょう。
/**
* 🦊 kintone App Meister Ray's Work
* Project: kintoneアプリ設計書出力(Markdown版)
* Version: 2.9
* Author: rex0220
* Description: 現在開いているアプリの設定情報を取得し、Markdownファイルとしてダウンロードします。
* Update: カスタマイズビューのHTMLソース表示を修正(エスケープ処理・余計な装飾の削除)。
*/
(async () => {
// --- 設定 ---
const IS_PREVIEW = false;
const APP_ID = kintone.app.getId();
const NOW = new Date();
const SCRIPT_VER = '2.9';
const AUTHOR = 'rex0220';
const BASE_URL = location.origin;
if (!APP_ID) {
console.error('🦊 レイ: アプリの画面を開いてから実行してください!');
alert('アプリの画面を開いてから実行してください。');
return;
}
console.log(`🦊 レイ: アプリ(ID: ${APP_ID}) の情報を収集中です...`);
try {
// -------------------------------------------------------
// 1. APIパス & ヘルパー関数定義
// -------------------------------------------------------
const pathApp = (endpoint) => kintone.api.url(`/k/v1/${IS_PREVIEW ? 'preview/' : ''}app/${endpoint}`, true);
const pathRoot = (endpoint) => kintone.api.url(`/k/v1/${IS_PREVIEW ? 'preview/' : ''}${endpoint}`, true);
const pathBase = (endpoint) => kintone.api.url(`/k/v1/${endpoint}`, true);
const pad = (n) => n.toString().padStart(2, '0');
const formatDate = (dateObjOrIsoString) => {
if (!dateObjOrIsoString) return '-';
const d = new Date(dateObjOrIsoString);
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
};
const formatDateForFile = (d) => `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}`;
const formatEntity = (entity) => {
if (!entity) return '-';
const typeMap = { 'USER': 'ユーザー', 'GROUP': 'グループ', 'ORGANIZATION': '組織', 'CREATOR': '作成者', 'FIELD_ENTITY': 'フィールド値', 'CUSTOM_FIELD': 'カスタムフィールド' };
const typeStr = typeMap[entity.type] || entity.type;
const codeStr = entity.code ? ` (${entity.code})` : '';
return `${typeStr}${codeStr}`;
};
const formatTargets = (targets) => {
if (!targets || targets.length === 0) return '設定なし';
return targets.map(t => {
const ent = formatEntity(t.entity);
return t.includeSubs ? `${ent} (下部組織含む)` : ent;
}).join('<br>');
};
// 【追加】HTMLエスケープ関数
const escapeHtml = (str) => {
if (!str) return '';
return str.replace(/[&<>"']/g, (m) => ({
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
})[m]);
};
// -------------------------------------------------------
// 2. 先行データ取得 (アクション & フィールド定義)
// -------------------------------------------------------
const [actionsResp, fieldsResp] = await Promise.all([
kintone.api(pathApp('actions'), 'GET', { app: APP_ID }),
kintone.api(pathApp('form/fields'), 'GET', { app: APP_ID })
]);
const fieldProps = fieldsResp.properties;
let updatedTimeCode = '更新日時';
let createdTimeCode = '作成日時';
Object.values(fieldProps).forEach(f => {
if (f.type === 'UPDATED_TIME') updatedTimeCode = f.code;
if (f.type === 'CREATED_TIME') createdTimeCode = f.code;
});
// -------------------------------------------------------
// 3. 関連アプリIDの収集
// -------------------------------------------------------
const relatedAppIds = new Set();
if (actionsResp.actions) {
Object.values(actionsResp.actions).forEach(a => {
if (a.destApp && a.destApp.app) relatedAppIds.add(Number(a.destApp.app));
});
}
Object.values(fieldProps).forEach(f => {
if (f.lookup && f.lookup.relatedApp && f.lookup.relatedApp.app) {
relatedAppIds.add(Number(f.lookup.relatedApp.app));
}
if (f.type === 'REFERENCE_TABLE' && f.referenceTable && f.referenceTable.relatedApp && f.referenceTable.relatedApp.app) {
relatedAppIds.add(Number(f.referenceTable.relatedApp.app));
}
if (f.type === 'SUBTABLE' && f.fields) {
Object.values(f.fields).forEach(sf => {
if (sf.lookup && sf.lookup.relatedApp && sf.lookup.relatedApp.app) {
relatedAppIds.add(Number(sf.lookup.relatedApp.app));
}
});
}
});
const allAppIds = [...new Set([Number(APP_ID), ...relatedAppIds])];
// -------------------------------------------------------
// 4. 残りのデータ取得 (APIコール)
// -------------------------------------------------------
const fetchSystemPlugins = async () => {
try { return (await kintone.api(kintone.api.url('/k/v1/plugins', true), 'GET', {})).plugins || []; }
catch (e) { return []; }
};
const fetchCategoriesJsApi = async () => {
if (kintone.app.getCategories) {
try { return await kintone.app.getCategories(); }
catch (e) { return { enabled: false, categories: [] }; }
}
return { enabled: false, categories: [] };
};
const fetchRecordStats = async () => {
try {
const [updateResp, createResp] = await Promise.all([
kintone.api(kintone.api.url('/k/v1/records', true), 'GET', { app: APP_ID, query: `order by ${updatedTimeCode} desc limit 1`, totalCount: true }),
kintone.api(kintone.api.url('/k/v1/records', true), 'GET', { app: APP_ID, query: 'order by $id desc limit 1', fields: ['$id', createdTimeCode] })
]);
return {
totalCount: updateResp.totalCount,
lastUpdate: (updateResp.records[0]) ? updateResp.records[0][updatedTimeCode].value : null,
lastRecordId: (createResp.records[0]) ? createResp.records[0]['$id'].value : null,
lastCreate: (createResp.records[0]) ? createResp.records[0][createdTimeCode].value : null
};
} catch (e) { return null; }
};
const externalFieldMap = {};
if(relatedAppIds.size > 0){
await Promise.all([...relatedAppIds].map(async (rid) => {
if(rid === Number(APP_ID)) { externalFieldMap[rid] = fieldProps; return; }
try {
const res = await kintone.api(kintone.api.url('/k/v1/app/form/fields', true), 'GET', { app: rid });
externalFieldMap[rid] = res.properties;
} catch(e){ externalFieldMap[rid] = {}; }
}));
}
const [
appsResp, settings, formLayout, views, reports, status,
appPlugins, systemPlugins, notifyGeneral, notifyRecord, notifyReminder,
customize, aclApp, aclRecord, aclField, recordStats, categoriesResp
] = await Promise.all([
kintone.api(pathBase('apps'), 'GET', { ids: allAppIds }),
kintone.api(pathApp('settings'), 'GET', { app: APP_ID }),
kintone.api(pathApp('form/layout'), 'GET', { app: APP_ID }),
kintone.api(pathApp('views'), 'GET', { app: APP_ID }),
kintone.api(pathApp('reports'), 'GET', { app: APP_ID }),
kintone.api(pathApp('status'), 'GET', { app: APP_ID }),
kintone.api(pathApp('plugins'), 'GET', { app: APP_ID }),
fetchSystemPlugins(),
kintone.api(pathApp('notifications/general'), 'GET', { app: APP_ID }),
kintone.api(pathApp('notifications/perRecord'), 'GET', { app: APP_ID }),
kintone.api(pathApp('notifications/reminder'), 'GET', { app: APP_ID }),
kintone.api(pathApp('customize'), 'GET', { app: APP_ID }),
kintone.api(pathApp('acl'), 'GET', { app: APP_ID }),
kintone.api(kintone.api.url('/k/v1/preview/record/acl.json', true), 'GET', { app: APP_ID }),
kintone.api(pathRoot('field/acl'), 'GET', { app: APP_ID }),
fetchRecordStats(),
fetchCategoriesJsApi()
]);
// -------------------------------------------------------
// 5. 変数定義と前処理
// -------------------------------------------------------
const appNameMap = {};
if (appsResp.apps) {
appsResp.apps.forEach(app => { appNameMap[app.appId] = app; });
}
const appInfo = appNameMap[APP_ID] || {};
const appName = settings.name || appInfo.name || '名称不明';
const appCode = appInfo.code || '(なし)';
const isGuest = location.pathname.includes('/guest/');
let appUrl = `${BASE_URL}/k/${APP_ID}/`;
if (isGuest && appInfo.spaceId) {
appUrl = `${BASE_URL}/k/guest/${appInfo.spaceId}/${APP_ID}/`;
}
const spaceIdStr = appInfo.spaceId ? `${appInfo.spaceId}${isGuest ? ' (ゲスト)' : ''}` : '-';
let spaceName = 'なし (ポータル)';
if (appInfo.spaceId) {
try {
const spaceResp = await kintone.api(pathBase('space'), 'GET', { id: appInfo.spaceId });
spaceName = spaceResp.name;
} catch (e) { spaceName = `(ID: ${appInfo.spaceId} / 取得失敗)`; }
}
let iconStr = '-';
if (settings.icon) {
if (settings.icon.type === 'PRESET') iconStr = `Preset: ${settings.icon.key}`;
else if (settings.icon.type === 'FILE') iconStr = `File: ${settings.icon.file.name}`;
}
const formatCategories = (catsObj, depth = 0) => {
if (!catsObj) return '';
const catsArray = Object.values(catsObj).sort((a, b) => Number(a.index) - Number(b.index));
let result = '';
catsArray.forEach(c => {
const indent = ' '.repeat(depth);
const icon = depth > 0 ? '└ ' : '• ';
result += `<br>${indent}${icon}${c.name || c.label}`;
if (c.children && Object.keys(c.children).length > 0) result += formatCategories(c.children, depth + 1);
});
return result;
};
const categoryTreeStr = categoriesResp.categories ? formatCategories(categoriesResp.categories) : '';
// -------------------------------------------------------
// 6. レイアウト解析 (POS計算)
// -------------------------------------------------------
const orderedFields = [];
const processedCodes = new Set();
let topLevelCounter = 0;
const traverseLayout = (layoutList, parentPrefix = '', parentGroupCode = null) => {
let groupInnerRowCounter = 0;
layoutList.forEach(row => {
let currentPosPrefix = '';
if (!parentPrefix) {
topLevelCounter++;
currentPosPrefix = `${topLevelCounter}`;
} else {
groupInnerRowCounter++;
currentPosPrefix = `${parentPrefix}-${groupInnerRowCounter}`;
}
let locationStr = 'row';
if (parentGroupCode) locationStr = `group (${parentGroupCode})`;
if (row.type === 'ROW') {
row.fields.forEach((f, idx) => {
const pos = `${currentPosPrefix}-${idx + 1}`;
const sizeInfo = f.size || {};
if (fieldProps[f.code]) {
const fieldProp = fieldProps[f.code];
if (fieldProp.type === 'REFERENCE_TABLE') {
orderedFields.push({ ...fieldProp, _pos: pos, _location: 'refer', _size: sizeInfo });
processedCodes.add(f.code);
if (fieldProp.referenceTable && fieldProp.referenceTable.displayFields) {
fieldProp.referenceTable.displayFields.forEach((df, dfIdx) => {
orderedFields.push({
type: '(Display)', code: df, label: df, _pos: `${pos}-${dfIdx + 1}`, _location: `refer (${f.code})`,
_isReferenceChild: true, _parentRef: fieldProp
});
});
}
} else {
orderedFields.push({ ...fieldProp, _pos: pos, _location: locationStr, _size: sizeInfo });
processedCodes.add(f.code);
}
} else if (f.elementId) {
orderedFields.push({ type: 'SPACER', code: f.elementId, label: '(スペース)', _pos: pos, _location: locationStr, _size: sizeInfo, _isSpacer: true });
} else if (f.type === 'LABEL') {
orderedFields.push({ type: 'LABEL', code: '-', label: f.label || '(ラベル)', _pos: pos, _location: locationStr, _size: sizeInfo, _isLayoutItem: true });
} else if (f.type === 'HR') {
orderedFields.push({ type: 'HR', code: '-', label: '(罫線)', _pos: pos, _location: locationStr, _size: sizeInfo, _isLayoutItem: true });
}
});
} else if (row.type === 'SUBTABLE') {
if (fieldProps[row.code]) {
orderedFields.push({ ...fieldProps[row.code], _pos: currentPosPrefix, _location: 'table' });
processedCodes.add(row.code);
}
row.fields.forEach((f, idx) => {
const pos = `${currentPosPrefix}-${idx + 1}`;
const parentProp = fieldProps[row.code];
const sizeInfo = f.size || {};
if (parentProp && parentProp.fields && parentProp.fields[f.code]) {
orderedFields.push({ ...parentProp.fields[f.code], _pos: pos, _location: `table (${row.code})`, _inTable: true, _size: sizeInfo });
}
});
} else if (row.type === 'GROUP') {
if (fieldProps[row.code]) {
orderedFields.push({ ...fieldProps[row.code], _pos: currentPosPrefix, _location: 'group' });
processedCodes.add(row.code);
}
if (row.layout) traverseLayout(row.layout, currentPosPrefix, row.code);
}
});
};
traverseLayout(formLayout.layout);
const unplacedFields = [];
const lookupDestinations = {};
const collectLookupDest = (props) => {
Object.keys(props).forEach(key => {
const f = props[key];
if (f.lookup && f.lookup.fieldMappings) {
f.lookup.fieldMappings.forEach(mapping => {
lookupDestinations[mapping.field] = {
lookupCode: f.code,
relatedField: mapping.relatedField
};
});
}
if (f.type === 'SUBTABLE' && f.fields) collectLookupDest(f.fields);
});
};
collectLookupDest(fieldProps);
Object.keys(fieldProps).forEach(key => {
if (!processedCodes.has(key)) {
const f = fieldProps[key];
if (f.type !== 'SUBTABLE') {
let loc = '(未配置)';
if (f.type === 'CATEGORY') loc = '(カテゴリー)';
else if (['STATUS', 'STATUS_ASSIGNEE'].includes(f.type)) loc = '(プロセス管理)';
else if (['RECORD_NUMBER', 'CREATOR', 'MODIFIER', 'CREATED_TIME', 'UPDATED_TIME'].includes(f.type)) loc = '(システム)';
unplacedFields.push({ ...f, _pos: '-', _location: loc });
}
}
});
const definedOrder = ['STATUS', 'STATUS_ASSIGNEE', 'CATEGORY'];
unplacedFields.sort((a, b) => {
const idxA = definedOrder.indexOf(a.type);
const idxB = definedOrder.indexOf(b.type);
if (idxA !== -1 && idxB !== -1) return idxA - idxB;
if (idxA !== -1) return -1;
if (idxB !== -1) return 1;
return 0;
});
// -------------------------------------------------------
// 7. Markdown生成処理
// -------------------------------------------------------
const md = [];
const statusMap = { 'ACTIVATED': '運用中', 'DEACTIVATED': '停止中', 'FLUSHED': '削除済み' };
let appStatus = statusMap[appInfo.status] || appInfo.status || '運用中 (推定)';
const check = (bool) => bool ? '✅ 有効' : 'ー 無効';
const roundMap = { 'HALF_EVEN': '最近接偶数への丸め', 'UP': '切り上げ', 'DOWN': '切り捨て' };
const recCountStr = recordStats ? `${recordStats.totalCount} 件` : '(取得不可)';
const lastRecUpdateStr = (recordStats && recordStats.lastUpdate) ? formatDate(recordStats.lastUpdate) : '-';
const lastRecIdStr = (recordStats && recordStats.lastRecordId) ? recordStats.lastRecordId : '-';
const lastRecCreateStr = (recordStats && recordStats.lastCreate) ? formatDate(recordStats.lastCreate) : '-';
// --- ヘッダー ---
md.push(`# アプリ設計書: ${appName}`);
md.push(`> Ver.${SCRIPT_VER} / By ${AUTHOR}`);
md.push(`\n**基本情報**\n`);
md.push(`| 項目 | 内容 |`);
md.push(`| --- | --- |`);
md.push(`| 環境URL | ${BASE_URL} |`);
md.push(`| アプリID | ${APP_ID} |`);
md.push(`| アプリコード | ${appCode} |`);
md.push(`| アプリ名 | [${appName}](${appUrl}) |`);
md.push(`| ステータス | ${appStatus} |`);
md.push(`| テーマ | ${settings.theme || 'Default'} |`);
md.push(`| アイコン | ${iconStr} |`);
md.push(`| リビジョン | ${settings.revision || '-'} |`);
md.push(`| 所属スペース | ${spaceName} (ID: ${spaceIdStr}) |`);
md.push(`| スレッドID | ${appInfo.threadId || '-'} |`);
md.push(`| 作成者 | ${appInfo.creator ? appInfo.creator.name : '-'} (${appInfo.creator ? appInfo.creator.code : '-'}) |`);
md.push(`| 作成日時 | ${formatDate(appInfo.createdAt)} |`);
md.push(`| 最終更新者 | ${appInfo.modifier ? appInfo.modifier.name : '-'} (${appInfo.modifier ? appInfo.modifier.code : '-'}) |`);
md.push(`| 最終更新日時 | ${formatDate(appInfo.modifiedAt)} |`);
md.push(`| 情報取得日時 | ${formatDate(NOW)} |`);
md.push(`| レコード数 | ${recCountStr} |`);
md.push(`| 最終レコードID | ${lastRecIdStr} |`);
md.push(`| レコード最終追加日時 | ${lastRecCreateStr} |`);
md.push(`| レコード最終更新日時 | ${lastRecUpdateStr} |`);
// --- 詳細設定 ---
md.push(`\n**詳細設定**\n`);
md.push(`| 項目 | 設定値 |`);
md.push(`| --- | --- |`);
md.push(`| レコードタイトル | ${settings.titleField ? `\`${settings.titleField.code}\`` : 'なし'} |`);
md.push(`| 年度開始月 | ${settings.firstMonthOfFiscalYear}月 |`);
md.push(`| サムネイル表示 | ${check(settings.enableThumbnails)} |`);
md.push(`| レコード一括削除 | ${check(settings.enableBulkDeletion)} |`);
md.push(`| コメント機能 | ${check(settings.enableComments)} |`);
md.push(`| レコード再利用 | ${check(settings.enableDuplicateRecord)} |`);
md.push(`| インライン編集 | ${check(settings.enableInlineRecordEditing)} |`);
if (settings.numberPrecision) {
md.push(`\n**数値と計算の精度**\n`);
md.push(`| 項目 | 設定値 |`);
md.push(`| --- | --- |`);
md.push(`| 全体の桁数 | ${settings.numberPrecision.digits} |`);
md.push(`| 小数部の桁数 | ${settings.numberPrecision.decimalPlaces} |`);
md.push(`| 丸め方 | ${roundMap[settings.numberPrecision.roundingMode] || settings.numberPrecision.roundingMode} |`);
}
md.push(`\n**アプリメモ (説明)**\n`);
if (settings.description) md.push('```html\n' + settings.description + '\n```'); else md.push('(なし)');
md.push('\n');
// --- フィールド一覧 ---
md.push(`## 1. フィールド一覧`);
md.push(`| No. | POS | 配置 | ラベル | フィールドコード | タイプ | サイズ | 必須 | 重複 | 初期値 | 備考 |`);
md.push(`| :---: | :---: | --- | --- | --- | --- | :---: | :---: | :---: | --- | --- |`);
let fieldCounter = 1;
const createFieldRow = (f) => {
const no = fieldCounter++;
let sizeStr = '-';
if (f._size) {
const p = [];
if (f._size.width) p.push(`W:${f._size.width}`);
if (f._size.height) p.push(`H:${f._size.height}`);
if (f._size.innerHeight) p.push(`IH:${f._size.innerHeight}`);
if (p.length > 0) sizeStr = p.join(', ');
}
if (f._isReferenceChild) {
let realType = '(Display)';
const refAppId = f._parentRef.referenceTable.relatedApp.app;
if (externalFieldMap[refAppId] && externalFieldMap[refAppId][f.code]) {
realType = externalFieldMap[refAppId][f.code].type;
}
return `| ${no} | ${f._pos} | ${f._location} | └ ${f.label} | \`${f.code}\` | ${realType} | - | - | - | - | in ${f._parentRef.code} |`;
}
if (f._isSpacer) return `| ${no} | ${f._pos} | ${f._location} | (スペース) | \`${f.code}\` | SPACER | ${sizeStr} | - | - | - | Element ID |`;
if (f._isLayoutItem) return `| ${no} | ${f._pos} | ${f._location} | ${f.label} | \`${f.code}\` | ${f.type} | ${sizeStr} | - | - | - | - |`;
const isTableParent = f.type === 'SUBTABLE';
const labelPrefix = f._inTable ? '└ ' : '';
const req = f.required ? '✅' : '';
const uniq = f.unique ? '✅' : '';
let def = f.defaultValue !== undefined ? JSON.stringify(f.defaultValue) : '';
let typeDisplay = f.type;
let note = '';
if (['STATUS', 'STATUS_ASSIGNEE'].includes(f.type) && status && !status.enable) note += ' (機能無効)';
if (f.type === 'CATEGORY') {
if (categoryTreeStr) note += `設定値: ${categoryTreeStr}`;
if (categoriesResp && categoriesResp.enabled === false) note += ' (機能無効)';
}
if (f.lookup) {
typeDisplay += '<br>Lookup';
const relatedAppId = f.lookup.relatedApp ? f.lookup.relatedApp.app : '不明';
const relatedAppName = appNameMap[relatedAppId] ? appNameMap[relatedAppId].name : '';
note += `LookUp: ${relatedAppName} (ID: ${relatedAppId})<br>キー: ${f.lookup.relatedKeyField} `;
if (f.lookup.fieldMappings && f.lookup.fieldMappings.length > 0) {
const copyFields = f.lookup.fieldMappings.map(m => m.field).join(', ');
note += `<br>コピー先: [${copyFields}]`;
}
}
if (lookupDestinations[f.code]) {
const info = lookupDestinations[f.code];
typeDisplay += `<br>Copy (${info.relatedField})`;
note += `Lookup: ${info.lookupCode} `;
}
if (f.type === 'REFERENCE_TABLE') {
const ref = f.referenceTable;
const rId = ref.relatedApp.app;
const rName = appNameMap[rId] ? appNameMap[rId].name : '';
note += `参照アプリ: ${rName} (ID: ${rId})<br>`;
note += `条件: ${ref.condition.field} = ${ref.condition.relatedField}<br>`;
if (ref.filterCond) note += `絞り込み: ${ref.filterCond}`;
}
if (f.expression) note += `式: \`${f.expression}\` `;
if (f.options) {
const opts = Object.values(f.options).sort((a, b) => Number(a.index) - Number(b.index));
const optStr = opts.map(o => o.label).join(', ');
note += `選択肢: [${optStr}] `;
}
if (f.hideLabel) note += 'ラベル非表示 ';
if (f.minLength || f.maxLength) {
const min = f.minLength ? `${f.minLength}文字` : '';
const max = f.maxLength ? `${f.maxLength}文字` : '';
const range = (min && max) ? `${min}〜${max}` : (min ? `${min}以上` : `${max}以下`);
note += `文字数: ${range} `;
}
if (f.minValue !== undefined || f.maxValue !== undefined) {
const min = (f.minValue !== null && f.minValue !== '') ? f.minValue : null;
const max = (f.maxValue !== null && f.maxValue !== '') ? f.maxValue : null;
if (min !== null || max !== null) {
const limitStr = (min !== null && max !== null) ? `${min}〜${max}` : (min !== null ? `${min}以上` : `${max}以下`);
note += `値制限: ${limitStr} `;
}
}
if (f.unit) {
const pos = f.unitPosition === 'BEFORE' ? '前' : '後';
note += `単位: ${f.unit} (${pos}) `;
}
if (isTableParent) note += 'サブテーブル';
return `| ${no} | ${f._pos} | ${f._location} | ${labelPrefix}**${f.label}** | \`${f.code}\` | ${typeDisplay} | ${sizeStr} | ${req} | ${uniq} | ${def} | ${note} |`;
};
orderedFields.forEach(f => md.push(createFieldRow(f)));
if (unplacedFields.length > 0) {
md.push(`| - | - | (未配置) | **(未配置フィールド)** | | | | | | | |`);
unplacedFields.forEach(f => md.push(createFieldRow(f)));
}
md.push('\n');
// --- 一覧 (Views) ---
md.push(`## 2. 一覧設定`);
const viewNames = Object.keys(views.views);
if (viewNames.length === 0) { md.push(`- 設定なし`); } else {
md.push(`| No. | ID | 一覧名 | タイプ | 絞り込み | ソート | 表示フィールド | HTMLソース (CUSTOMのみ) |`);
md.push(`| :---: | :---: | --- | :---: | --- | --- | --- | --- |`);
const sortedViews = Object.values(views.views).sort((a, b) => Number(a.index) - Number(b.index));
sortedViews.forEach((v, index) => {
const filterStr = v.filterCond ? `\`${v.filterCond}\`` : '-';
const sortStr = v.sort ? `\`${v.sort}\`` : '-';
let fieldsContent = '-';
let htmlContent = '-';
if (v.type === 'LIST' && v.fields) fieldsContent = v.fields.join(', ');
else if (v.type === 'CALENDAR') fieldsContent = `日付: ${v.date || '-'} / タイトル: ${v.title || '-'}`;
else if (v.type === 'CUSTOM') {
fieldsContent = '(HTMLカスタマイズ)';
if (v.html) htmlContent = `<code>${escapeHtml(v.html).replace(/\r?\n/g, '<br>')}</code>`;
}
md.push(`| ${index + 1} | ${v.id} | **${v.name}** | ${v.type} | ${filterStr} | ${sortStr} | ${fieldsContent} | ${htmlContent} |`);
});
}
md.push('\n');
// --- グラフ (Reports) ---
md.push(`## 3. グラフ設定`);
const reportNames = Object.keys(reports.reports);
if (reportNames.length === 0) { md.push(`- 設定なし`); } else {
md.push(`| No. | ID | グラフ名 | タイプ | 分類 (大/中/小) | 集計 | ソート | 絞り込み |`);
md.push(`| :---: | :---: | --- | :---: | --- | --- | --- | --- |`);
const chartModeMap = { 'NORMAL': '集合', 'STACKED': '積み上げ', 'PERCENTAGE': '100%積み上げ' };
const perMap = { 'YEAR': '年', 'QUARTER': '四半期', 'MONTH': '月', 'WEEK': '週', 'DAY': '日', 'HOUR': '時', 'MINUTE': '分' };
const sortedReports = Object.values(reports.reports).sort((a, b) => Number(a.index) - Number(b.index));
sortedReports.forEach((r, index) => {
let typeStr = r.chartType;
if (r.chartMode && chartModeMap[r.chartMode]) typeStr += `<br>(${chartModeMap[r.chartMode]})`;
let groupsStr = '-';
if (r.groups && r.groups.length > 0) {
groupsStr = r.groups.map(g => {
let str = g.code;
if (g.per && perMap[g.per]) str += ` (${perMap[g.per]})`;
else if (g.level) str += ` (${g.level})`;
return str;
}).join('<br>');
}
let aggStr = '-';
if (r.aggregations && r.aggregations.length > 0) {
aggStr = r.aggregations.map(a => `${a.type}: ${a.code || '(件数)'}`).join('<br>');
}
let sortStr = '-';
if (r.sorts && r.sorts.length > 0) sortStr = r.sorts.map(s => `${s.by} (${s.order})`).join('<br>');
else if (r.sort) sortStr = r.sort;
const filterStr = r.filterCond ? `\`${r.filterCond}\`` : '-';
md.push(`| ${index + 1} | ${r.id} | **${r.name}** | ${typeStr} | ${groupsStr} | ${aggStr} | ${sortStr} | ${filterStr} |`);
});
}
md.push('\n');
// --- アプリアクション ---
md.push(`## 4. アプリアクション設定`);
if (!actionsResp.actions || Object.keys(actionsResp.actions).length === 0) {
md.push(`- 設定なし`);
} else {
md.push(`| No. | アクション名 | ID | 連携先アプリ | コピー設定 (元 ➡ 先) | 利用条件 |`);
md.push(`| :---: | --- | --- | --- | --- | --- |`);
const getAnyValue = (obj) => {
if (!obj) return '(なし)';
if (typeof obj === 'string') return obj;
if (obj.value !== undefined && obj.value !== null) return obj.value;
if (obj.code) return obj.code;
if (obj.type) return `[${obj.type}]`;
try { return JSON.stringify(obj); } catch (e) { return '(不明)'; }
};
const sortedActions = Object.values(actionsResp.actions).sort((a, b) => Number(a.index) - Number(b.index));
sortedActions.forEach((act, idx) => {
const targetAppInfo = appNameMap[act.destApp.app];
const targetAppName = targetAppInfo ? targetAppInfo.name : '名称取得不可';
const destAppStr = `**${targetAppName}** (ID: ${act.destApp.app})`;
let mappingsStr = '-';
if (act.mappings && Array.isArray(act.mappings)) {
mappingsStr = act.mappings.map(m => {
let src = '';
if (m.srcType === 'FIELD') src = m.srcField;
else if (m.srcType === 'RECORD_URL') src = 'レコードURL';
else if (m.src) src = getAnyValue(m.src);
else src = `[${m.srcType}]`;
const dest = m.destField || getAnyValue(m.dest);
return `${src} ➡ ${dest}`;
}).join('<br>');
}
const filter = act.filterCond || '-';
md.push(`| ${idx + 1} | **${act.name}** | ${act.id} | ${destAppStr} | ${mappingsStr} | \`${filter}\` |`);
});
}
md.push('\n');
// --- プロセス管理 ---
md.push(`## 5. プロセス管理`);
if (!status.enable) {
md.push(`- 無効`);
} else {
md.push(`- **有効**`);
const states = Object.values(status.states).sort((a, b) => Number(a.index) - Number(b.index));
const sortedStateNames = states.map(s => s.name);
const statusAssigneeMap = {};
states.forEach(s => {
let assigneesStr = '設定なし';
if (s.assignee && s.assignee.entities && s.assignee.entities.length > 0) {
const entities = s.assignee.entities.map(e => formatEntity(e.entity)).join(', ');
const typeMap = { 'ONE': '作業者が指定', 'ALL': '次のユーザー全員', 'ANY': '次のユーザーのうち1人' };
const typeLabel = typeMap[s.assignee.type] || s.assignee.type;
assigneesStr = `[${typeLabel}] ${entities}`;
}
statusAssigneeMap[s.name] = assigneesStr;
});
if (sortedStateNames.length > 0) {
md.push(`\n### 5-1. ステータス遷移図 (Mermaid)`);
md.push('```mermaid');
md.push('stateDiagram-v2');
md.push(' direction LR');
md.push(` [*] --> ${sortedStateNames[0]}`);
status.actions.forEach(a => { md.push(` ${a.from} --> ${a.to} : ${a.name}`); });
const lastState = sortedStateNames[sortedStateNames.length - 1];
md.push(` ${lastState} --> [*]`);
md.push('```');
}
md.push(`\n### 5-2. ステータス一覧`);
md.push(`| No. | ステータス名 | 担当者設定 |`);
md.push(`| :---: | --- | --- |`);
states.forEach((s, idx) => {
md.push(`| ${idx + 1} | ${s.name} | ${statusAssigneeMap[s.name]} |`);
});
md.push(`\n### 5-3. アクション一覧`);
md.push(`| No. | アクション名 | 作業者 (From担当者) | 実行前 | 実行後 | 利用条件 |`);
md.push(`| :---: | --- | --- | --- | --- | --- |`);
status.actions.forEach((a, idx) => {
const filter = a.filterCond ? `\`${a.filterCond}\`` : '(なし)';
const worker = statusAssigneeMap[a.from] || '-';
md.push(`| ${idx + 1} | **${a.name}** | ${worker} | ${a.from} | ${a.to} | ${filter} |`);
});
}
md.push('\n');
// --- 通知設定 ---
md.push(`## 6. 通知設定`);
md.push(`### 一般通知`);
if (notifyGeneral.notifications.length === 0) md.push(`- 設定なし`);
else {
md.push(`| No. | 対象 | レコード追加 |`);
md.push(`| :---: | --- | :---: |`);
notifyGeneral.notifications.forEach((n, idx) => {
const includeSubs = n.includeSubs ? '含む' : '含まない';
md.push(`| ${idx + 1} | ${formatEntity(n.entity)} | ${includeSubs} |`);
});
}
md.push(`### レコード通知`);
if (notifyRecord.notifications.length === 0) md.push(`- 設定なし`);
else {
md.push(`| No. | 通知の条件 | 通知先 | タイトル |`);
md.push(`| :---: | --- | --- | --- |`);
notifyRecord.notifications.forEach((n, idx) => {
const cond = n.filterCond ? `\`${n.filterCond}\`` : '(すべてのレコード)';
const targets = formatTargets(n.targets);
md.push(`| ${idx + 1} | ${cond} | ${targets} | ${n.title} |`);
});
}
md.push(`### リマインダー通知`);
if (notifyReminder.notifications.length === 0) md.push(`- 設定なし`);
else {
md.push(`**基準タイムゾーン**: ${notifyReminder.timezone || 'User Default'}\n`);
md.push(`| No. | タイトル | 通知のタイミング | 通知先 | 条件 |`);
md.push(`| :---: | --- | --- | --- | --- |`);
notifyReminder.notifications.forEach((n, idx) => {
let timingStr = `${n.timing.code}`;
const days = Number(n.timing.daysLater);
const time = n.timing.time;
if (!isNaN(days)) {
if (days === 0) timingStr += ` 当日`;
else if (days < 0) timingStr += ` ${Math.abs(days)}日前`;
else timingStr += ` ${days}日後`;
}
if (time) timingStr += ` ${time}`;
const targets = formatTargets(n.targets);
const filter = n.filterCond ? `\`${n.filterCond}\`` : '(すべてのレコード)';
md.push(`| ${idx + 1} | ${n.title} | ${timingStr} | ${targets} | ${filter} |`);
});
}
md.push('\n');
// --- アクセス権 ---
md.push(`## 7. アクセス権設定`);
md.push(`### 7-1. アプリのアクセス権`);
if (aclApp.rights.length === 0) md.push(`- 設定なし`);
else {
md.push(`| No. | 対象 | 閲覧 | 追加 | 編集 | 削除 | アプリ管理 | File読 | File出 |`);
md.push(`| :---: | --- | :---: | :---: | :---: | :---: | :---: | :---: | :---: |`);
aclApp.rights.forEach((r, index) => {
const check = (bool) => bool ? '✅' : '-';
md.push(`| ${index + 1} | ${formatEntity(r.entity)} | ${check(r.recordViewable)} | ${check(r.recordAddable)} | ${check(r.recordEditable)} | ${check(r.recordDeletable)} | ${check(r.appEditable)} | ${check(r.recordImportable)} | ${check(r.recordExportable)} |`);
});
}
md.push('\n');
md.push(`### 7-2. レコードのアクセス権`);
if (!aclRecord || !aclRecord.rights || aclRecord.rights.length === 0) {
md.push(`- 設定なし (またはAPI未提供)`);
} else {
md.push(`| 条件No. | 条件 | 項番 | 対象 | 閲覧 | 編集 | 削除 |`);
md.push(`| :---: | --- | :---: | --- | :---: | :---: | :---: |`);
aclRecord.rights.forEach((r, idx) => {
const condNo = idx + 1;
const condition = r.filterCond ? `\`${r.filterCond}\`` : '(すべてのレコード)';
r.entities.forEach((e, entityIdx) => {
const itemNo = entityIdx + 1;
const check = (bool) => bool ? '✅' : '-';
md.push(`| ${condNo} | ${condition} | ${itemNo} | ${formatEntity(e.entity)} | ${check(e.viewable)} | ${check(e.editable)} | ${check(e.deletable)} |`);
});
});
}
md.push('\n');
md.push(`### 7-3. フィールドのアクセス権`);
if (aclField.rights.length === 0) md.push(`- 設定なし`);
else {
const fieldNameMap = {};
const extractLabels = (props) => {
Object.keys(props).forEach(key => {
const f = props[key];
fieldNameMap[key] = f.label;
if (f.type === 'SUBTABLE' && f.fields) extractLabels(f.fields);
});
};
extractLabels(fieldProps);
md.push(`| Field No. | フィールド | コード | 項番 | 対象 | 権限 |`);
md.push(`| :---: | --- | --- | :---: | --- | :---: |`);
aclField.rights.forEach((r, fIdx) => {
const fieldNo = fIdx + 1;
const label = fieldNameMap[r.code] || r.code;
r.entities.forEach((e, eIdx) => {
const itemNo = eIdx + 1;
let access = '';
if (e.accessibility === 'WRITE') access = '閲覧・編集';
else if (e.accessibility === 'READ') access = '閲覧のみ';
else access = 'なし';
md.push(`| ${fieldNo} | ${label} | \`${r.code}\` | ${itemNo} | ${formatEntity(e.entity)} | ${access} |`);
});
});
}
md.push('\n');
// --- プラグイン ---
md.push(`## 8. プラグイン情報`);
if (appPlugins.plugins.length === 0) { md.push(`- 設定なし`); } else {
md.push(`| No. | プラグイン名 | ID | バージョン | ステータス |`);
md.push(`| :---: | --- | --- | --- | --- |`);
appPlugins.plugins.forEach((p, index) => {
const sysInfo = systemPlugins.find(sp => sp.id === p.id);
const version = sysInfo ? sysInfo.version : '(不明)';
md.push(`| ${index + 1} | ${p.name} | ${p.id} | ${version} | ${p.enabled ? '有効' : '無効'} |`);
});
}
md.push('\n');
// --- カスタマイズ ---
md.push(`## 9. JavaScript / CSS カスタマイズ`);
md.push(`**適用範囲**: ${customize.scope || '-'}`);
const renderResourceTable = (title, resources) => {
if (!resources || resources.length === 0) return;
md.push(`\n**${title}**`);
md.push(`| No. | タイプ | 内容 | サイズ |`);
md.push(`| :---: | :---: | --- | :---: |`);
resources.forEach((r, index) => {
const type = r.type;
let content = '-';
let size = '-';
if (type === 'URL') { content = `[Link](${r.url})`; }
else if (type === 'FILE') { content = r.file.name; size = `${Math.round(r.file.size / 1024)} KB`; }
md.push(`| ${index + 1} | ${type} | ${content} | ${size} |`);
});
};
let hasCustomize = false;
if (customize.desktop) {
if (customize.desktop.js && customize.desktop.js.length > 0) { renderResourceTable('PC用 JavaScript', customize.desktop.js); hasCustomize = true; }
if (customize.desktop.css && customize.desktop.css.length > 0) { renderResourceTable('PC用 CSS', customize.desktop.css); hasCustomize = true; }
}
if (customize.mobile) {
if (customize.mobile.js && customize.mobile.js.length > 0) { renderResourceTable('スマホ用 JavaScript', customize.mobile.js); hasCustomize = true; }
}
if (!hasCustomize) { md.push(`- 設定なし`); }
// -------------------------------------------------------
// 8. ファイルダウンロード処理
// -------------------------------------------------------
const output = md.join('\n');
const blob = new Blob([output], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `APP_${APP_ID}_${appName.replace(/\s+/g, '_')}-${formatDateForFile(NOW)}.md`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
console.log('🎉 レイ: 出力が完了しました!');
} catch (error) {
console.error('🦊 レイ: エラーが発生しました。', error);
}
})();
アプリ情報取得コードの作成手順
要件を指定して、コードを実行・検証・修正を繰り返します。
結局アプリのステータスが取得できなかったので、削除しました。
指定した kintone アプリの情報をマークダウン形式で出力したい。
下記のアプリの情報を出力するコードを作成して
・対象アプリを開いた画面で実行
・アプリ名、アプリメモ、所属スペース、"運用中"等のアプリステータス等
・項目、タイプ、制約等をまとめた表
・一覧画面、グラフ等の情報も表示(一覧、グラフが無い場合は無しと表示)
・プロセス管理設置情報:ステータス、アクション等
・通知設定
・プラグイン情報:プラグイン名、プラグインID、有効・無効
・アプリ情報は、 「Markdownファイルとしてダウンロード」 もされるように
ファイル名は、APPIDとアプリ名を付けて※例 APP_100_顧客マスタ.md
アプリ基本情報のステータスが取得されていない
| ステータス | undefined |
...
*アプリメモ (説明)** には、html で記述されているので、``` でコード表示して

