Zhi Chen 3de2e9c7dd
chore(user-data-sync): optimize wordings by updating prompt (#5244)
* chore: update prompt

* feat: support sync main dept

* chore: adjust the display format of sync error messages

* chore: optimize error handling

* chore:  error message translation

* chore:  error handing

---------

Co-authored-by: xilesun <2013xile@gmail.com>
2024-09-13 22:46:24 +08:00

275 lines
8.0 KiB
TypeScript

/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { Toposort, ToposortOptions } from '@nocobase/utils';
import Database, { Repository } from '@nocobase/database';
import { SystemLogger } from '@nocobase/logger';
export type FormatUser = {
uid: string;
username?: string;
email?: string;
nickname?: string;
phone?: string;
departments?: (string | FormatUserDepartment)[];
isDeleted?: boolean;
[key: string]: any;
};
export type FormatDepartment = {
uid: string;
title?: string;
parentUid?: string;
isDeleted?: boolean;
[key: string]: any;
};
export type FormatUserDepartment = {
uid: string;
isOwner?: boolean;
isMain?: boolean;
};
export type UserDataRecord = FormatUser | FormatDepartment;
export type SyncDataType = 'user' | 'department';
export type SyncAccept = SyncDataType;
export type OriginRecord = {
id: number;
sourceName: string;
sourceUk: string;
dataType: SyncDataType;
metaData: UserDataRecord;
resources: {
resource: string;
resourcePk: string;
}[];
};
export type UserData = {
dataType: SyncDataType;
matchKey?: string;
records: UserDataRecord[];
sourceName: string;
};
export type PrimaryKey = number | string;
export type RecordResourceChanged = {
resourcesPk: PrimaryKey;
isDeleted: boolean;
};
export abstract class UserDataResource {
name: string;
accepts: SyncAccept[];
db: Database;
logger: SystemLogger;
constructor(db: Database, logger: SystemLogger) {
this.db = db;
this.logger = logger;
}
abstract update(record: OriginRecord, resourcePks: PrimaryKey[], matchKey?: string): Promise<RecordResourceChanged[]>;
abstract create(record: OriginRecord, matchKey: string): Promise<RecordResourceChanged[]>;
get syncRecordRepo() {
return this.db.getRepository('userDataSyncRecords');
}
get syncRecordResourceRepo() {
return this.db.getRepository('userDataSyncRecordsResources');
}
}
export type SyncResult = {
resource: string;
detail: {
count: {
all: number;
success: number;
failed: number;
};
failedRecords: {
record: UserDataRecord;
message: string;
}[];
};
};
export class UserDataResourceManager {
resources = new Toposort<UserDataResource>();
syncRecordRepo: Repository;
syncRecordResourceRepo: Repository;
logger: SystemLogger;
registerResource(resource: UserDataResource, options?: ToposortOptions) {
if (!resource.name) {
throw new Error('"name" for user data synchronize resource is required');
}
if (!resource.accepts) {
throw new Error('"accepts" for user data synchronize resource is required');
}
this.resources.add(resource, { tag: resource.name, ...options });
}
set db(value: Database) {
this.syncRecordRepo = value.getRepository('userDataSyncRecords');
this.syncRecordResourceRepo = value.getRepository('userDataSyncRecordsResources');
}
async saveOriginRecords(data: UserData): Promise<void> {
for (const record of data.records) {
if (record.uid === undefined) {
throw new Error(`record must has uid, error record: ${JSON.stringify(record)}`);
}
const syncRecord = await this.syncRecordRepo.findOne({
where: {
sourceName: data.sourceName,
sourceUk: record.uid,
dataType: data.dataType,
},
});
if (syncRecord) {
syncRecord.lastMetaData = syncRecord.metaData;
syncRecord.metaData = record;
await syncRecord.save();
} else {
await this.syncRecordRepo.create({
values: {
sourceName: data.sourceName,
sourceUk: record.uid,
dataType: data.dataType,
metaData: record,
},
});
}
}
}
async findOriginRecords({ sourceName, dataType, sourceUks }): Promise<OriginRecord[]> {
return await this.syncRecordRepo.find({
appends: ['resources'],
filter: { sourceName, dataType, sourceUk: { $in: sourceUks } },
});
}
async addResourceToOriginRecord({ recordId, resource, resourcePk }): Promise<void> {
const syncRecord = await this.syncRecordRepo.findOne({
filter: {
id: recordId,
},
});
if (syncRecord) {
await syncRecord.createResource({
resource,
resourcePk,
});
}
}
async removeResourceFromOriginRecord({ recordId, resource, resourcePk }): Promise<void> {
const recordResource = await this.syncRecordResourceRepo.findOne({
where: {
recordId,
resource,
resourcePk,
},
});
if (recordResource) {
await recordResource.destroy();
}
}
async updateOrCreate(data: UserData): Promise<SyncResult[]> {
await this.saveOriginRecords(data);
const { dataType, sourceName, records, matchKey } = data;
const sourceUks = records.map((record) => record.uid);
let processed = false;
const syncResults: SyncResult[] = [];
for (const resource of this.resources.nodes) {
if (!resource.accepts.includes(dataType)) {
continue;
}
const associateResource = resource.name;
processed = true;
const originRecords = await this.findOriginRecords({ sourceName, sourceUks, dataType });
if (!(originRecords && originRecords.length)) {
continue;
}
const successRecords = [];
const failedRecords = [];
for (const originRecord of originRecords) {
const resourceRecords = originRecord.resources?.filter(
(r: { resource: string }) => r.resource === associateResource,
);
let recordResourceChangeds: RecordResourceChanged[];
if (resourceRecords && resourceRecords.length > 0) {
const resourcePks = resourceRecords.map((r: { resourcePk: string }) => r.resourcePk);
try {
recordResourceChangeds = await resource.update(originRecord, resourcePks, matchKey);
this.logger?.debug(`update record success. Data changed: ${JSON.stringify(recordResourceChangeds)}`);
successRecords.push(originRecord.metaData);
} catch (error) {
this.logger?.warn(`update record error: ${error.message}`, { originRecord });
failedRecords.push({ record: originRecord.metaData, message: error.message });
continue;
}
} else {
try {
recordResourceChangeds = await resource.create(originRecord, matchKey);
this.logger?.debug(`create record success. Data changed: ${JSON.stringify(recordResourceChangeds)}`);
successRecords.push(originRecord.metaData);
} catch (error) {
this.logger?.warn(`create record error: ${error.message}`, { originRecord });
failedRecords.push({ record: originRecord.metaData, message: error.message });
continue;
}
}
if (!recordResourceChangeds || recordResourceChangeds.length === 0) {
continue;
}
for (const { resourcesPk, isDeleted } of recordResourceChangeds) {
if (isDeleted) {
await this.removeResourceFromOriginRecord({
recordId: originRecord.id,
resource: associateResource,
resourcePk: resourcesPk,
});
} else {
await this.addResourceToOriginRecord({
recordId: originRecord.id,
resource: associateResource,
resourcePk: resourcesPk,
});
}
}
}
syncResults.push({
resource: associateResource,
detail: {
count: {
all: originRecords.length,
success: successRecords.length,
failed: failedRecords.length,
},
failedRecords,
},
});
}
if (!processed) {
throw new Error(`dataType "${dataType}" is not support`);
}
return syncResults;
}
}