import * as _ from 'lodash' import * as path from 'path' import * as fs from 'fs' import AdminBroOptions 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 userComponentsBunlder from './backend/bundler/user-components-bundler' import { RouterType } from './backend/router' import Action from './backend/actions/action.interface' import loginTemplate from './frontend/login-template' const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8')) export const VERSION = pkg.version const defaults: AdminBroOptions = { rootPath: '/admin', logoutPath: '/admin/logout', loginPath: '/admin/login', databases: [], resources: [], branding: { companyName: 'Company', softwareBrothers: true, }, dashboard: {}, assets: { styles: [], scripts: [], globalsFromCDN: true, }, } type ActionsMap = {[key: string]: Action } 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 public options: AdminBroOptions public static registeredAdapters: Array public static Router: RouterType public static BaseDatabase: typeof BaseDatabase public static BaseRecord: typeof BaseRecord public static BaseResource: typeof BaseResource public static BaseProperty: typeof BaseProperty public static Filter: typeof Filter public static ValidationError: typeof ValidationError public static ACTIONS: ActionsMap public static VERSION: string 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) this.options.branding.logo = this.options.branding.logo || `${this.options.rootPath}/frontend/assets/logo-mini.svg` 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 BaseDatabase * @param {typeof BaseResource} options.Resource subclass of 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 subclassess of AdminBro.BaseResource and AdminBro.BaseDatabase') } } /** * Initializes AdminBro instance in production. This function should be called by * all external plugins. */ async initialize(): Promise { if (process.env.NODE_ENV === 'production') { console.log('AdminBro: bundling user components...') await userComponentsBunlder(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} HTML of the rendered page */ static async renderLogin({ action, errorMessage }): Promise { 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 */ findResource(resourceId): BaseResource { return this.resources.find(m => m.id() === resourceId) } /** * 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'] 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]+\)/) 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 /** * @description * Contains set of routes availables 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. * }) * * @memberof AdminBro * @static * @name Router * @alias AdminBro.Router * @type RouterType */ /** * abstract class for all databases. External adapters have to implement that. * @memberof AdminBro * @static * @abstract * @name AdminBro.BaseDatabase * @type {typeof BaseDatabase} */ /** * abstract class for all records. External adapters have to implement that or at least * their BaseResource implementation should return records of this type. * @memberof AdminBro * @static * @abstract * @name AdminBro.BaseRecord * @type {typeof BaseRecord} */ /** * abstract class for all resources. External adapters have to implement that. * @memberof AdminBro * @static * @abstract * @name AdminBro.BaseResource * @type {typeof BaseResource} */ /** * abstract class for all properties. External adapters have to implement that or at least * their BaseResource implementation should return records of this type. * @memberof AdminBro * @static * @abstract * @name AdminBro.BaseProperty * @type {typeof BaseProperty} */ /** * Filter object passed to find method of BaseResource. External adapters have to use it * @memberof AdminBro * @static * @abstract * @name AdminBro.Filter * @type {typeof Filter} */ /** * Validation error which is thrown when record fails validation. External adapters have * to use it. * @memberof AdminBro * @static * @name AdminBro.ValidationError * @type {typeof ValidationError} */ /** * List of all default actions. If you want to change behaviour for all actions lika list, * edit, show and delete you can do this here. * * @example Modifying accessibility rules for all show actions * const { ACTIONS } = require('admin-bro') * ACTIONS.show.isAccessible = () => {...} * * * @memberof AdminBro * @static * @name AdminBro.ACTIONS * @type {ActionsMap} */ /** * AdminBro version * @memberof AdminBro * @static * @name AdminBro.VERSION * @type {string} */ /** * List of all bundled components * @memberof AdminBro * @static * @name AdminBro.UserComponents * @type {UserComponentsMap} */