import * as flat from 'flat'
import _ from 'lodash'
import BaseResource from './base-resource'
import ValidationError, { RecordError, PropertyErrors } from '../utils/validation-error'
import RecordJSON from '../decorators/record-json.interface'
import { CurrentAdmin } from '../../current-admin.interface'
export type ParamsType = Record<string, any>
/**
* Representation of an particular ORM/ODM Record in given Resource in AdminBro
*
* @category Base
*/
class BaseRecord {
/**
* Resource to which record belongs
*/
public resource: BaseResource
/**
* Actual record data stored as a flatten object
*/
private params: ParamsType
/**
* Object containing all validation errors: this.errors[path] = 'errorMessage'
*/
private errors: PropertyErrors
/**
* Object containing all populated relations.
*/
private populated: {[key: string]: BaseRecord}
/**
* @param {Object} params all resource data. I.e. field values
* @param {BaseResource} resource resource to which given record belongs
*/
constructor(params, resource) {
this.resource = resource
this.params = params ? flat.flatten(params) : {}
this.errors = {}
this.populated = {}
}
/**
* Returns value for given field.
* @param {string} path path (name) for given field: i.e. 'email' or 'authentication.email'
* if email is nested within the authentication object in the data
* store
* @return {any} value for given field
*/
param(path: string): any {
if (this.params && this.params[path]) {
return this.params[path]
}
const subParams = this.namespaceParams(path)
if (subParams) {
const unflattenSubParams = flat.unflatten(subParams) as Record<string, any>
return path.split('.').reduce((m, v) => m[v], unflattenSubParams)
}
return undefined
}
/**
* Returns object containing all params keys starting with prefix
* @param {String} prefix
*
* @return {Object}
*/
namespaceParams(prefix: string): Record<string, any> {
const regex = new RegExp(`^${prefix}`)
const keys = Object.keys(this.params).filter(key => key.match(regex))
if (keys.length) {
return keys.reduce((memo, key) => ({ ...memo, [key]: this.params[key] }), {})
}
return {}
}
/**
* Updates given Record in the data store. Practically it invokes
* {@link BaseResource.update} method.
*
* When validation error occurs it stores that to {@link BaseResource.errors}
*
* @param {Object} params all field with values which has to be updated
* @return {BaseRecord} given record (this)
*/
async update(params): Promise<BaseRecord> {
try {
this.storeParams(params)
this.params = await this.resource.update(this.id(), params)
} catch (e) {
if (e instanceof ValidationError) {
this.errors = e.propertyErrors
return this
}
throw e
}
this.errors = {}
return this
}
/**
* Saves the record in the database. When record already exists - it updates, otherwise
* it creates new one.
*
* Practically it invokes
* {@link BaseResource#create} or {@link BaseResource#update} methods.
*
* When validation error occurs it stores that to {@link BaseResource#errors}
*
* @return {BaseRecord} given record (this)
*/
async save(): Promise<BaseRecord> {
try {
if (this.id()) {
this.params = await this.resource.update(this.id(), this.params)
} else {
this.params = await this.resource.create(this.params)
}
} catch (e) {
if (e instanceof ValidationError) {
this.errors = e.propertyErrors
return this
}
throw e
}
this.errors = {}
return this
}
/**
* Returns uniq id of the Record.
* @return {String | Number} id of the Record
*/
id(): string {
const idProperty = this.resource.properties().find(p => p.isId())
if (!idProperty) {
throw new Error(`Resource: "${this.resource.id()}" does not have an id property`)
}
return this.param(idProperty.name())
}
/**
* Returns title of the record. Usually title is an value for fields like: email, topic,
* title etc.
*
* Title will be shown in the breadcrumbs for example.
*
* @return {String} title of the record
*/
title(): string {
const nameProperty = this.resource.properties().find(p => p.isTitle())
return nameProperty ? this.param(nameProperty.name()) : this.id()
}
/**
* Return state of validation for given record
* @return {Boolean} if record is valid or not.
*/
isValid(): boolean {
return Object.keys(this.errors).length === 0
}
/**
* Stores incoming payloadData in record params
*/
storeParams(payloadData: object): void {
this.params = _.merge(this.params, payloadData)
}
/**
* Returns error message for given property path (name)
* @param {String} path path (name) of property which we want to check if is valid
* @return {String | null} validation message of null
*/
error(path: string): RecordError | null {
return this.errors[path]
}
/**
* Populate record relations
*
* @param {String} propertyName name of the property which should be populated
* @param {BaseRecord} record record to which property relates
*/
populate(propertyName: string, record: BaseRecord): void {
this.populated[propertyName] = record
}
/**
* Returns JSON representation of an record
* @param {CurrentAdmin} currentAdmin
* @return {RecordJSON}
*/
toJSON(currentAdmin?: CurrentAdmin): RecordJSON {
const populated = Object.keys(this.populated).reduce((m, key) => ({
...m,
[key]: this.populated[key].toJSON(currentAdmin),
}), {})
return {
params: this.params,
populated,
errors: this.errors,
id: this.id(),
title: this.resource.decorate().titleOf(this),
recordActions: this.resource.decorate().recordActions(
this, currentAdmin,
).map(recordAction => recordAction.toJSON()),
bulkActions: this.resource.decorate().bulkActions(
this, currentAdmin,
).map(recordAction => recordAction.toJSON()),
}
}
}
export default BaseRecord
Source