@ditojs/server 1.13.1 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ditojs/server",
3
- "version": "1.13.1",
3
+ "version": "1.14.0",
4
4
  "type": "module",
5
5
  "description": "Dito.js Server – Dito.js is a declarative and modern web framework, based on Objection.js, Koa.js and Vue.js",
6
6
  "repository": "https://github.com/ditojs/dito/tree/master/packages/server",
@@ -25,15 +25,14 @@
25
25
  "node >= 18"
26
26
  ],
27
27
  "dependencies": {
28
- "@aws-sdk/client-s3": "^3.200.0",
29
- "@ditojs/admin": "^1.13.1",
30
- "@ditojs/build": "^1.13.0",
31
- "@ditojs/router": "^1.13.1",
32
- "@ditojs/utils": "^1.13.1",
28
+ "@ditojs/admin": "^1.14.0",
29
+ "@ditojs/build": "^1.14.0",
30
+ "@ditojs/router": "^1.14.0",
31
+ "@ditojs/utils": "^1.14.0",
33
32
  "@koa/cors": "^4.0.0",
34
- "@koa/multer": "^3.0.0",
33
+ "@koa/multer": "^3.0.2",
35
34
  "@originjs/vite-plugin-commonjs": "^1.0.3",
36
- "ajv": "^8.11.0",
35
+ "ajv": "^8.11.2",
37
36
  "ajv-formats": "^2.1.1",
38
37
  "bcryptjs": "^2.4.3",
39
38
  "bytes": "^3.1.2",
@@ -69,17 +68,19 @@
69
68
  "pluralize": "^8.0.0",
70
69
  "repl": "^0.1.3",
71
70
  "uuid": "^9.0.0",
72
- "vite": "^3.2.2",
71
+ "vite": "^3.2.4",
73
72
  "vite-plugin-vue2": "^2.0.2",
74
- "vue": "^2.7.13",
75
- "vue-template-compiler": "^2.7.13"
73
+ "vue": "^2.7.14",
74
+ "vue-template-compiler": "^2.7.14"
76
75
  },
77
76
  "peerDependencies": {
77
+ "@aws-sdk/client-s3": "^3.0.0",
78
78
  "knex": ">=2.0.5",
79
79
  "objection": "^3.0.1"
80
80
  },
81
81
  "devDependencies": {
82
- "@types/koa-bodyparser": "^4.3.9",
82
+ "@aws-sdk/client-s3": "^3.200.0",
83
+ "@types/koa-bodyparser": "^4.3.10",
83
84
  "@types/koa-compress": "^4.0.3",
84
85
  "@types/koa-logger": "^3.1.2",
85
86
  "@types/koa-pino-logger": "^3.0.1",
@@ -90,9 +91,9 @@
90
91
  "@types/node": "^18.11.9",
91
92
  "knex": "^2.3.0",
92
93
  "objection": "^3.0.1",
93
- "type-fest": "^3.1.0",
94
- "typescript": "^4.8.4"
94
+ "type-fest": "^3.2.0",
95
+ "typescript": "^4.9.3"
95
96
  },
96
97
  "types": "types",
97
- "gitHead": "0c6a508e04ca915691c1fb0f3706827c8ecfe0ab"
98
+ "gitHead": "ba197ae5254deb657b2c3b5dab7a851f488e022a"
98
99
  }
@@ -19,6 +19,7 @@ import session from 'koa-session'
19
19
  import etag from 'koa-etag'
20
20
  import helmet from 'koa-helmet'
21
21
  import responseTime from 'koa-response-time'
22
+ import { Model, knexSnakeCaseMappers, ref } from 'objection'
22
23
  import Router from '@ditojs/router'
