feat:lookup同期アクション

This commit is contained in:
2024-07-05 17:20:51 +09:00
parent c8f9cbda9a
commit 832d46d360
39 changed files with 2713 additions and 372 deletions

View File

@@ -0,0 +1 @@
@import 'bootstrap/scss/bootstrap';

View File

@@ -0,0 +1,272 @@
import {
IAction,
IActionResult,
IActionNode,
IActionProperty,
IContext,
IField,
} from "../types/ActionTypes";
import { actionAddins } from ".";
import type { Record} from "@kintone/rest-api-client/lib/src/client/types";
import { KintoneRestAPIClient} from "@kintone/rest-api-client";
import "./auto-lookup.scss";
import "bootstrap/js/dist/modal";
// import "bootstrap/js/dist/spinner";
import {Modal} from "bootstrap"
import $ from "jquery";
interface Props {
displayName: string;
lookupField: LookupField;
condition: Condition;
}
interface Condition {
queryString: string;
index: number;
type: string;
children: Child[];
parent: null;
logicalOperator: string;
}
interface Child {
index: number;
type: string;
parent: string;
object: any;
operator: string;
value: string;
}
interface LookupField {
app: App;
fields: Field[];
}
interface Field {
name: string;
type: string;
code: string;
label: string;
noLabel: boolean;
required: boolean;
lookup: Lookup;
}
interface Lookup {
relatedApp: RelatedApp;
relatedKeyField: string;
fieldMappings: FieldMapping[];
lookupPickerFields: any[];
filterCond: string;
sort: string;
}
interface FieldMapping {
field: string;
relatedField: string;
}
interface RelatedApp {
app: string;
code: string;
}
interface App {
id: string;
name: string;
description: string;
createdate: string;
}
export class AutoLookUpAction implements IAction {
name: string;
actionProps: IActionProperty[];
props: Props;
constructor() {
this.name = "ルックアップ更新";
this.actionProps = [];
this.props = {} as Props;
this.register();
}
/***
* アクセスのメインの処理関数
*/
async process(
prop: IActionNode,
event: any,
context: IContext
): Promise<IActionResult> {
this.actionProps = prop.actionProps;
this.props = {
...prop.ActionValue,
condition: JSON.parse((prop.ActionValue as any).condition),
} as Props;
// console.log(context);
let result = {
canNext: true,
result: "",
} as IActionResult;
try {
const lookUpFields = this.props.lookupField.fields.filter(
(f) => f.lookup && f.lookup.relatedApp.app === String(kintone.app.getId())
);
if (!lookUpFields || lookUpFields.length===0) {
throw new Error(
`ルックアップの設定は不正です。${this.props.lookupField.fields[0].label} `
);
}
const lookUpField = this.props.lookupField.fields[0];
const key = event.record[lookUpField.lookup.relatedKeyField].value;
const targetRecords = await this.getUpdateRecords(lookUpField, key);
//更新対象がない時にスキップ
if(targetRecords.length===0){
return result;
}
const updateRecords = this.convertForLookup(targetRecords,lookUpField,key);
console.log("updateRecords", updateRecords);
this.showSpinnerModel(this.props.lookupField.app);
await this.updateLookupTarget(updateRecords);
this.showResult(this.props.lookupField.app,updateRecords.length);
} catch (error) {
console.error("ルックアップ更新中例外が発生しました。", error);
if(error instanceof Error){
event.error = error.message;
}else{
event.error = "ルックアップ更新中例外が発生しました。";
}
result.canNext = false;
}
console.log("autoLookupProps", this.props);
return result;
}
/**
* REST API用クエリ作成
* TODO:共通関数として作成
* @param lookUpField
* @param key
* @returns
*/
makeQuery=(lookUpField:Field,key:any)=>{
if(typeof key==='number'){
return `${lookUpField.code} = ${key}`
}
if(typeof key==='string'){
return `${lookUpField.code} = "${key}"`
}
}
/**
* 更新対象のレコードを取得する
*/
getUpdateRecords = async (lookUpField:Field,key:any):Promise< Record[]>=>{
const client=new KintoneRestAPIClient();
const resp = await client.record.getAllRecords({
app:this.props.lookupField.app.id,
fields:["$id"],
condition:this.makeQuery(lookUpField,key)
});
return resp;
}
/**
* ルックアップ更新用レコードに変換する
* @param targetRecords 更新対象レコード
* @param lookUpField ルックアップフィールド
* @param key ルックアップフィールドの値
* @returns
*/
convertForLookup = (targetRecords:Record[],lookUpField:Field,key:any):Array<any>=>{
return targetRecords.map((r) => ({
id: Number(r["$id"].value),
record: { [lookUpField.code]: { value: key } },
}));
}
/**
* ルックアップ先を更新する
* @param updateRecords
*/
updateLookupTarget = async (updateRecords:Array<any>)=>{
if (updateRecords && updateRecords.length > 0) {
const client=new KintoneRestAPIClient();
client.record.updateAllRecords({
app:this.props.lookupField.app.id,
records:updateRecords
});
// await kintone.api(kintone.api.url("/k/v1/records.json", true), "PUT", {
// app: this.props.lookupField.app.id,
// records: updateRecords
// });
}
}
/**
* 更新中のダイアログ表示
* @param app
*/
showSpinnerModel = (app:App) => {
let dialog = $("#alcLookupModal");
if(dialog.length===0){
const modalHTML = `
<div class="modal" id="alcLookupModal" tabindex="-1" aria-labelledby="staticBackdropLabel" aria-hidden="true">
<div class="modal-dialog-centered">
<div class="modal-dialog modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="alcLookupModalLabel">ルックアップ同期処理</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row" id="app${app.id}">
<div class="spinner-border text-secondary col-1 " role="alert"></div>
<div class="col">${app.name}</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">OK</button>
</div>
</div>
</div>
</div>`;
dialog = $(modalHTML).appendTo("body");
dialog.get()[0].addEventListener('hidden.bs.modal',(ev)=>{
Modal.getOrCreateInstance(dialog.get()[0]).dispose();
$("#alcLookupModal").remove();
});
}else{
const dialogBody=$("#alcLookupModal .modal-body");
const htmlrow=`
<div class="row" id="app${app.id}">
<div class="spinner-border text-secondary col-1 " role="alert">
</div>
<div class="col">${app.name}</div>
<div>`;
dialogBody.append(htmlrow);
}
Modal.getOrCreateInstance(dialog.get()[0]).show();
}
/**
* 更新結果を表示する
* @param app  更新先アプリ情報
* @param count 更新件数
*/
showResult=(app:App,count:number)=>{
const dialogBody=$(`#alcLookupModal .modal-body #app${app.id}`);
const html=` <div class="col-1 text-success">✔</div>
<div class="col">${app.name}</div>
<div class="col">更新件数:${count}件</div>`;
dialogBody.html(html);
}
register(): void {
actionAddins[this.name] = this;
}
}
new AutoLookUpAction();

