@radio-garden/ditojs-server 2.85.2-0.5067ad799

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.
Files changed (137) hide show
  1. package/README.md +6 -0
  2. package/package.json +95 -0
  3. package/src/app/Application.js +1186 -0
  4. package/src/app/Validator.js +405 -0
  5. package/src/app/index.js +2 -0
  6. package/src/cli/console.js +152 -0
  7. package/src/cli/db/createMigration.js +241 -0
  8. package/src/cli/db/index.js +7 -0
  9. package/src/cli/db/listAssetConfig.js +10 -0
  10. package/src/cli/db/migrate.js +12 -0
  11. package/src/cli/db/reset.js +23 -0
  12. package/src/cli/db/rollback.js +12 -0
  13. package/src/cli/db/seed.js +80 -0
  14. package/src/cli/db/unlock.js +9 -0
  15. package/src/cli/index.js +72 -0
  16. package/src/controllers/AdminController.js +322 -0
  17. package/src/controllers/CollectionController.js +274 -0
  18. package/src/controllers/Controller.js +657 -0
  19. package/src/controllers/ControllerAction.js +370 -0
  20. package/src/controllers/MemberAction.js +27 -0
  21. package/src/controllers/ModelController.js +63 -0
  22. package/src/controllers/RelationController.js +93 -0
  23. package/src/controllers/UsersController.js +64 -0
  24. package/src/controllers/index.js +5 -0
  25. package/src/errors/AssetError.js +7 -0
  26. package/src/errors/AuthenticationError.js +7 -0
  27. package/src/errors/AuthorizationError.js +7 -0
  28. package/src/errors/ControllerError.js +14 -0
  29. package/src/errors/DatabaseError.js +37 -0
  30. package/src/errors/GraphError.js +7 -0
  31. package/src/errors/ModelError.js +12 -0
  32. package/src/errors/NotFoundError.js +7 -0
  33. package/src/errors/NotImplementedError.js +7 -0
  34. package/src/errors/QueryBuilderError.js +7 -0
  35. package/src/errors/RelationError.js +21 -0
  36. package/src/errors/ResponseError.js +56 -0
  37. package/src/errors/ValidationError.js +7 -0
  38. package/src/errors/index.js +13 -0
  39. package/src/graph/DitoGraphProcessor.js +213 -0
  40. package/src/graph/expression.js +53 -0
  41. package/src/graph/graph.js +258 -0
  42. package/src/graph/index.js +3 -0
  43. package/src/index.js +9 -0
  44. package/src/lib/EventEmitter.js +66 -0
  45. package/src/lib/KnexHelper.js +30 -0
  46. package/src/lib/index.js +2 -0
  47. package/src/middleware/attachLogger.js +8 -0
  48. package/src/middleware/createTransaction.js +33 -0
  49. package/src/middleware/extendContext.js +10 -0
  50. package/src/middleware/findRoute.js +20 -0
  51. package/src/middleware/handleConnectMiddleware.js +99 -0
  52. package/src/middleware/handleError.js +29 -0
  53. package/src/middleware/handleRoute.js +23 -0
  54. package/src/middleware/handleSession.js +77 -0
  55. package/src/middleware/handleUser.js +31 -0
  56. package/src/middleware/index.js +11 -0
  57. package/src/middleware/logRequests.js +125 -0
  58. package/src/middleware/setupRequestStorage.js +14 -0
  59. package/src/mixins/AssetMixin.js +78 -0
  60. package/src/mixins/SessionMixin.js +17 -0
  61. package/src/mixins/TimeStampedMixin.js +41 -0
  62. package/src/mixins/UserMixin.js +171 -0
  63. package/src/mixins/index.js +4 -0
  64. package/src/models/AssetModel.js +4 -0
  65. package/src/models/Model.js +1205 -0
  66. package/src/models/RelationAccessor.js +41 -0
  67. package/src/models/SessionModel.js +4 -0
  68. package/src/models/TimeStampedModel.js +4 -0
  69. package/src/models/UserModel.js +4 -0
  70. package/src/models/definitions/assets.js +5 -0
  71. package/src/models/definitions/filters.js +121 -0
  72. package/src/models/definitions/hooks.js +8 -0
  73. package/src/models/definitions/index.js +22 -0
  74. package/src/models/definitions/modifiers.js +5 -0
  75. package/src/models/definitions/options.js +5 -0
  76. package/src/models/definitions/properties.js +73 -0
  77. package/src/models/definitions/relations.js +5 -0
  78. package/src/models/definitions/schema.js +5 -0
  79. package/src/models/definitions/scopes.js +36 -0
  80. package/src/models/index.js +5 -0
  81. package/src/query/QueryBuilder.js +1077 -0
  82. package/src/query/QueryFilters.js +66 -0
  83. package/src/query/QueryParameters.js +79 -0
  84. package/src/query/Registry.js +29 -0
  85. package/src/query/index.js +3 -0
  86. package/src/schema/formats/_empty.js +4 -0
  87. package/src/schema/formats/_required.js +4 -0
  88. package/src/schema/formats/index.js +2 -0
  89. package/src/schema/index.js +5 -0
  90. package/src/schema/keywords/_computed.js +7 -0
  91. package/src/schema/keywords/_foreign.js +7 -0
  92. package/src/schema/keywords/_hidden.js +7 -0
  93. package/src/schema/keywords/_index.js +7 -0
  94. package/src/schema/keywords/_instanceof.js +45 -0
  95. package/src/schema/keywords/_primary.js +7 -0
  96. package/src/schema/keywords/_range.js +18 -0
  97. package/src/schema/keywords/_relate.js +13 -0
  98. package/src/schema/keywords/_specificType.js +7 -0
  99. package/src/schema/keywords/_unique.js +7 -0
  100. package/src/schema/keywords/_unsigned.js +7 -0
  101. package/src/schema/keywords/_validate.js +73 -0
  102. package/src/schema/keywords/index.js +12 -0
  103. package/src/schema/relations.js +324 -0
  104. package/src/schema/relations.test.js +177 -0
  105. package/src/schema/schema.js +289 -0
  106. package/src/schema/schema.test.js +720 -0
  107. package/src/schema/types/_asset.js +31 -0
  108. package/src/schema/types/_color.js +4 -0
  109. package/src/schema/types/index.js +2 -0
  110. package/src/services/Service.js +35 -0
  111. package/src/services/index.js +1 -0
  112. package/src/storage/AssetFile.js +81 -0
  113. package/src/storage/DiskStorage.js +114 -0
  114. package/src/storage/S3Storage.js +169 -0
  115. package/src/storage/Storage.js +231 -0
  116. package/src/storage/index.js +9 -0
  117. package/src/utils/duration.js +15 -0
  118. package/src/utils/emitter.js +8 -0
  119. package/src/utils/fs.js +10 -0
  120. package/src/utils/function.js +17 -0
  121. package/src/utils/function.test.js +77 -0
  122. package/src/utils/handler.js +17 -0
  123. package/src/utils/json.js +3 -0
  124. package/src/utils/model.js +35 -0
  125. package/src/utils/net.js +17 -0
  126. package/src/utils/object.js +82 -0
  127. package/src/utils/object.test.js +86 -0
  128. package/src/utils/scope.js +7 -0
  129. package/types/index.d.ts +3547 -0
  130. package/types/tests/application.test-d.ts +26 -0
  131. package/types/tests/controller.test-d.ts +113 -0
  132. package/types/tests/errors.test-d.ts +53 -0
  133. package/types/tests/fixtures.ts +19 -0
  134. package/types/tests/model.test-d.ts +193 -0
  135. package/types/tests/query-builder.test-d.ts +106 -0
  136. package/types/tests/relation.test-d.ts +83 -0
  137. package/types/tests/storage.test-d.ts +113 -0
