uui-db/src/tempIds.tsx (197 lines of code) (raw):
import {
DbTablePatch, DbTablesSet, DbPatch, DbFieldSchema, DbSaveResponse,
} from './types';
import { Db } from './Db';
import { objectKeys } from './utils';
import { DataQueryFilter, DataQueryFilterCondition } from '@epam/uui-core';
let lastTempId = -1;
export function getTempId() {
return --lastTempId;
}
export function isTempId(val: any): val is number {
return typeof val === 'number' && val < 0;
}
export interface IClientIdsMap {
clientToServer(id: number): any;
serverToClient(tableName: string, id: any): number;
clientToServerRequest<T>(request: T): T;
clientToServerDataFilter(filter: DataQueryFilter<any>): DataQueryFilter<any>;
}
export class TempIdMap<TTables extends DbTablesSet<TTables>> implements IClientIdsMap {
private serverToClientIds = new Map<keyof TTables, Map<any, number>>();
private clientToServerIds = new Map<number, any>();
constructor(private db: Db<TTables>) {}
public clientToServer = (id: number): any => this.clientToServerIds.get(id);
public serverToClient = (tableName: string, id: any): number => this.getServerToClientMap(tableName as any).get(id);
public clientToServerRequest = (request: any) => {
request = { ...request };
if (request.filter) {
request.filter = this.clientToServerDataFilter(request.filter);
}
return request;
};
public clientToServerDataFilter = (filter: DataQueryFilter<any>) => {
filter = { ...filter };
const keys = Object.keys(filter);
// I hope that we can rely that there won't be any negative numbers, which are not IDs, in queries.
// We need to invent better heuristics or a schema otherwise.
const clientToServer = (id: number) => {
const found = this.clientToServer(id);
if (found) {
return found;
} else {
return id;
}
};
for (let n = 0; n < keys.length; n++) {
const key = keys[n];
let condition = filter[key] as DataQueryFilterCondition<any>;
if (condition != null && typeof condition === 'object') {
condition = { ...condition };
if ('in' in condition && Array.isArray(condition.in) && condition.in.length) {
condition.in = condition.in.map(clientToServer);
}
} else {
condition = clientToServer(condition);
}
filter[key] = condition;
}
return filter;
};
private copyPatch(patch: DbPatch<TTables>) {
const newPatch = { ...patch };
objectKeys(patch).forEach((tableName) => {
const tablePatch: DbTablePatch<any> = patch[tableName].map((entityPatch) => ({ ...entityPatch }));
newPatch[tableName] = tablePatch;
});
return newPatch;
}
// Iterate thru every field in every patch for every table, matching certain field criteria. The patch is mutated!
protected mapFields(
patch: DbPatch<TTables>,
filter: (schema: DbFieldSchema<any, any>, name: string) => boolean,
callback: (fieldName: any, fieldSchema: DbFieldSchema<any, any>, fieldValue: any, entityPatch: any, tableName: keyof TTables) => void,
): void {
objectKeys(patch).forEach((tableName) => {
const schema = this.db.tables[tableName].schema;
const fields = objectKeys(schema.fields || [])
.map((fieldName) => {
const fieldSchema = schema.fields[fieldName as any];
if (filter(fieldSchema, fieldName as string)) {
return { name: fieldName, schema: fieldSchema };
}
})
.filter((f) => !!f);
if (!fields.length) {
return;
}
patch[tableName].forEach((entityPatch) => {
for (let n = 0; n < fields.length; n++) {
const field = fields[n];
const fieldValue = entityPatch[field.name as any];
callback(field.name, field.schema, fieldValue, entityPatch, tableName);
}
});
});
}
protected getServerToClientMap(tableName: keyof TTables) {
const existing = this.serverToClientIds.get(tableName);
if (existing) {
return existing;
} else {
const newMap = new Map();
this.serverToClientIds.set(tableName, newMap);
return newMap;
}
}
public serverToClientPatch(patch: DbPatch<TTables>) {
patch = this.copyPatch(patch);
// We map PK in a separate pass, as we need to remap all PKs before re-mapping FKs
this.mapFields(
patch,
(f) => f.isGenerated,
(fieldName, fieldSchema, fieldValue, entityPatch, tableName) => {
let clientId = this.getServerToClientMap(tableName).get(fieldValue);
if (!clientId) {
clientId = getTempId();
this.clientToServerIds.set(clientId, fieldValue);
this.getServerToClientMap(tableName).set(fieldValue, clientId);
}
entityPatch[fieldName] = clientId;
},
);
this.mapFields(
patch,
(f) => !!(f.fk || f.default != null || f.toClient),
(fieldName, fieldSchema, fieldValue, entityPatch) => {
let isUndefined = typeof fieldValue === 'undefined';
if (fieldSchema.default != null && fieldValue == null) {
fieldValue = fieldSchema.default;
isUndefined = false;
}
if (fieldSchema.fk && fieldValue != null) {
const fkTableName = typeof fieldSchema.fk.tableName === 'function' ? fieldSchema.fk.tableName(entityPatch) : fieldSchema.fk.tableName;
let clientId = this.getServerToClientMap(fkTableName as keyof TTables).get(fieldValue);
if (!clientId) {
clientId = getTempId();
this.clientToServerIds.set(clientId, fieldValue);
this.getServerToClientMap(fkTableName as keyof TTables).set(fieldValue, clientId);
}
fieldValue = clientId;
}
if (!isUndefined && fieldSchema.toClient) {
fieldValue = fieldSchema.toClient(fieldValue);
}
if (!isUndefined) {
entityPatch[fieldName] = fieldValue;
}
},
);
return patch;
}
public clientToServerPatch(patch: DbPatch<TTables>) {
patch = this.copyPatch(patch);
this.mapFields(
patch,
(f) => f.isGenerated || !!f.fk,
(fieldName, fieldSchema, fieldValue, entityPatch) => {
if (fieldValue != null) {
const existingId = this.clientToServerIds.get(fieldValue);
if (existingId) {
entityPatch[fieldName] = existingId;
}
}
},
);
this.mapFields(
patch,
(f) => !(f.isClientOnly || f.isReadOnly) && !!f.toServer,
(fieldName, fieldSchema, _, entityPatch) => {
entityPatch[fieldName] = fieldSchema.toServer(entityPatch[fieldName]);
},
);
this.mapFields(
patch,
(meta) => meta.isClientOnly || meta.isReadOnly,
(fieldName, _, __, entityPatch) => {
delete entityPatch[fieldName];
},
);
return patch;
}
public appendServerMapping(response: DbSaveResponse<TTables>) {
objectKeys(response.submit).forEach((tableName) => {
if (this.db.tables[tableName]) {
const tablePatch = response.submit[tableName];
if (tablePatch) {
const serverToClientIds = this.getServerToClientMap(tableName);
const table = this.db.tables[tableName];
tablePatch.forEach((patch) => {
if (isTempId(patch.id)) {
const clientId = patch.id;
const serverId = table.getId(patch.payload);
serverToClientIds.set(serverId, clientId);
this.clientToServerIds.set(clientId, serverId);
}
});
}
}
});
}
}