UNPKG

9.84 kBPlain TextView Raw
1import * as _ from 'lodash'
2import * as path from 'path'
3import * as fs from 'fs'
4
5import AdminBroOptions from './admin-bro-options.interface'
6import BaseResource from './backend/adapters/base-resource'
7import BaseDatabase from './backend/adapters/base-database'
8import BaseRecord from './backend/adapters/base-record'
9import BaseProperty from './backend/adapters/base-property'
10import Filter from './backend/utils/filter'
11import ValidationError from './backend/utils/validation-error'
12import ConfigurationError from './backend/utils/configuration-error'
13import ResourcesFactory from './backend/utils/resources-factory'
14import userComponentsBunlder from './backend/bundler/user-components-bundler'
15import { RouterType } from './backend/router'
16import Action from './backend/actions/action.interface'
17
18import loginTemplate from './frontend/login-template'
19
20const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'))
21export const VERSION = pkg.version
22
23const defaults: AdminBroOptions = {
24 rootPath: '/admin',
25 logoutPath: '/admin/logout',
26 loginPath: '/admin/login',
27 databases: [],
28 resources: [],
29 branding: {
30 companyName: 'Company',
31 softwareBrothers: true,
32 },
33 dashboard: {},
34 assets: {
35 styles: [],
36 scripts: [],
37 globalsFromCDN: true,
38 },
39}
40
41type ActionsMap = {[key: string]: Action }
42
43type UserComponentsMap = {[key: string]: string}
44
45export type Adapter = { Database: typeof BaseDatabase; Resource: typeof BaseResource }
46
47/**
48 * Main class for AdminBro extension. It takes {@link AdminBroOptions} as a
49 * parameter and creates an admin instance.
50 *
51 * Its main responsibility is to fetch all the resources and/or databases given by a
52 * user. Its instance is a currier - injected in all other classes.
53 *
54 * @example
55 * const { AdminBro } = require('admin-bro')
56 * const admin = new AdminBro(AdminBroOptions)
57 *
58 */
59class AdminBro {
60 public resources: Array<BaseResource>
61
62 public options: AdminBroOptions
63
64 public static registeredAdapters: Array<Adapter>
65
66 public static Router: RouterType
67
68 public static BaseDatabase: typeof BaseDatabase
69
70 public static BaseRecord: typeof BaseRecord
71
72 public static BaseResource: typeof BaseResource
73
74 public static BaseProperty: typeof BaseProperty
75
76 public static Filter: typeof Filter
77
78 public static ValidationError: typeof ValidationError
79
80 public static ACTIONS: ActionsMap
81
82 public static VERSION: string
83
84 public static UserComponents: UserComponentsMap
85
86 /**
87 * @param {AdminBroOptions} options options passed to adminBro
88 */
89 constructor(options: AdminBroOptions = {}) {
90 /**
91 * @type {BaseResource[]}
92 * @description List of all resources available for the AdminBro.
93 * They can be fetched with the {@link AdminBro#findResource} method
94 */
95 this.resources = []
96
97 /**
98 * @type {AdminBroOptions}
99 * @description Options given by a user
100 */
101 this.options = _.merge({}, defaults, options)
102
103 this.options.branding.logo = this.options.branding.logo || `${this.options.rootPath}/frontend/assets/logo-mini.svg`
104
105 const { databases, resources } = this.options
106 const resourcesFactory = new ResourcesFactory(this, AdminBro.registeredAdapters)
107 this.resources = resourcesFactory.buildResources({ databases, resources })
108 }
109
110 /**
111 * Registers various database adapters written for AdminBro.
112 *
113 * @example
114 * const AdminBro = require('admin-bro')
115 * const MongooseAdapter = require('admin-bro-mongoose')
116 * AdminBro.registerAdapter(MongooseAdapter)
117 *
118 * @param {Object} options
119 * @param {typeof BaseDatabase} options.Database subclass of BaseDatabase
120 * @param {typeof BaseResource} options.Resource subclass of BaseResource
121 */
122 static registerAdapter({ Database, Resource }: {
123 Database: typeof BaseDatabase;
124 Resource: typeof BaseResource;
125 }): void {
126 if (!Database || !Resource) {
127 throw new Error('Adapter has to have both Database and Resource')
128 }
129 // checking if both Database and Resource have at least isAdapterFor method
130 if (Database.isAdapterFor && Resource.isAdapterFor) {
131 AdminBro.registeredAdapters.push({ Database, Resource })
132 } else {
133 throw new Error('Adapter elements has to be subclassess of AdminBro.BaseResource and AdminBro.BaseDatabase')
134 }
135 }
136
137 /**
138 * Initializes AdminBro instance in production. This function should be called by
139 * all external plugins.
140 */
141 async initialize(): Promise<void> {
142 if (process.env.NODE_ENV === 'production') {
143 console.log('AdminBro: bundling user components...')
144 await userComponentsBunlder(this, { write: true })
145 }
146 }
147
148 /**
149 * Renders an entire login page with email and password fields
150 * using {@link Renderer}.
151 *
152 * Used by external plugins
153 *
154 * @param {Object} options
155 * @param {String} options.action login form action url - it could be
156 * '/admin/login'
157 * @param {String} [options.errorMessage] optional error message. When set,
158 * renderer will print this message in
159 * the form
160 * @return {Promise<string>} HTML of the rendered page
161 */
162 static async renderLogin({ action, errorMessage }): Promise<string> {
163 return loginTemplate({ action, errorMessage })
164 }
165
166 /**
167 * Returns resource base on its ID
168 *
169 * @example
170 * const User = admin.findResource('users')
171 * await User.findOne(userId)
172 *
173 * @param {String} resourceId ID of a resource defined under {@link BaseResource#id}
174 * @return {BaseResource} found resource
175 */
176 findResource(resourceId): BaseResource {
177 return this.resources.find(m => m.id() === resourceId)
178 }
179
180 /**
181 * Requires given .jsx/.tsx file, that it can be bundled to the frontend.
182 * It will be available under AdminBro.UserComponents[componentId].
183 *
184 * @param {String} src path to a file containing react component.
185 *
186 * @return {String} componentId - uniq id of a component
187 *
188 * @example
189 * const adminBroOptions = {
190 * dashboard: {
191 * component: AdminBro.bundle('./path/to/component'),
192 * }
193 * }
194 */
195 public static bundle(src: string): string {
196 const extensions = ['.jsx', '.js']
197 let filePath = ''
198 const componentId = _.uniqueId('Component')
199 if (src[0] === '/') {
200 filePath = src
201 } else {
202 const stack = ((new Error()).stack).split('\n')
203 const m = stack[2].match(/\((.*):[0-9]+:[0-9]+\)/)
204 filePath = path.join(path.dirname(m[1]), src)
205 }
206
207 const { root, dir, name } = path.parse(filePath)
208 if (!extensions.find((ext) => {
209 const fileName = path.format({ root, dir, name, ext })
210 return fs.existsSync(fileName)
211 })) {
212 throw new ConfigurationError(`Given file "${src}", doesn't exist.`, 'AdminBro.html')
213 }
214
215 AdminBro.UserComponents[componentId] = path.format({ root, dir, name })
216
217 return componentId
218 }
219}
220
221AdminBro.UserComponents = {}
222AdminBro.registeredAdapters = []
223AdminBro.VERSION = VERSION
224
225export const { registerAdapter } = AdminBro
226export const { bundle } = AdminBro
227
228export default AdminBro
229
230
231/**
232 * @description
233 * Contains set of routes availables within the application.
234 * It is used by external plugins.
235 *
236 * @example
237 * const { Router } = require('admin-bro')
238 * Router.routes.forEach(route => {
239 * // map your framework routes to admin-bro routes
240 * // see how `admin-bro-expressjs` plugin does it.
241 * })
242 *
243 * @memberof AdminBro
244 * @static
245 * @name Router
246 * @alias AdminBro.Router
247 * @type RouterType
248 */
249
250/**
251 * abstract class for all databases. External adapters have to implement that.
252 * @memberof AdminBro
253 * @static
254 * @abstract
255 * @name AdminBro.BaseDatabase
256 * @type {typeof BaseDatabase}
257 */
258
259/**
260 * abstract class for all records. External adapters have to implement that or at least
261 * their BaseResource implementation should return records of this type.
262 * @memberof AdminBro
263 * @static
264 * @abstract
265 * @name AdminBro.BaseRecord
266 * @type {typeof BaseRecord}
267 */
268
269/**
270 * abstract class for all resources. External adapters have to implement that.
271 * @memberof AdminBro
272 * @static
273 * @abstract
274 * @name AdminBro.BaseResource
275 * @type {typeof BaseResource}
276 */
277
278/**
279 * abstract class for all properties. External adapters have to implement that or at least
280 * their BaseResource implementation should return records of this type.
281 * @memberof AdminBro
282 * @static
283 * @abstract
284 * @name AdminBro.BaseProperty
285 * @type {typeof BaseProperty}
286 */
287
288/**
289 * Filter object passed to find method of BaseResource. External adapters have to use it
290 * @memberof AdminBro
291 * @static
292 * @abstract
293 * @name AdminBro.Filter
294 * @type {typeof Filter}
295 */
296
297/**
298 * Validation error which is thrown when record fails validation. External adapters have
299 * to use it.
300 * @memberof AdminBro
301 * @static
302 * @name AdminBro.ValidationError
303 * @type {typeof ValidationError}
304 */
305
306/**
307 * List of all default actions. If you want to change behaviour for all actions lika list,
308 * edit, show and delete you can do this here.
309 *
310 * @example <caption>Modifying accessibility rules for all show actions</caption>
311 * const { ACTIONS } = require('admin-bro')
312 * ACTIONS.show.isAccessible = () => {...}
313 *
314 *
315 * @memberof AdminBro
316 * @static
317 * @name AdminBro.ACTIONS
318 * @type {ActionsMap}
319 */
320
321/**
322 * AdminBro version
323 * @memberof AdminBro
324 * @static
325 * @name AdminBro.VERSION
326 * @type {string}
327 */
328
329/**
330 * List of all bundled components
331 * @memberof AdminBro
332 * @static
333 * @name AdminBro.UserComponents
334 * @type {UserComponentsMap}
335 */