@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,370 @@
|
|
|
1
|
+
import { isString, isObject, asArray, clone, deprecate } from '@ditojs/utils'
|
|
2
|
+
import { convertModelsToJson } from '../utils/model.js'
|
|
3
|
+
|
|
4
|
+
export default class ControllerAction {
|
|
5
|
+
constructor(
|
|
6
|
+
controller,
|
|
7
|
+
actions,
|
|
8
|
+
handler,
|
|
9
|
+
type,
|
|
10
|
+
name,
|
|
11
|
+
method,
|
|
12
|
+
path,
|
|
13
|
+
_authorize
|
|
14
|
+
) {
|
|
15
|
+
const {
|
|
16
|
+
core = false,
|
|
17
|
+
scope,
|
|
18
|
+
authorize,
|
|
19
|
+
transacted,
|
|
20
|
+
parameters,
|
|
21
|
+
// TODO: `returns` was deprecated in May 2025 in favour of `response`.
|
|
22
|
+
// Remove this in 2026.
|
|
23
|
+
returns,
|
|
24
|
+
response = returns,
|
|
25
|
+
options = {},
|
|
26
|
+
...additional
|
|
27
|
+
} = handler
|
|
28
|
+
|
|
29
|
+
if (returns) {
|
|
30
|
+
deprecate(
|
|
31
|
+
'The `returns` property is deprecated in favour of `response`. ' +
|
|
32
|
+
'Update your handler definition to use `response` instead.'
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.app = controller.app
|
|
37
|
+
this.controller = controller
|
|
38
|
+
this.actions = actions
|
|
39
|
+
this.handler = handler
|
|
40
|
+
this.type = type
|
|
41
|
+
this.name = name
|
|
42
|
+
this.identifier = `${type}:${name}`
|
|
43
|
+
this.method = method
|
|
44
|
+
this.path = path
|
|
45
|
+
this.scope = scope
|
|
46
|
+
// Allow action handlers to override the predetermined defaults for
|
|
47
|
+
// `authorize`:
|
|
48
|
+
this.authorize = authorize || _authorize
|
|
49
|
+
this.transacted = !!(
|
|
50
|
+
transacted ??
|
|
51
|
+
controller.transacted ??
|
|
52
|
+
// Core graph and assets operations are always transacted, unless the
|
|
53
|
+
// method is 'get':
|
|
54
|
+
(
|
|
55
|
+
core &&
|
|
56
|
+
method !== 'get' && (
|
|
57
|
+
controller.graph ||
|
|
58
|
+
controller.assets
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
this.authorization = controller.processAuthorize(this.authorize)
|
|
63
|
+
this.paramsName = ['post', 'put', 'patch'].includes(this.method)
|
|
64
|
+
? 'body'
|
|
65
|
+
: 'query'
|
|
66
|
+
this.parameters = this.app.compileParametersValidator(parameters, {
|
|
67
|
+
async: true,
|
|
68
|
+
...options.parameters,
|
|
69
|
+
dataName: this.paramsName
|
|
70
|
+
})
|
|
71
|
+
this.response = this.app.compileParametersValidator(asArray(response), {
|
|
72
|
+
async: true,
|
|
73
|
+
// Use patch validation for response, as we often don't return the
|
|
74
|
+
// full model with all properties, but only a subset of them.
|
|
75
|
+
patch: true,
|
|
76
|
+
// TODO: `returns` was deprecated in May 2025 in favour of `response`.
|
|
77
|
+
// Remove this in 2026.
|
|
78
|
+
...options.returns,
|
|
79
|
+
...options.response,
|
|
80
|
+
dataName: 'response'
|
|
81
|
+
})
|
|
82
|
+
// Copy over the additional properties, e.g. `cached` so application
|
|
83
|
+
// middleware can implement caching mechanisms:
|
|
84
|
+
Object.assign(this, additional)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Possible values for `from` are:
|
|
88
|
+
// - 'path': Use `ctx.params` which is mapped to the route / path
|
|
89
|
+
// - 'query': Use `ctx.request.query`, regardless of the action's method.
|
|
90
|
+
// - 'body': Use `ctx.request.body`, regardless of the action's method.
|
|
91
|
+
getParams(ctx, from = this.paramsName) {
|
|
92
|
+
const params = from === 'path' ? ctx.params : ctx.request[from]
|
|
93
|
+
// koa-bodyparser always sets an object, even when there is no body.
|
|
94
|
+
// Detect this here and return null instead.
|
|
95
|
+
const isNull = (
|
|
96
|
+
from === 'body' &&
|
|
97
|
+
ctx.request.headers['content-length'] === '0' &&
|
|
98
|
+
Object.keys(params).length === 0
|
|
99
|
+
)
|
|
100
|
+
return isNull ? null : params
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async callAction(ctx) {
|
|
104
|
+
const { params, wrapped } = await this.validateParameters(ctx)
|
|
105
|
+
const { args, member } = await this.collectArguments(ctx, params)
|
|
106
|
+
let filteredQuery = null
|
|
107
|
+
Object.defineProperty(ctx, 'filteredQuery', {
|
|
108
|
+
get: () => {
|
|
109
|
+
filteredQuery ??=
|
|
110
|
+
params && !wrapped && this.paramsName === 'query'
|
|
111
|
+
? this.filterParameters(params)
|
|
112
|
+
: ctx.query
|
|
113
|
+
return filteredQuery
|
|
114
|
+
},
|
|
115
|
+
enumerable: false,
|
|
116
|
+
configurable: true
|
|
117
|
+
})
|
|
118
|
+
await this.controller.handleAuthorization(this.authorization, ctx, member)
|
|
119
|
+
const { identifier } = this
|
|
120
|
+
await this.controller.emitHook(`before:${identifier}`, false, ctx, ...args)
|
|
121
|
+
const response = await this.callHandler(ctx, ...args)
|
|
122
|
+
const result =
|
|
123
|
+
// Don't convert response to JSON if it isn't being validated, or if the
|
|
124
|
+
// response validation schema contains model references.
|
|
125
|
+
!this.response.validate || this.response.hasModelRefs
|
|
126
|
+
? response
|
|
127
|
+
: convertModelsToJson(response)
|
|
128
|
+
return this.validateResponse(
|
|
129
|
+
await this.controller.emitHook(`after:${identifier}`, true, ctx, result)
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async callHandler(ctx, ...args) {
|
|
134
|
+
return this.handler.call(this.actions, ctx, ...args)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
createValidationError(options) {
|
|
138
|
+
return this.app.createValidationError(options)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async validateParameters(ctx) {
|
|
142
|
+
if (!this.parameters.validate) {
|
|
143
|
+
return { params: null, wrapped: false }
|
|
144
|
+
}
|
|
145
|
+
// NOTE: The data can be either an object or an array.
|
|
146
|
+
const data = this.getParams(ctx)
|
|
147
|
+
let params = {}
|
|
148
|
+
const { dataName } = this.parameters
|
|
149
|
+
let unwrapRoot = false
|
|
150
|
+
const errors = []
|
|
151
|
+
for (const {
|
|
152
|
+
name, // String: Property name to fetch from data. Overridable by `root`
|
|
153
|
+
type, // String: What type should this validated against / coerced to.
|
|
154
|
+
from // String: Allow parameters to be 'borrowed' from other objects.
|
|
155
|
+
} of this.parameters.list) {
|
|
156
|
+
// Don't validate member parameters as they get resolved separately after.
|
|
157
|
+
if (from === 'member') continue
|
|
158
|
+
const root = from === 'root'
|
|
159
|
+
let wrapRoot = root
|
|
160
|
+
let paramName = name
|
|
161
|
+
// If no name is provided, wrap the full root object as value and unwrap
|
|
162
|
+
// at the end, see `unwrapRoot`.
|
|
163
|
+
if (!paramName) {
|
|
164
|
+
paramName = dataName
|
|
165
|
+
wrapRoot = true
|
|
166
|
+
unwrapRoot = true
|
|
167
|
+
}
|
|
168
|
+
// Since validation also performs coercion, always create clones of the
|
|
169
|
+
// params so that this doesn't modify the data on `ctx`.
|
|
170
|
+
if (from && !root) {
|
|
171
|
+
// Allow parameters to be 'borrowed' from other objects.
|
|
172
|
+
const source = this.getParams(ctx, from)
|
|
173
|
+
// See above for an explanation of `clone()`:
|
|
174
|
+
params[paramName] = clone(wrapRoot ? source : source?.[paramName])
|
|
175
|
+
} else if (wrapRoot) {
|
|
176
|
+
// If root is to be used, replace `params` with a new object on which
|
|
177
|
+
// to set the root object to validate under `parameters.paramName`
|
|
178
|
+
if (params === data) {
|
|
179
|
+
params = {}
|
|
180
|
+
}
|
|
181
|
+
params[paramName] = clone(data)
|
|
182
|
+
} else {
|
|
183
|
+
params[paramName] = clone(data[paramName])
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const value = params[paramName]
|
|
187
|
+
// `parameters.validate(params)` coerces data in the query to the
|
|
188
|
+
// required formats, according to the rules specified here:
|
|
189
|
+
// https://github.com/epoberezkin/ajv/blob/master/COERCION.md
|
|
190
|
+
// Coercion isn't currently offered for 'object' and 'date' types,
|
|
191
|
+
// so handle these cases prior to the call of `parameters.validate()`:
|
|
192
|
+
const coerced = this.coerceValue(type, value, {
|
|
193
|
+
// The model validation is handled separately through `$ref`.
|
|
194
|
+
skipValidation: true
|
|
195
|
+
})
|
|
196
|
+
// If coercion happened, replace value in params with coerced one:
|
|
197
|
+
if (coerced !== value) {
|
|
198
|
+
params[paramName] = coerced
|
|
199
|
+
}
|
|
200
|
+
} catch (err) {
|
|
201
|
+
// Convert error to Ajv validation error format:
|
|
202
|
+
errors.push({
|
|
203
|
+
dataPath: `.${paramName}`, // JavaScript property access notation
|
|
204
|
+
keyword: 'type',
|
|
205
|
+
message: err.message || err.toString(),
|
|
206
|
+
params: { type }
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const getData = () => (unwrapRoot ? params[dataName] : params)
|
|
212
|
+
try {
|
|
213
|
+
await this.parameters.validate(params)
|
|
214
|
+
const data = getData()
|
|
215
|
+
return { params: data, wrapped: data !== params }
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (error.errors) {
|
|
218
|
+
errors.push(...error.errors)
|
|
219
|
+
} else {
|
|
220
|
+
throw error
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (errors.length > 0) {
|
|
224
|
+
throw this.createValidationError({
|
|
225
|
+
type: 'ParameterValidation',
|
|
226
|
+
message: 'The provided action parameters are not valid',
|
|
227
|
+
errors,
|
|
228
|
+
json: getData()
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async validateResponse(response) {
|
|
234
|
+
if (this.response.validate) {
|
|
235
|
+
const responseName = this.handler.response.name
|
|
236
|
+
const responseWrapped = !!responseName
|
|
237
|
+
// Use dataName if no name is given, see:
|
|
238
|
+
// Application.compileParametersValidator(response, { dataName })
|
|
239
|
+
const dataName = responseName || this.response.dataName
|
|
240
|
+
const wrapped = { [dataName]: response }
|
|
241
|
+
// If a named result is defined, return the data wrapped,
|
|
242
|
+
// otherwise return the original unwrapped result object.
|
|
243
|
+
const getResult = () => (responseWrapped ? wrapped : response)
|
|
244
|
+
try {
|
|
245
|
+
await this.response.validate(wrapped)
|
|
246
|
+
return getResult()
|
|
247
|
+
} catch (error) {
|
|
248
|
+
// If the error contains errors, add them to the validation error:
|
|
249
|
+
const { errors } = error
|
|
250
|
+
const regexp = new RegExp(`^/${dataName}`)
|
|
251
|
+
throw this.createValidationError({
|
|
252
|
+
type: 'ResultValidation',
|
|
253
|
+
message: 'The returned action result is not valid',
|
|
254
|
+
errors: responseWrapped
|
|
255
|
+
? errors
|
|
256
|
+
: errors.map(error => ({
|
|
257
|
+
...error,
|
|
258
|
+
instancePath: error.instancePath.replace(regexp, '')
|
|
259
|
+
})),
|
|
260
|
+
json: getResult()
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return response
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async collectArguments(ctx, params) {
|
|
268
|
+
const { list, asObject } = this.parameters
|
|
269
|
+
|
|
270
|
+
const args = asObject ? [{}] : []
|
|
271
|
+
const addArgument = (name, value) => {
|
|
272
|
+
if (asObject) {
|
|
273
|
+
args[0][name] = value
|
|
274
|
+
} else {
|
|
275
|
+
args.push(value)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
let member = null
|
|
280
|
+
// If we have parameters, add them to the arguments now,
|
|
281
|
+
// while also keeping track of consumed parameters:
|
|
282
|
+
for (const param of list) {
|
|
283
|
+
const { name, from } = param
|
|
284
|
+
// Handle `{ from: 'member' }` parameters separately, by delegating to
|
|
285
|
+
// `getMember()` to resolve to the given member.
|
|
286
|
+
if (from === 'member') {
|
|
287
|
+
member = await this.getMember(ctx, param)
|
|
288
|
+
addArgument(name, member)
|
|
289
|
+
} else {
|
|
290
|
+
// If no name is provided, use the body object (params)
|
|
291
|
+
addArgument(name, name ? params[name] : params)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return { args, member }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
filterParameters(params) {
|
|
298
|
+
const filtered = {}
|
|
299
|
+
const consumedNames = Object.fromEntries(
|
|
300
|
+
this.parameters.list
|
|
301
|
+
.filter(param => !!param.name)
|
|
302
|
+
.map(param => [param.name, true])
|
|
303
|
+
)
|
|
304
|
+
for (const [key, value] of Object.entries(params)) {
|
|
305
|
+
if (!consumedNames[key]) {
|
|
306
|
+
filtered[key] = value
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return filtered
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
coerceValue(type, value, modelOptions) {
|
|
313
|
+
// See if param needs additional coercion:
|
|
314
|
+
if (value && ['date', 'datetime', 'timestamp'].includes(type)) {
|
|
315
|
+
value = new Date(value)
|
|
316
|
+
} else {
|
|
317
|
+
// See if the defined type(s) require coercion to objects:
|
|
318
|
+
const objectType = asArray(type).find(
|
|
319
|
+
// Coerce to object if type is 'object' or a known model name.
|
|
320
|
+
type => type === 'object' || type in this.app.models
|
|
321
|
+
)
|
|
322
|
+
if (objectType) {
|
|
323
|
+
if (value && isString(value)) {
|
|
324
|
+
if (!/^\{.*\}$/.test(value)) {
|
|
325
|
+
// Convert simplified Dito.js object notation to JSON, supporting:
|
|
326
|
+
// - `"key1":X, "key2":Y` (curly braces are added and parsed through
|
|
327
|
+
// `JSON.parse()`)
|
|
328
|
+
// - `key1:X,key2:Y` (a simple parser is applied, splitting into
|
|
329
|
+
// entries and key/value pairs, values are parsed with
|
|
330
|
+
// `JSON.parse()`, falling back to string.
|
|
331
|
+
if (/"/.test(value)) {
|
|
332
|
+
// Just add the curly braces and parse as JSON
|
|
333
|
+
value = JSON.parse(`{${value}}`)
|
|
334
|
+
} else {
|
|
335
|
+
// A simple version of named key/value pairs, values can be
|
|
336
|
+
// strings or numbers.
|
|
337
|
+
value = Object.fromEntries(
|
|
338
|
+
value.split(/\s*,\s*/g).map(entry => {
|
|
339
|
+
let [key, val] = entry.split(/\s*:\s*/)
|
|
340
|
+
try {
|
|
341
|
+
// Try parsing basic types, but fall back to unquoted
|
|
342
|
+
// string.
|
|
343
|
+
val = JSON.parse(val)
|
|
344
|
+
} catch {}
|
|
345
|
+
return [key, val]
|
|
346
|
+
})
|
|
347
|
+
)
|
|
348
|
+
}
|
|
349
|
+
} else {
|
|
350
|
+
value = JSON.parse(value)
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (objectType !== 'object' && isObject(value)) {
|
|
354
|
+
// Convert the Pojo to the desired Dito.js model:
|
|
355
|
+
const modelClass = this.app.models[objectType]
|
|
356
|
+
if (modelClass && !(value instanceof modelClass)) {
|
|
357
|
+
value = modelClass.fromJson(value, modelOptions)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return value
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async getMember(/* ctx, param */) {
|
|
366
|
+
// This is only defined in `MemberAction`, where it resolves to the member
|
|
367
|
+
// represented by the given route.
|
|
368
|
+
return null
|
|
369
|
+
}
|
|
370
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import ControllerAction from './ControllerAction.js'
|
|
2
|
+
|
|
3
|
+
export default class MemberAction extends ControllerAction {
|
|
4
|
+
// @override
|
|
5
|
+
async callAction(ctx) {
|
|
6
|
+
// Include `ctx.memberId` for convenient access in member actions.
|
|
7
|
+
return super.callAction(this.controller.getContextWithMemberId(ctx))
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// @override
|
|
11
|
+
async getMember(ctx, param) {
|
|
12
|
+
// member parameters can provide special query parameters as well,
|
|
13
|
+
// and they can even control `forUpdate()` behavior:
|
|
14
|
+
// {
|
|
15
|
+
// from: 'member',
|
|
16
|
+
// query: { ... },
|
|
17
|
+
// forUpdate: true,
|
|
18
|
+
// modify: query => query.debug()
|
|
19
|
+
// }
|
|
20
|
+
// These are passed on to and handled in `CollectionController#getMember()`.
|
|
21
|
+
// For handling of `from: 'member'` and calling of
|
|
22
|
+
// `MemberAction.getMember()`, see `ControllerAction#collectArguments()`.
|
|
23
|
+
// Pass on `this.handler` as `base` for `setupQuery()`,
|
|
24
|
+
// to handle the setting of `handler.scope` & co. on the query.
|
|
25
|
+
return this.controller.getMember(ctx, this.handler, param)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import pluralize from 'pluralize'
|
|
2
|
+
import { isObject, camelize } from '@ditojs/utils'
|
|
3
|
+
import { CollectionController } from './CollectionController.js'
|
|
4
|
+
import { RelationController } from './RelationController.js'
|
|
5
|
+
import { ControllerError } from '../errors/index.js'
|
|
6
|
+
import { setupPropertyInheritance } from '../utils/object.js'
|
|
7
|
+
|
|
8
|
+
export class ModelController extends CollectionController {
|
|
9
|
+
configure() {
|
|
10
|
+
super.configure()
|
|
11
|
+
this.modelClass ||=
|
|
12
|
+
this.app.models[camelize(pluralize.singular(this.name), true)]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
setup() {
|
|
16
|
+
super.setup()
|
|
17
|
+
this.setProperty('relations', this.setupRelations())
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
setupRelations() {
|
|
21
|
+
// Inherit `relations` from the controller and / or its sub-classes,
|
|
22
|
+
// then build inheritance chains for each relation object through
|
|
23
|
+
// `setupPropertyInheritance()`, before creating the relation controllers,
|
|
24
|
+
// which then carry on with setting up inheritance for their actions.
|
|
25
|
+
const relations = this.inheritValues('relations')
|
|
26
|
+
for (const name in relations) {
|
|
27
|
+
const relation = setupPropertyInheritance(relations, name)
|
|
28
|
+
if (isObject(relation)) {
|
|
29
|
+
relations[name] = this.setupRelation(relation, name)
|
|
30
|
+
} else {
|
|
31
|
+
throw new ControllerError(this, `Invalid relation '${name}'.`)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return relations
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
setupRelation(object, name) {
|
|
38
|
+
const relationInstance = this.modelClass.getRelations()[name]
|
|
39
|
+
const relationDefinition = this.modelClass.definition.relations[name]
|
|
40
|
+
if (!relationInstance || !relationDefinition) {
|
|
41
|
+
throw new ControllerError(this, `Relation '${name}' not found.`)
|
|
42
|
+
}
|
|
43
|
+
const relation = new RelationController(
|
|
44
|
+
this,
|
|
45
|
+
object,
|
|
46
|
+
relationInstance,
|
|
47
|
+
relationDefinition
|
|
48
|
+
)
|
|
49
|
+
// RelationController instances are not registered with the app, but are
|
|
50
|
+
// managed by their parent controller instead.
|
|
51
|
+
relation.configure()
|
|
52
|
+
relation.setup()
|
|
53
|
+
return relation
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// @override
|
|
57
|
+
async execute(ctx, execute) {
|
|
58
|
+
const trx = ctx.transaction
|
|
59
|
+
const query = this.modelClass.query(trx)
|
|
60
|
+
this.setupQuery(query)
|
|
61
|
+
return execute(query, trx)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { asArray } from '@ditojs/utils'
|
|
2
|
+
import { CollectionController } from './CollectionController.js'
|
|
3
|
+
import { ControllerError } from '../errors/index.js'
|
|
4
|
+
import { setupPropertyInheritance } from '../utils/object.js'
|
|
5
|
+
import { getScope } from '../utils/scope.js'
|
|
6
|
+
|
|
7
|
+
export class RelationController extends CollectionController {
|
|
8
|
+
constructor(parent, object, relationInstance, relationDefinition) {
|
|
9
|
+
super(parent.app, null)
|
|
10
|
+
if (parent.modelClass !== relationInstance.ownerModelClass) {
|
|
11
|
+
throw new ControllerError(
|
|
12
|
+
parent,
|
|
13
|
+
`Invalid parent controller for relation '${relationInstance.name}'.`
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
this.parent = parent
|
|
17
|
+
this.object = object
|
|
18
|
+
this.relationInstance = relationInstance
|
|
19
|
+
this.relationDefinition = relationDefinition
|
|
20
|
+
this.name = relationInstance.name
|
|
21
|
+
this.modelClass = relationInstance.relatedModelClass
|
|
22
|
+
this.isOneToOne = relationInstance.isOneToOne()
|
|
23
|
+
this.relate = !relationDefinition.owner
|
|
24
|
+
this.unrelate = !relationDefinition.owner
|
|
25
|
+
// Inherit parent values:
|
|
26
|
+
this.graph = parent.graph
|
|
27
|
+
this.transacted = parent.transacted
|
|
28
|
+
this.level = parent.level + 1
|
|
29
|
+
if (parent.scope) {
|
|
30
|
+
// Inherit only the graph scopes since it's in its nature to propagate to
|
|
31
|
+
// relations:
|
|
32
|
+
this.scope = asArray(parent.scope).filter(scope => getScope(scope).graph)
|
|
33
|
+
}
|
|
34
|
+
// Copy over all fields in the relation object.
|
|
35
|
+
for (const key in object) {
|
|
36
|
+
// On the relation objects, the `collection` actions are stored in a
|
|
37
|
+
// `relation` object, to make sense both for one- and many-relations:
|
|
38
|
+
this.setProperty(key === 'relation' ? 'collection' : key, object[key])
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// @override
|
|
43
|
+
configure() {
|
|
44
|
+
super.configure()
|
|
45
|
+
this.url = `${this.parent.url}/${this.parent.getPath('member', this.path)}`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// @override
|
|
49
|
+
inheritValues(type) {
|
|
50
|
+
// Since RelationController are mapped to nested `relations` objects in
|
|
51
|
+
// ModelController parents and are never extended directly in the user land
|
|
52
|
+
// code, inheritance works differently here than on the other controllers:
|
|
53
|
+
// ModelController already sets up inheritance for its `relations` object
|
|
54
|
+
// and its entries for each relation from which `object` is retrieved.
|
|
55
|
+
// But the actions per relation still need to have inheritance set up,
|
|
56
|
+
// using the values in its parent controller and potential super-classes,
|
|
57
|
+
// falling back on the definitions in RelationController and its inherited
|
|
58
|
+
// values from ModelController.
|
|
59
|
+
const values = setupPropertyInheritance(
|
|
60
|
+
this.object,
|
|
61
|
+
// On the relation objects, the `collection` actions are stored in a
|
|
62
|
+
// `relation` object, to make sense both for one- and many-relations:
|
|
63
|
+
type === 'collection' ? 'relation' : type,
|
|
64
|
+
// Set up inheritance for RelationController's override of `collection`
|
|
65
|
+
// and `member` objects, and use it as the base for further inheritance.
|
|
66
|
+
// NOTE: Currently they're empty, but they could allow local overrides.
|
|
67
|
+
super.inheritValues(type)
|
|
68
|
+
)
|
|
69
|
+
return values
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// @override
|
|
73
|
+
async execute(ctx, execute) {
|
|
74
|
+
const id = this.parent.getMemberId(ctx)
|
|
75
|
+
return this.parent.execute(ctx, async (parentQuery, trx) => {
|
|
76
|
+
const model = await parentQuery
|
|
77
|
+
.ignoreScope()
|
|
78
|
+
.findById(id)
|
|
79
|
+
.throwIfNotFound()
|
|
80
|
+
// Explicitly only select the foreign key ids for more efficiency.
|
|
81
|
+
.select(...this.relationInstance.ownerProp.props)
|
|
82
|
+
// This is the same as `ModelController.execute()`, except for the use
|
|
83
|
+
// of `model.$relatedQuery()` instead of `modelClass.query()`:
|
|
84
|
+
const query = model.$relatedQuery(this.relationInstance.name, trx)
|
|
85
|
+
this.setupQuery(query)
|
|
86
|
+
return execute(query, trx)
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
collection = this.markAsCoreActions({})
|
|
91
|
+
|
|
92
|
+
member = this.markAsCoreActions({})
|
|
93
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ModelController } from './ModelController.js'
|
|
2
|
+
|
|
3
|
+
export class UsersController extends ModelController {
|
|
4
|
+
collection = {
|
|
5
|
+
async 'post login'(ctx) {
|
|
6
|
+
let user
|
|
7
|
+
let error
|
|
8
|
+
try {
|
|
9
|
+
user = await this.modelClass.login(ctx)
|
|
10
|
+
await user.$patch({ lastLogin: new Date() }, ctx.transaction)
|
|
11
|
+
} catch (err) {
|
|
12
|
+
this.app.emit('error', err, ctx)
|
|
13
|
+
user = null
|
|
14
|
+
error = err.data?.message || err.message
|
|
15
|
+
ctx.status = err.status || 401
|
|
16
|
+
}
|
|
17
|
+
const success = !!user
|
|
18
|
+
return {
|
|
19
|
+
success,
|
|
20
|
+
authenticated: success && this.isAuthenticated(ctx),
|
|
21
|
+
user,
|
|
22
|
+
error
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
async 'post logout'(ctx) {
|
|
27
|
+
let success = false
|
|
28
|
+
if (this.isAuthenticated(ctx)) {
|
|
29
|
+
await ctx.logout()
|
|
30
|
+
success = ctx.isUnauthenticated()
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
success,
|
|
34
|
+
authenticated: this.isAuthenticated(ctx)
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
'get session'(ctx) {
|
|
39
|
+
const authenticated = this.isAuthenticated(ctx)
|
|
40
|
+
return {
|
|
41
|
+
authenticated,
|
|
42
|
+
user: authenticated ? ctx.state.user : null
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
'get self'(ctx) {
|
|
47
|
+
return this.isAuthenticated(ctx)
|
|
48
|
+
? this.member.get.call(
|
|
49
|
+
this,
|
|
50
|
+
this.getContextWithMemberId(ctx, ctx.state.user.$id())
|
|
51
|
+
)
|
|
52
|
+
: null
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
member = {
|
|
57
|
+
authorize: ['$self']
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
isAuthenticated(ctx) {
|
|
61
|
+
// Make sure the currently logged in user has the correct model-class:
|
|
62
|
+
return ctx.isAuthenticated() && ctx.state.user instanceof this.modelClass
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { isFunction } from '@ditojs/utils'
|
|
2
|
+
import { ResponseError } from './ResponseError.js'
|
|
3
|
+
|
|
4
|
+
export class ControllerError extends ResponseError {
|
|
5
|
+
constructor(controller, error) {
|
|
6
|
+
const { name } = isFunction(controller)
|
|
7
|
+
? controller
|
|
8
|
+
: controller.constructor
|
|
9
|
+
super(`Controller ${name}: ${error}`, {
|
|
10
|
+
message: `Controller ${name}: Controller error`,
|
|
11
|
+
status: 400
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
}
|