@ditojs/server 1.4.3 → 1.5.2

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.4.3",
3
+ "version": "1.5.2",
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",
@@ -21,22 +21,22 @@
21
21
  "node >= 16"
22
22
  ],
23
23
  "dependencies": {
24
- "@ditojs/admin": "^1.4.3",
25
- "@ditojs/router": "^1.4.3",
26
- "@ditojs/utils": "^1.4.3",
27
- "@koa/cors": "^3.2.0",
24
+ "@ditojs/admin": "^1.5.0",
25
+ "@ditojs/build": "^1.5.0",
26
+ "@ditojs/router": "^1.5.0",
27
+ "@ditojs/utils": "^1.5.0",
28
+ "@koa/cors": "^3.3.0",
28
29
  "@koa/multer": "^3.0.0",
29
30
  "@originjs/vite-plugin-commonjs": "^1.0.3",
30
31
  "ajv": "^8.11.0",
31
32
  "ajv-formats": "^2.1.1",
32
- "aws-sdk": "^2.1098.0",
33
+ "aws-sdk": "^2.1108.0",
33
34
  "axios": "^0.26.1",
34
35
  "bcryptjs": "^2.4.3",
35
36
  "bytes": "^3.1.2",
36
37
  "data-uri-to-buffer": "^4.0.0",
37
38
  "eventemitter2": "^6.4.5",
38
39
  "file-type": "^17.1.1",
39
- "find-up": "^6.3.0",
40
40
  "fs-extra": "^10.0.1",
41
41
  "image-size": "^1.0.1",
42
42
  "is-svg": "^4.3.2",
@@ -55,31 +55,31 @@
55
55
  "mime-types": "^2.1.35",
56
56
  "multer": "^1.4.4",
57
57
  "multer-s3": "^2.10.0",
58
- "nanoid": "^3.3.1",
58
+ "nanoid": "^3.3.2",
59
59
  "parse-duration": "^1.0.2",
60
60
  "passport-local": "^1.0.0",
61
61
  "passthrough-counter": "^1.0.0",
62
62
  "picocolors": "^1.0.0",
63
63
  "picomatch": "^2.3.1",
64
64
  "pino": "^7.9.2",
65
- "pino-pretty": "^7.5.4",
65
+ "pino-pretty": "^7.6.0",
66
66
  "pluralize": "^8.0.0",
67
67
  "repl": "^0.1.3",
68
68
  "uuid": "^8.3.2",
69
- "vite": "^2.8.6",
69
+ "vite": "^2.9.1",
70
70
  "vite-plugin-vue2": "^1.9.3",
71
71
  "vue": "^2.6.14",
72
72
  "vue-template-compiler": "^2.6.14"
73
73
  },
74
74
  "peerDependencies": {
75
- "knex": "^0.21.0",
76
- "objection": "^2.2.0"
75
+ "knex": "^1.0.4",
76
+ "objection": "^3.0.1"
77
77
  },
78
78
  "devDependencies": {
79
- "knex": "^0.21.21",
80
- "objection": "^2.2.18",
79
+ "knex": "^1.0.5",
80
+ "objection": "^3.0.1",
81
81
  "pg": "^8.7.3",
82
82
  "sqlite3": "^5.0.2"
83
83
  },
84
- "gitHead": "f2a544aef8ab0a12c7019650cd86558f8365b007"
84
+ "gitHead": "b7861d7cea5426a8a487ce4c3b9d9f1ff3e908ce"
85
85
  }
@@ -1,11 +1,13 @@
1
+ import os from 'os'
2
+ import path from 'path'
3
+ import util from 'util'
4
+ import zlib from 'zlib'
5
+ import fs from 'fs-extra'
1
6
  import Koa from 'koa'
2
7
  import Knex from 'knex'
3
- import util from 'util'
4
8
  import axios from 'axios'
5
9
  import pico from 'picocolors'
6
- import zlib from 'zlib'
7
10
  import pino from 'pino'
8
- import os from 'os'
9
11
  import parseDuration from 'parse-duration'
10
12
  import bodyParser from 'koa-bodyparser'
11
13
  import cors from '@koa/cors'
