0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

rex0220 アプリマイスター・レイ アプリ情報の取得

Last updated at Posted at 2025-11-27

rex0220 アプリマイスター・レイで、アプリ情報を取得してみます。

関連記事

概要

rex0220 アプリマイスター・レイで、kintone アプリ情報をマークダウン形式で出力してみます。

取得するアプリ情報

  • 基本情報
  • アプリメモ (説明)
  • フィールド一覧
  • 一覧設定
  • グラフ設定
  • プロセス管理
  • 通知設定
  • プラグイン情報
  • JavaScript / CSS カスタマイズ

アプリ情報の例

アプリ情報の一部

2025-11-27_14h03_04.png

アプリ情報取得コード

対象アプリを開いた画面で、コードを実行します。

2025-11-27_14h09_22.png

情報追加・変更は、アプリマイスター・レイに依頼しましょう。

/**
 * 🦊 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) => ({ 
            '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' 
        })[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 = '&nbsp;&nbsp;'.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 で記述されているので、``` でコード表示して
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?