View File

@@ -0,0 +1,74 @@
import {
IAction,
IActionResult,
IActionNode,
IActionProperty,
IContext,
} from "../types/ActionTypes";
import { actionAddins } from ".";
interface Props {
displayName: string;
field: Field;
verName: VerName;
}
interface VerName {
name: string;
}
interface Field {
name: string;
type: string;
code: string;
label: string;
noLabel: boolean;
required: boolean;
minLength: string;
maxLength: string;
expression: string;
hideExpression: boolean;
unique: boolean;
defaultValue: string;
}
export class CurrentFieldGetAction implements IAction {
name: string;
actionProps: IActionProperty[];
currentFieldGetProps: Props;
constructor() {
this.name = "フィールドの値を取得する";
this.actionProps = [];
this.currentFieldGetProps = {} as Props;
this.register();
}
async process(
prop: IActionNode,
event: any,
context: IContext
): Promise<IActionResult> {
this.currentFieldGetProps = prop.ActionValue as Props;
this.actionProps = prop.actionProps;
let result = {
canNext: true,
result: "",
} as IActionResult;
try {
context.variables[this.currentFieldGetProps.verName.name] = context.record[this.currentFieldGetProps.field.code].value;
} catch (error) {
console.error("CurrentFieldGetAction error", error);
result.canNext = false;
}
return result;
}
register(): void {
actionAddins[this.name] = this;
}
}
new CurrentFieldGetAction();