@@ -20,8 +22,8 @@ import helmet from 'koa-helmet'
20
22
  import responseTime from 'koa-response-time'
21
23
  import Router from '@ditojs/router'
22
24
  import {
23
- isArray, isObject, isString, asArray, isPlainObject, hyphenate, clone, merge,
24
- parseDataPath, normalizeDataPath, isModule
25
+ isArray, isObject, isString, asArray, isPlainObject, isModule,
26
+ hyphenate, clone, merge, parseDataPath, normalizeDataPath, toPromiseCallback
25
27
  } from '@ditojs/utils'
26
28
  import SessionStore from './SessionStore.js'
27
29
  import { Validator } from './Validator.js'
@@ -46,11 +48,10 @@ import {
46
48
  handleUser,
47
49
  logRequests
48
50
  } from '../middleware/index.js'
49
- import objection, {
51
+ import {
50
52
  Model,
51
53
  BelongsToOneRelation,
52
- // TODO: Import directly once we can move to Objection 3
53
- // knexSnakeCaseMappers,
54
+ knexSnakeCaseMappers,
54
55
  ref
55
56
  } from 'objection'
56
57
 
@@ -73,6 +74,7 @@ export class Application extends Koa {
73
74
  log = {},
74
75
  ...rest
75
76
  } = config
77
+ this.server = null
76
78
  this.config = {
77
79
  app,
78
80
  log: log.silent || process.env.DITO_SILENT ? {} : log,
@@ -105,6 +107,10 @@ export class Application extends Koa {
105
107
  }
106
108
  }
107
109
 
110
+ get isRunning() {
111
+ return !!this.server
112
+ }
113
+
108
114
  addRoute(
109
115
  method, path, transacted, middlewares, controller = null, action = null
110
116
  ) {
@@ -282,10 +288,6 @@ export class Application extends Koa {
282
288
  return Object.values(this.services).find(callback)
283
289
  }
284
290
 
285
- forEachService(callback) {
286
- return Promise.all(Object.values(this.services).map(callback))
287
- }
288
-
289
291
  addControllers(controllers, namespace) {
290
292
  for (const [key, value] of Object.entries(controllers)) {
291
293
  if (isModule(value) || isPlainObject(value)) {
@@ -626,7 +628,7 @@ export class Application extends Koa {
626
628
  if (snakeCaseOptions) {
627
629
  knex = {
628
630
  ...knex,
629
- ...objection.knexSnakeCaseMappers(snakeCaseOptions)
631
+ ...knexSnakeCaseMappers(snakeCaseOptions)
630
632
  }
631
633
  }
632
634
  this.knex = Knex(knex)
@@ -717,46 +719,54 @@ export class Application extends Koa {
717
719
  this.on('error', this.logError)
718
720
  }
719
721
  await this.emit('before:start')
720
- await this.forEachService(service => service.start())
721
- const { server: { host, port } } = this.config
722
- this.server = await new Promise((resolve, reject) => {
723
- const server = this.listen(port, host, () => {
724
- const { port } = server.address()
722
+ this.server = await new Promise(resolve => {
723
+ const server = this.listen(this.config.server, () => {
724
+ const { address, port } = server.address()
725
725
  console.info(
726
- `Dito server started at http://${host}:${port}`
726
+ `Dito.js server started at http://${address}:${port}`
727
727
  )
728
728
  resolve(server)
729
729
  })
730
- if (!server) {
731
- reject(new Error(`Unable to start server at http://${host}:${port}`))
732
- }
733
730
  })
731
+ if (!this.server) {
732
+ throw new Error('Unable to start Dito.js server')
733
+ }
734
734
  await this.emit('after:start')
735
735
  }
736
736
 
737
- async stop() {
738
- await this.emit('before:stop')
739
- this.server = await new Promise((resolve, reject) => {
740
- const { server } = this
741
- if (server) {
742
- server.close(err => {
743
- if (err) {
744
- reject(err)
745
- } else {
746
- resolve(null)
747
- }
748
- })
749
- // Hack to make sure that we close the server,
750
- // even if sockets are still open.
751
- // Taken from https://stackoverflow.com/a/36830072.
752
- // A proper solution would be to use a library, ex: https://github.com/godaddy/terminus
753
- setImmediate(() => server.emit('close'))
754
- } else {
755
- reject(new Error('Server is not running'))
756
- }
757
- })
758
- await this.forEachService(service => service.stop())
759
- await this.emit('after:stop')
737
+ async stop(timeout = 0) {
738
+ if (!this.server) {
739
+ throw new Error('Dito.js server is not running')
740
+ }
741
+
742
+ const promise = (async () => {
743
+ await this.emit('before:stop')
744
+ await new Promise((resolve, reject) => {
745
+ this.server.close(toPromiseCallback(resolve, reject))
746
+ })
747
+ // Hack to make sure that the server is closed, even if sockets are still
748
+ // open after `server.close()`, see: https://stackoverflow.com/a/36830072
749
+ this.server.emit('close')
750
+ this.server = null
751
+ await this.emit('after:stop')
752
+ })()
753
+
754
+ if (timeout > 0) {
755
+ await Promise.race([
756
+ promise,
757
+ new Promise((resolve, reject) =>
758
+ setTimeout(reject,
759
+ timeout,
760
+ new Error(
761
+ `Timeout reached while stopping Dito.js server (${timeout}ms)`
762
+ )
763
+ )
764
+ )
765
+ ])
766
+ } else {
767
+ await promise
768
+ }
769
+
760
770
  if (this.config.log.errors !== false) {
761
771
  this.off('error', this.logError)
762
772
  }
@@ -853,23 +863,38 @@ export class Application extends Koa {
853
863
  if (file.data || file.url) {
854
864
  let { data } = file
855
865
  if (!data) {
866
+ const { url } = file
867
+ if (!storage.isImportSourceAllowed(url)) {
868
+ throw new AssetError(
869
+ `Unable to import asset from foreign source: '${
870
+ file.name
871
+ }' ('${
872
+ url
873
+ }'): The source needs to be explicitly allowed.`
874
+ )
875
+ }
856
876
  console.info(
857
877
  `${
858
878
  pico.red('INFO:')
859
879
  } Asset ${
860
880
  pico.green(`'${file.name}'`)
861
881
  } is from a foreign source, fetching from ${
862
- pico.green(`'${file.url}'`)
882
+ pico.green(`'${url}'`)
863
883
  } and adding to storage ${
864
884
  pico.green(`'${storage.name}'`)
865
885
  }...`
866
886
  )
867
- const response = await axios.request({
868
- method: 'get',
869
- url: file.url,
870
- responseType: 'arraybuffer'
871
- })
872
- data = response.data
887
+ if (url.startsWith('file://')) {
888
+ const filepath = path.resolve(url.substring(7))
889
+ data = await fs.readFile(filepath)
890
+ } else {
891
+ const response = await axios.request({
892
+ method: 'get',
893
+ responseType: 'arraybuffer',
894
+ url
895
+ })
896
+ data = response.data
897
+ }
873
898
  }
874
899
  const importedFile = await storage.addFile(file, data)
875
900
  await this.createAssets(storage, [importedFile], 0, trx)
@@ -1,5 +1,5 @@
1
1
  import path from 'path'
2
- import fs from 'fs'
2
+ import { exit } from 'process'
3
3
  import Koa from 'koa'
4
4
  import serve from 'koa-static'
5
5
  import { defineConfig, createServer } from 'vite'
@@ -7,8 +7,10 @@ import { createVuePlugin } from 'vite-plugin-vue2'
7
7
  import {
8
8
  viteCommonjs as createCommonJsPlugin
9
9
  } from '@originjs/vite-plugin-commonjs'
10
- import picomatch from 'picomatch'
11
- import { findUpSync } from 'find-up'
10
+ import {
11
+ createRollupImportsResolver,
12
+ testModuleIdentifier
13
+ } from '@ditojs/build'
12
14
  import { merge } from '@ditojs/utils'
13
15
  import { Controller } from './Controller.js'
14
16
  import { handleConnectMiddleware } from '../middleware/index.js'
@@ -89,6 +91,7 @@ export class AdminController extends Controller {
89
91
  }
90
92
  }
91
93
 
94
+ // @override
92
95
  compose() {
93
96
  this.koa = new Koa()
94
97
  this.koa.use(this.middleware())
@@ -127,6 +130,32 @@ export class AdminController extends Controller {
127
130
  }
128
131
  }
129
132
  })
133
+
134
+ let closed = false
135
+
136
+ // Monkey-patch `process.exit()` to filter out the calls caused by vite's
137
+ // handling of SIGTERM, see: https://github.com/vitejs/vite/issues/7627
138
+ process.exit = code => {
139
+ // Filter out calls from inside vite by looking at the stack trace.
140
+ if (new Error().stack.includes('/vite/dist/')) {
141
+ // vite's own `exitProcess()` just called `process.exit(), and this
142
+ // means it has already called `server.close()` internally.
143
+ closed = true
144
+ process.exit = exit
145
+ } else {
146
+ exit(code)
147
+ }
148
+ }
149
+
150
+ this.app.once('before:stop', () => {
151
+ // For good timing it seems crucial to not add more ticks with async
152
+ // signature, so we directly return the `server.close()` promise instead.
153
+ process.exit = exit
154
+ if (!closed) {
155
+ closed = true
156
+ return server.close()
157
+ }
158
+ })
130
159
  this.koa.use(handleConnectMiddleware(server.middlewares, {
131
160
  expandMountPath: true
132
161
  }))
@@ -135,16 +164,11 @@ export class AdminController extends Controller {
135
164
  getViteConfig(config = {}) {
136
165
  const development = this.mode === 'development'
137
166
 
138
- const cwd = path.resolve('.')
167
+ const cwd = process.cwd()
139
168
  const root = this.getPath('root')
140
169
  const base = `${this.url}/`
141
170
  const views = path.join(root, 'views')
142
171
 
143
- // Read `package.json` from the closest package.json, so we can emulate
144
- // ESM-style imports mappings in rollup / vite.
145
- const pkg = findUpSync('package.json', { cwd: root })
146
- const { imports = {} } = JSON.parse(fs.readFileSync(pkg, 'utf8'))
147
-
148
172
  return defineConfig(merge({
149
173
  root,
150
174
  base,
@@ -185,7 +209,7 @@ export class AdminController extends Controller {
185
209
  return 'common'
186
210
  } else {
187
211
  const module = id.match(/node_modules\/([^/$]*)/)?.[1] || ''
188
- return picomatch.isMatch(module, CORE_DEPENDENCIES)
212
+ return testModuleIdentifier(module, CORE_DEPENDENCIES)
189
213
  ? 'core'
190
214
  : 'vendor'
191
215
  }
@@ -210,27 +234,7 @@ export class AdminController extends Controller {
210
234
  find: '@',
211
235
  replacement: root
212
236
  },
213
- {
214
- // Use a custom rollup resolver to emulate ESM-style imports
215
- // mappings in vite, as read from `package.json` above:
216
- find: /^#/,
217
- replacement: '#',
218
- customResolver(id) {
219
- for (const [find, replacement] of Object.entries(imports)) {
220
- picomatch.isMatch(id, find.replace('*', '**'), {
221
- capture: true,
222
- onMatch({ input, regex }) {
223
- const replacementPath = path.resolve(replacement)
224
- const match = input.match(regex)?.[1]
225
- id = match
226
- ? replacementPath.replace('*', match)
227
- : replacementPath
228
- }
229
- })
230
- }
231
- return id
232
- }
233
- }
237
+ createRollupImportsResolver({ cwd: root })
234
238
  ]
235
239
  }
236
240
  }, config))
@@ -68,17 +68,18 @@ export class CollectionController extends Controller {
68
68
  return this.extendContext(ctx, { memberId })
69
69
  }
70
70
 
71
- getCollectionIds(ctx) {
71
+ getModelId(model) {
72
72
  const idProperty = this.modelClass.getIdProperty()
73
73
  // Handle both composite keys and normal ones.
74
- const getId = isArray(idProperty)
75
- ? model => idProperty.reduce(
76
- (id, key) => {
77
- id.push(model[key])
78
- return id
79
- }, [])
80
- : model => model[idProperty]
81
- return asArray(ctx.request.body).map(model => this.validateId(getId(model)))
74
+ return isArray(idProperty)
75
+ ? idProperty.map(property => model[property])
76
+ : model[idProperty]
77
+ }
78
+
79
+ getCollectionIds(ctx) {
80
+ return asArray(ctx.request.body).map(
81
+ model => this.validateId(this.getModelId(model))
82
+ )
82
83
  }
83
84
 
84
85
  getIds(ctx) {
@@ -219,9 +220,9 @@ export class CollectionController extends Controller {
219
220
  .modify(getModify(modify, trx))
220
221
  )
221
222
  : await this.executeAndFetch('insert', ctx, modify)
222
- ctx.status = 201
223
+ ctx.status = 201 // Created
223
224
  if (isObject(result)) {
224
- ctx.set('Location', this.getUrl('collection', result.id))
225
+ ctx.set('Location', this.getUrl('collection', this.getModelId(result)))
225
226
  }
226
227
  return result
227
228
  },
@@ -26,6 +26,12 @@ export class Controller {
26
26
  initialize() {
27
27
  }
28
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
+
29
35
  setup(isRoot = true, setupActionsObject = true) {
30
36
  this._setupEmitter(this.hooks, {
31
37
  // Support wildcard hooks only on controllers:
@@ -246,12 +252,6 @@ export class Controller {
246
252
  ])
247
253
  }
248
254
 
249
- // @return {Application|Function} [app or function]
250
- compose() {
251
- // To be overridden in sub-classes, if the controller needs to install
252
- // middleware. For normal routes, use `this.app.addRoute()` instead.
253
- }
254
-
255
255
  getPath(type, path) {
256
256
  // To be overridden by sub-classes.
257
257
  return path
@@ -929,11 +929,9 @@ export class Model extends objection.Model {
929
929
  )
930
930
  : assetDataPaths
931
931
 
932
- // `dataPaths` will be empty in the case of an update/insert that do not
932
+ // `dataPaths` is empty in the case of an update/insert that does not
933
933
  // affect the assets.
934
- if (dataPaths.length === 0) {
935
- return
936
- }
934
+ if (dataPaths.length === 0) return
937
935
 
938
936
  // Load the model's asset files in their current state before the query is
939
937
  // executed.
@@ -972,7 +970,7 @@ export class Model extends objection.Model {
972
970
  if (modifiedFiles.length > 0) {
973
971
  // TODO: `modifiedFiles` should be restored as well, but that's far
974
972
  // from trivial since no backup is kept in `handleModifiedAssets`
975
- console.info(
973
+ console.warn(
976
974
  `Unable to restore these already modified files: ${
977
975
  modifiedFiles.map(file => `'${file.name}'`)
978
976
  }`
@@ -5,7 +5,7 @@ import {
5
5
  HasManyRelation,
6
6
  ManyToManyRelation
7
7
  } from 'objection'
8
- import { Model } from '../models.js'
8
+ import { Model } from '../models/index.js'
9
9
  import {
10
10
  getRelationClass, convertRelation, addRelationSchemas
11
11
  } from './relations.js'
@@ -11,6 +11,8 @@ export class Service {
11
11
 
12
12
  setup(config) {
13
13
  this.config = config
14
+ this.app.on('before:start', () => this.start())
15
+ this.app.on('after:stop', () => this.stop())
14
16
  }
15
17
 
16
18
  // @overridable
@@ -1,8 +1,9 @@
1
1
  import path from 'path'
2
+ import { URL } from 'url'
2
3
  import multer from '@koa/multer'
4
+ import picomatch from 'picomatch'
3
5
  import imageSize from 'image-size'
4
6
  import { PassThrough } from 'stream'
5
- import { URL } from 'url'
6
7
  import { hyphenate, toPromiseCallback } from '@ditojs/utils'
7
8
  import { AssetFile } from './AssetFile.js'
8
9
 
@@ -57,6 +58,10 @@ export class Storage {
57
58
  return AssetFile.getUniqueKey(name)
58
59
  }
59
60
 
61
+ isImportSourceAllowed(url) {
62
+ return picomatch.isMatch(url, this.config.allowedImports || [])
63
+ }
64
+
60
65
  convertAssetFile(file) {
61
66
  return AssetFile.convert(file, this)
62
67
  }