next-gs
Version:
NPM package for building a React+NextJS+Prisma admin application.
247 lines (182 loc) • 6.26 kB
text/typescript
import _ from "@next-gs/utils/funcs";
import { FieldType, type UpdaterOptions, type IRecord } from "./types";
import type { QueryWhere, Entity, JsonValue, TableRow } from "@next-gs/client";
import type { Model } from "./model";
import type { Table } from "./table";
import { DataRecord } from "./record";
const isSameType = (a: JsonValue, b: JsonValue) => typeof a === typeof b;
export class Updater<E extends Entity, R extends TableRow> {
public oldData = new DataRecord<E>();
public newData = new DataRecord<E>();
public allData = new DataRecord<E>();
public updData = new DataRecord<E>();
constructor(
private model: Model<E, R>,
private opts: UpdaterOptions,
) { }
get repo() {
return this.model.repo;
}
get exists() {
return !this.oldData.empty;
}
async prepareDelta(table: Table) {
const oldData = this.oldData.safe();
const newData = this.newData.safe();
const updData = await this.model.beforeUpdate({
oldData,
newData,
mixData: _.assign({}, oldData, newData),
allData: this.allData.safe(),
});
let changes = 0;
const delta = _.pickBy(updData, (val, key) => {
if (table.isPrimary(key)) return true;
if (key === "$more" || key === "$master") return false;
const f = table.getField(key);
const changed =
f && !f.readOnly && !f.lookup && this.oldData.isDiff(key, val);
if (changed) changes++;
return changed;
});
this.updData.assign(updData);
return changes > 0 || !this.exists ? delta : undefined;
}
async checkFieldValues(table: Table) {
await Promise.all(
_.map(table.fields, async (field, name) => {
let value;
if (
field.type === FieldType.Object ||
field.type === FieldType.ObjectText
) {
const pfx = name + "_";
const oldObj = _.reduce<object, object>(
this.allData.data,
(obj, val, key) => {
if (key.startsWith(pfx)) _.set(obj, [key.slice(pfx.length)], val);
return obj;
},
this.exists ? _.strToObj(this.oldData.str(name)) : {},
);
value = _.objToStr(oldObj);
} else {
value = table.fmtFieldValue(field, this.allData.val(name));
if (field.type === FieldType.Image) {
if (_.isObject(value)) {
value = await this.repo.storeBlob(_.get(value, [0]));
}
} else if (field.type === FieldType.ObjectJson) {
value = _.isObject(value) ? JSON.stringify(value) : "{}";
} else if (field.type === FieldType.Number) {
if (_.isNaN(value)) value = undefined;
} else if (field.type === FieldType.Text) {
if (_.isString(value)) value = _.trim(value);
}
}
//**** set field if is different
if (value !== undefined && this.oldData.isDiff(name, value))
this.newData.put(name, value);
}),
);
}
async updateTable(table: Table, tid: QueryWhere) {
await this.checkFieldValues(table);
const delta = await this.prepareDelta(table);
if (delta) {
/*this.opts.upsert
? await this.ctx.upsert(table.name, delta)
: this.exists
? await this.ctx.update(table.name, tid, delta)
: await this.ctx.insert(table.name, delta);*/
return true;
}
}
async updateLookups(table: Table, tid: JsonValue) {
await Promise.all(
_.map(table.meta.lookupFields, async (lkp) => {
const { ref, relation, update } = lkp;
if (!ref || update !== true) return;
if (!lkp.model) lkp.model = this.repo.createModel(ref);
const lkpTable = (lkp.model as Model<E>).table;
const lkpData = this.allData.clone();
lkpData.id = isSameType(lkpTable.primary, table.primary)
? tid
: table.objToId(lkpData);
_.forEach(relation, (key, src) => {
_.set(lkpData, [key], this.allData.val(src));
});
await (lkp.model as Model<E>).updateOne(lkpData, {
upsert: _.toString(update) === "upsert",
});
}),
);
}
async updateDetails(table: Table) {
const $master = _.assign({}, this.allData.data, this.updData.data);
await Promise.all(
_.map(table.meta.detailFields, async (dtl, name) => {
if (!(dtl.update && dtl.ref)) return;
const detail = this.allData.val(name);
if (_.isArray(detail)) {
const { ref, relation, onUpdate } = dtl;
const dtlRows = await Promise.all(
_.map(detail, async (record, idx) => {
const dtlData = { ...record, $master };
_.forEach(relation, (m, d) => (dtlData[d] = this.updData.val(m)));
return onUpdate ? await onUpdate($master, dtlData, idx) : dtlData;
}),
);
if (!dtl.model) {
dtl.model = this.repo.createModel(ref);
}
const dtlData = await (dtl.model as Model<E>).updateMany(dtlRows);
this.updData.put(name, dtlData);
}
}),
);
}
async execute(data: Partial<E>) {
const m = this.model;
const t = this.model.table;
const { match = t.primary } = this.opts;
const {
parsed: { id: _id, ...all },
} = await m.validate(data);
let id = _id;
this.oldData.reset();
this.newData.reset();
this.allData.reset(all as Partial<E>);
this.updData.reset();
const key = _.isNil(id)
? _.isArray(match)
? this.allData.pick(match)
: this.allData.val(match)
: t.idToObj(id);
if (_.notNil(key)) this.oldData.reset(await m.findOne(key));
id = this.exists
? this.oldData.val("id")
: _.isArray(t.primary)
? t.objToId(this.allData)
: await m.repo.generateId(t.generator);
if (!this.exists) {
this.newData.reset(await m.initialValues(this.allData.safe()));
this.allData.assign(this.newData);
}
let updated = 0;
await this.model.queueTables(async (table: Table) => {
const tid = table.idToObj(id) as QueryWhere;
this.newData.assign(tid as IRecord<E>);
if (await this.updateTable(table, tid)) updated++;
await this.updateLookups(table, id);
await this.updateDetails(table);
}, true);
if (updated > 0) await m.afterUpdate(this.updData);
const saved = await m.findOneOrFail(id);
return this.allData
.assign(this.updData)
.assign(saved)
.remove("$master")
.safe();
}
}