@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,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,5 @@
1
+ export * from './Controller.js'
2
+ export * from './ModelController.js'
3
+ export * from './RelationController.js'
4
+ export * from './AdminController.js'
5
+ export * from './UsersController.js'
@@ -0,0 +1,7 @@
1
+ import { ResponseError } from './ResponseError.js'
2
+
3
+ export class AssetError extends ResponseError {
4
+ constructor(error) {
5
+ super(error, { message: 'Asset error', status: 400 })
6
+ }
7
+ }
@@ -0,0 +1,7 @@
1
+ import { ResponseError } from './ResponseError.js'
2
+
3
+ export class AuthenticationError extends ResponseError {
4
+ constructor(error) {
5
+ super(error, { message: 'Authentication error', status: 401 })
6
+ }
7
+ }
@@ -0,0 +1,7 @@
1
+ import { ResponseError } from './ResponseError.js'
2
+
3
+ export class AuthorizationError extends ResponseError {
4
+ constructor(error) {
5
+ super(error, { message: 'Unauthorized Access', status: 401 })
6
+ }
7
+ }
@@ -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
+ }