@ditojs/server 1.13.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,36 +15,35 @@ import {
15
15
  } from '@ditojs/utils'
16
16
 
17
17
  export class Controller {
18
+ name = null
19
+ path = null
20
+ url = null
21
+ actions = null
22
+ assets = null
23
+ transacted = false
24
+ initialized = false
25
+
18
26
  constructor(app, namespace) {
19
27
  this.app = app
20
- this.namespace = this.namespace || namespace
28
+ this.namespace = namespace
21
29
  this.logging = this.app.config.log.routes
22
30
  this.level = 0
23
31
  }
24
32
 
33
+ // `configure()` is called right after the constructor, but before `setup()`
34
+ // which sets up the actions and routes, and the custom `async initialize()`.
25
35
  // @overridable
26
- initialize() {
27
- }
28
-
29
- // @return {Application|Function} [app or function]
30
- compose() {
31
- // To be overridden in sub-classes, if the controller needs to install
32
- // middleware. For normal routes, use `this.app.addRoute()` instead.
33
- }
34
-
35
- setup(isRoot = true, setupActionsObject = true) {
36
- this._setupEmitter(this.hooks, {
36
+ configure() {
37
+ this._configureEmitter(this.hooks, {
37
38
  // Support wildcard hooks only on controllers:
38
39
  wildcard: true
39
40
  })
40
41
  // If the class name ends in 'Controller', remove it from controller name.
41
- this.name = this.name ||
42
- this.constructor.name.match(/^(.*?)(?:Controller|)$/)[1]
43
- if (this.path === undefined) {
44
- this.path = this.app.normalizePath(this.name)
45
- }
46
- this.transacted = !!this.transacted
47
- if (isRoot) {
42
+ this.name ||= this.constructor.name.match(/^(.*?)(?:Controller|)$/)[1]
43
+ this.path ??= this.app.normalizePath(this.name)
44
+ if (!this.url) {
45
+ // NOTE: `RelationController.configure()` sets the `url` before calling
46
+ // `super.configure()` and has its own handling of logging.
48
47
  const { path, namespace } = this
49
48
  // TODO: The distinction between `url` and `path` is a bit tricky, since
50
49
  // what we call `url` here is called `path` in Router, and may contain
@@ -61,16 +60,30 @@ export class Controller {
61
60
  }`,
62
61
  this.level
63
62
  )
64
- if (setupActionsObject) {
65
- this.actions = this.actions || this.reflectActionsObject()
66
- // Now that the instance fields are reflected in the `controller` object
67
- // we can use the normal inheritance mechanism through `setupActions()`:
68
- this.actions = this.setupActions('actions')
69
- }
70
- this.assets = this.setupAssets()
71
63
  }
72
64
  }
73
65
 
66
+ // @overridable
67
+ setup() {
68
+ this.actions ||= this.reflectActionsObject()
69
+ // Now that the instance fields are reflected in the `controller` object
70
+ // we can use the normal inheritance mechanism through `setupActions()`:
71
+ this.actions = this.setupActions('actions')
72
+ this.assets = this.setupAssets()
73
+ }
74
+
75
+ // @overridable
76
+ async initialize() {
77
+ // To be overridden in sub-classes, if the controller needs to initialize.
78
+ }
79
+
80
+ // @return {Application|Function} [app or function]
81
+ // @overridable
82
+ compose() {
83
+ // To be overridden in sub-classes, if the controller needs to install
84
+ // middleware. For normal routes, use `this.app.addRoute()` instead.
85
+ }
86
+
74
87
  reflectActionsObject() {
75
88
  // On base controllers, the actions can be defined directly in the class
76
89
  // instead of inside an actions object, as is done with model and relation
@@ -78,7 +91,7 @@ export class Controller {
78
91
  // these other controllers, we reflect these instance fields in a separate
79
92
  // `actions` object.
80
93
  const { allow } = this
81
- const controller = allow ? { allow } : {}
94
+ const actions = allow ? { allow } : {}
82
95
 
83
96
  const addAction = key => {
84
97
  const value = this[key]
@@ -86,7 +99,7 @@ export class Controller {
86
99
  // in turn sets the `method` property on the method, as well as action
87
100
  // objects which provide the `method` property:
88
101
  if (value?.method) {
89
- controller[key] = value
102
+ actions[key] = value
90
103
  }
91
104
  }
92
105
  // Use `Object.getOwnPropertyNames()` to get the fields, in order to
@@ -95,7 +108,7 @@ export class Controller {
95
108
  const proto = Object.getPrototypeOf(this)
96
109
  Object.getOwnPropertyNames(proto).forEach(addAction)
97
110
  Object.getOwnPropertyNames(this).forEach(addAction)
98
- return controller
111
+ return actions
99
112
  }
100
113
 
101
114
  setupRoute(method, url, transacted, authorize, action, middlewares) {
@@ -6,10 +6,14 @@ import { ControllerError } from '../errors/index.js'
6
6
  import { setupPropertyInheritance } from '../utils/index.js'
7
7
 
8
8
  export class ModelController extends CollectionController {
9
- setup() {
10
- super.setup(true)
11
- this.modelClass = this.modelClass ||
9
+ configure() {
10
+ super.configure()
11
+ this.modelClass ||=
12
12
  this.app.models[camelize(pluralize.singular(this.name), true)]
13
+ }
14
+
15
+ setup() {
16
+ super.setup()
13
17
  this.relations = this.setupRelations()
14
18
  }
15
19
 
@@ -36,9 +40,14 @@ export class ModelController extends CollectionController {
36
40
  if (!relationInstance || !relationDefinition) {
37
41
  throw new ControllerError(this, `Relation '${name}' not found.`)
38
42
  }
39
- return new RelationController(
43
+ const relation = new RelationController(
40
44
  this, object, relationInstance, relationDefinition
41
45
  )
46
+ // RelationController instances are not registered with the app, but are
47
+ // manged by their parent controller instead.
48
+ relation.configure()
49
+ relation.setup()
50
+ return relation
42
51
  }
43
52
 
44
53
  // @override
@@ -29,10 +29,6 @@ export class RelationController extends CollectionController {
29
29
  // relations:
30
30
  this.scope = asArray(parent.scope).filter(scope => getScope(scope).graph)
31
31
  }
32
- // Initialize:
33
- this.path = this.app.normalizePath(this.name)
34
- this.url = `${this.parent.url}/${this.parent.getPath('member', this.path)}`
35
- this.log(`${pico.blue(this.path)}${pico.white(':')}`, this.level)
36
32
  // Copy over all fields in the relation object except the ones that are
37
33
  // going to be inherited in `setup()` (relation, member, allow), for
38
34
  // settings like scope, etc.
@@ -41,7 +37,15 @@ export class RelationController extends CollectionController {
41
37
  this[key] = this.object[key]
42
38
  }
43
39
  }
44
- this.setup(false)
40
+ }
41
+
42
+ // @override
43
+ configure() {
44
+ // Setup the `url` before calling `super.configure()` to override its
45
+ // default behavior for `RelationController`:
46
+ this.url = `${this.parent.url}/${this.parent.getPath('member', this.path)}`
47
+ this.log(`${pico.blue(this.path)}${pico.white(':')}`, this.level)
48
+ super.configure()
45
49
  }
46
50
 
47
51
  // @override
@@ -3,7 +3,7 @@ import { isPlainObject, isString, isArray, asArray } from '@ditojs/utils'
3
3
 
4
4
  export class EventEmitter extends EventEmitter2 {
5
5
  // Method for classes that use `EventEmitter.mixin()` to setup the emitter.
6
- _setupEmitter(events, options) {
6
+ _configureEmitter(events, options) {
7
7
  EventEmitter2.call(this, {
8
8
  delimiter: ':',
9
9
  maxListeners: 0,
@@ -68,8 +68,7 @@ export const UserMixin = mixin(Model => class extends Model {
68
68
  return this.$is(ctx.state.user)
69
69
  }
70
70
 
71
- static initialize() {
72
- super.initialize()
71
+ static setup() {
73
72
  userClasses[this.name] = this
74
73
  const {
75
74
  usernameProperty,
@@ -23,6 +23,10 @@ import RelationAccessor from './RelationAccessor.js'
23
23
  import definitions from './definitions/index.js'
24
24
 
25
25
  export class Model extends objection.Model {
26
+ static app = null // Set by `Application.addModel()`
27
+ static initialized = false
28
+ static referenceValidator = null
29
+
26
30
  // Define a default constructor to allow new Model(json) as a short-cut to
27
31
  // `Model.fromJson(json, { skipValidation: true })`
28
32
  constructor(json) {
@@ -32,19 +36,24 @@ export class Model extends objection.Model {
32
36
  }
33
37
  }
34
38
 
35
- static setup(knex) {
36
- this.knex(knex)
39
+ static configure(app) {
40
+ this.app = app
41
+ this.knex(app.knex)
42
+ const { hooks, assets } = this.definition
43
+ this._configureEmitter(hooks)
44
+ if (assets) {
45
+ this._configureAssetsEvents(assets)
46
+ }
37
47
  try {
38
48
  for (const relation of Object.values(this.getRelations())) {
39
- this.setupRelation(relation)
49
+ this._configureRelation(relation)
40
50
  }
41
51
  } catch (error) {
42
52
  throw error instanceof RelationError ? error : new RelationError(error)
43
53
  }
44
- this.referenceValidator = null
45
54
  }
46
55
 
47
- static setupRelation(relation) {
56
+ static _configureRelation(relation) {
48
57
  // Add this relation to the related model's relatedRelations, so it can
49
58
  // register all required foreign keys in its properties.
50
59
  relation.relatedModelClass.getRelatedRelations().push(relation)
@@ -87,13 +96,12 @@ export class Model extends objection.Model {
87
96
  defineAccessor(this.prototype, false)
88
97
  }
89
98
 
99
+ // @overridable
100
+ static setup() {
101
+ }
102
+
90
103
  // @overridable
91
104
  static initialize() {
92
- const { hooks, assets } = this.definition
93
- this._setupEmitter(hooks)
94
- if (assets) {
95
- this._setupAssetsEvents(assets)
96
- }
97
105
  }
98
106
 
99
107
  // @overridable
@@ -900,7 +908,7 @@ export class Model extends objection.Model {
900
908
 
901
909
  // Assets handling
902
910
 
903
- static _setupAssetsEvents(assets) {
911
+ static _configureAssetsEvents(assets) {
904
912
  const assetDataPaths = Object.keys(assets)
905
913
 
906
914
  this.on([
@@ -167,7 +167,7 @@ export class QueryBuilder extends objection.QueryBuilder {
167
167
  }
168
168
 
169
169
  allowScope(...scopes) {
170
- this._allowScopes = this._allowScopes || {
170
+ this._allowScopes ||= {
171
171
  default: true // The default scope is always allowed.
172
172
  }
173
173
  for (const expr of scopes) {
@@ -279,7 +279,7 @@ export class QueryBuilder extends objection.QueryBuilder {
279
279
  }
280
280
 
281
281
  allowFilter(...filters) {
282
- this._allowFilters = this._allowFilters || {}
282
+ this._allowFilters ||= {}
283
283
  for (const filter of filters) {
284
284
  this._allowFilters[filter] = true
285
285
  }
@@ -1,6 +1,8 @@
1
1
  import { camelize } from '@ditojs/utils'
2
2
 
3
3
  export class Service {
4
+ initialized = false
5
+
4
6
  constructor(app, name) {
5
7
  this.app = app
6
8
  this.name = camelize(
@@ -16,7 +18,7 @@ export class Service {
16
18
  }
17
19
 
18
20
  // @overridable
19
- initialize() {
21
+ async initialize() {
20
22
  }
21
23
 
22
24
  // @overridable
@@ -34,7 +36,7 @@ export class Service {
34
36
 
35
37
  get logger() {
36
38
  const value = this.getLogger()
37
- Object.defineProperties(this, 'logger', { value })
39
+ Object.defineProperty(this, 'logger', { value })
38
40
  return value
39
41
  }
40
42
  }
@@ -6,12 +6,10 @@ import { Storage } from './Storage.js'
6
6
  export class DiskStorage extends Storage {
7
7
  static type = 'disk'
8
8
 
9
- constructor(app, config) {
10
- super(app, config)
9
+ setup() {
11
10
  if (!this.path) {
12
11
  throw new Error(`Missing configuration (path) for storage ${this.name}`)
13
12
  }
14
-
15
13
  this.storage = multer.diskStorage({
16
14
  destination: (req, storageFile, cb) => {
17
15
  // Add `storageFile.key` property to internal storage file object.
@@ -1,4 +1,3 @@
1
- import { S3 } from '@aws-sdk/client-s3'
2
1
  import multerS3 from 'multer-s3'
3
2
  import { fileTypeFromBuffer } from 'file-type'
4
3
  import isSvg from 'is-svg'
@@ -9,15 +8,22 @@ import consumers from 'stream/consumers'
9
8
  export class S3Storage extends Storage {
10
9
  static type = 's3'
11
10
 
12
- constructor(app, config) {
13
- super(app, config)
11
+ s3 = null
12
+ acl = null
13
+ bucket = null
14
+
15
+ async setup() {
14
16
  const {
15
17
  name,
16
18
  s3,
17
19
  acl,
18
20
  bucket,
19
21
  ...options
20
- } = config
22
+ } = this.config
23
+
24
+ // "@aws-sdk/client-s3" is a peer-dependency, and importing it costly,
25
+ // so we do it lazily.
26
+ const { S3 } = await import('@aws-sdk/client-s3')
21
27
  this.s3 = new S3(s3)
22
28
  this.acl = acl
23
29
  this.bucket = bucket
@@ -10,6 +10,8 @@ import { AssetFile } from './AssetFile.js'
10
10
  const storageClasses = {}
11
11
 
12
12
  export class Storage {
13
+ initialized = false
14
+
13
15
  constructor(app, config) {
14
16
  this.app = app
15
17
  this.config = config
@@ -20,6 +22,14 @@ export class Storage {
20
22
  this.storage = null
21
23
  }
22
24
 
25
+ // @overridable
26
+ async setup() {
27
+ }
28
+
29
+ // @overridable
30
+ async initialize() {
31
+ }
32
+
23
33
  static register(storageClass) {
24
34
  const type = (
25
35
  storageClass.type ||