hsueh chiahao
2025-10-13 11:14:10 +08:00
commit cea4d4eea1
57 changed files with 20980 additions and 0 deletions

View File

@@ -0,0 +1,342 @@
import type { FieldLayout, FieldsJoinMapping, JoinTable, Record, RecordForParameter, SavedData, StringValue,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<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',
});
button.addEventListener('click', () => this.handleButtonClick());
headerSpace.appendChild(button);
}
// ボタンクリック
private handleButtonClick = async (): Promise<void> => {
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<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);
// console.log("LeftJoin", mainData);
};
//現在のデータをクリアする
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);
}
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<FieldLayout>[]): 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, value: StringValue): string {
if (!value) return "\"\"";
if(this.isStringArray(value)){
//マルチデータの場合
const items = (value as string[]).map(item => `"${item.trim()}"`);
return `(${items.join(",")})`;
}
const data = value as string;
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)){
return `"${data}"`;
}else if(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}"`;
}
}
private isStringArray=(value:any)=>{
if(Array.isArray(value) && value.every(x=>typeof x ==='string')){
return true;
}
return false;
}
/**
* 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<FieldLayout>
): 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 || matchedRecords.length==0) {
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<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 }])
)
);
}
}