@@ -0,0 +1,31 @@
1
+ export const asset = {
2
+ type: 'object',
3
+ properties: {
4
+ key: {
5
+ type: 'string',
6
+ required: true
7
+ },
8
+ name: {
9
+ type: 'string',
10
+ required: true
11
+ },
12
+ type: {
13
+ type: 'string',
14
+ required: true
15
+ },
16
+ size: {
17
+ type: 'integer',
18
+ required: true
19
+ },
20
+ url: {
21
+ type: 'string',
22
+ format: 'uri'
23
+ },
24
+ width: {
25
+ type: 'integer'
26
+ },
27
+ height: {
28
+ type: 'integer'
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,4 @@
1
+ export const color = {
2
+ type: 'string',
3
+ format: 'hexcolor'
4
+ }
@@ -0,0 +1,2 @@
1
+ export * from './_asset.js'
2
+ export * from './_color.js'
@@ -0,0 +1,35 @@
1
+ import { camelize, hyphenate } from '@ditojs/utils'
2
+
3
+ export class Service {
4
+ initialized = false
5
+ #loggerName
6
+
7
+ constructor(app, name) {
8
+ this.app = app
9
+ this.name = camelize(
10
+ (name || this.constructor.name).match(/^(.*?)(?:Service|)$/)[1]
11
+ )
12
+ this.#loggerName = hyphenate(this.name)
13
+ this.config = null
14
+ }
15
+
16
+ setup(config) {
17
+ this.config = config
18
+ this.app.on('before:start', () => this.start())
19
+ this.app.on('after:stop', () => this.stop())
20
+ }
21
+
22
+ // @overridable
23
+ async initialize() {}
24
+
25
+ // @overridable
26
+ async start() {}
27
+
28
+ // @overridable
29
+ async stop() {}
30
+
31
+ get logger() {
32
+ const logger = this.app.requestLocals.logger ?? this.app.logger
33
+ return logger.child({ name: this.#loggerName })
34
+ }
35
+ }
@@ -0,0 +1 @@
1
+ export * from './Service.js'
@@ -0,0 +1,81 @@
1
+ import path from 'path'
2
+ import mime from 'mime-types'
3
+ import { v4 as uuidv4 } from 'uuid'
4
+ import { dataUriToBuffer } from 'data-uri-to-buffer'
5
+ import { isString } from '@ditojs/utils'
6
+
7
+ const SYMBOL_STORAGE = Symbol('storage')
8
+ const SYMBOL_DATA = Symbol('data')
9
+
10
+ export class AssetFile {
11
+ constructor({ name, data, type, width, height }) {
12
+ this.key = AssetFile.getUniqueKey(name)
13
+ this.name = name
14
+ // Set `type` before `data`, so it can be used as default in `set data`
15
+ this.type = type
16
+ this.width = width
17
+ this.height = height
18
+ this.data = data
19
+ }
20
+
21
+ get storage() {
22
+ return this[SYMBOL_STORAGE] || null
23
+ }
24
+
25
+ get data() {
26
+ return this[SYMBOL_DATA] || null
27
+ }
28
+
29
+ set data(data) {
30
+ if (isString(data)) {
31
+ if (data.startsWith('data:')) {
32
+ const { type, buffer } = dataUriToBuffer(data)
33
+ data = Buffer.from(buffer)
34
+ this.type ||= type || mime.lookup(this.name)
35
+ } else {
36
+ data = Buffer.from(data)
37
+ this.type ||= mime.lookup(this.name) || 'text/plain'
38
+ }
39
+ } else {
40
+ // Buffer & co.
41
+ data = Buffer.isBuffer(data) ? data : Buffer.from(data)
42
+ this.type ||= (
43
+ data.type || // See Storage._readFile()
44
+ mime.lookup(this.name) ||
45
+ 'application/octet-stream'
46
+ )
47
+ }
48
+ this.size = Buffer.byteLength(data)
49
+ setHiddenProperty(this, SYMBOL_DATA, data)
50
+ }
51
+
52
+ get path() {
53
+ return this.storage?.getFilePath(this)
54
+ }
55
+
56
+ async read() {
57
+ return this.storage?.readFile(this) || null
58
+ }
59
+
60
+ static convert(object, storage) {
61
+ Object.setPrototypeOf(object, AssetFile.prototype)
62
+ setHiddenProperty(object, SYMBOL_STORAGE, storage)
63
+ }
64
+
65
+ static create(options) {
66
+ return new AssetFile(options)
67
+ }
68
+
69
+ static getUniqueKey(name) {
70
+ return `${uuidv4()}${path.extname(name).toLowerCase()}`
71
+ }
72
+ }
73
+
74
+ function setHiddenProperty(object, key, value) {
75
+ Object.defineProperty(object, key, {
76
+ configurable: true,
77
+ enumerable: false,
78
+ writable: true,
79
+ value
80
+ })
81
+ }
@@ -0,0 +1,114 @@
1
+ import fs from 'fs/promises'
2
+ import path from 'path'
3
+ import multer from '@koa/multer'
4
+ import { mapConcurrently } from '@ditojs/utils'
5
+ import { Storage } from './Storage.js'
6
+
7
+ export class DiskStorage extends Storage {
8
+ static type = 'disk'
9
+
10
+ setup() {
11
+ if (!this.path) {
12
+ throw new Error(`Missing configuration (path) for storage ${this.name}`)
13
+ }
14
+ this.storage = multer.diskStorage({
15
+ destination: (req, storageFile, cb) => {
16
+ // Add `storageFile.key` property to internal storage file object.
17
+ storageFile.key = this.getUniqueKey(storageFile.originalname)
18
+ const dir = this._getPath(this._getNestedFolder(storageFile.key))
19
+ fs.mkdir(dir, { recursive: true })
20
+ .then(() => cb(null, dir))
21
+ .catch(cb)
22
+ },
23
+
24
+ filename: (req, storageFile, cb) => {
25
+ // Use added `storageFile.key` property for multer's `filename` also.
26
+ cb(null, storageFile.key)
27
+ }
28
+ })
29
+ }
30
+
31
+ // @override
32
+ _getFilePath(file) {
33
+ return this._getPath(this._getNestedFolder(file.key), file.key)
34
+ }
35
+
36
+ // @override
37
+ _getFileUrl(file) {
38
+ return this._getUrl(this._getNestedFolder(file.key, true), file.key)
39
+ }
40
+
41
+ // @override
42
+ async _addFile(file, data) {
43
+ const filePath = this._getFilePath(file)
44
+ const dir = path.dirname(filePath)
45
+ await fs.mkdir(dir, { recursive: true })
46
+ await fs.writeFile(filePath, data)
47
+ }
48
+
49
+ // @override
50
+ async _removeFile(file) {
51
+ const filePath = this._getFilePath(file)
52
+ await fs.unlink(filePath)
53
+ const removeIfEmpty = async dir => {
54
+ if ((await fs.readdir(dir)).length === 0) {
55
+ try {
56
+ await fs.rmdir(dir)
57
+ } catch (err) {
58
+ // The directory may already have been deleted by another async call,
59
+ // fail silently here in this case.
60
+ if (err.code !== 'ENOENT') {
61
+ throw err
62
+ }
63
+ }
64
+ }
65
+ }
66
+ // Clean up nested folders created with first two chars of `file.key` also:
67
+ const dir = path.dirname(filePath)
68
+ const parentDir = path.dirname(dir)
69
+ await removeIfEmpty(dir)
70
+ await removeIfEmpty(parentDir)
71
+ }
72
+
73
+ // @override
74
+ async _readFile(file) {
75
+ return fs.readFile(this._getFilePath(file))
76
+ }
77
+
78
+ // @override
79
+ async _listKeys() {
80
+ const readDir = (...parts) =>
81
+ fs.readdir(this._getPath(...parts), { withFileTypes: true })
82
+
83
+ const files = []
84
+ const list1 = await readDir()
85
+ await mapConcurrently(
86
+ list1,
87
+ async level1 => {
88
+ if (level1.isDirectory() && level1.name.length === 1) {
89
+ const list2 = await readDir(level1.name)
90
+ await mapConcurrently(
91
+ list2,
92
+ async level2 => {
93
+ if (level2.isDirectory() && level2.name.length === 1) {
94
+ const nestedFolder = this._getPath(level1.name, level2.name)
95
+ for (const file of await fs.readdir(nestedFolder)) {
96
+ if (!file.startsWith('.')) {
97
+ files.push(file)
98
+ }
99
+ }
100
+ }
101
+ }
102
+ )
103
+ }
104
+ }
105
+ )
106
+ return files
107
+ }
108
+
109
+ _getNestedFolder(key, posix = false) {
110
+ // Store files in nested folders created with the first two chars of the
111
+ // key, for faster access & management with large amounts of files.
112
+ return (posix ? path.posix : path).join(key[0], key[1])
113
+ }
114
+ }
@@ -0,0 +1,169 @@
1
+ import multerS3 from 'multer-s3'
2
+ import { fileTypeFromBuffer } from 'file-type'
3
+ import { Storage } from './Storage.js'
4
+ import { PassThrough } from 'stream'
5
+ import consumers from 'stream/consumers'
6
+ import { readMediaAttributes } from 'leather'
7
+
8
+ export class S3Storage extends Storage {
9
+ static type = 's3'
10
+
11
+ s3 = null
12
+ acl = null
13
+ bucket = null
14
+
15
+ async setup() {
16
+ const {
17
+ name,
18
+ s3,
19
+ acl,
20
+ bucket,
21
+ ...options
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')
27
+ this.s3 = new S3(s3)
28
+ this.acl = acl
29
+ this.bucket = bucket
30
+
31
+ this.storage = multerS3({
32
+ s3: this.s3,
33
+ acl,
34
+ bucket,
35
+ ...options,
36
+
37
+ key: (req, file, cb) => {
38
+ cb(null, this.getUniqueKey(file.originalname))
39
+ },
40
+
41
+ contentType: (req, file, cb) => {
42
+ const { mimetype, stream } = file
43
+ if (mimetype) {
44
+ // 1. Trust file.mimetype if provided.
45
+ cb(null, mimetype)
46
+ } else {
47
+ let data = null
48
+
49
+ const done = type => {
50
+ stream.off('data', onData)
51
+ const outStream = new PassThrough()
52
+ outStream.write(data)
53
+ stream.pipe(outStream)
54
+ cb(null, type, outStream)
55
+ }
56
+
57
+ const onData = chunk => {
58
+ if (!data) {
59
+ // 2. Try reading the mimetype from the first chunk.
60
+ const type = getFileTypeFromBuffer(chunk)
61
+ if (type) {
62
+ done(type)
63
+ } else {
64
+ // 3. If that fails, keep collecting all chunks and determine
65
+ // the mimetype using the full data.
66
+ stream.once('end', () =>
67
+ done(
68
+ getFileTypeFromBuffer(data) || 'application/octet-stream'
69
+ )
70
+ )
71
+ }
72
+ }
73
+ data = data ? Buffer.concat([data, chunk]) : chunk
74
+ }
75
+
76
+ stream.on('data', onData)
77
+ }
78
+ },
79
+
80
+ metadata: (req, file, cb) => {
81
+ // Store the determined width and height as meta-data on the s3 object
82
+ // as well. You never know, it may become useful :)
83
+ const { width, height } = file
84
+ if (width != null || height != null) {
85
+ cb(null, {
86
+ width: `${width}`,
87
+ height: `${height}`
88
+ })
89
+ } else {
90
+ cb(null, {})
91
+ }
92
+ }
93
+ })
94
+ }
95
+
96
+ // @override
97
+ _getFilePath(_file) {
98
+ // There is no "local" file-path to files on S3.
99
+ return undefined
100
+ }
101
+
102
+ // @override
103
+ _getFileUrl(file) {
104
+ return this._getUrl(file.key) ?? file.url
105
+ }
106
+
107
+ // @override
108
+ async _addFile(file, data) {
109
+ const result = await this.s3.putObject({
110
+ Bucket: this.bucket,
111
+ ACL: this.acl,
112
+ Key: file.key,
113
+ ContentType: file.type,
114
+ Body: data
115
+ })
116
+ // In `Storage.addFile()` this will get overridden with the result of
117
+ // `_getUrl()` if it exists, but is used as a fallback otherwise,
118
+ // see `_getFileUrl()`.
119
+ file.url = result.Location
120
+ }
121
+
122
+ // @override
123
+ async _removeFile(file) {
124
+ await this.s3.deleteObject({
125
+ Bucket: this.bucket,
126
+ Key: file.key
127
+ })
128
+ // TODO: Check for errors and throw?
129
+ }
130
+
131
+ // @override
132
+ async _readFile(file) {
133
+ const {
134
+ ContentType: type,
135
+ Body: stream
136
+ } = await this.s3.getObject({
137
+ Bucket: this.bucket,
138
+ Key: file.key
139
+ })
140
+ const buffer = await consumers.buffer(stream)
141
+ // See `AssetFile.data` setter:
142
+ buffer.type = type
143
+ return buffer
144
+ }
145
+
146
+ // @override
147
+ async _listKeys() {
148
+ const files = []
149
+ const params = { Bucket: this.bucket }
150
+ let result
151
+ do {
152
+ result = await this.s3.listObjectsV2(params)
153
+ for (const { Key: key } of result.Contents ?? []) {
154
+ files.push(key)
155
+ }
156
+ // Continue it if results are truncated.
157
+ params.ContinuationToken = result.NextContinuationToken
158
+ } while (result.IsTruncated)
159
+ return files
160
+ }
161
+ }
162
+
163
+ function getFileTypeFromBuffer(buffer) {
164
+ try {
165
+ // Use leather as fall-back for better media file mime type detection.
166
+ return fileTypeFromBuffer(buffer)?.mime || readMediaAttributes(buffer)?.mime
167
+ } catch {}
168
+ return null
169
+ }
@@ -0,0 +1,231 @@
1
+ import path from 'path'
2
+ import { URL } from 'url'
3
+ import multer from '@koa/multer'
4
+ import picomatch from 'picomatch'
5
+ import { PassThrough } from 'stream'
6
+ import { readMediaAttributes } from 'leather'
7
+ import { hyphenate, toPromiseCallback } from '@ditojs/utils'
8
+ import { AssetFile } from './AssetFile.js'
9
+
10
+ const storageClasses = {}
11
+
12
+ export class Storage {
13
+ initialized = false
14
+
15
+ constructor(app, config) {
16
+ this.app = app
17
+ this.config = config
18
+ this.name = config.name
19
+ this.url = config.url
20
+ this.path = config.path
21
+ // Use a default concurrency of 8 for storage IO, e.g. the importing of
22
+ // foreign assets.
23
+ this.concurrency = config.concurrency ?? 8
24
+ // The actual multer storage object.
25
+ this.storage = null
26
+ }
27
+
28
+ // @overridable
29
+ async setup() {}
30
+
31
+ // @overridable
32
+ async initialize() {}
33
+
34
+ static register(storageClass) {
35
+ const type = (
36
+ storageClass.type ||
37
+ hyphenate(storageClass.name.match(/^(.*?)(?:Storage|)$/)[1])
38
+ )
39
+ storageClass.type = type
40
+ storageClasses[type] = storageClass
41
+ }
42
+
43
+ static get(type) {
44
+ return storageClasses[type] || null
45
+ }
46
+
47
+ getUploadStorage(config) {
48
+ // Returns a storage that inherits from this.storage but overrides
49
+ // _handleFile to pass on `config` to the call of `handleUpload()`
50
+ return this.storage
51
+ ? Object.setPrototypeOf(
52
+ {
53
+ _handleFile: async (req, file, callback) => {
54
+ try {
55
+ callback(null, await this._handleUpload(req, file, config))
56
+ } catch (err) {
57
+ callback(err)
58
+ }
59
+ }
60
+ },
61
+ this.storage
62
+ )
63
+ : null
64
+ }
65
+
66
+ getUploadHandler(config) {
67
+ const storage = this.getUploadStorage(config)
68
+ return storage ? multer({ ...config, storage }).any() : null
69
+ }
70
+
71
+ getUniqueKey(name) {
72
+ return AssetFile.getUniqueKey(name)
73
+ }
74
+
75
+ isImportSourceAllowed(url) {
76
+ return picomatch.isMatch(url, this.config.allowedImports || [])
77
+ }
78
+
79
+ convertAssetFile(file) {
80
+ AssetFile.convert(file, this)
81
+ }
82
+
83
+ convertStorageFile(storageFile) {
84
+ // Convert multer file object to our own file object format:
85
+ return {
86
+ key: storageFile.key,
87
+ name: storageFile.originalname,
88
+ type: storageFile.mimetype,
89
+ size: storageFile.size,
90
+ url: this._getFileUrl(storageFile),
91
+ // In case `config.readDimensions` is set:
92
+ width: storageFile.width,
93
+ height: storageFile.height
94
+ }
95
+ }
96
+
97
+ convertStorageFiles(storageFiles) {
98
+ return storageFiles.map(storageFile => this.convertStorageFile(storageFile))
99
+ }
100
+
101
+ async addFile(file, data) {
102
+ await this._addFile(file, data)
103
+ file.size = Buffer.byteLength(data)
104
+ file.url = this._getFileUrl(file)
105
+ // TODO: Support `config.readDimensions`, but this can only be done once
106
+ // there are separate storage instances per model assets config!
107
+ this.convertAssetFile(file)
108
+ return file
109
+ }
110
+
111
+ async removeFile(file) {
112
+ await this._removeFile(file)
113
+ }
114
+
115
+ async readFile(file) {
116
+ return this._readFile(file)
117
+ }
118
+
119
+ async listKeys() {
120
+ return this._listKeys()
121
+ }
122
+
123
+ getFilePath(file) {
124
+ return this._getFilePath(file)
125
+ }
126
+
127
+ getFileUrl(file) {
128
+ return this._getFileUrl(file)
129
+ }
130
+
131
+ _getUrl(...parts) {
132
+ return this.url
133
+ ? new URL(path.posix.join(...parts), this.url).toString()
134
+ : undefined // So that it doesn't show up in JSON data.
135
+ }
136
+
137
+ _getPath(...parts) {
138
+ return this.path
139
+ ? path.resolve(this.app.basePath, this.path, ...parts)
140
+ : undefined // So that it doesn't show up in JSON data.
141
+ }
142
+
143
+ // @overridable
144
+ _getFilePath(_file) {}
145
+
146
+ // @overridable
147
+ _getFileUrl(_file) {}
148
+
149
+ // @overridable
150
+ async _addFile(_file, _data) {}
151
+
152
+ // @overridable
153
+ async _removeFile(_file) {}
154
+
155
+ // @overridable
156
+ async _readFile(_file) {}
157
+
158
+ // @overridable
159
+ async _listKeys() {}
160
+
161
+ async _handleUpload(req, file, config) {
162
+ if (
163
+ config.readDimensions &&
164
+ /^(image|video)\//.test(file.mimetype)
165
+ ) {
166
+ return this._handleMediaFile(req, file)
167
+ } else {
168
+ return this._handleFile(req, file)
169
+ }
170
+ }
171
+
172
+ _handleFile(req, file, stream = null) {
173
+ // Calls the original `storage._handleFile()`, wrapped in a promise:
174
+ return new Promise((resolve, reject) => {
175
+ if (stream) {
176
+ // Replace the original `file.stream` with the pass-through stream:
177
+ Object.defineProperty(file, 'stream', {
178
+ configurable: true,
179
+ enumerable: false,
180
+ value: stream
181
+ })
182
+ }
183
+ this.storage._handleFile(req, file, toPromiseCallback(resolve, reject))
184
+ })
185
+ }
186
+
187
+ async _handleMediaFile(req, file) {
188
+ const { size, stream } = await new Promise(resolve => {
189
+ let data = null
190
+
191
+ const done = size => {
192
+ const stream = new PassThrough()
193
+ stream.write(data)
194
+ file.stream
195
+ .off('data', onData)
196
+ .off('end', onEnd)
197
+ .pipe(stream)
198
+ resolve({ size, stream })
199
+ }
200
+
201
+ const onEnd = () => {
202
+ this.app.emit('error', 'Unable to determine image size')
203
+ done(null)
204
+ }
205
+
206
+ const onData = chunk => {
207
+ data = data ? Buffer.concat([data, chunk]) : chunk
208
+ try {
209
+ const size = readMediaAttributes(data)
210
+ // On partial data, sometimes we get results back from leather without
211
+ // actual dimensions, so check for that.
212
+ if (size.mime && (size.width > 0 || size.height > 0)) {
213
+ done(size)
214
+ }
215
+ } catch {
216
+ // Ignore errors in `readMediaAttributes()` on partial data.
217
+ }
218
+ }
219
+
220
+ file.stream
221
+ .on('data', onData)
222
+ .on('end', onEnd)
223
+ })
224
+
225
+ if (size) {
226
+ file.width = size.width
227
+ file.height = size.height
228
+ }
229
+ return this._handleFile(req, file, stream)
230
+ }
231
+ }
@@ -0,0 +1,9 @@
1
+ import { Storage } from './Storage.js'
2
+ import { DiskStorage } from './DiskStorage.js'
3
+ import { S3Storage } from './S3Storage.js'
4
+
5
+ Storage.register(DiskStorage)
6
+ Storage.register(S3Storage)
7
+
8
+ export * from './AssetFile.js'
9
+ export { Storage }
@@ -0,0 +1,15 @@
1
+ import { isNumber } from '@ditojs/utils'
2
+ import parseDuration from 'parse-duration'
3
+
4
+ export function getDuration(duration) {
5
+ return isNumber(duration) ? duration : parseDuration(duration)
6
+ }
7
+
8
+ export function addDuration(date, duration) {
9
+ date.setMilliseconds(date.getMilliseconds() + getDuration(duration))
10
+ return date
11
+ }
12
+
13
+ export function subtractDuration(date, duration) {
14
+ return addDuration(date, -getDuration(duration))
15
+ }
@@ -0,0 +1,8 @@
1
+ import { mapConcurrently } from '@ditojs/utils'
2
+
3
+ export function emitAsync(emitter, event, ...args) {
4
+ return mapConcurrently(
5
+ emitter.listeners(event),
6
+ listener => listener.call(emitter, ...args)
7
+ )
8
+ }