mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-06 14:09:25 +08:00
* 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>
275 lines
8.0 KiB
TypeScript
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;
|
|
}
|
|
}
|