@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ditojs/server",
3
- "version": "1.13.0",
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.0",
30
- "@ditojs/build": "^1.13.0",
31
- "@ditojs/router": "^1.13.0",
32
- "@ditojs/utils": "^1.13.0",
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": "b65d7d15871b0d11579565df92780536ef2926da"
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