@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,258 @@
1
+ import { isObject, isArray, asArray, mapConcurrently } from '@ditojs/utils'
2
+ import { QueryBuilder } from '../query/index.js'
3
+ import { collectExpressionPaths, expressionPathToString } from './expression.js'
4
+
5
+ // Similar to Objection's private `modelClass.ensureModel(model)`:
6
+ export function ensureModel(modelClass, model, options) {
7
+ return !model
8
+ ? null
9
+ : model instanceof modelClass
10
+ ? parseRelationsIntoModelInstances(model, model, options)
11
+ : modelClass.fromJson(model, options)
12
+ }
13
+
14
+ // Similar to Objection's private `modelClass.ensureModelArray(data)`:
15
+ export function ensureModelArray(modelClass, data, options) {
16
+ return data
17
+ ? asArray(data).map(model => ensureModel(modelClass, model, options))
18
+ : []
19
+ }
20
+
21
+ function parseRelationsIntoModelInstances(model, json, options = {}) {
22
+ if (!options.cache) {
23
+ options = { ...options, cache: new Map() }
24
+ }
25
+ options.cache.set(json, model)
26
+
27
+ for (const relationName of model.constructor.getRelationNames()) {
28
+ const jsonRelation = json[relationName]
29
+ if (jsonRelation !== undefined) {
30
+ const relation = model.constructor.getRelation(relationName)
31
+ const parsedRelation = parseRelation(jsonRelation, relation, options)
32
+ if (parsedRelation !== jsonRelation) {
33
+ model[relation.name] = parsedRelation
34
+ }
35
+ }
36
+ }
37
+
38
+ return model
39
+ }
40
+
41
+ function parseRelation(json, relation, options) {
42
+ return isArray(json)
43
+ ? parseRelationArray(json, relation, options)
44
+ : parseRelationObject(json, relation, options)
45
+ }
46
+
47
+ function parseRelationArray(json, relation, options) {
48
+ const models = new Array(json.length)
49
+ let changed = false
50
+ for (let i = 0, l = json.length; i < l; i++) {
51
+ const model = parseRelationObject(json[i], relation, options)
52
+ changed ||= model !== json[i]
53
+ models[i] = model
54
+ }
55
+ return changed ? models : json
56
+ }
57
+
58
+ function parseRelationObject(json, relation, options) {
59
+ if (isObject(json)) {
60
+ const modelClass = relation.relatedModelClass
61
+ let model = options.cache.get(json)
62
+ if (model === undefined) {
63
+ if (json instanceof modelClass) {
64
+ model = parseRelationsIntoModelInstances(json, json, options)
65
+ } else {
66
+ model = modelClass.fromJson(json, options)
67
+ }
68
+ }
69
+ return model
70
+ }
71
+ return json
72
+ }
73
+
74
+ export function walkGraph(data, callback, path = []) {
75
+ if (isObject(data) || isArray(data)) {
76
+ for (const [key, value] of Object.entries(data)) {
77
+ const dataPath = [...path, key]
78
+ callback(value, dataPath, data, key)
79
+ walkGraph(value, callback, dataPath)
80
+ }
81
+ }
82
+ }
83
+
84
+ export function filterGraph(rootModelClass, modelGraph, expr) {
85
+ expr = QueryBuilder.parseRelationExpression(expr)
86
+ const models = ensureModelArray(rootModelClass, modelGraph, {
87
+ skipValidation: true
88
+ })
89
+ for (const model of models) {
90
+ if (model) {
91
+ const relations = model.constructor.getRelations()
92
+ for (const key of Object.keys(model)) {
93
+ const relation = relations[key]
94
+ if (relation) {
95
+ const child = expr[key]
96
+ if (child) {
97
+ // Allowed relation, keep filtering recursively:
98
+ filterGraph(
99
+ relation.relatedModelClass,
100
+ model[key],
101
+ child
102
+ )
103
+ } else {
104
+ // Disallowed relation, delete:
105
+ delete model[key]
106
+ }
107
+ }
108
+ }
109
+ }
110
+ }
111
+ return modelGraph
112
+ }
113
+
114
+ export async function populateGraph(rootModelClass, graph, expr, trx) {
115
+ // TODO: Optimize the scenario where an item that isn't a leaf is a
116
+ // reference and has multiple sub-paths to be populated. We may need to
117
+ // move away from graph path handling and directly process the expression
118
+ // tree, composing paths to the current place on the fly, and process data
119
+ // with addToGroup(). Then use the sub-tree as graph expression when
120
+ // encountering references (needs toString() for caching also?)
121
+ // TODO: Better idea: First cache groups by the path up to their location in
122
+ // the graph, and collect modify + graph expressions there for references
123
+ // that are not leaves. Then use the resulting nodes to create new groups by
124
+ // model name / modify / graph expressions.
125
+ expr = QueryBuilder.parseRelationExpression(expr)
126
+ // Convert the relation expression to an array of paths, that themselves
127
+ // contain path entries with relation names and modify settings.
128
+
129
+ const grouped = {}
130
+ const addToGroup = (
131
+ item,
132
+ modelClass,
133
+ isReference,
134
+ modify,
135
+ relation,
136
+ expr
137
+ ) => {
138
+ const id = item.$id()
139
+ if (id != null) {
140
+ // Group models by model-name + modify + expr, for faster loading:
141
+ const key = `${modelClass.name}_${modify}_${expr || ''}`
142
+ const group = (
143
+ grouped[key] ||
144
+ (grouped[key] = {
145
+ modelClass,
146
+ modify,
147
+ relation,
148
+ expr,
149
+ targets: [],
150
+ ids: [],
151
+ modelsById: {}
152
+ })
153
+ )
154
+ group.targets.push({ item, isReference })
155
+ // Collect ids to be loaded for the targets.
156
+ group.ids.push(id)
157
+ }
158
+ }
159
+
160
+ for (const path of collectExpressionPaths(expr)) {
161
+ let modelClass = rootModelClass
162
+ const modelClasses = []
163
+ let lastModify
164
+ for (const entry of path) {
165
+ modelClasses.push(modelClass)
166
+ modelClass = modelClass.getRelation(entry.relation).relatedModelClass
167
+ lastModify = entry.modify
168
+ }
169
+ for (const model of asArray(graph)) {
170
+ if (model) {
171
+ let items = asArray(model)
172
+ // Collect all graph items described by the current relation path in
173
+ // one loop:
174
+ for (let i = 0, l = path.length; i < l; i++) {
175
+ if (items.length === 0) break
176
+ const { relation } = path[i]
177
+ const modelClass = modelClasses[i]
178
+ items = items.reduce((items, item) => {
179
+ item = ensureModel(modelClass, item, { skipValidation: true })
180
+ let add = false
181
+ const isReference = modelClass.isReference(item)
182
+ if (isReference) {
183
+ // Detected a reference item that isn't a leaf: We need to
184
+ // eager-load the rest of the path, and respect modify settings.
185
+ add = true
186
+ } else {
187
+ const value = item[relation]
188
+ // Add the models of this relation to items, so they can be
189
+ // filtered further in the next iteration of this loop.
190
+ if (value != null) {
191
+ items.push(...asArray(value))
192
+ } else if (value === undefined) {
193
+ // If the full relation is missing try eager-loading it.
194
+ // NOTE: Values of `null` are respected here, not loaded again.
195
+ add = true
196
+ }
197
+ }
198
+ if (add) {
199
+ // `path[i].modify` is the current relation's modify expression,
200
+ // get the parent one for `modelClass`:
201
+ const modify = i > 0 ? path[i - 1].modify : null
202
+ const expr = expressionPathToString(path, i)
203
+ addToGroup(item, modelClass, isReference, modify, relation, expr)
204
+ }
205
+ return items
206
+ }, [])
207
+ }
208
+ // Add all encountered leaf-references to groups to be loaded.
209
+ for (const item of items) {
210
+ // Only load leafs that are references.
211
+ if (modelClass.isReference(item)) {
212
+ addToGroup(item, modelClass, true, lastModify)
213
+ }
214
+ }
215
+ }
216
+ }
217
+ }
218
+
219
+ const groups = Object.values(grouped).filter(({ ids }) => ids.length > 0)
220
+ if (groups.length > 0) {
221
+ // Load all found models by ids asynchronously, within provided transaction.
222
+ // NOTE: Using the same transaction means that all involved tables need to
223
+ // be in the same database.
224
+ await mapConcurrently(
225
+ groups,
226
+ async ({ modelClass, modify, expr, ids, modelsById }) => {
227
+ const query = modelClass.query(trx).findByIds(ids).modify(modify)
228
+ if (expr) {
229
+ // TODO: Make algorithm configurable through options.
230
+ query.withGraph(expr)
231
+ }
232
+ const models = await query
233
+ // Fill the group.modelsById lookup:
234
+ for (const model of models) {
235
+ modelsById[model.$id()] = model
236
+ }
237
+ }
238
+ )
239
+
240
+ // Finally populate the targets with the loaded models.
241
+ for (const { targets, modelsById, relation } of groups) {
242
+ for (const { item, isReference } of targets) {
243
+ const model = modelsById[item.$id()]
244
+ if (model) {
245
+ if (isReference) {
246
+ // Copy over the full item.
247
+ Object.assign(item, model)
248
+ } else {
249
+ // Just copy the eager-loaded relation.
250
+ item[relation] = model[relation]
251
+ }
252
+ }
253
+ }
254
+ }
255
+ }
256
+
257
+ return graph
258
+ }
@@ -0,0 +1,3 @@
1
+ export * from './DitoGraphProcessor.js'
2
+ export * from './graph.js'
3
+ export * from './expression.js'
package/src/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export * from './app/index.js'
2
+ export * from './query/index.js'
3
+ export * from './schema/index.js'
4
+ export * from './errors/index.js'
5
+ export * from './mixins/index.js'
6
+ export * from './models/index.js'
7
+ export * from './controllers/index.js'
8
+ export * from './services/index.js'
9
+ export * from './storage/index.js'
@@ -0,0 +1,66 @@
1
+ import EventEmitter2 from 'eventemitter2'
2
+ import { isPlainObject, isString, isArray, asArray } from '@ditojs/utils'
3
+
4
+ export class EventEmitter extends EventEmitter2 {
5
+ // Method for classes that use `EventEmitter.mixin()` to setup the emitter.
6
+ _configureEmitter(events, options) {
7
+ EventEmitter2.call(this, {
8
+ delimiter: ':',
9
+ maxListeners: 0,
10
+ ...options
11
+ })
12
+ for (const key in events) {
13
+ for (const part of key.split(',')) {
14
+ const event = part.trim()
15
+ for (const callback of asArray(events[key])) {
16
+ this.on(event, callback)
17
+ }
18
+ }
19
+ }
20
+ }
21
+
22
+ emit(event, ...args) {
23
+ // Always use async version to emit events: It will perform the same as
24
+ // the normal one when the methods aren't actually async.
25
+ return this.emitAsync(event, ...args)
26
+ }
27
+
28
+ on(event, callback) {
29
+ return this._handle('on', event, callback)
30
+ }
31
+
32
+ off(event, callback) {
33
+ return this._handle('off', event, callback)
34
+ }
35
+
36
+ once(event, callback) {
37
+ return this._handle('once', event, callback)
38
+ }
39
+
40
+ _handle(method, event, callback) {
41
+ if (isString(event)) {
42
+ super[method](event, callback)
43
+ } else if (isArray(event)) {
44
+ for (const ev of event) {
45
+ super[method](ev, callback)
46
+ }
47
+ } else if (isPlainObject(event)) {
48
+ for (const key in event) {
49
+ super[method](key, event[key])
50
+ }
51
+ }
52
+ return this
53
+ }
54
+
55
+ static mixin(target) {
56
+ Object.defineProperties(target, properties)
57
+ }
58
+ }
59
+
60
+ const {
61
+ constructor, // Don't extract constructor, but everything else
62
+ ...properties
63
+ } = {
64
+ ...Object.getOwnPropertyDescriptors(EventEmitter2.prototype),
65
+ ...Object.getOwnPropertyDescriptors(EventEmitter.prototype)
66
+ }
@@ -0,0 +1,30 @@
1
+ export class KnexHelper {
2
+ getDialect() {
3
+ return this.knex()?.client?.dialect || null
4
+ }
5
+
6
+ isPostgreSQL() {
7
+ return this.getDialect() === 'postgresql'
8
+ }
9
+
10
+ isMySQL() {
11
+ return this.getDialect() === 'mysql'
12
+ }
13
+
14
+ isSQLite() {
15
+ return this.getDialect() === 'sqlite3'
16
+ }
17
+
18
+ isMsSQL() {
19
+ return this.getDialect() === 'mssql'
20
+ }
21
+
22
+ static mixin(target) {
23
+ Object.defineProperties(target, properties)
24
+ }
25
+ }
26
+
27
+ const {
28
+ constructor, // Don't extract constructor, but everything else
29
+ ...properties
30
+ } = Object.getOwnPropertyDescriptors(KnexHelper.prototype)
@@ -0,0 +1,2 @@
1
+ export * from './EventEmitter.js'
2
+ export * from './KnexHelper.js'
@@ -0,0 +1,8 @@
1
+ import { nanoid } from 'nanoid'
2
+
3
+ export function attachLogger(logger) {
4
+ return (ctx, next) => {
5
+ ctx.logger = logger.child({ requestId: nanoid(6) })
6
+ return next()
7
+ }
8
+ }
@@ -0,0 +1,33 @@
1
+ import { transaction } from 'objection'
2
+ import { emitAsync } from '../utils/emitter.js'
3
+
4
+ export function createTransaction() {
5
+ return async (ctx, next) => {
6
+ const { route } = ctx
7
+ if (route.transacted) {
8
+ const trx = await transaction.start(
9
+ route.controller.modelClass ||
10
+ ctx.app.knex
11
+ )
12
+ ctx.transaction = trx
13
+ // Knex doesn't offer a mechanism for code dealing with transactions to
14
+ // be notified when the transaction is rolled back. So add support for
15
+ // 'commit' and 'rollback' events here:
16
+ try {
17
+ await next()
18
+ await trx.commit()
19
+ await emitAsync(trx, 'commit')
20
+ } catch (err) {
21
+ await trx.rollback()
22
+ await emitAsync(trx, 'rollback', err)
23
+ throw err
24
+ } finally {
25
+ delete ctx.transaction
26
+ }
27
+ } else {
28
+ // TODO: Consider setting `ctx.transaction = ctx.app.knex`, just like
29
+ // Objection does in static hooks so `transaction is alway set.
30
+ await next()
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,10 @@
1
+ export function extendContext() {
2
+ return (ctx, next) => {
3
+ ctx.extend = function (object) {
4
+ // Create a copy of this context that inherits from the real one, but
5
+ // overrides some properties with the ones from the passed `object`.
6
+ return Object.setPrototypeOf(object, this)
7
+ }
8
+ return next()
9
+ }
10
+ }
@@ -0,0 +1,20 @@
1
+ export function findRoute(router) {
2
+ return (ctx, next) => {
3
+ const result = router.find(ctx.method, ctx.path)
4
+ // We use `result.handler` as returned from the router to store the route
5
+ // object for matched routes. If none is found, set `ctx.route = result`,
6
+ // for `{ status, allowed }`.
7
+ const route = result.handler ? { ...result.handler } : result
8
+ ctx.route = route
9
+ // NOTE: The name for `ctx.params` was inherited from `koa-router`
10
+ ctx.params = result.params || {}
11
+ const { controller, action } = route
12
+ if (controller) {
13
+ ctx.controller = controller
14
+ }
15
+ if (action) {
16
+ ctx.action = action
17
+ }
18
+ return next()
19
+ }
20
+ }
@@ -0,0 +1,99 @@
1
+ import { PassThrough } from 'stream'
2
+ import { isString, isArray, isObject } from '@ditojs/utils'
3
+
4
+ export function handleConnectMiddleware(middleware, {
5
+ expandMountPath = false
6
+ }) {
7
+ return (ctx, next) => {
8
+ return new Promise(resolve => {
9
+ let body = null
10
+
11
+ const res = {
12
+ locals: ctx.state,
13
+
14
+ get statusCode() {
15
+ return ctx.status
16
+ },
17
+
18
+ set statusCode(status) {
19
+ ctx.status = status
20
+ },
21
+
22
+ getHeader(field) {
23
+ // console.log('getHeader', ...arguments)
24
+ return ctx.get(field)
25
+ },
26
+
27
+ setHeader(field, value) {
28
+ // console.log('setHeader', ...arguments)
29
+ ctx.set(field, value)
30
+ },
31
+
32
+ appendHeader(field, value) {
33
+ // console.log('appendHeader', ...arguments)
34
+ ctx.append(field, value)
35
+ },
36
+
37
+ writeHead(status, message, headers) {
38
+ // console.log('writeHead', ...arguments)
39
+ ctx.status = status
40
+ if (isString(message)) {
41
+ ctx.body = message
42
+ } else {
43
+ headers = message
44
+ }
45
+ if (isArray(headers)) {
46
+ // Convert raw headers array to object.
47
+ headers = Object.fromEntries(
48
+ headers.reduce(
49
+ // Translate raw array to [field, value] tuples.
50
+ (entries, value, index) => {
51
+ if (index & 1) {
52
+ // Odd: value
53
+ entries[entries.length - 1].push(value)
54
+ } else {
55
+ // Even: field
56
+ entries.push([value])
57
+ }
58
+ return entries
59
+ },
60
+ []
61
+ )
62
+ )
63
+ }
64
+ if (isObject(headers)) {
65
+ ctx.set(headers)
66
+ }
67
+ },
68
+
69
+ write(...args) {
70
+ // console.log('write', ...arguments)
71
+ if (!body) {
72
+ body = new PassThrough()
73
+ ctx.body = body
74
+ }
75
+ body.write(...args)
76
+ },
77
+
78
+ end(body) {
79
+ // console.log('end', body?.substring?.(0, 256))
80
+ if (body !== undefined) {
81
+ ctx.body = body
82
+ }
83
+ resolve()
84
+ }
85
+ }
86
+
87
+ if (expandMountPath && ctx.mountPath) {
88
+ // Create an inheriting `ctx` object with the expanded `ctx.path`,
89
+ // without actually modifying the original `ctx` object.
90
+ ctx = Object.create(ctx, {
91
+ path: {
92
+ value: ctx.mountPath + ctx.path
93
+ }
94
+ })
95
+ }
96
+ middleware(ctx.req, res, next)
97
+ })
98
+ }
99
+ }
@@ -0,0 +1,29 @@
1
+ import { ResponseError } from '../errors/index.js'
2
+
3
+ export function handleError() {
4
+ return async (ctx, next) => {
5
+ try {
6
+ await next()
7
+ } catch (err) {
8
+ ctx.body = undefined
9
+ ctx.status = err.status || 500
10
+ // If the browser accepts json, return a JSON representation of the
11
+ // error. But don't do so if the request actually went to js file.
12
+ if (ctx.accepts('json') && !ctx.request.path.endsWith('.js')) {
13
+ // Format error as JSON
14
+ ctx.body =
15
+ err instanceof ResponseError
16
+ ? err.toJSON()
17
+ : {
18
+ message: err.message || 'An error has occurred.'
19
+ }
20
+ } else {
21
+ // TODO: Consider handling html and xml responses also, see:
22
+ // https://github.com/strongloop/strong-error-handler/blob/master/lib/send-html.js
23
+ // https://github.com/strongloop/strong-error-handler/blob/master/lib/send-xml.js
24
+ // https://github.com/strongloop/strong-error-handler/blob/master/views/default-error.ejs
25
+ }
26
+ ctx.app.emit('error', err, ctx)
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,23 @@
1
+ export function handleRoute() {
2
+ return async (ctx, next) => {
3
+ const { middleware } = ctx.route
4
+ if (middleware) {
5
+ await middleware(ctx, next)
6
+ } else {
7
+ // No route was found. See if the remaining middleware does something with
8
+ // this request. If not, return the errors as received from the router:
9
+ try {
10
+ await next()
11
+ } finally {
12
+ if (ctx.body === undefined && ctx.status === 404) {
13
+ ctx.status = ctx.route.status || 404
14
+ // Only retrieve `allowed` now, because it involves calling a getter
15
+ // and some internal processing:
16
+ if (ctx.status !== 404 && ctx.route.allowed) {
17
+ ctx.set('Allow', ctx.route.allowed.join(', '))
18
+ }
19
+ }
20
+ }
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,77 @@
1
+ import session from 'koa-session'
2
+ import compose from 'koa-compose'
3
+ import { isString } from '@ditojs/utils'
4
+
5
+ export function handleSession(app, {
6
+ modelClass,
7
+ autoCommit = true,
8
+ ...options
9
+ } = {}) {
10
+ if (modelClass) {
11
+ // Create a ContextStore that resolved the specified model class,
12
+ // uses it to persist and retrieve the session, and automatically
13
+ // binds all db operations to `ctx.transaction`, if it is set.
14
+ options.ContextStore = createSessionStore(modelClass)
15
+ }
16
+ options.autoCommit = false
17
+ return compose([
18
+ session(options, app),
19
+ async (ctx, next) => {
20
+ // Get hold of `session` now, since it may not be available from the
21
+ // `ctx` if the session is destroyed.
22
+ const {
23
+ session,
24
+ route: { transacted }
25
+ } = ctx
26
+ try {
27
+ await next()
28
+ if (autoCommit && transacted) {
29
+ // When transacted, only commit when there are no errors. Otherwise,
30
+ // the commit will fail and the original error will be lost.
31
+ await session.commit()
32
+ }
33
+ } finally {
34
+ // When not transacted, keep the original behavior of always
35
+ // committing.
36
+ if (autoCommit && !transacted) {
37
+ await session.commit()
38
+ }
39
+ }
40
+ }
41
+ ])
42
+ }
43
+
44
+ const createSessionStore = modelClass =>
45
+ class SessionStore {
46
+ constructor(ctx) {
47
+ this.ctx = ctx
48
+ this.modelClass = isString(modelClass)
49
+ ? ctx.app.models[modelClass]
50
+ : modelClass
51
+ if (!this.modelClass) {
52
+ throw new Error(`Unable to find model class: '${modelClass}'`)
53
+ }
54
+ }
55
+
56
+ query() {
57
+ return this.modelClass.query(this.ctx.transaction)
58
+ }
59
+
60
+ async get(id) {
61
+ const session = await this.query().findById(id)
62
+ return session?.value || {}
63
+ }
64
+
65
+ async set(id, value) {
66
+ await this.query()
67
+ .findById(id)
68
+ .upsert({
69
+ ...this.modelClass.getReference(id),
70
+ value
71
+ })
72
+ }
73
+
74
+ async destroy(key) {
75
+ await this.query().deleteById(key)
76
+ }
77
+ }