1 | import * as _ from 'lodash'
|
2 | import * as path from 'path'
|
3 | import * as fs from 'fs'
|
4 |
|
5 | import AdminBroOptions from './admin-bro-options.interface'
|
6 | import BaseResource from './backend/adapters/base-resource'
|
7 | import BaseDatabase from './backend/adapters/base-database'
|
8 | import BaseRecord from './backend/adapters/base-record'
|
9 | import BaseProperty from './backend/adapters/base-property'
|
10 | import Filter from './backend/utils/filter'
|
11 | import ValidationError from './backend/utils/validation-error'
|
12 | import ConfigurationError from './backend/utils/configuration-error'
|
13 | import ResourcesFactory from './backend/utils/resources-factory'
|
14 | import userComponentsBunlder from './backend/bundler/user-components-bundler'
|
15 | import { RouterType } from './backend/router'
|
16 | import Action from './backend/actions/action.interface'
|
17 |
|
18 | import loginTemplate from './frontend/login-template'
|
19 |
|
20 | const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'))
|
21 | export const VERSION = pkg.version
|
22 |
|
23 | const 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 |
|
41 | type ActionsMap = {[key: string]: Action }
|
42 |
|
43 | type UserComponentsMap = {[key: string]: string}
|
44 |
|
45 | export 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 | */
|
59 | class 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 |
|
221 | AdminBro.UserComponents = {}
|
222 | AdminBro.registeredAdapters = []
|
223 | AdminBro.VERSION = VERSION
|
224 |
|
225 | export const { registerAdapter } = AdminBro
|
226 | export const { bundle } = AdminBro
|
227 |
|
228 | export 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 | */
|