23
24
  import {
24
25
  isArray, isObject, isString, asArray, isPlainObject, isModule,
@@ -31,7 +32,7 @@ import { Controller, AdminController } from '../controllers/index.js'
31
32
  import { Service } from '../services/index.js'
32
33
  import { Storage } from '../storage/index.js'
33
34
  import { convertSchema } from '../schema/index.js'
34
- import { formatJson } from '../utils/index.js'
35
+ import { deprecate, formatJson } from '../utils/index.js'
35
36
  import {
36
37
  ResponseError,
37
38
  ValidationError,
@@ -47,12 +48,6 @@ import {
47
48
  handleUser,
48
49
  logRequests
49
50
  } from '../middleware/index.js'
50
- import {
51
- Model,
52
- BelongsToOneRelation,
53
- knexSnakeCaseMappers,
54
- ref
55
- } from 'objection'
56
51
 
57
52
  export class Application extends Koa {
58
53
  constructor({
@@ -61,12 +56,12 @@ export class Application extends Koa {
61
56
  router,
62
57
  events,
63
58
  middleware,
64
- models,
65
59
  services,
60
+ models,
66
61
  controllers
67
62
  } = {}) {
68
63
  super()
69
- this._setupEmitter(events)
64
+ this._configureEmitter(events)
70
65
  const {
71
66
  // Pluck keys out of `config.app` to keep them secret
72
67
  app: { keys, ...app } = {},
@@ -84,12 +79,13 @@ export class Application extends Koa {
84
79
  this.router = router || new Router()
85
80
  this.validator.app = this
86
81
  this.storages = Object.create(null)
87
- this.models = Object.create(null)
88
82
  this.services = Object.create(null)
83
+ this.models = Object.create(null)
89
84
  this.controllers = Object.create(null)
90
85
  this.server = null
91
86
  this.isRunning = false
92
87
 
88
+ // TODO: Rename setup to configure?
93
89
  this.setupLogger()
94
90
  this.setupKnex()
95
91
  this.setupMiddleware(middleware)
@@ -97,17 +93,24 @@ export class Application extends Koa {
97
93
  if (config.storages) {
98
94
  this.addStorages(config.storages)
99
95
  }
100
- if (models) {
101
- this.addModels(models)
102
- }
103
96
  if (services) {
104
97
  this.addServices(services)
105
98
  }
99
+ if (models) {
100
+ this.addModels(models)
101
+ }
106
102
  if (controllers) {
107
103
  this.addControllers(controllers)
108
104
  }
109
105
  }
110
106
 
107
+ async setup() {
108
+ await this.setupStorages()
109
+ await this.setupServices()
110
+ await this.setupModels()
111
+ await this.setupControllers()
112
+ }
113
+
111
114
  addRoute(
112
115
  method, path, transacted, middlewares, controller = null, action = null
113
116
  ) {
@@ -133,121 +136,56 @@ export class Application extends Koa {
133
136
  this.router[method](path, route)
134
137
  }
135
138
 
136
- addModels(models) {
137
- // First add all models then call initialize() for each in a second loop,
138
- // since they may be referencing each other in relations.
139
- for (const modelClass of Object.values(models)) {
140
- this.addModel(modelClass)
141
- }
142
- // Now (re-)sort all models based on their relations.
143
- this.models = this.sortModels(this.models)
144
- // Filter through all sorted models, keeping only the newly added ones.
145
- const sortedModels = Object.values(this.models).filter(
146
- modelClass => models[modelClass.name] === modelClass
147
- )
148
- // Initialize the added models in correct sorted sequence, so that for every
149
- // model, getRelatedRelations() returns the full list of relating relations.
150
- for (const modelClass of sortedModels) {
151
- if (models[modelClass.name] === modelClass) {
152
- modelClass.setup(this.knex)
153
- // Now that the modelClass is set up, call `initialize()`, which can be
154
- // overridden by sub-classes,without having to call `super.initialize()`
155
- modelClass.initialize()
156
- this.validator.addSchema(modelClass.getJsonSchema())
139
+ getStorage(name) {
140
+ return this.storages[name] || null
141
+ }
142
+
143
+ addStorage(config, name) {
144
+ let storage = null
145
+ if (isPlainObject(config)) {
146
+ const storageClass = Storage.get(config.type)
147
+ if (!storageClass) {
148
+ throw new Error(`Unsupported storage: ${config}`)
157
149
  }
150
+ // eslint-disable-next-line new-cap
151
+ storage = new storageClass(this, config)
152
+ } else if (config instanceof Storage) {
153
+ storage = config
158
154
  }
159
- const { log } = this.config
160
- if (log.schema || log.relations) {
161
- for (const modelClass of sortedModels) {
162
- const shouldLog = option => (
163
- option === true ||
164
- asArray(option).includes(modelClass.name)
165
- )
166
- const data = {}
167
- if (shouldLog(log.schema)) {
168
- data.schema = modelClass.getJsonSchema()
169
- }
170
- if (shouldLog(log.relations)) {
171
- data.relations = clone(modelClass.relationMappings, value =>
172
- Model.isPrototypeOf(value) ? `[Model: ${value.name}]` : value
173
- )
174
- }
175
- if (Object.keys(data).length > 0) {
176
- console.info(
177
- pico.yellow(pico.bold(`\n${modelClass.name}:\n`)),
178
- util.inspect(data, {
179
- colors: true,
180
- depth: null,
181
- maxArrayLength: null
182
- })
183
- )
184
- }
155
+ if (storage) {
156
+ if (name) {
157
+ storage.name = name
185
158
  }
159
+ this.storages[storage.name] = storage
186
160
  }
161
+ return storage
187
162
  }
188
163
 
189
- addModel(modelClass) {
190
- if (Model.isPrototypeOf(modelClass)) {
191
- modelClass.app = this
192
- this.models[modelClass.name] = modelClass
193
- } else {
194
- throw new Error(`Invalid model class: ${modelClass}`)
195
- }
196
- }
197
-
198
- sortModels(models) {
199
- const sortByRelations = (list, collected = {}, excluded = {}) => {
200
- for (const modelClass of list) {
201
- const { name } = modelClass
202
- if (!collected[name] && !excluded[name]) {
203
- for (const relation of Object.values(modelClass.getRelations())) {
204
- if (!(relation instanceof BelongsToOneRelation)) {
205
- const { relatedModelClass, joinTableModelClass } = relation
206
- for (const related of [joinTableModelClass, relatedModelClass]) {
207
- // Exclude self-references and generated join models:
208
- if (related && related !== modelClass && models[related.name]) {
209
- sortByRelations([related], collected, {
210
- // Exclude modelClass to prevent endless recursions:
211
- [name]: modelClass,
212
- ...excluded
213
- })
214
- }
215
- }
216
- }
217
- }
218
- collected[name] = modelClass
219
- }
220
- }
221
- return Object.values(collected)
222
- }
223
- // Return a new object with the sorted models as its key/value pairs.
224
- // NOTE: We need to reverse for the above algorithm to sort properly,
225
- // and then reverse the result back.
226
- return sortByRelations(Object.values(models).reverse()).reverse().reduce(
227
- (models, modelClass) => {
228
- models[modelClass.name] = modelClass
229
- return models
230
- },
231
- Object.create(null)
232
- )
164
+ addStorages(storages) {
165
+ for (const [name, config] of Object.entries(storages)) {
166
+ this.addStorage(config, name)
167
+ }
233
168
  }
234
169
 
235
- getModel(name) {
236
- return (
237
- this.models[name] ||
238
- !name.endsWith('Model') && this.models[`${name}Model`] ||
239
- null
170
+ async setupStorages() {
171
+ await Promise.all(Object.values(this.storages)
172
+ .filter(storage => !storage.initialized)
173
+ .map(async storage => {
174
+ // Different from models, services and controllers, storages can have
175
+ // async `setup()` methods, as used by `S3Storage`.
176
+ await storage.setup()
177
+ await storage.initialize()
178
+ storage.initialized = true
179
+ })
240
180
  )
241
181
  }
242
182
 
243
- findModel(callback) {
244
- return Object.values(this.models).find(callback)
183
+ getService(name) {
184
+ return this.services[name] || null
245
185
  }
246
186
 
247
- addServices(services) {
248
- for (const [name, service] of Object.entries(services)) {
249
- this.addService(service, name)
250
- }
187
+ findService(callback) {
188
+ return Object.values(this.services).find(callback) || null
251
189
  }
252
190
 
253
191
  addService(service, name) {
@@ -259,40 +197,126 @@ export class Application extends Koa {
259
197
  if (!(service instanceof Service)) {
260
198
  throw new Error(`Invalid service: ${service}`)
261
199
  }
262
- // Only after the constructor is called, `service.name` is guaranteed to be
263
- // set to the correct value, e.g. with an after-constructor class property.
264
- ({ name } = service)
265
- const config = this.config.services[name]
266
- if (config === undefined) {
267
- throw new Error(`Configuration missing for service '${name}'`)
268
- }
269
- // As a convention, the configuration of a service can be set to `false`
270
- // in order to entirely deactivate the service.
271
- if (config !== false) {
272
- service.setup(config)
273
- this.services[name] = service
274
- // Now that the service is set up, call `initialize()` which can be
275
- // overridden by services.
276
- service.initialize()
200
+ this.services[service.name] = service
201
+ }
202
+
203
+ addServices(services) {
204
+ for (const [name, service] of Object.entries(services)) {
205
+ this.addService(service, name)
277
206
  }
278
207
  }
279
208
 
280
- getService(name) {
281
- return this.services[name] || null
209
+ async setupServices() {
210
+ await Promise.all(Object.values(this.services)
211
+ .filter(service => !service.initialized)
212
+ .map(async service => {
213
+ const { name } = service
214
+ const config = this.config.services[name]
215
+ if (config === undefined) {
216
+ throw new Error(`Configuration missing for service '${name}'`)
217
+ }
218
+ // As a convention, the configuration of a service can be set to `false`
219
+ // in order to entirely deactivate the service.
220
+ if (config === false) {
221
+ delete this.services[name]
222
+ } else {
223
+ service.setup(config)
224
+ await service.initialize()
225
+ service.initialized = true
226
+ }
227
+ })
228
+ )
282
229
  }
283
230
 
284
- findService(callback) {
285
- return Object.values(this.services).find(callback)
231
+ getModel(name) {
232
+ return (
233
+ this.models[name] ||
234
+ !name.endsWith('Model') && this.models[`${name}Model`] ||
235
+ null
236
+ )
286
237
  }
287
238
 
288
- addControllers(controllers, namespace) {
289
- for (const [key, value] of Object.entries(controllers)) {
290
- if (isModule(value) || isPlainObject(value)) {
291
- this.addControllers(value, namespace ? `${namespace}/${key}` : key)
239
+ findModel(callback) {
240
+ return Object.values(this.models).find(callback) || null
241
+ }
242
+
243
+ addModel(modelClass) {
244
+ this.addModels([modelClass])
245
+ }
246
+
247
+ addModels(models) {
248
+ models = Object.values(models)
249
+ // First, add all models to the application, so that they can be referenced
250
+ // by other models, e.g. in `jsonSchema` and `relationMappings`:
251
+ for (const modelClass of models) {
252
+ if (Model.isPrototypeOf(modelClass)) {
253
+ this.models[modelClass.name] = modelClass
292
254
  } else {
293
- this.addController(value, namespace)
255
+ throw new Error(`Invalid model class: ${modelClass}`)
294
256
  }
295
257
  }
258
+ // Then, configure all models and add their schemas to the validator:
259
+ for (const modelClass of models) {
260
+ modelClass.configure(this)
261
+ this.validator.addSchema(modelClass.getJsonSchema())
262
+ }
263
+ this.logModels(models)
264
+ }
265
+
266
+ async setupModels() {
267
+ await Promise.all(Object.values(this.models)
268
+ .filter(modelClass => !modelClass.initialized)
269
+ .map(async modelClass => {
270
+ // While `setup()` is used for internal dito things, `initialize()` is
271
+ // called async and meant to be used by the user, without the need to
272
+ // call `super.initialize()`.
273
+ modelClass.setup()
274
+ await modelClass.initialize()
275
+ modelClass.initialized = true
276
+ })
277
+ )
278
+ }
279
+
280
+ logModels(models) {
281
+ const { log } = this.config
282
+ if (log.schema || log.relations) {
283
+ for (const modelClass of models) {
284
+ const shouldLog = option => (
285
+ option === true ||
286
+ asArray(option).includes(modelClass.name)
287
+ )
288
+ const data = {}
289
+ if (shouldLog(log.schema)) {
290
+ data.schema = modelClass.getJsonSchema()
291
+ }
292
+ if (shouldLog(log.relations)) {
293
+ data.relations = clone(
294
+ modelClass.getRelationMappings(),
295
+ value => Model.isPrototypeOf(value)
296
+ ? `[Model: ${value.name}]`
297
+ : value
298
+ )
299
+ }
300
+ if (Object.keys(data).length > 0) {
301
+ console.info(
302
+ pico.yellow(pico.bold(`\n${modelClass.name}:\n`)),
303
+ util.inspect(data, {
304
+ colors: true,
305
+ depth: null,
306
+ maxArrayLength: null
307
+ })
308
+ )
309
+ }
310
+ }
311
+ }
312
+ }
313
+
314
+ getController(url) {
315
+ return this.controllers[url] || null
316
+ }
317
+
318
+ findController(callback) {
319
+ return Object.values(this.controllers).find(callback) || null
296
320
  }
297
321
 
298
322
  addController(controller, namespace) {
@@ -305,26 +329,36 @@ export class Application extends Koa {
305
329
  throw new Error(`Invalid controller: ${controller}`)
306
330
  }
307
331
  // Inheritance of action methods cannot happen in the constructor itself,
308
- // so call separate `setup()` method after in order to take care of it.
309
- controller.setup()
332
+ // so call separate `configure()` method after in order to take care of it.
333
+ controller.configure()
310
334
  this.controllers[controller.url] = controller
311
- // Now that the controller is set up, call `initialize()` which can be
312
- // overridden by controllers.
313
- controller.initialize()
314
- // Each controller can also compose their own middleware (or app), e.g. as
315
- // used in `AdminController`:
316
- const composed = controller.compose()
317
- if (composed) {
318
- this.use(mount(controller.url, composed))
319
- }
320
335
  }
321
336
 
322
- getController(url) {
323
- return this.controllers[url] || null
337
+ addControllers(controllers, namespace) {
338
+ for (const [key, value] of Object.entries(controllers)) {
339
+ if (isModule(value) || isPlainObject(value)) {
340
+ this.addControllers(value, namespace ? `${namespace}/${key}` : key)
341
+ } else {
342
+ this.addController(value, namespace)
343
+ }
344
+ }
324
345
  }
325
346
 
326
- findController(callback) {
327
- return Object.values(this.controllers).find(callback)
347
+ async setupControllers() {
348
+ await Promise.all(Object.values(this.controllers)
349
+ .filter(controller => !controller.initialized)
350
+ .map(async controller => {
351
+ controller.setup()
352
+ await controller.initialize()
353
+ // Each controller can also compose their own middleware (or app), e.g.
354
+ // as used in `AdminController`:
355
+ const composed = controller.compose()
356
+ if (composed) {
357
+ this.use(mount(controller.url, composed))
358
+ }
359
+ controller.initialized = true
360
+ })
361
+ )
328
362
  }
329
363
 
330
364
  getAdminController() {
@@ -377,37 +411,6 @@ export class Application extends Koa {
377
411
  return assetConfig
378
412
  }
379
413
 
380
- addStorages(storages) {
381
- for (const [name, config] of Object.entries(storages)) {
382
- this.addStorage(config, name)
383
- }
384
- }
385
-
386
- addStorage(config, name) {
387
- let storage = null
388
- if (isPlainObject(config)) {
389
- const storageClass = Storage.get(config.type)
390
- if (!storageClass) {
391
- throw new Error(`Unsupported storage: ${config}`)
392
- }
393
- // eslint-disable-next-line new-cap
394
- storage = new storageClass(this, config)
395
- } else if (config instanceof Storage) {
396
- storage = config
397
- }
398
- if (storage) {
399
- if (name) {
400
- storage.name = name
401
- }
402
- this.storages[storage.name] = storage
403
- }
404
- return storage
405
- }
406
-
407
- getStorage(name) {
408
- return this.storages[name] || null
409
- }
410
-
411
414
  compileValidator(jsonSchema, options) {
412
415
  return jsonSchema
413
416
  ? this.validator.compile(jsonSchema, options)
@@ -715,6 +718,10 @@ export class Application extends Koa {
715
718
  if (this.config.log.errors !== false) {
716
719
  this.on('error', this.logError)
717
720
  }
721
+ // It's ok to call this multiple times, because only the entries in the
722
+ // registres (storages, services, models, controllers) that weren't
723
+ // initialized yet will be initialized.
724
+ await this.setup()
718
725
  await this.emit('before:start')
719
726
  this.server = await new Promise(resolve => {
720
727
  const server = this.listen(this.config.server, () => {
@@ -771,7 +778,7 @@ export class Application extends Koa {
771
778
  }
772
779
  }
773
780
 
774
- async startOrExit() {
781
+ async execute() {
775
782
  try {
776
783
  await this.start()
777
784
  } catch (err) {
@@ -780,6 +787,11 @@ export class Application extends Koa {
780
787
  }
781
788
  }
782
789
 
790
+ startOrExit() {
791
+ deprecate(`app.startOrExit() is deprecated. Call app.execute() instead.`)
792
+ return this.execute()
793
+ }
794
+
783
795
  // Assets handling
784
796
 
785
797
  async createAssets(storage, files, count = 0, trx = null) {
@@ -4,27 +4,31 @@ import { ControllerError } from '../errors/index.js'
4
4
 
5
5
  // Abstract base class for ModelController and RelationController
6
6
  export class CollectionController extends Controller {
7
- constructor(app, namespace) {
8
- super(app, namespace)
9
- this.modelClass = null // To be defined by sub-classes
10
- this.isOneToOne = false
11
- this.relate = false
12
- this.unrelate = false
13
- }
7
+ graph = false
8
+ scope = null
9
+ relate = false
10
+ unrelate = false
11
+ modelClass = null // To be defined by sub-classes
12
+ isOneToOne = false
13
+ idParam = null
14
+ idValidator = null
14
15
 
15
- setup(isRoot) {
16
- super.setup(isRoot, false)
16
+ // @override
17
+ configure() {
18
+ super.configure()
17
19
  this.idParam = this.level ? `id${this.level}` : 'id'
18
- this.graph = !!this.graph
19
- this.transacted = !!this.transacted
20
- this.scope = this.scope || null
21
- this.collection = this.setupActions('collection')
22
- this.member = this.isOneToOne ? {} : this.setupActions('member')
23
20
  // Create a dummy model instance to validate the requested id against.
24
21
  // eslint-disable-next-line new-cap
25
22
  this.idValidator = new this.modelClass()
26
23
  }
27
24
 
25
+ // @override
26
+ setup() {
27
+ this.collection = this.setupActions('collection')
28
+ this.member = this.isOneToOne ? {} : this.setupActions('member')
29
+ this.assets = this.setupAssets()
30
+ }
31
+
28
32
  // @override
29
33
  setupAssets() {
30
34
  const { modelClass } = this
@@ -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 ||
package/types/index.d.ts CHANGED
@@ -27,11 +27,13 @@ import {
27
27
  Class,
28
28
  ConditionalExcept,
29
29
  ConditionalKeys,
30
- Constructor, SetOptional, SetReturnType
30
+ Constructor,
31
+ SetOptional,
32
+ SetReturnType
31
33
  } from 'type-fest'
32
34
  import { UserConfig } from 'vite'
33
35
 
34
- export type Page<$Model extends Model> = {
36
+ export type Page<$Model extends Model = Model> = {
35
37
  total: number
36
38
  results: $Model[]
37
39
  }
@@ -357,14 +359,14 @@ export interface ApiConfig {
357
359
 
358
360
  export interface ApplicationControllers {
359
361
  [k: string]:
360
- | Class<ModelController<Model>>
362
+ | Class<ModelController>
361
363
  | Class<Controller>
362
364
  | ApplicationControllers
363
365
  }
364
366
 
365
367
  export type Models = Record<string, Class<Model>>
366
368
 
367
- export class Application<$Models extends Models> {
369
+ export class Application<$Models extends Models = Models> {
368
370
  constructor(options: {
369
371
  config?: ApplicationConfig
370
372
  validator?: Validator
@@ -383,17 +385,22 @@ export class Application<$Models extends Models> {
383
385
  })
384
386
 
385
387
  models: $Models
388
+ setup(): Promise<void>
389
+ execute(): Promise<void>
386
390
  start(): Promise<void>
387
391
  stop(timeout?: number): Promise<void>
388
- startOrExit(): Promise<void>
389
- addServices(services: Services): void
392
+ addStorage(storage: StorageConfig): void
393
+ addStorages(storages: StorageConfigs): void
394
+ setupStorages(): Promise<void>
390
395
  addService(service: Service): void
396
+ addServices(services: Services): void
397
+ setupServices(): Promise<void>
398
+ addModel(model: Class<Model>): void
399
+ addModels(models: Models): void
400
+ setupModels(): Promise<void>
391
401
  addController(controllers: Controller, namespace?: string): void
392
402
  addControllers(controllers: ApplicationControllers, namespace?: string): void
393
- addStorages(storages: StorageConfigs): void
394
- addStorage(storage: StorageConfig): void
395
- addModels(models: Models): void
396
- addModel(model: Class<Model>): void
403
+ setupControllers(): Promise<void>
397
404
  getAdminViteConfig(config?: UserConfig): UserConfig
398
405
  }
399
406
  export interface Application
@@ -564,23 +571,23 @@ export type ModelProperty<T = any> = Schema<T> & {
564
571
  hidden?: boolean
565
572
  }
566
573
 
567
- export type ModelScope<$Model extends Model> = (
574
+ export type ModelScope<$Model extends Model = Model> = (
568
575
  this: $Model,
569
576
  query: QueryBuilder<$Model>,
570
577
  applyParentScope: (query: QueryBuilder<$Model>) => QueryBuilder<$Model>
571
578
  ) => QueryBuilder<$Model, any> | void
572
579
 
573
- export type ModelScopes<$Model extends Model> = Record<
580
+ export type ModelScopes<$Model extends Model = Model> = Record<
574
581
  string,
575
582
  ModelScope<$Model>
576
583
  >
577
584
 
578
- export type ModelFilterFunction<$Model extends Model> = (
585
+ export type ModelFilterFunction<$Model extends Model = Model> = (
579
586
  queryBuilder: QueryBuilder<$Model>,
580
587
  ...args: any[]
581
588
  ) => void
582
589
 
583
- export type ModelFilter<$Model extends Model> =
590
+ export type ModelFilter<$Model extends Model = Model> =
584
591
  | {
585
592
  filter: 'text' | 'date-range'
586
593
  properties?: string[]
@@ -593,7 +600,7 @@ export type ModelFilter<$Model extends Model> =
593
600
  }
594
601
  | ModelFilterFunction<$Model>
595
602
 
596
- export type ModelFilters<$Model extends Model> = Record<
603
+ export type ModelFilters<$Model extends Model = Model> = Record<
597
604
  string,
598
605
  ModelFilter<$Model>
599
606
  >
@@ -614,7 +621,8 @@ export interface ModelOptions extends objection.ModelOptions {
614
621
  type ModelHookFunction<$Model extends Model> = (
615
622
  args: objection.StaticHookArguments<$Model>
616
623
  ) => void
617
- export type ModelHooks<$Model extends Model> = {
624
+
625
+ export type ModelHooks<$Model extends Model = Model> = {
618
626
  [key in `${'before' | 'after'}:${
619
627
  | 'find'
620
628
  | 'insert'
@@ -823,7 +831,7 @@ export type ModelRelations = Record<string, ModelRelation>
823
831
 
824
832
  export type ModelProperties = Record<string, ModelProperty>
825
833
 
826
- export type ControllerAction<$Controller extends Controller> =
834
+ export type ControllerAction<$Controller extends Controller = Controller> =
827
835
  | ControllerActionOptions<$Controller>
828
836
  | ControllerActionHandler<$Controller>
829
837
 
@@ -860,11 +868,12 @@ export class Controller {
860
868
  authorize?: Authorize
861
869
  actions?: ControllerActions<this>
862
870
 
871
+ configure(): void
872
+ setup(): void
863
873
  initialize(): void
864
- setup(isRoot: boolean, setupActionsObject: boolean): void
865
874
  // TODO: type reflectActionsObject
866
875
  reflectActionsObject(): any
867
- setupRoute<$ControllerAction extends ControllerAction<any>>(
876
+ setupRoute<$ControllerAction extends ControllerAction = ControllerAction>(
868
877
  method: HTTPMethod,
869
878
  url: string,
870
879
  transacted: boolean,
@@ -915,14 +924,12 @@ export class Controller {
915
924
  export type ActionParameter = Schema & { name: string }
916
925
 
917
926
  export type ModelControllerActionHandler<
918
- $ModelController extends ModelController<Model>
927
+ $ModelController extends ModelController = ModelController
919
928
  > = (this: $ModelController, ctx: KoaContext, ...args: any[]) => any
920
929
 
921
- export type ControllerActionHandler<$Controller extends Controller> = (
922
- this: $Controller,
923
- ctx: KoaContext,
924
- ...args: any[]
925
- ) => any
930
+ export type ControllerActionHandler<
931
+ $Controller extends Controller = Controller
932
+ > = (this: $Controller, ctx: KoaContext, ...args: any[]) => any
926
933
 
927
934
  export type ExtractModelProperties<$Model> = {
928
935
  [$Key in SelectModelPropertyKeys<$Model>]: $Model[$Key] extends Model
@@ -1005,19 +1012,20 @@ export type BaseControllerActionOptions = {
1005
1012
  transacted?: boolean
1006
1013
  }
1007
1014
 
1008
- export type ControllerActionOptions<$Controller extends Controller> =
1009
- BaseControllerActionOptions & {
1010
- handler: ControllerActionHandler<$Controller>
1011
- }
1015
+ export type ControllerActionOptions<
1016
+ $Controller extends Controller = Controller
1017
+ > = BaseControllerActionOptions & {
1018
+ handler: ControllerActionHandler<$Controller>
1019
+ }
1012
1020
 
1013
1021
  export type ModelControllerActionOptions<
1014
- $ModelController extends ModelController<Model>
1022
+ $ModelController extends ModelController = ModelController
1015
1023
  > = BaseControllerActionOptions & {
1016
1024
  /** The function to be called when the action route is requested. */
1017
1025
  handler: ModelControllerActionHandler<$ModelController>
1018
1026
  }
1019
1027
 
1020
- export type MemberActionParameter<M extends Model> =
1028
+ export type MemberActionParameter<$Model extends Model = Model> =
1021
1029
  | Schema
1022
1030
  | {
1023
1031
  member: true
@@ -1037,17 +1045,17 @@ export type MemberActionParameter<M extends Model> =
1037
1045
  */
1038
1046
  forUpdate?: boolean
1039
1047
  /** Modify the member query. */
1040
- modify?: (query: QueryBuilder<M>) => QueryBuilder<M>
1048
+ modify?: (query: QueryBuilder<$Model>) => QueryBuilder<$Model>
1041
1049
  }
1042
1050
 
1043
1051
  export type ModelControllerAction<
1044
- $ModelController extends ModelController<Model>
1052
+ $ModelController extends ModelController = ModelController
1045
1053
  > =
1046
1054
  | ModelControllerActionOptions<$ModelController>
1047
1055
  | ModelControllerActionHandler<$ModelController>
1048
1056
 
1049
1057
  export type ModelControllerActions<
1050
- $ModelController extends ModelController<Model> = ModelController<Model>
1058
+ $ModelController extends ModelController = ModelController
1051
1059
  > = {
1052
1060
  [name: ControllerActionName]: ModelControllerAction<$ModelController>
1053
1061
  allow?: OrReadOnly<ControllerActionName[]>
@@ -1055,19 +1063,19 @@ export type ModelControllerActions<
1055
1063
  }
1056
1064
 
1057
1065
  type ModelControllerMemberAction<
1058
- $ModelController extends ModelController<Model>
1066
+ $ModelController extends ModelController = ModelController
1059
1067
  > =
1060
1068
  | (Omit<ModelControllerActionOptions<$ModelController>, 'parameters'> & {
1061
1069
  parameters?: {
1062
1070
  [key: string]: MemberActionParameter<
1063
- modelFromModelController<$ModelController>
1071
+ ModelFromModelController<$ModelController>
1064
1072
  >
1065
1073
  }
1066
1074
  })
1067
1075
  | ModelControllerActionHandler<$ModelController>
1068
1076
 
1069
1077
  export type ModelControllerMemberActions<
1070
- $ModelController extends ModelController<Model>
1078
+ $ModelController extends ModelController = ModelController
1071
1079
  > = {
1072
1080
  [name: ControllerActionName]: ModelControllerMemberAction<$ModelController>
1073
1081
  allow?: OrReadOnly<ControllerActionName[]>
@@ -1076,7 +1084,7 @@ export type ModelControllerMemberActions<
1076
1084
 
1077
1085
  export type ControllerActionName = `${HTTPMethod}${string}`
1078
1086
 
1079
- export type ControllerActions<$Controller extends Controller> = {
1087
+ export type ControllerActions<$Controller extends Controller = Controller> = {
1080
1088
  [name: ControllerActionName]: ControllerAction<$Controller>
1081
1089
  allow?: OrReadOnly<ControllerActionName[]>
1082
1090
  authorize?: Authorize
@@ -1110,15 +1118,15 @@ type ModelControllerHookKeys<
1110
1118
  | ControllerActionName
1111
1119
  | '*'}`
1112
1120
  type ModelControllerHook<
1113
- $ModelController extends ModelController<Model> = ModelController<Model>
1121
+ $ModelController extends ModelController = ModelController
1114
1122
  > = (
1115
1123
  ctx: KoaContext,
1116
- result: objection.Page<modelFromModelController<$ModelController>>
1124
+ result: objection.Page<ModelFromModelController<$ModelController>>
1117
1125
  ) => any
1118
1126
 
1119
1127
  type HookHandler = () => void
1120
1128
 
1121
- type HookKeysFromController<$ModelController extends ModelController<Model>> =
1129
+ type HookKeysFromController<$ModelController extends ModelController> =
1122
1130
  | ModelControllerHookKeys<
1123
1131
  Exclude<
1124
1132
  keyof Exclude<$ModelController['collection'], undefined>,
@@ -1135,7 +1143,7 @@ type HookKeysFromController<$ModelController extends ModelController<Model>> =
1135
1143
  >
1136
1144
 
1137
1145
  type HandlerFromHookKey<
1138
- $ModelController extends ModelController<Model>,
1146
+ $ModelController extends ModelController,
1139
1147
  $Key extends HookKeysFromController<$ModelController>
1140
1148
  > = $Key extends `${'before' | 'after' | '*'}:${
1141
1149
  | 'collection'
@@ -1145,7 +1153,7 @@ type HandlerFromHookKey<
1145
1153
  : never
1146
1154
 
1147
1155
  type ModelControllerHooks<
1148
- $ModelController extends ModelController<Model> = ModelController<Model>
1156
+ $ModelController extends ModelController = ModelController
1149
1157
  > = {
1150
1158
  [$Key in HookKeysFromController<$ModelController>]?: HandlerFromHookKey<
1151
1159
  $ModelController,
@@ -1428,13 +1436,9 @@ export type QueryParameterOptionKey = keyof QueryParameterOptions
1428
1436
 
1429
1437
  export class Service {
1430
1438
  constructor(app: Application<Models>, name?: string)
1431
-
1432
1439
  setup(config: any): void
1433
-
1434
1440
  initialize(): void
1435
-
1436
1441
  start(): Promise<void>
1437
-
1438
1442
  stop(): Promise<void>
1439
1443
  }
1440
1444
  export type Services = Record<string, Class<Service> | Service>
@@ -1853,7 +1857,7 @@ type OrReadOnly<T> = Readonly<T> | T
1853
1857
 
1854
1858
  type OrPromiseOf<T> = Promise<T> | T
1855
1859
 
1856
- type modelFromModelController<$ModelController extends ModelController<Model>> =
1860
+ type ModelFromModelController<$ModelController extends ModelController> =
1857
1861
  InstanceType<Exclude<$ModelController['modelClass'], undefined>>
1858
1862
 
1859
1863
  export type SelectModelProperties<T> = {