View File

@@ -0,0 +1,166 @@
import {
IAction,
IActionResult,
IActionNode,
IActionProperty,
IContext,
} from "../types/ActionTypes";
import { actionAddins } from ".";
interface Props {
displayName: string;
sources: Sources;
dataMapping: DataMapping[];
}
interface DataMapping {
id: string;
from: From;
to: To;
}
interface To {
app: App;
fields: Field[];
isDialogVisible: boolean;
}
interface Field {
name: string;
type: string;
code: string;
label: string;
noLabel: boolean;
required: boolean;
minLength: string;
maxLength: string;
expression: string;
hideExpression: boolean;
unique: boolean;
defaultValue: string;
}
interface From {
sharedText: string;
_t: string;
id: string;
objectType: string;
name: Name;
actionName: string;
displayName: string;
}
interface Name {
name: string;
}
interface Sources {
app: App;
}
interface App {
id: string;
name: string;
description: string;
createdate: string;
}
export class DataMappingAction implements IAction {
name: string;
actionProps: IActionProperty[];
dataMappingProps: Props;
constructor() {
this.name = "データマッピング";
this.actionProps = [];
this.dataMappingProps = {} as Props;
this.register();
}
async process(
prop: IActionNode,
event: any,
context: IContext
): Promise<IActionResult> {
this.actionProps = prop.actionProps;
this.dataMappingProps = prop.ActionValue as Props;
console.log(prop.ActionValue);
// this.initTypedActionProps();
let result = {
canNext: true,
result: "",
} as IActionResult;
try {
const record = this.dataMappingProps.dataMapping
.filter(
(item) =>
item.from.objectType === "variable" &&
item.from.name.name &&
item.to.app &&
item.to.fields &&
item.to.fields.length > 0
)
.reduce((accumulator, item) => {
return {...accumulator, [item.to.fields[0].code]: {
value: getValueByPath(context.variables, item.from.name.name),
}};
}, {});
if (record && Object.keys(record).length > 0) {
await kintone.api(kintone.api.url("/k/v1/record.json", true), "POST", {
app: this.dataMappingProps.sources.app.id,
record: record,
});
}
} catch (error) {
console.error("DataMappingAction error", error);
result.canNext = false;
}
console.log("dataMappingProps", this.dataMappingProps);
return result;
}
register(): void {
actionAddins[this.name] = this;
}
}
new DataMappingAction();
const getValueByPath = (obj: any, path: string) => {
return path.split(".").reduce((o, k) => (o || {})[k], obj);
};
type Resp = { records: RespRecordType[] };
type RespRecordType = {
[key: string]: {
type: string;
value: any;
};
};
type Result = {
type: string;
value: any[];
};
const selectData = async (appid: string, field: string): Promise<Result> => {
return kintone
.api(kintone.api.url("/k/v1/records", true), "GET", {
app: appid ?? kintone.app.getId(),
fields: [field],
})
.then((resp: Resp) => {
const result: Result = { type: "", value: [] };
resp.records.forEach((element) => {
for (const [key, value] of Object.entries(element)) {
if (result.type === "") {
result.type = value.type;
}
result.value.push(value.value);
}
});
return result;
});
};

View File

