UNPKG

next-gs

Version:

NPM package for building a React+NextJS+Prisma admin application.

567 lines (447 loc) 14.1 kB
import fn from "@next-gs/utils/funcs"; import { FieldType, type QueryBuilder, type QueryBuilderOptions, type TableMeta, type IModel, } from "./types"; import type { JsonValue, Query, Entity, EntityID, Dictionary, TableRow, QueryWhere, UpdaterState, UpdaterOptions, Any, } from "@next-gs/client"; import type { DataRecord } from "./record"; import { PK_VALUE_SEP, Table, type TableFunc } from "./table"; import * as Where from "./where"; import { Updater } from "./updater"; import type { Repository } from "./repository"; const asArray = (val: JsonValue) => fn.notNil(val) ? fn.isArray(val) ? val : fn.isString(val) ? fn.split(val, ",") : [val] : []; export const FieldFormat = { numbers: (val: JsonValue) => (val ? String(val).replace(/\D/g, "") : val), }; export abstract class Model< E extends Entity = Entity, R extends TableRow = TableRow, > implements IModel { private _table?: Table<E>; private _tables: Table[] = []; private _query?: QueryBuilder; private _queryOptions?: QueryBuilderOptions<E>; constructor(public repo: Repository) { } get table() { return this._table; } get meta() { return this._table?.meta as TableMeta<E>; } set meta(value: TableMeta<E>) { if (!value.primary) value.primary = `COD${value.name}`; if (value.deleteField !== false && value.deleteField === undefined) { value.deleteField = `DEL${value.name}`; } if (this._table) { value.join = { pk: value.primary, pkPath: `${value.name}.${value.primary}`, fk: this._table.primary, fkPath: `${this._table.name}.${this._table.primary}`, }; } this._table = new Table(value, this._table as unknown as Table); this._tables.push(this._table as unknown as Table); } get queryOptions() { if (this._queryOptions === undefined) { const objFields = this.table?.getFieldList( ({ type }) => type === FieldType.ObjectJson || type === FieldType.ObjectText, ); let calcNodes = 0; this.forEachTable((table) => { if (table.meta.onCalculate) calcNodes++; return true; }); // **** initialize query options this._queryOptions = calcNodes || objFields ? { onCalculate: (row) => { let out = row as E; this.forEachTable(({ meta }) => { if (meta.onCalculate) out = meta.onCalculate(out as TableRow) as E; return true; }); if (objFields) fn.forEach(objFields, (fld, key) => { let val = out[key]; if (fld.type === FieldType.ObjectJson) { val = val ? JSON.parse(val as string) : {}; } else if (fld.type === FieldType.ObjectText) { val = fn.strToObj(val as string); } fn.set(out, [key], val); }); return out; }, } : {}; } return this._queryOptions; } forEachTable(fn: TableFunc<boolean>, inverse?: boolean) { if (inverse) { for (let t = 0; t < this._tables.length; t++) { if (!fn(this._tables[t])) break; } } else { for (let t = this._tables.length - 1; t >= 0; t--) { if (!fn(this._tables[t])) break; } } } async queueTables<R = void>(cb: TableFunc<Promise<R>>, inverse?: boolean) { const tables: Table[] = []; this.forEachTable((table) => { tables.push(table); return true; }, inverse); return await fn.queue<Table, R>(tables, cb); } async fetch(full?: boolean) { const rows = await this._query; if (rows && full) { await Promise.all( fn.map(rows, async (row, rowIdx) => this.queueTables(async (table) => { const { detailFields, computeFields } = table.meta; await Promise.all( fn.map(detailFields, async (dtl, name) => { const { ref, relation, raw, format, sorter } = dtl; let dtlData = undefined; if (raw) { const mstKey = fn.map(relation, (m) => row[m]); if (fn.isFunction(raw)) { dtlData = await raw(mstKey); } else { dtlData = await this.repo.queryRaw(raw, [mstKey]); } } else if (ref) { if (!dtl.model) dtl.model = this.repo.createModel(ref); const dtlFilter = fn.reduce( relation, (f, m, d) => { f[d] = row[m]; return f; }, {} as { [key: string]: JsonValue }, ); dtlData = await (dtl.model as Model).findMany({ where: dtlFilter, sort: sorter, }); } if (dtlData) { row[name] = format ? format(dtlData) : dtlData; } }), ); if (fn.isFunction(computeFields)) { rows[rowIdx] = await computeFields(row); } else { await Promise.all( fn.map(computeFields, async (func, name) => { row[name] = func ? await func(row) : undefined; }), ); } }), ), ); } return rows as E[]; } getFieldValue(field: string, value: JsonValue) { return this.table?.fmtFieldValue(field, value); } query(where?: QueryWhere) { if (!this.table) throw new Error("Model table is undefined"); const query = this.repo.query(this.table.name, where, this.queryOptions); return query; } prepare({ where, sort }: Query<E>) { const query = this.query(); const primary = this.table?.primary; const withField = (field: string, cb: (f: string) => void) => fn.tryEach(field, (f) => cb((f === "id" ? primary : f) as string)); this.forEachTable((table) => { const { join } = table.meta; if (join) { query .select(table.selectFields()) .innerJoin(table.name, join.pkPath, join.fkPath); } else { const pk = table.primary; const pkRaw = fn.isArray(pk) ? `CONCAT(${pk.join(`,'${PK_VALUE_SEP}',`)})` : pk; query .select(this.repo.raw(`${pkRaw} as id`)) .select(table.selectFields()) .table(table.name); } return true; }, true); this.onPrepare(query); const { _q, _removed, ...w } = where || {}; this.prepareJoins(query, null, _removed === true); console.log("-- QUERY WHERE 1 ---", where); if (_q) query.where((qry: QueryBuilder) => { console.log("-- QUERY WHERE 2 ---", _q); this.forEachTable((table) => { const { queryFields } = table.meta; fn.forEach(queryFields, (op, col) => { const field = col.replace("$", "."); console.log("-- QUERY WHERE 3 ---", field, op, _q); if (op === "%") qry.orWhere(field, "LIKE", `%${_q}%`); else qry.orWhere(field, _q); }); return true; }); }); if (w) for (const attr in w) { const { f: field, o } = Where.extract(attr); withField(field, (f) => { let v: JsonValue = fn.strToJson(w[attr]); if (fn.isNil(v)) { query.whereNull(f); } else if (f === "_raw") { query.whereRaw(v as string); } else if (o === "IN") { query.whereIn(f, asArray(v)); } else if (o === "&" || o === "|") { v = asArray(v).reduce((sum, num) => sum + Number.parseInt(num), 0); if (v) query.whereRaw(`${f} ${o} ${v} <> 0`); } else { query.where((qry: QueryBuilder) => fn.forEach(asArray(v), (val) => { const partial = fn.includes(val, "%"); if (o === "LIKE" || partial) { qry.orWhere(f, "LIKE", partial ? val : `%${val}%`); } else { const v = this.getFieldValue(f, val); fn.notNil(v) ? qry.orWhere(f, o || "=", v) : qry.orWhereNull(f); } }), ); } }); } // **** sort query fn.forEach(sort, (d, f) => query.orderBy(f, d)); this._query = query; return this._query; } // **** includes fixed fields on query filter prepareFixed(query: QueryBuilder, table: Table, alias?: string) { fn.forEach(table.fields, ({ fixed }, name) => { console.log("prepareFixed", alias, name, fixed); const fullName = `${alias}.${name}`; this.onPrepareFixed(query, name, fullName); if (fixed !== undefined) query.where(fullName, fixed); }); } prepareJoins( query: QueryBuilder, rootAlias: string | null, showRemoved: boolean, ) { this.forEachTable((table) => { const tblName = table.name; if (rootAlias === null) this.prepareFixed(query, table, tblName); const modelAlias = table.parent ? tblName : rootAlias || tblName; // **** includes lookupfixed fields on query filter fn.forEach(table.meta.lookupFields, (lkp, key) => { if (fn.isString(lkp.raw)) { query.select(this.repo.raw(`(${lkp.raw}) as ${key}`)); return; } const { ref, relation, lkpName = "*", lkpJoin, lkpCols } = lkp; if (fn.isArray(lkpJoin)) { const [p1, p2, p3] = lkpJoin; return query.select(`${key}.*`).leftJoin(p1, p2, p3); } const lkpModel = ref ? (this.repo.createModel(ref) as Model) : undefined; const lkpTable = lkpModel?.table; const lkpAlias = lkp.lkpAlias || `${modelAlias}_${lkpTable?.name}_${fn.map( relation, (key, src) => src, ).join()}`; if (lkpName === "*") { const refFields = lkpCols ? fn.isPlainObject(lkpCols) ? fn.map(lkpCols, (a, f) => `${lkpAlias}.${f} as ${a}`) : fn.map(lkpCols, (f) => `${lkpAlias}.${f}`) : lkpTable ? lkpTable.selectFields(lkpAlias) : `${lkpAlias}.*`; query.select(refFields); } else { query.select(`${lkpAlias}.${lkpName} as ${key}`); } if (fn.isString(lkpJoin)) { query.joinRaw(lkpJoin); } else if (fn.isFunction(lkpJoin)) { query.joinRaw(lkpJoin(lkpAlias, modelAlias)); } else if (lkpTable) { query.leftJoin(`${lkpTable.name} as ${lkpAlias}`, function () { fn.forEach(relation, (key, src) => { this.on(`${modelAlias}.${src}`, "=", `${lkpAlias}.${key}`); }); }); } if (lkpName === "*" && lkpModel && lkpTable) { if (!lkpCols) lkpModel.prepareJoins(query, lkpAlias, showRemoved); lkpModel.prepareFixed(query, lkpTable, lkpAlias); } }); if (!showRemoved && table.deleteField) { query.whereNull(`${modelAlias}.${table.deleteField}`); } return true; }); } async initialValues(data: Partial<E>) { /*await this.queueTables(async (table) => { _.forEach(table.fields, ({ initial }, name) => { let val = data[name]; if (_.isNil(val)) { if (name === `LOJ${table.name}`) { val = this.repo.shop; } else if (initial !== undefined) { val = this.repo.parseExpr(_.toString(initial), initial); } } if (!(_.isUndefined(val) || _.isNaN(val))) _.set(data, [name], val); }); });*/ return data; } async findOne(id: EntityID, full?: boolean) { if (fn.notNil(id)) { this.prepare({ where: { id } }); const rows = await this.fetch(full); return rows ? rows[0] : undefined; } } async findOneOrFail(id: EntityID, full?: boolean) { const found = await this.findOne(id, full); if (!found) throw new Error("Record not found!"); return found; } async findFirst(where: QueryWhere) { const { data } = await this.findMany({ where, skip: 0, take: 1 }); return data[0]; } async findMany({ full, skip = 0, take = 200, ...rest }: Query<E> = {}) { const query = this.prepare(rest); if (!query) return { data: [], total: 0 }; const total = await this.repo.queryCount(query); if (skip > 0) query.offset(skip); if (take > 0) query.limit(take); const data = await this.fetch(full); return { data, total }; } async updateOne(data: Partial<E>, opts?: UpdaterOptions) { if (fn.isNil(data)) return data; const updater = new Updater<E, R>(this, opts || {}); return await updater.execute(data); } async updateMany(data: Partial<E>[], opts?: UpdaterOptions) { return await fn.queue<Partial<E>, E>(data, (item) => this.updateOne(item, opts), ); } async removeOne(id: EntityID) { const previous = await this.findOneOrFail(id); await this.onDelete(id, previous); return previous; } async removeMany(ids: EntityID[]) { return await fn.queue(ids, (id) => this.removeOne(id)); } async onDelete(id: JsonValue, previous?: E) { await this.beforeDelete(previous); await this.queueTables(async (table) => { const filter = table.idToObj(id); if (!filter) return; const { name, deleteField } = table; const qry = this.repo.query(name, filter); if (deleteField) { await qry.update({ [deleteField as string]: fn.time() }); } else { await qry.delete(); } }); await this.afterDelete(previous); } async validate(data: Partial<E>) { this.forEachTable((table) => { fn.forEach(table.fields, ({ fixed, format: validators }, name) => { if (fixed !== undefined) fn.set(data, [name], fixed); fn.tryEach(validators, (func) => { if (func) fn.set(data, [name], func(data[name])); }); }); return true; }); return { parsed: data } as { parsed: Partial<E>; errors?: Dictionary<string>; }; } async checkAbility(action: string, subject: Partial<E> | undefined) { console.log("can", action, subject); } async beforeDelete(previous?: E) { console.log("beforeDelete", previous); } async beforeUpdate(state: UpdaterState<E>) { return state.newData; } async afterDelete(previous?: E) { console.log("afterDelete", previous); } async afterUpdate(data: DataRecord<E>) { console.log("afterUpdate", data); } abstract onPrepare(query: QueryBuilder): Promise<void>; abstract onPrepareFixed( query: QueryBuilder, field: string, fixed: JsonValue, ): Promise<void>; }