@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.
- package/README.md +6 -0
- package/package.json +95 -0
- package/src/app/Application.js +1186 -0
- package/src/app/Validator.js +405 -0
- package/src/app/index.js +2 -0
- package/src/cli/console.js +152 -0
- package/src/cli/db/createMigration.js +241 -0
- package/src/cli/db/index.js +7 -0
- package/src/cli/db/listAssetConfig.js +10 -0
- package/src/cli/db/migrate.js +12 -0
- package/src/cli/db/reset.js +23 -0
- package/src/cli/db/rollback.js +12 -0
- package/src/cli/db/seed.js +80 -0
- package/src/cli/db/unlock.js +9 -0
- package/src/cli/index.js +72 -0
- package/src/controllers/AdminController.js +322 -0
- package/src/controllers/CollectionController.js +274 -0
- package/src/controllers/Controller.js +657 -0
- package/src/controllers/ControllerAction.js +370 -0
- package/src/controllers/MemberAction.js +27 -0
- package/src/controllers/ModelController.js +63 -0
- package/src/controllers/RelationController.js +93 -0
- package/src/controllers/UsersController.js +64 -0
- package/src/controllers/index.js +5 -0
- package/src/errors/AssetError.js +7 -0
- package/src/errors/AuthenticationError.js +7 -0
- package/src/errors/AuthorizationError.js +7 -0
- package/src/errors/ControllerError.js +14 -0
- package/src/errors/DatabaseError.js +37 -0
- package/src/errors/GraphError.js +7 -0
- package/src/errors/ModelError.js +12 -0
- package/src/errors/NotFoundError.js +7 -0
- package/src/errors/NotImplementedError.js +7 -0
- package/src/errors/QueryBuilderError.js +7 -0
- package/src/errors/RelationError.js +21 -0
- package/src/errors/ResponseError.js +56 -0
- package/src/errors/ValidationError.js +7 -0
- package/src/errors/index.js +13 -0
- package/src/graph/DitoGraphProcessor.js +213 -0
- package/src/graph/expression.js +53 -0
- package/src/graph/graph.js +258 -0
- package/src/graph/index.js +3 -0
- package/src/index.js +9 -0
- package/src/lib/EventEmitter.js +66 -0
- package/src/lib/KnexHelper.js +30 -0
- package/src/lib/index.js +2 -0
- package/src/middleware/attachLogger.js +8 -0
- package/src/middleware/createTransaction.js +33 -0
- package/src/middleware/extendContext.js +10 -0
- package/src/middleware/findRoute.js +20 -0
- package/src/middleware/handleConnectMiddleware.js +99 -0
- package/src/middleware/handleError.js +29 -0
- package/src/middleware/handleRoute.js +23 -0
- package/src/middleware/handleSession.js +77 -0
- package/src/middleware/handleUser.js +31 -0
- package/src/middleware/index.js +11 -0
- package/src/middleware/logRequests.js +125 -0
- package/src/middleware/setupRequestStorage.js +14 -0
- package/src/mixins/AssetMixin.js +78 -0
- package/src/mixins/SessionMixin.js +17 -0
- package/src/mixins/TimeStampedMixin.js +41 -0
- package/src/mixins/UserMixin.js +171 -0
- package/src/mixins/index.js +4 -0
- package/src/models/AssetModel.js +4 -0
- package/src/models/Model.js +1205 -0
- package/src/models/RelationAccessor.js +41 -0
- package/src/models/SessionModel.js +4 -0
- package/src/models/TimeStampedModel.js +4 -0
- package/src/models/UserModel.js +4 -0
- package/src/models/definitions/assets.js +5 -0
- package/src/models/definitions/filters.js +121 -0
- package/src/models/definitions/hooks.js +8 -0
- package/src/models/definitions/index.js +22 -0
- package/src/models/definitions/modifiers.js +5 -0
- package/src/models/definitions/options.js +5 -0
- package/src/models/definitions/properties.js +73 -0
- package/src/models/definitions/relations.js +5 -0
- package/src/models/definitions/schema.js +5 -0
- package/src/models/definitions/scopes.js +36 -0
- package/src/models/index.js +5 -0
- package/src/query/QueryBuilder.js +1077 -0
- package/src/query/QueryFilters.js +66 -0
- package/src/query/QueryParameters.js +79 -0
- package/src/query/Registry.js +29 -0
- package/src/query/index.js +3 -0
- package/src/schema/formats/_empty.js +4 -0
- package/src/schema/formats/_required.js +4 -0
- package/src/schema/formats/index.js +2 -0
- package/src/schema/index.js +5 -0
- package/src/schema/keywords/_computed.js +7 -0
- package/src/schema/keywords/_foreign.js +7 -0
- package/src/schema/keywords/_hidden.js +7 -0
- package/src/schema/keywords/_index.js +7 -0
- package/src/schema/keywords/_instanceof.js +45 -0
- package/src/schema/keywords/_primary.js +7 -0
- package/src/schema/keywords/_range.js +18 -0
- package/src/schema/keywords/_relate.js +13 -0
- package/src/schema/keywords/_specificType.js +7 -0
- package/src/schema/keywords/_unique.js +7 -0
- package/src/schema/keywords/_unsigned.js +7 -0
- package/src/schema/keywords/_validate.js +73 -0
- package/src/schema/keywords/index.js +12 -0
- package/src/schema/relations.js +324 -0
- package/src/schema/relations.test.js +177 -0
- package/src/schema/schema.js +289 -0
- package/src/schema/schema.test.js +720 -0
- package/src/schema/types/_asset.js +31 -0
- package/src/schema/types/_color.js +4 -0
- package/src/schema/types/index.js +2 -0
- package/src/services/Service.js +35 -0
- package/src/services/index.js +1 -0
- package/src/storage/AssetFile.js +81 -0
- package/src/storage/DiskStorage.js +114 -0
- package/src/storage/S3Storage.js +169 -0
- package/src/storage/Storage.js +231 -0
- package/src/storage/index.js +9 -0
- package/src/utils/duration.js +15 -0
- package/src/utils/emitter.js +8 -0
- package/src/utils/fs.js +10 -0
- package/src/utils/function.js +17 -0
- package/src/utils/function.test.js +77 -0
- package/src/utils/handler.js +17 -0
- package/src/utils/json.js +3 -0
- package/src/utils/model.js +35 -0
- package/src/utils/net.js +17 -0
- package/src/utils/object.js +82 -0
- package/src/utils/object.test.js +86 -0
- package/src/utils/scope.js +7 -0
- package/types/index.d.ts +3547 -0
- package/types/tests/application.test-d.ts +26 -0
- package/types/tests/controller.test-d.ts +113 -0
- package/types/tests/errors.test-d.ts +53 -0
- package/types/tests/fixtures.ts +19 -0
- package/types/tests/model.test-d.ts +193 -0
- package/types/tests/query-builder.test-d.ts +106 -0
- package/types/tests/relation.test-d.ts +83 -0
- 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
|
+
}
|
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)
|
package/src/lib/index.js
ADDED
|
@@ -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
|
+
}
|