import type { FieldLayout, FieldsJoinMapping, JoinTable, Record, RecordForParameter, SavedData, WhereCondition } from "@/types/model"; import { type OneOf, isType } from "./field-types"; import type { ConditionValue } from "./conditions"; declare var KintoneRestAPIClient: typeof import("@kintone/rest-api-client").KintoneRestAPIClient; export class KintoneIndexEventHandler { private config: SavedData; private currentApp: string; constructor(config: SavedData, currentApp: string) { this.config = config; this.currentApp = currentApp; } public init(): void { this.addButtonToView(); } // ボタン追加 private addButtonToView(): void { const headerSpace = kintone.app.getHeaderMenuSpaceElement(); if (!headerSpace) { throw new Error('このページではヘッダー要素が利用できません。'); }; // ボタン追加 if (document.getElementById('btn-data-fetch')) return; const kuc = Kucs['1.18.0']; const button = new kuc.Button({ text: this.config.buttonName, type: "submit", id: 'btn-data-fetch', }); button.addEventListener('click', () => this.handleButtonClick()); headerSpace.appendChild(button); } // ボタンクリック private handleButtonClick = async (): Promise => { const spinner = this.showSpinner(); try { console.log('データ収集開始...'); await this.execDataFectch(); spinner.close(); location.reload(); } catch (error) { spinner.close(); const detailError = (error instanceof Error) ? "\n詳細:" + error.message : ""; const errorMsg = `データ収集中処理中例外発生しました。${detailError}`; console.error(errorMsg, error); window.alert(errorMsg); } } private showSpinner() { const kuc = Kucs['1.18.0']; const spinner = new kuc.Spinner({ text: 'データ収集中', container: document.body }); spinner.open(); return spinner; } /** * 検索データ取得&作成処理 */ private execDataFectch = async (): Promise => { const mainTable = this.config.joinTables[0]; const mainRecords = await this.fetchDataFromApp(mainTable); // フィールド結合 let mainData = mainRecords.map((record) => { const rightRecord: Record = {}; mainTable.fieldsMapping.forEach((mapping => { rightRecord[this.fieldCode(mapping.rightField)] = record[this.fieldCode(mapping.leftField)] })); return rightRecord; }); const joinTables = this.config.joinTables.filter((table, index) => index > 0); for (const table of joinTables) { const subDatas = await this.fetchDataFromApp(table); mainData = this.leftJoin(mainData, subDatas, table); // console.log("LeftJoin", mainData); }; //現在のデータをクリアする await this.deleteCurrentRecords(); //データを更新する await this.saveDataToCurrentApp(mainData); } /** * Appからデータを取得する * @param joinTable 対象アプリかられレコードを取得する * @returns */ private fetchDataFromApp = async (joinTable: JoinTable): Promise => { // Filter 条件作成 const filter = this.getWhereCondition(joinTable.whereConditions); //取得列を設定する const fetchFields = joinTable.fieldsMapping.map(map => this.fieldCode(map.leftField)); if (joinTable.table) { fetchFields.push(joinTable.table); } const onFields =joinTable.onConditions.map(cond=>this.fieldCode(cond.leftField)); onFields.forEach(fld=>{ if(!fetchFields.includes(fld)){ fetchFields.push(fld); } }); // KintoneRESTAPI const client = new KintoneRestAPIClient(); const records = await client.record.getAllRecords({ app: joinTable.app, fields: fetchFields, condition: filter }); //console.log("Data Fetch", records); //SubTableが含まれる場合、フラットなデータに変換する return this.convertToFlatDatas(records, joinTable.table); } /** * 絞り込み条件式作成 * @param whereCondifions * @returns */ private getWhereCondition(whereCondifions: WhereCondition[]): string { const conds = whereCondifions .filter((cond) => this.fieldCode(cond.field) !== ''); const condition = conds.map((cond) => { let condition = cond.condition; if ("subField" in cond.field && cond.field.subField) { condition = this.mapConditionForSubField(cond.condition); } const condValue = this.getConditionValue(cond.field as OneOf, condition, cond.data); return `${this.fieldCode(cond.field)} ${condition} ${condValue}`; }).join(' and '); return condition; } /** * サブフィールドの演算子対応 * @param condition * @returns */ private mapConditionForSubField(condition: ConditionValue): ConditionValue { switch (condition) { case "=": return "in"; case "!=": return "not in"; default: return condition; // 既存の条件をそのまま使用 } } private getConditionValue(field: OneOf, condition: ConditionValue, data: string): string { if (!data) return ""; if (isType.NUMBER(field) || isType.RECORD_NUMBER(field)) { // For numbers, return as is return data; } else if (isType.DATE(field)) { // If field is DATE, format as "yyyy-MM-dd" unless it's a reserved function if (/^\d{4}-\d{2}-\d{2}$/.test(data) || data.match(/^\w+\(.*\)$/)) { return `"${data}"`; } const date = new Date(data); return `"${date.toISOString().split('T')[0]}"`; } else if (isType.DATETIME(field) || isType.CREATED_TIME(field) || isType.UPDATED_TIME(field)) { // If field is DATETIME, format as "yyyy-MM-ddTHH:mm:ssZ" if (data.match(/^\w+\(.*\)$/)) { return `"${data}"`; } const dateTime = new Date(data); return `"${dateTime.toISOString()}"`; } else if ((condition === "in" || condition === "not in")) { if (data.includes(",")) { // Handle "in" and "not in" with comma-separated strings const items = data.split(",").map(item => `"${item.trim()}"`); return `(${items.join(",")})`; } else { return `("${data}")`; } } else { // Default case for other types (treat as text) return `"${data}"`; } } /** * fieldからコードを取得する * @param field * @returns */ private fieldCode(field: any): string { if (!field) { return ""; } if (typeof field === 'string' && field) { return field; } else if (typeof field === 'object' && 'code' in field) { return field.code; } return ""; } /** * ネストされたサブテーブルデータをフラットなデータに変換し、親レコードを複製します。 * @param records レコードの配列 * @returns 変換後のフラットなレコード配列 */ private convertToFlatDatas(records: Record[], subTable: string): Record[] { if (!subTable) { return records; } const flattenedData: Record[] = []; records.forEach((record) => { // テーブルフィールドが存在するかを確認 if (record[subTable]?.type === "SUBTABLE" && record[subTable].value.length > 0) { // サブテーブル内の各レコードを処理 record[subTable].value.forEach((nested: Record) => { // 親レコードのコピーを作成 const flatRecord = { ...record }; // サブテーブルフィールドを抽出してフラットな構造に追加 Object.entries(nested.value).forEach(([key, field]) => { flatRecord[key] = { value: field.value, type: field.type }; }); // テーブルフィールドを削除 delete flatRecord[subTable]; // 結果の配列に追加 flattenedData.push(flatRecord); }); } else { // サブテーブルが空の場合、親レコードをそのまま追加 const flatRecord = { ...record }; delete flatRecord[subTable]; flattenedData.push(flatRecord); } }); // console.log("FlatDatas=>", flattenedData); return flattenedData; } /** * データLeftJoin処理 * @param mainData * @param subData * @param onConditions * @param fieldsMapping * @returns */ private leftJoin( mainData: Record[], subData: Record[], joinTable: JoinTable ): Record[] { const joinedRecords: Record[] = []; mainData.forEach((mainRecord) => { const matchedRecords = subData.filter((subRecord) => joinTable.onConditions.every( (cond) => mainRecord[this.fieldCode(cond.rightField)]?.value === subRecord[this.fieldCode(cond.leftField)]?.value ) ); // マッチ出来ない場合、LEFTの列のみ返す if (!matchedRecords) { joinedRecords.push(mainRecord); } else { matchedRecords.forEach((matchedRecord) => { // フィールド結合 const combinedRecord: Record = { ...mainRecord }; joinTable.fieldsMapping.forEach((mapping) => { combinedRecord[this.fieldCode(mapping.rightField)] = matchedRecord[this.fieldCode(mapping.leftField)]; }); joinedRecords.push(combinedRecord); }); } }); return joinedRecords; } /** * 現在アプリのすべてレコードを削除する */ private async deleteCurrentRecords(): Promise { const client = new KintoneRestAPIClient(); const currentRecords = await client.record.getAllRecords({ app: this.currentApp, fields: ["$id"], }); const deleteRecords = currentRecords.map(record => { return { id: record.$id.value as string } }); await client.record.deleteAllRecords({ app: this.currentApp, records: deleteRecords }); client.record.addAllRecords } /** * 結合後のデータを現在のアプリに挿入する * @param records */ private async saveDataToCurrentApp(records: Record[]): Promise { try { const client = new KintoneRestAPIClient(); const result = await client.record.addAllRecords({ app: this.currentApp, records: this.convertForUpdate(records) }); } catch (error) { console.error('データ作成時エラーが発生しました:', error); throw error; } } /** * Recordを更新時の形式を変換する * @param resords * @returns */ private convertForUpdate(resords: Record[]): RecordForParameter[] { return resords.map((record) => Object.fromEntries( Object.entries(record).map(([fieldCode, { value }]) => [fieldCode, { value }]) ) ); } }