@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,657 @@
1
+ import pico from 'picocolors'
2
+ import { EventEmitter } from '../lib/index.js'
3
+ import ControllerAction from './ControllerAction.js'
4
+ import MemberAction from './MemberAction.js'
5
+ import {
6
+ ResponseError,
7
+ ControllerError,
8
+ AuthorizationError
9
+ } from '../errors/index.js'
10
+ import {
11
+ getOwnProperty,
12
+ getOwnKeys,
13
+ getAllKeys,
14
+ getInheritanceChain
15
+ } from '../utils/object.js'
16
+ import { processHandlerParameters } from '../utils/handler.js'
17
+ import { describeFunction } from '../utils/function.js'
18
+ import { formatJson } from '../utils/json.js'
19
+ import {
20
+ isObject,
21
+ isString,
22
+ isArray,
23
+ isBoolean,
24
+ isFunction,
25
+ asArray,
26
+ equals,
27
+ parseDataPath,
28
+ normalizeDataPath,
29
+ deprecate
30
+ } from '@ditojs/utils'
31
+
32
+ export class Controller {
33
+ name = null
34
+ path = null
35
+ url = null
36
+ assets = null
37
+ actions = null
38
+ transacted = null
39
+ initialized = false
40
+
41
+ constructor(app, namespace) {
42
+ this.app = app
43
+ this.namespace = namespace
44
+ this.logRoutes = this.app.config.log.routes
45
+ this.level = 0
46
+ }
47
+
48
+ // `configure()` is called right after the constructor, but before `setup()`
49
+ // which sets up the actions and routes, and the custom `async initialize()`.
50
+ // @overridable
51
+ configure() {
52
+ const hooks = this.inheritValues('hooks')
53
+ // Collect callbacks from the full inheritance chain of the hooks, so that
54
+ // the callbacks from base classes are also run. And reverse the chain so
55
+ // that the base class callbacks are run first.
56
+ const chain = getInheritanceChain(hooks).reverse()
57
+ const keys = Object.keys(Object.assign({}, hooks, ...chain))
58
+ const events = Object.fromEntries(
59
+ keys.map(event => [
60
+ event,
61
+ chain
62
+ .map(hooks => (hooks.hasOwnProperty(event) ? hooks[event] : null))
63
+ .filter(Boolean)
64
+ ])
65
+ )
66
+ this._configureEmitter(events, {
67
+ // Support wildcard hooks only on controllers:
68
+ wildcard: true
69
+ })
70
+ // If the class name ends in 'Controller', remove it from controller name.
71
+ this.name ||= this.constructor.name.match(/^(.*?)(?:Controller|)$/)[1]
72
+ this.path ??= this.app.normalizePath(this.name)
73
+ const { path, namespace } = this
74
+ // TODO: The distinction between `url` and `path` is a bit tricky, since
75
+ // what we call `url` here is called `path` in Router, and may contain
76
+ // mapped parameters or wildcards. Consider `path` / `route` instead?
77
+ const url = path ? `/${path}` : ''
78
+ this.url = namespace ? `/${namespace}${url}` : url
79
+ }
80
+
81
+ // @overridable
82
+ setup() {
83
+ this.logController()
84
+ // Now that the instance fields are reflected in the `controller` object
85
+ // we can use the normal inheritance mechanism through `setupActions()`:
86
+ this.setProperty('actions', this.setupActions('actions'))
87
+ this.setProperty('assets', this.setupAssets())
88
+ }
89
+
90
+ // @overridable
91
+ async initialize() {
92
+ // To be overridden in sub-classes, if the controller needs to initialize.
93
+ }
94
+
95
+ // @return {Application|Function} [app or function]
96
+ // @overridable
97
+ compose() {
98
+ // To be overridden in sub-classes, if the controller needs to install
99
+ // middleware. For normal routes, use `this.app.addRoute()` instead.
100
+ }
101
+
102
+ // @overridable
103
+ logController() {
104
+ const { path, namespace } = this
105
+ this.logRoute(
106
+ `${
107
+ namespace ? pico.green(`/${namespace}/`) : ''
108
+ }${
109
+ pico.cyan(path)
110
+ }${
111
+ pico.white(':')
112
+ }`,
113
+ this.level
114
+ )
115
+ }
116
+
117
+ setProperty(key, value) {
118
+ Object.defineProperty(this, key, {
119
+ value,
120
+ writable: false,
121
+ enumerable: true,
122
+ configurable: true
123
+ })
124
+ }
125
+
126
+ logRoute(str, indent = 0) {
127
+ if (this.logRoutes) {
128
+ console.info(`${' '.repeat(indent)}${str}`)
129
+ }
130
+ }
131
+
132
+ // Only use this method to get a logger instance that is bound to the context,
133
+ // otherwise use the cached getter.
134
+ getLogger(ctx) {
135
+ const logger = ctx?.logger ?? this.app.logger
136
+ return logger.child({ name: this.name })
137
+ }
138
+
139
+ get logger() {
140
+ const value = this.getLogger()
141
+ Object.defineProperty(this, 'logger', { value })
142
+ return value
143
+ }
144
+
145
+ markAsCoreActions(actions) {
146
+ // Mark action object and methods as core, so `Controller.processValues()`
147
+ // can filter correctly.
148
+ for (const action of Object.values(actions)) {
149
+ // Mark action functions also, so ControllerAction can use it to determine
150
+ // value for `transacted`.
151
+ action.core = true
152
+ }
153
+ actions.$core = true
154
+ return actions
155
+ }
156
+
157
+ setupRoute(method, url, transacted, authorize, action, middlewares) {
158
+ this.logRoute(
159
+ `${
160
+ pico.magenta(method.toUpperCase())
161
+ } ${
162
+ pico.green(this.url)
163
+ }${
164
+ pico.cyan(url.slice(this.url.length))
165
+ } ${
166
+ pico.white(this.describeAuthorize(authorize))
167
+ }`,
168
+ this.level + 1
169
+ )
170
+ this.app.addRoute(method, url, transacted, middlewares, this, action)
171
+ }
172
+
173
+ setupActions(type) {
174
+ const {
175
+ values: actions,
176
+ authorize
177
+ } = this.processValues(this.inheritValues(type))
178
+ if (actions) {
179
+ for (const [name, action] of Object.entries(actions)) {
180
+ // Replace the action object with the converted action handler, so they
181
+ // too can benefit from prototypal inheritance:
182
+ actions[name] = this.setupAction(
183
+ type,
184
+ actions,
185
+ name,
186
+ action,
187
+ authorize[name]
188
+ )
189
+ }
190
+ // Expose a direct reference to the controller on the action object, but
191
+ // also make it inherit from the controller so that all its public fields
192
+ // and functions (`app`, `query()`, `execute()`, etc.) can be accessed
193
+ // directly through `this` from actions.
194
+ // NOTE: Inheritance is also set up by `inheritValues()` so that from the
195
+ // handlers, `super` points to the parent controller's actions object, so
196
+ // that calling `super.patch()` from a patch handler magically works.
197
+ actions.controller = this
198
+ Object.setPrototypeOf(actions, this)
199
+ }
200
+ return actions
201
+ }
202
+
203
+ setupAction(type, actions, name, action, authorize) {
204
+ const handler = isFunction(action)
205
+ ? action
206
+ : isObject(action)
207
+ ? convertActionObject(name, action, actions)
208
+ : null
209
+ // Action naming convention: `'<method> <path>'`, or just `'<method>'` for
210
+ // the default methods.
211
+ let [method, path = ''] = name.split(' ')
212
+ if (!isMethodAction(method)) {
213
+ path = name
214
+ }
215
+ // Custom member actions have their own class so they can fetch the members
216
+ // ahead of their call.
217
+ const actionClass = type === 'member' ? MemberAction : ControllerAction
218
+ this.setupActionRoute(
219
+ type,
220
+ // eslint-disable-next-line new-cap
221
+ new actionClass(
222
+ this,
223
+ actions,
224
+ handler,
225
+ type,
226
+ name,
227
+ method,
228
+ path,
229
+ authorize
230
+ )
231
+ )
232
+ return handler
233
+ }
234
+
235
+ setupActionRoute(type, action) {
236
+ const url = this.getUrl(type, action.path)
237
+ const { method, transacted, authorize } = action
238
+ this.setupRoute(method, url, transacted, authorize, action, [
239
+ async ctx => {
240
+ try {
241
+ const res = await action.callAction(ctx)
242
+ if (res !== undefined) {
243
+ ctx.body = res
244
+ }
245
+ } catch (err) {
246
+ throw err instanceof ResponseError ? err : new ResponseError(err)
247
+ }
248
+ }
249
+ ])
250
+ }
251
+
252
+ setupAssets() {
253
+ const {
254
+ values: assets,
255
+ authorize
256
+ } = this.processValues(this.inheritValues('assets'))
257
+ for (const [dataPath, config] of Object.entries(assets || {})) {
258
+ this.setupAssetRoute(dataPath, config, authorize[dataPath])
259
+ }
260
+ return assets
261
+ }
262
+
263
+ setupAssetRoute(dataPath, config, authorize) {
264
+ const {
265
+ storage: storageName,
266
+ // TODO: What exactly should control the use of `transacted`?
267
+ transacted,
268
+ ...settings
269
+ } = config
270
+ const storage = this.app.getStorage(storageName)
271
+ if (!storage) {
272
+ throw new ControllerError(
273
+ this,
274
+ `Unknown storage configuration: '${storageName}'`
275
+ )
276
+ }
277
+ const tokens = parseDataPath(dataPath)
278
+ const getDataPath = callback => normalizeDataPath(tokens.map(callback))
279
+
280
+ const normalizedPath = getDataPath(
281
+ // Router supports both shallow & deep wildcards, no normalization needed.
282
+ token =>
283
+ token === '*' || token === '**'
284
+ ? token
285
+ : this.app.normalizePath(token)
286
+ )
287
+
288
+ // Convert `dataPath` to a regular expression to match field names
289
+ // against, but convert wildcards (*) to match both numeric ids and words,
290
+ // e.g. 'create':
291
+ const matchDataPath = new RegExp(
292
+ `^${
293
+ getDataPath(
294
+ // Use the exact same regexps as in `Router`:
295
+ token =>
296
+ token === '*'
297
+ ? '[^/]+' // shallow wildcard
298
+ : token === '**'
299
+ ? '.+?' // deep wildcard
300
+ : token
301
+ )
302
+ }$`
303
+ )
304
+
305
+ const url = this.getUrl('controller', `upload/${normalizedPath}`)
306
+ const upload = storage.getUploadHandler({
307
+ ...settings,
308
+ // Only let uploads pass that match the normalizePath + wildcards:
309
+ fileFilter: (req, file, cb) => {
310
+ cb(null, matchDataPath.test(file.fieldname))
311
+ }
312
+ })
313
+
314
+ const authorization = this.processAuthorize(authorize)
315
+ this.setupRoute('post', url, transacted, authorize, null, [
316
+ async (ctx, next) => {
317
+ await this.handleAuthorization(authorization, ctx)
318
+ return next()
319
+ },
320
+
321
+ upload,
322
+
323
+ async (ctx, next) => {
324
+ const files = storage.convertStorageFiles(ctx.request.files)
325
+ await this.app.createAssets(storage, files, 0, ctx.transaction)
326
+ // Send the file objects back for the upload component to store in the
327
+ // data.
328
+ ctx.body = files
329
+ return next()
330
+ }
331
+ ])
332
+ }
333
+
334
+ getPath(type, path) {
335
+ // To be overridden by sub-classes.
336
+ return path
337
+ }
338
+
339
+ getUrl(type, path) {
340
+ path = this.getPath(type, path)
341
+ // Use '.' as the path for the controller's "index" action.
342
+ return path && path !== '.' ? `${this.url}/${path}` : this.url
343
+ }
344
+
345
+ inheritValues(type) {
346
+ // Gets the controller class's instance field for a given action type, e.g.
347
+ // `controller` (`Controller`), `collection`, `member` (`ModelController`,
348
+ // `RelationController`), `relation` (`RelationController`), and sets up an
349
+ // inheritance chain for it that goes all the way up to it base class (e.g.
350
+ // `CollectionController`), so that the default definitions for all HTTP
351
+ // methods can be inherited and overridden while using `super.<action>()`.
352
+ const parentClass = Object.getPrototypeOf(this.constructor)
353
+ // Create one instance of each controller class up the chain in order to
354
+ // get to their definitions of the inheritable values. Cache both instance
355
+ // and resolved values per parentClass in an inheritanceMap.
356
+ if (!inheritanceMap.has(parentClass)) {
357
+ inheritanceMap.set(parentClass, {
358
+ // eslint-disable-next-line new-cap
359
+ instance: new parentClass(this.app, this.namespace)
360
+ })
361
+ }
362
+ const entry = inheritanceMap.get(parentClass)
363
+ if (!entry[type]) {
364
+ const parent = entry.instance
365
+ let values = parent[type]
366
+ if (parentClass !== Controller) {
367
+ // Recursively set up inheritance chains.
368
+ values = parent.inheritValues(type)
369
+ }
370
+ entry[type] = values
371
+ }
372
+ // If there are no values defined on `this` that differ from the parent,
373
+ // set to an empty object so inheritance can be set up and `processValues()`
374
+ // can still be called.
375
+ // NOTE: We can't check with `this.hasOwnProperty(type)` because the
376
+ // field can be on the class prototype as well, in case of accessors.
377
+ const parentValues = entry[type]
378
+ let currentValues = this[type]
379
+ if (currentValues && currentValues === parentValues) {
380
+ currentValues = this[type] = {}
381
+ }
382
+ // Combine parentValues and currentValues with correct inheritance.
383
+ return isObject(parentValues) && isObject(currentValues)
384
+ ? Object.setPrototypeOf(currentValues, parentValues)
385
+ : currentValues
386
+ }
387
+
388
+ processValues(values) {
389
+ if (!values) return {}
390
+ // Respect `allow` settings and clear entries that aren't allowed.
391
+ // Also collect and expand `authorize` settings so that in the end, an
392
+ // `authorize` object can be returned with valid settings for all values.
393
+ //
394
+ // Rules:
395
+ // - Own values on objects that don't define an `allow` array are
396
+ // automatically allowed. If an `allow` array is defined as well, then
397
+ // these own values need to be explicitly listed.
398
+ // - If no `allow` arrays are defined in the prototypal hierarchy, each
399
+ // level allows its own values, and these are merged, except for those
400
+ // marked as `$core`, which need to be explicitly listed in `allow`.
401
+
402
+ // NOTE: `handleAllow()` and `handleAuthorize()` are applied in sequence of
403
+ // the `values` inheritance, from sub-class to base-class.
404
+
405
+ let allowMap = {}
406
+ const authorizeMap = {}
407
+
408
+ const includeKey = key => !['allow', 'authorize'].includes(key)
409
+
410
+ const handleAllow = (allow, current) => {
411
+ const getFilteredMap = keys =>
412
+ Object.fromEntries(keys.filter(includeKey).map(key => [key, true]))
413
+
414
+ if (allow) {
415
+ // The controller action object provides its own allow setting:
416
+ // - Clear whatever has been collected in `mergedAllow` so far
417
+ // - Merge the `allow` setting with all the own keys of the object,
418
+ // unless:
419
+ // - If the allow setting includes '*', allow all keys of the object,
420
+ // even the inherited ones.
421
+ let keys = asArray(allow)
422
+ if (keys.includes('*')) {
423
+ keys = getAllKeys(current)
424
+ } else {
425
+ keys = [
426
+ ...keys,
427
+ ...getOwnKeys(current)
428
+ ]
429
+ }
430
+ allowMap = getFilteredMap(keys) // Clear previous keys by overriding.
431
+ } else {
432
+ // The controller action object does not provide its own allow setting,
433
+ // so add its own keys to the already allowed inherited keys so far.
434
+ Object.assign(allowMap, getFilteredMap(getOwnKeys(current)))
435
+ }
436
+ }
437
+
438
+ const handleAuthorize = (authorize, allowOverride) => {
439
+ const add = (key, value) => {
440
+ if (key in values && includeKey(key)) {
441
+ if (allowOverride || !(key in authorizeMap)) {
442
+ authorizeMap[key] = value
443
+ }
444
+ }
445
+ }
446
+
447
+ if (isObject(authorize)) {
448
+ for (const key in authorize) {
449
+ add(key, authorize[key])
450
+ }
451
+ } else if (authorize != null) {
452
+ // This is a values-wide setting. Loop through all values, not just
453
+ // current ones, and apply to any action that doesn't already have one:
454
+ for (const key in values) {
455
+ add(key, authorize)
456
+ }
457
+ }
458
+ }
459
+
460
+ // Process the `allow` and `authorize` settings in reversed sequence of the
461
+ // `values` inheritance, from base-class to sub-class.
462
+ const chain = []
463
+ let current = values
464
+ while (current !== Object.prototype && !current.hasOwnProperty('$core')) {
465
+ chain.unshift(current)
466
+ current = Object.getPrototypeOf(current)
467
+ }
468
+
469
+ for (const current of chain) {
470
+ handleAllow(getOwnProperty(current, 'allow'), current)
471
+ handleAuthorize(getOwnProperty(current, 'authorize'), true)
472
+ }
473
+
474
+ // At the end of the chain, also support authorize settings on the
475
+ // controller-level, acting as a fallback for actions that don't already
476
+ // have authorization.
477
+ if (this.authorize) {
478
+ handleAuthorize(this.authorize, false)
479
+ }
480
+
481
+ return {
482
+ // Create a filtered `values` object that only contains the allowed fields
483
+ values: getAllKeys(values).reduce(
484
+ (result, key) => {
485
+ if (allowMap[key]) {
486
+ result[key] = values[key]
487
+ }
488
+ return result
489
+ },
490
+ // Create a new object for the filtered `values` that keeps inheritance
491
+ // intact. This is required by `convertActionObject()`, to support
492
+ // `super` in handler functions.
493
+ Object.create(Object.getPrototypeOf(values))
494
+ ),
495
+ allow: Object.keys(allowMap),
496
+ authorize: authorizeMap
497
+ }
498
+ }
499
+
500
+ async emitHook(type, handleResult, ctx, ...args) {
501
+ let result = handleResult ? args.shift() : undefined
502
+ for (const listener of this.listeners(type)) {
503
+ if (handleResult) {
504
+ const res = await listener.call(this, ctx, result, ...args)
505
+ if (res !== undefined) {
506
+ result = res
507
+ }
508
+ } else {
509
+ await listener.call(this, ctx, ...args)
510
+ }
511
+ }
512
+ return result
513
+ }
514
+
515
+ async getMember(/* ctx, base = this, param = { ... } */) {
516
+ // This is only defined in `CollectionController`, where it resolves to the
517
+ // member represented by the given route.
518
+ return null
519
+ }
520
+
521
+ /**
522
+ * Converts the authorize config settings into an authorization function that
523
+ * can be passed to `handleAuthorization()`.
524
+ *
525
+ * @param {boolean|function|string|string[]} authorize the authorize config
526
+ * settings
527
+ * @return {function} the authorization function
528
+ */
529
+ processAuthorize(authorize) {
530
+ if (authorize == null) {
531
+ return () => true
532
+ } else if (isBoolean(authorize)) {
533
+ return () => authorize
534
+ } else if (isFunction(authorize)) {
535
+ return async (ctx, member) => {
536
+ const res = await authorize(ctx, member)
537
+ // Pass res through `processAuthorize()` to support strings & arrays.
538
+ return this.processAuthorize(res)(ctx, member)
539
+ }
540
+ } else if (isString(authorize) || isArray(authorize)) {
541
+ return async (ctx, member) => {
542
+ const { user } = ctx.state
543
+ if (!user) {
544
+ return false
545
+ }
546
+ const values = asArray(authorize)
547
+ // For '$owner', fetch `member` now in case the action parameters
548
+ // didn't already request it:
549
+ if (!member && values.includes('$owner')) {
550
+ member = await this.getMember(ctx)
551
+ }
552
+ return !!values.find(
553
+ // Support 3 scenarios:
554
+ // - '$self': The requested member is checked against `ctx.state.user`
555
+ // and the action is only authorized if it matches the member.
556
+ // - '$owner': The member is asked if it is owned by `ctx.state.user`
557
+ // through the optional `Model.$hasOwner()` method.
558
+ // - any string: `ctx.state.user` is checked for this role through
559
+ // the overridable `UserModel.hasRole()` method.
560
+ value => {
561
+ return value === '$self'
562
+ ? user.constructor === this.modelClass &&
563
+ equals(user.$id(), ctx.memberId)
564
+ : value === '$owner'
565
+ ? member?.$hasOwner?.(user)
566
+ : user.$hasRole(value)
567
+ }
568
+ )
569
+ }
570
+ } else {
571
+ throw new ControllerError(
572
+ this,
573
+ `Unsupported authorize setting: '${authorize}'`
574
+ )
575
+ }
576
+ }
577
+
578
+ describeAuthorize(authorize) {
579
+ return isFunction(authorize)
580
+ ? describeFunction(authorize)
581
+ : isString(authorize)
582
+ ? `'${authorize}'`
583
+ : isArray(authorize)
584
+ ? `[${authorize.map(value => `'${value}'`).join(', ')}]`
585
+ : ''
586
+ }
587
+
588
+ async handleAuthorization(authorization, ctx, member) {
589
+ const ok = await authorization(ctx, member)
590
+ if (ok !== true) {
591
+ throw new AuthorizationError()
592
+ }
593
+ }
594
+ }
595
+
596
+ EventEmitter.mixin(Controller.prototype)
597
+
598
+ const inheritanceMap = new WeakMap()
599
+
600
+ function convertActionObject(name, object, actions) {
601
+ const {
602
+ handler,
603
+ action,
604
+ authorize,
605
+ transacted,
606
+ scope,
607
+ parameters,
608
+ // TODO: `returns` was deprecated in May 2025 in favour of `response`.
609
+ // Remove this in 2026.
610
+ returns,
611
+ response = returns,
612
+ ...rest
613
+ } = object
614
+
615
+ if (returns) {
616
+ deprecate(
617
+ 'The `returns` property is deprecated in favour of `response`. ' +
618
+ 'Update your handler definition to use `response` instead.'
619
+ )
620
+ }
621
+
622
+ // In order to support `super` calls in the `handler` function in object
623
+ // notation, deploy this crazy JS sorcery:
624
+ Object.setPrototypeOf(object, Object.getPrototypeOf(actions))
625
+
626
+ if (!handler) {
627
+ throw new Error(
628
+ `Missing handler in '${name}' action: ${formatJson(object)}`
629
+ )
630
+ }
631
+
632
+ handler.authorize = authorize ?? null
633
+ handler.transacted = transacted ?? null
634
+ handler.scope = scope ? asArray(scope) : null
635
+
636
+ processHandlerParameters(handler, 'parameters', parameters)
637
+ processHandlerParameters(handler, 'response', response)
638
+
639
+ return Object.assign(handler, rest)
640
+ }
641
+
642
+ function isMethodAction(name) {
643
+ return (
644
+ {
645
+ get: true,
646
+ delete: true,
647
+ post: true,
648
+ put: true,
649
+ patch: true,
650
+ head: true,
651
+ options: true,
652
+ trace: true,
653
+ connect: true
654
+ }[name] ||
655
+ false
656
+ )
657
+ }