317 lines
10 KiB
TypeScript
317 lines
10 KiB
TypeScript
import type { FieldLayout, FieldsJoinMapping, JoinTable, Record, RecordForParameter, SavedData, WhereCondition } from "@/types/model";
|
||
import type { isType, OneOf } from "./kintone-rest-api-client";
|
||
declare var KintoneRestAPIClient: typeof import("@kintone/rest-api-client").KintoneRestAPIClient;
|
||
export class KintoneIndexEventHandler {
|
||
private config: SavedData<FieldLayout>;
|
||
private currentApp: string;
|
||
constructor(config: SavedData<FieldLayout>, 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',
|
||
});
|
||
|
||
// const button = document.createElement('button');
|
||
// button.id = 'btn-data-fetch';
|
||
// button.textContent = this.config.buttonName;
|
||
// button.style.margin = '0 8px';
|
||
button.addEventListener('click', () => this.handleButtonClick());
|
||
|
||
headerSpace.appendChild(button);
|
||
}
|
||
|
||
// ボタンクリック
|
||
private handleButtonClick = async (): Promise<void> => {
|
||
try {
|
||
console.log('データ収集開始...');
|
||
await this.execDataFectch();
|
||
} catch (error) {
|
||
console.error('Error during data processing:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* 検索データ取得&作成処理
|
||
*/
|
||
private execDataFectch = async (): Promise<void> => {
|
||
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);
|
||
};
|
||
//現在のデータをクリアする
|
||
await this.deleteCurrentRecords();
|
||
//データを更新する
|
||
await this.saveDataToCurrentApp(mainData);
|
||
}
|
||
/**
|
||
* Appからデータを取得する
|
||
* @param joinTable 対象アプリかられレコードを取得する
|
||
* @returns
|
||
*/
|
||
private fetchDataFromApp = async (joinTable: JoinTable<FieldLayout>): Promise<Record[]> => {
|
||
// Filter 条件作成
|
||
const filter = this.getWhereCondition(joinTable.whereConditions);
|
||
|
||
const fetchFields = joinTable.fieldsMapping.map(map => this.fieldCode(map.leftField));
|
||
if (joinTable.table) {
|
||
fetchFields.push(joinTable.table);
|
||
}
|
||
// KintoneRESTAPI
|
||
const client = new KintoneRestAPIClient();
|
||
const records = await client.record.getAllRecords({
|
||
app: joinTable.app,
|
||
fields: fetchFields,
|
||
condition: filter
|
||
});
|
||
//SubTableが含まれる場合、フラットなデータに変換する
|
||
return this.convertToFlatDatas(records, joinTable.table);
|
||
|
||
}
|
||
|
||
private getWhereCondition(whereCondifions: WhereCondition<FieldLayout>[]): string {
|
||
const conds = whereCondifions
|
||
.filter((cond) => this.fieldCode(cond.field) !== '');
|
||
const condition = conds.map((cond) => {
|
||
const condValue = this.getConditionValue(cond);
|
||
return `${this.fieldCode(cond.field)} ${cond.condition} ${condValue}`;
|
||
}).join(' and ');
|
||
return condition;
|
||
}
|
||
|
||
private getConditionValue(condi: WhereCondition<FieldLayout>): string {
|
||
const field = condi.field as OneOf;
|
||
const data = condi.data;
|
||
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 ((condi.condition === "in" || condi.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] = field.value;
|
||
});
|
||
|
||
// テーブルフィールドを削除
|
||
delete flatRecord[subTable];
|
||
|
||
// 結果の配列に追加
|
||
flattenedData.push(flatRecord);
|
||
});
|
||
} else {
|
||
// サブテーブルが空の場合、親レコードをそのまま追加
|
||
const flatRecord = { ...record };
|
||
delete flatRecord[subTable];
|
||
flattenedData.push(flatRecord);
|
||
}
|
||
});
|
||
|
||
return flattenedData;
|
||
}
|
||
|
||
/**
|
||
* データLeftJoin処理
|
||
* @param mainData
|
||
* @param subData
|
||
* @param onConditions
|
||
* @param fieldsMapping
|
||
* @returns
|
||
*/
|
||
private leftJoin(
|
||
mainData: Record[],
|
||
subData: Record[],
|
||
joinTable: JoinTable<FieldLayout>
|
||
): Record[] {
|
||
|
||
const joinedRecords = mainData.map((mainRecord) => {
|
||
const matchedRecord = subData.find((subRecord) =>
|
||
joinTable.onConditions.every(
|
||
(cond) => mainRecord[this.fieldCode(cond.leftField)] === subRecord[this.fieldCode(cond.rightField)]
|
||
)
|
||
);
|
||
|
||
// マッチ出来ない場合、LEFTの列のみ返す
|
||
if (!matchedRecord) return mainRecord;
|
||
|
||
// フィールド結合
|
||
const combinedRecord: Record = { ...mainRecord };
|
||
joinTable.fieldsMapping.forEach((mapping) => {
|
||
combinedRecord[this.fieldCode(mapping.rightField)] = matchedRecord[this.fieldCode(mapping.leftField)];
|
||
});
|
||
return combinedRecord;
|
||
});
|
||
return joinedRecords;
|
||
}
|
||
|
||
/**
|
||
* 取得先はテーブルの場合の特別対応
|
||
*/
|
||
private leftJoinForTable(
|
||
mainData: Record[],
|
||
subData: Record[],
|
||
joinTable: JoinTable
|
||
): any[] {
|
||
return mainData.map((mainRecord) => {
|
||
const matchedRecord = subData.find((subRecord) => {
|
||
const subRows = subRecord[joinTable.table].value as Record[];
|
||
joinTable.onConditions.every(
|
||
(cond) => mainRecord[cond.leftField] === subRecord[cond.rightField]
|
||
)
|
||
|
||
}
|
||
|
||
);
|
||
|
||
// マッチ出来ない場合、LEFTの列のみ返す
|
||
if (!matchedRecord) return mainRecord;
|
||
|
||
// フィールド結合
|
||
const combinedRecord = { ...mainRecord };
|
||
joinTable.fieldsMapping.forEach((mapping) => {
|
||
combinedRecord[mapping.rightField] = matchedRecord[mapping.leftField];
|
||
});
|
||
return combinedRecord;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 現在アプリのすべてレコードを削除する
|
||
*/
|
||
private async deleteCurrentRecords(): Promise<void> {
|
||
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<void> {
|
||
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 }])
|
||
)
|
||
);
|
||
}
|
||
} |