import * as _ from 'lodash'
import * as path from 'path'
import * as fs from 'fs'
import AdminBroOptions, { AdminBroOptionsWithDefault } from './admin-bro-options.interface'
import BaseResource from './backend/adapters/base-resource'
import BaseDatabase from './backend/adapters/base-database'
import BaseRecord from './backend/adapters/base-record'
import BaseProperty from './backend/adapters/base-property'
import Filter from './backend/utils/filter'
import ValidationError from './backend/utils/validation-error'
import ConfigurationError from './backend/utils/configuration-error'
import ResourcesFactory from './backend/utils/resources-factory'
import userComponentsBundler from './backend/bundler/user-components-bundler'
import { RouterType } from './backend/router'
import Action, { RecordActionResponse } from './backend/actions/action.interface'
import { DEFAULT_PATHS } from './constants'
import loginTemplate from './frontend/login-template'
import { ListActionResponse } from './backend/actions/list-action'
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'))
export const VERSION = pkg.version
const defaults: AdminBroOptionsWithDefault = {
rootPath: DEFAULT_PATHS.rootPath,
logoutPath: DEFAULT_PATHS.logoutPath,
loginPath: DEFAULT_PATHS.loginPath,
databases: [],
resources: [],
branding: {
companyName: 'Company',
softwareBrothers: true,
},
dashboard: {},
assets: {
styles: [],
scripts: [],
globalsFromCDN: false,
},
pages: {},
}
type ActionsMap = {
show: Action<RecordActionResponse>;
edit: Action<RecordActionResponse>;
delete: Action<RecordActionResponse>;
new: Action<RecordActionResponse>;
list: Action<ListActionResponse>;
}
type UserComponentsMap = {[key: string]: string}
export type Adapter = { Database: typeof BaseDatabase; Resource: typeof BaseResource }
/**
* Main class for AdminBro extension. It takes {@link AdminBroOptions} as a
* parameter and creates an admin instance.
*
* Its main responsibility is to fetch all the resources and/or databases given by a
* user. Its instance is a currier - injected in all other classes.
*
* @example
* const AdminBro = require('admin-bro')
* const admin = new AdminBro(AdminBroOptions)
*/
class AdminBro {
public resources: Array<BaseResource>
public options: AdminBroOptionsWithDefault
public static registeredAdapters: Array<Adapter>
/**
* Contains set of routes available within the application.
* It is used by external plugins.
*
* @example
* const { Router } = require('admin-bro')
* Router.routes.forEach(route => {
* // map your framework routes to admin-bro routes
* // see how `admin-bro-expressjs` plugin does it.
* })
*/
public static Router: RouterType
/**
* An abstract class for all Database Adapters.
* External adapters have to implement it.
*
* @example <caption>Creating Database Adapter for some ORM</caption>
* const { BaseDatabase } = require('admin-bro')
*
* class Database extends BaseDatabase {
* constructor(ormInstance) {
* this.ormInstance = ormInstance
* }
* resources() {
* // fetch resources from your orm and convert to BaseResource
* }
* }
*/
public static BaseDatabase: typeof BaseDatabase
/**
* Class representing all records. External adapters have to implement that or at least
* their {@link BaseResource} implementation should return records of this type.
*/
public static BaseRecord: typeof BaseRecord
/**
* An abstract class for all resources. External adapters have to implement that.
*/
public static BaseResource: typeof BaseResource
/**
* Class for all properties. External adapters have to implement that or at least
* their {@link BaseResource} implementation should return records of this type.
*/
public static BaseProperty: typeof BaseProperty
/**
* Filter object passed to find method of {@link BaseResource}.
* External adapters have to use it
*/
public static Filter: typeof Filter
/**
* Validation error which is thrown when record fails validation. External adapters have
* to use it, so AdminBro can print validation errors
*/
public static ValidationError: typeof ValidationError
/**
* List of all default actions. If you want to change the behavior for all actions like:
* _list_, _edit_, _show_, _delete_ and _bulk delete_ you can do this here.
*
* @example <caption>Modifying accessibility rules for all show actions</caption>
* const { ACTIONS } = require('admin-bro')
* ACTIONS.show.isAccessible = () => {...}
*/
public static ACTIONS: ActionsMap
/**
* AdminBro version
*/
public static VERSION: string
/**
* List of all bundled components
*/
public static UserComponents: UserComponentsMap
/**
* @param {AdminBroOptions} options Options passed to AdminBro
*/
constructor(options: AdminBroOptions = {}) {
/**
* @type {BaseResource[]}
* @description List of all resources available for the AdminBro.
* They can be fetched with the {@link AdminBro#findResource} method
*/
this.resources = []
/**
* @type {AdminBroOptions}
* @description Options given by a user
*/
this.options = _.merge({}, defaults, options)
const defaultLogo = `${this.options.rootPath}/frontend/assets/logo-mini.svg`
this.options.branding = this.options.branding || {}
this.options.branding.logo = this.options.branding.logo !== undefined
? this.options.branding.logo
: defaultLogo
const { databases, resources } = this.options
const resourcesFactory = new ResourcesFactory(this, AdminBro.registeredAdapters)
this.resources = resourcesFactory.buildResources({ databases, resources })
}
/**
* Registers various database adapters written for AdminBro.
*
* @example
* const AdminBro = require('admin-bro')
* const MongooseAdapter = require('admin-bro-mongoose')
* AdminBro.registerAdapter(MongooseAdapter)
*
* @param {Object} options
* @param {typeof BaseDatabase} options.Database subclass of {@link BaseDatabase}
* @param {typeof BaseResource} options.Resource subclass of {@link BaseResource}
*/
static registerAdapter({ Database, Resource }: {
Database: typeof BaseDatabase;
Resource: typeof BaseResource;
}): void {
if (!Database || !Resource) {
throw new Error('Adapter has to have both Database and Resource')
}
// checking if both Database and Resource have at least isAdapterFor method
if (Database.isAdapterFor && Resource.isAdapterFor) {
AdminBro.registeredAdapters.push({ Database, Resource })
} else {
throw new Error('Adapter elements has to be a subclass of AdminBro.BaseResource and AdminBro.BaseDatabase')
}
}
/**
* Initializes AdminBro instance in production. This function should be called by
* all external plugins.
*/
async initialize(): Promise<void> {
if (process.env.NODE_ENV === 'production') {
console.log('AdminBro: bundling user components...')
await userComponentsBundler(this, { write: true })
}
}
/**
* Renders an entire login page with email and password fields
* using {@link Renderer}.
*
* Used by external plugins
*
* @param {Object} options
* @param {String} options.action Login form action url - it could be
* '/admin/login'
* @param {String} [options.errorMessage] Optional error message. When set,
* renderer will print this message in
* the form
* @return {Promise<string>} HTML of the rendered page
*/
static async renderLogin({ action, errorMessage }): Promise<string> {
return loginTemplate({ action, errorMessage })
}
/**
* Returns resource base on its ID
*
* @example
* const User = admin.findResource('users')
* await User.findOne(userId)
*
* @param {String} resourceId ID of a resource defined under {@link BaseResource#id}
* @return {BaseResource} found resource
* @throws {Error} When resource with given id cannot be found
*/
findResource(resourceId): BaseResource {
const resource = this.resources.find(m => m.id() === resourceId)
if (!resource) {
throw new Error([
`There are no resources with given id: "${resourceId}"`,
'This is the list of all registered resources you can use:',
this.resources.map(r => r.id()).join(', '),
].join('\n'))
}
return resource
}
/**
* Requires given .jsx/.tsx file, that it can be bundled to the frontend.
* It will be available under AdminBro.UserComponents[componentId].
*
* @param {String} src Path to a file containing react component.
*
* @return {String} componentId - uniq id of a component
*
* @example
* const adminBroOptions = {
* dashboard: {
* component: AdminBro.bundle('./path/to/component'),
* }
* }
*/
public static bundle(src: string): string {
const extensions = ['.jsx', '.js', '.ts', '.tsx']
let filePath = ''
const componentId = _.uniqueId('Component')
if (src[0] === '/') {
filePath = src
} else {
const stack = ((new Error()).stack || '').split('\n')
const m = stack[2].match(/\((.*):[0-9]+:[0-9]+\)/)
if (!m) {
throw new Error('STACK does not have a file url. Check out if the node version >= 8')
}
filePath = path.join(path.dirname(m[1]), src)
}
const { root, dir, name } = path.parse(filePath)
if (!extensions.find((ext) => {
const fileName = path.format({ root, dir, name, ext })
return fs.existsSync(fileName)
})) {
throw new ConfigurationError(`Given file "${src}", doesn't exist.`, 'AdminBro.html')
}
AdminBro.UserComponents[componentId] = path.format({ root, dir, name })
return componentId
}
}
AdminBro.UserComponents = {}
AdminBro.registeredAdapters = []
AdminBro.VERSION = VERSION
export const { registerAdapter } = AdminBro
export const { bundle } = AdminBro
export default AdminBro
Source