@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,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
|
+
}
|