@@ -0,0 +1,374 @@
import {
IAction,
IActionResult,
IActionNode,
IActionProperty,
IContext,
} from "../types/ActionTypes";
import { actionAddins } from ".";
interface Props {
displayName: string;
sources: Sources;
condition: string;
conditionO: Condition;
verName: VerName;
}
interface Condition {
queryString: string;
index: number;
type: string;
children: Child[];
parent: null;
logicalOperator: string;
}
interface Child {
index: number;
type: string;
parent: string;
object: Object;
operator: ChildOperator;
value: Value;
}
interface Value {
sharedText: string;
_t: string;
objectType: string;
actionName: string;
displayName: string;
name: Name;
}
interface Name {
name: string;
}
interface ChildOperator {
label: string;
value: string;
}
interface Object {
sharedText: string;
_t: string;
name: string;
objectType: string;
type: string;
code: string;
label: string;
noLabel: boolean;
required: boolean;
minLength: string;
maxLength: string;
expression: string;
hideExpression: boolean;
unique: boolean;
defaultValue: string;
}
interface VerName {
name: string;
actionName: string;
displayName: string;
vars: Var[];
}
interface Var {
id: string;
field: Field2;
logicalOperator: LogicalOperator;
vName: string;
}
interface LogicalOperator {
operator: string;
label: string;
}
interface Field2 {
sharedText: string;
_t: string;
name: string;
objectType: string;
type: string;
code: string;
label: string;
noLabel: boolean;
required: boolean;
minLength: string;
maxLength: string;
expression: string;
hideExpression: boolean;
unique: boolean;
defaultValue: string;
}
interface Sources {
app: App;
fields: Field[];
}
interface Field {
name: string;
type: string;
code: string;
label: string;
noLabel: boolean;
required: boolean;
minLength: string;
maxLength: string;
expression: string;
hideExpression: boolean;
unique: boolean;
defaultValue: string;
}
interface App {
id: string;
name: string;
description: string;
createdate: string;
}
export class DataProcessingAction implements IAction {
name: string;
actionProps: IActionProperty[];
dataProcessingProps: Props | null;
constructor() {
this.name = "データ処理";
this.actionProps = [];
this.dataProcessingProps = null;
this.register();
}
async process(
nodes: IActionNode,
event: any,
context: IContext
): Promise<IActionResult> {
this.actionProps = nodes.actionProps;
this.dataProcessingProps = nodes.ActionValue as Props;
this.dataProcessingProps.conditionO = JSON.parse(
this.dataProcessingProps.condition
);
let result = {
canNext: true,
result: "",
} as IActionResult;
try {
if (!this.dataProcessingProps) {
return result;
}
const data = await selectData(
varGet(
this.dataProcessingProps.conditionO.queryString,
context.variables
)
);
console.log("data ", data);
context.variables[this.dataProcessingProps.verName.name] =
this.dataProcessingProps.verName.vars.reduce((acc, f) => {
const v = calc(f, data);
if (v) {
acc[f.vName] = calc(f, data);
}
return acc;
}, {} as AnyObject);
console.log("context ", context);
return result;
} catch (error) {
console.error(error);
event.error = error;
return result;
}
}
register(): void {
actionAddins[this.name] = this;
}
}
new DataProcessingAction();
const varGet = (str: string, vars: any) => {
console.log(str);
const regex = /var\((.*?)\)/g;
let match;
while ((match = regex.exec(str)) !== null) {
const varName = match[1];
if (varName in vars) {
str = str.replace(match[0], vars[varName]);
} else {
throw new Error(`変数${varName}が見つかりません`);
}
}
console.log(str);
return str;
};
const selectData = async (query?: string) => {
return kintone
.api(kintone.api.url("/k/v1/records", true), "GET", {
app: kintone.app.getId(),
query: query,
})
.then((resp: Resp) => {
const result: Result = {};
resp.records.forEach((element) => {
for (const [key, value] of Object.entries(element)) {
if (!result[key]) {
result[key] = { type: value.type, value: [] };
}
result[key].value.push(value.value);
}
});
return result;
});
};
type Resp = { records: RespRecordType[] };
type RespRecordType = {
[key: string]: {
type: string;
value: any;
};
};
type Result = {
[key: string]: {
type: string;
value: any[];
};
};
type AnyObject = {
[key: string]: any;
};
const ERROR_TYPE = "ERROR_TYPE";
const calc = (f: Var, result: Result) => {
const type = typeCheck(f.field.type);
if (!type) {
return ERROR_TYPE;
}
const fun =
calcFunc[
`${type}_${Operator[f.logicalOperator.operator as keyof typeof Operator]}`
];
if (!fun) {
return ERROR_TYPE;
}
const values = result[f.field.code].value;
if (!values) {
return null;
}
return fun(values);
};
const typeCheck = (type: string) => {
switch (type) {
case "RECORD_NUMBER":
case "NUMBER":
return CalcType.NUMBER;
case "SINGLE_LINE_TEXT":
case "MULTI_LINE_TEXT":
case "RICH_TEXT":
return CalcType.STRING;
case "DATE":
return CalcType.DATE;
case "TIME":
return CalcType.TIME;
case "DATETIME":
case "UPDATED_TIME":
return CalcType.DATETIME;
default:
return null;
}
};
enum Operator {
SUM = "SUM",
AVG = "AVG",
MAX = "MAX",
MIN = "MIN",
COUNT = "COUNT",
FIRST = "FIRST",
}
enum CalcType {
NUMBER = "number",
STRING = "string",
DATE = "date",
TIME = "time",
DATETIME = "datetime",
}
const calcFunc: Record<string, (value: string[]) => string | null> = {
[`${CalcType.NUMBER}_${Operator.COUNT}`]: (value: string[]) =>
value.length.toString(),
[`${CalcType.STRING}_${Operator.COUNT}`]: (value: string[]) =>
value.length.toString(),
[`${CalcType.DATE}_${Operator.COUNT}`]: (value: string[]) =>
value.length.toString(),
[`${CalcType.TIME}_${Operator.COUNT}`]: (value: string[]) =>
value.length.toString(),
[`${CalcType.DATETIME}_${Operator.COUNT}`]: (value: string[]) =>
value.length.toString(),
[`${CalcType.NUMBER}_${Operator.SUM}`]: (value: string[]) =>
value.reduce((acc, v) => acc + Number(v), 0).toString(),
[`${CalcType.NUMBER}_${Operator.AVG}`]: (value: string[]) =>
(value.reduce((acc, v) => acc + Number(v), 0) / value.length).toString(),
[`${CalcType.NUMBER}_${Operator.MAX}`]: (value: string[]) =>
Math.max(...value.map(Number)).toString(),
[`${CalcType.NUMBER}_${Operator.MIN}`]: (value: string[]) =>
Math.min(...value.map(Number)).toString(),
[`${CalcType.STRING}_${Operator.SUM}`]: (value: string[]) => value.join(" "),
[`${CalcType.DATE}_${Operator.MAX}`]: (value: string[]) =>
value.reduce((maxDate, currentDate) =>
maxDate > currentDate ? maxDate : currentDate
),
[`${CalcType.DATE}_${Operator.MIN}`]: (value: string[]) =>
value.reduce((minDate, currentDate) =>
minDate < currentDate ? minDate : currentDate
),
[`${CalcType.TIME}_${Operator.MAX}`]: (value: string[]) =>
value.reduce((maxTime, currentTime) =>
maxTime > currentTime ? maxTime : currentTime
),
[`${CalcType.TIME}_${Operator.MIN}`]: (value: string[]) =>
value.reduce((minTime, currentTime) =>
minTime < currentTime ? minTime : currentTime
),
[`${CalcType.DATETIME}_${Operator.MAX}`]: (value: string[]) =>
value.reduce((maxDateTime, currentDateTime) =>
new Date(maxDateTime) > new Date(currentDateTime)
? maxDateTime
: currentDateTime
),
[`${CalcType.DATETIME}_${Operator.MIN}`]: (value: string[]) =>
value.reduce((minDateTime, currentDateTime) =>
new Date(minDateTime) < new Date(currentDateTime)
? minDateTime
: currentDateTime
),
[`${CalcType.STRING}_${Operator.FIRST}`]: (value: string[]) => {
return value[0];
},
};