@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,322 @@
1
+ import path from 'path'
2
+ import Koa from 'koa'
3
+ import serve from 'koa-static'
4
+ import { defineConfig, createServer } from 'vite'
5
+ import createVuePlugin from '@vitejs/plugin-vue'
6
+ import { viteCommonjs as createCommonJsPlugin } from '@originjs/vite-plugin-commonjs'
7
+ import { testModuleIdentifier, getPostCssConfig } from '@ditojs/build'
8
+ import { assignDeeply } from '@ditojs/utils'
9
+ import { Controller } from './Controller.js'
10
+ import { handleConnectMiddleware } from '../middleware/index.js'
11
+ import { ControllerError } from '../errors/index.js'
12
+ import { getRandomFreePort } from '../utils/net.js'
13
+ import { formatJson } from '../utils/json.js'
14
+
15
+ export class AdminController extends Controller {
16
+ // @override
17
+ constructor(app, namespace) {
18
+ super(app, namespace)
19
+ // Merge `this.app.config.admin` into config as default, but allow overrides
20
+ // on the controller itself:
21
+ this.config = {
22
+ ...this.app.config.admin,
23
+ ...this.config
24
+ }
25
+ // If no mode is specified, use `production` since that's just the hosting
26
+ // of the pre-built admin files. `development` serves the admin directly
27
+ // sources with HRM, and thus should be explicitly activated.
28
+ this.mode = (
29
+ this.config.mode ||
30
+ (this.app.config.env === 'development' ? 'development' : 'production')
31
+ )
32
+ this.closed = false
33
+ }
34
+
35
+ getPath(name) {
36
+ const { config } = this
37
+ const str = config[name]
38
+ if (!str) {
39
+ throw new ControllerError(
40
+ this,
41
+ `Missing \`config.admin.${name}\` configuration.`
42
+ )
43
+ }
44
+ return path.resolve(str)
45
+ }
46
+
47
+ getDitoObject() {
48
+ // Expose api config and definitions to browser side:
49
+ // Pass on the `config.app.normalizePaths` setting to Dito.js Admin:
50
+ const {
51
+ api = {},
52
+ settings = {}
53
+ } = this.config
54
+ if (api.normalizePaths == null) {
55
+ api.normalizePaths = this.app.config.app.normalizePaths
56
+ }
57
+ return {
58
+ base: this.url,
59
+ api,
60
+ settings
61
+ }
62
+ }
63
+
64
+ sendDitoObject(ctx) {
65
+ // Send back the global dito object as JavaScript code.
66
+ ctx.type = 'text/javascript'
67
+ ctx.body = `window.dito = ${formatJson(this.getDitoObject())}`
68
+ }
69
+
70
+ middleware() {
71
+ // Shield admin views against unauthorized access.
72
+ const authorization = this.processAuthorize(this.authorize)
73
+ return async (ctx, next) => {
74
+ if (this.closed) {
75
+ // Avoid strange behavior during shut-down of the vite dev server.
76
+ // Sending back a 408 response seems to work best, while a 503 sadly
77
+ // would put the client into a state that prevents the server from a
78
+ // proper shut-down, and a 205 would kill future hot-reloads.
79
+ ctx.status = 408 // Request Timeout
80
+ } else if (ctx.url === '/dito.js') {
81
+ // Don't call `next()`
82
+ this.sendDitoObject(ctx)
83
+ } else {
84
+ if (/\/views\b/.test(ctx.url)) {
85
+ await this.handleAuthorization(authorization, ctx)
86
+ }
87
+ await next()
88
+ }
89
+ }
90
+ }
91
+
92
+ // @override
93
+ compose() {
94
+ this.koa = new Koa()
95
+ this.koa.use(this.middleware())
96
+ if (this.mode === 'development') {
97
+ // Calling getPath() throws exception if config.admin.root is not defined:
98
+ if (this.getPath('root')) {
99
+ this.app.once('after:start', () => this.setupViteServer())
100
+ }
101
+ } else {
102
+ // Statically serve the pre-built admin SPA. But in order for vue-router
103
+ // routes inside the SPA to work for sub-routes, use a tiny rewriting
104
+ // middleware that serves up the `index.html` fur sub-routes:
105
+ this.koa.use(async (ctx, next) => {
106
+ // // Exclude asset requests (css, js)
107
+ if (!ctx.url.match(/\.(?:css|js)$/)) {
108
+ ctx.url = '/'
109
+ }
110
+ await next()
111
+ })
112
+ this.koa.use(serve(this.getPath('dist')))
113
+ }
114
+ return this.koa
115
+ }
116
+
117
+ async setupViteServer() {
118
+ const defaultConfig = {
119
+ server: {
120
+ middlewareMode: true,
121
+ ...(this.mode === 'development' && {
122
+ watch: {
123
+ // Watch the @ditojs packages while in dev mode, although they are
124
+ // inside the node_modules folder.
125
+ ignored: ['!**/node_modules/@ditojs/**']
126
+ }
127
+ })
128
+ }
129
+ }
130
+
131
+ const viteConfig = (
132
+ (await this.app.loadAdminViteConfig()) ||
133
+ this.defineViteConfig()
134
+ )
135
+
136
+ const config = assignDeeply(defaultConfig, viteConfig)
137
+
138
+ if (config.server?.hmr?.port === 0) {
139
+ // Setting the port to 0 means use a random free port instead of vite's
140
+ // default 24678, since we may be running multiple servers in parallel.
141
+ config.server.hmr.port = await getRandomFreePort()
142
+ }
143
+
144
+ const server = await createServer(config)
145
+
146
+ this.closed = false
147
+
148
+ this.app.once('after:stop', () => {
149
+ // For good timing it seems crucial to not add more ticks with async
150
+ // signature, so we directly return the `server.close()` promise instead.
151
+ if (!this.closed) {
152
+ this.closed = true
153
+ return server.close()
154
+ }
155
+ })
156
+
157
+ this.koa.use(
158
+ handleConnectMiddleware(server.middlewares, {
159
+ expandMountPath: true
160
+ })
161
+ )
162
+ }
163
+
164
+ defineViteConfig(config = {}) {
165
+ const isDevelopment = this.mode === 'development'
166
+
167
+ const root = this.getPath('root')
168
+ const base = `${this.url}/`
169
+ const views = path.join(root, 'views')
170
+
171
+ return defineConfig(
172
+ assignDeeply(
173
+ {
174
+ root,
175
+ base,
176
+ mode: this.mode,
177
+ envFile: false,
178
+ configFile: false,
179
+ plugins: [
180
+ createVuePlugin(),
181
+ createCommonJsPlugin(),
182
+ {
183
+ // Private plugin to inject script tag above main module that
184
+ // loads the `dito` object through its own end-point, see:
185
+ // `sendDitoObject()`
186
+ name: 'inject-dito-object',
187
+ transformIndexHtml: {
188
+ order: 'post',
189
+ handler(html) {
190
+ return html.replace(
191
+ /(\s*)(<script type="module"[^>]*?><\/script>)/,
192
+ `$1<script src="${base}dito.js"></script>$1$2`
193
+ )
194
+ }
195
+ }
196
+ }
197
+ ],
198
+ build: isDevelopment
199
+ ? {}
200
+ : {
201
+ outDir: this.getPath('dist'),
202
+ assetsDir: '.',
203
+ emptyOutDir: true,
204
+ chunkSizeWarningLimit: 1000,
205
+ rollupOptions: {
206
+ output: {
207
+ manualChunks: id => {
208
+ if (id.startsWith(views)) {
209
+ return 'views'
210
+ } else if (id.startsWith(this.app.basePath)) {
211
+ return 'common'
212
+ } else {
213
+ const module = id.match(
214
+ /node_modules\/((?:@[^/]*\/)?[^/$]*)/
215
+ )?.[1]
216
+ // Internal ids (vite modules) don't come from
217
+ // node_modules, and the ids actually start with \0x00.
218
+ return (
219
+ !module ||
220
+ testModuleIdentifier(module, coreDependencies)
221
+ )
222
+ ? 'core'
223
+ : 'vendor'
224
+ }
225
+ }
226
+ }
227
+ }
228
+ },
229
+ css: {
230
+ postcss: getPostCssConfig(),
231
+ devSourcemap: isDevelopment,
232
+ preprocessorOptions: {
233
+ scss: {
234
+ // https://sass-lang.com/documentation/breaking-changes/legacy-js-api/
235
+ // TODO: Remove once vite has been updated to new API:
236
+ silenceDeprecations: ['legacy-js-api']
237
+ }
238
+ }
239
+ },
240
+ optimizeDeps: {
241
+ exclude: isDevelopment ? ditoPackages : [],
242
+ include: [
243
+ ...(
244
+ isDevelopment
245
+ ? // https://discuss.prosemirror.net/t/rangeerror-adding-different-instances-of-a-keyed-plugin-plugin/4242/13
246
+ [
247
+ 'prosemirror-state',
248
+ 'prosemirror-transform',
249
+ 'prosemirror-model',
250
+ 'prosemirror-view'
251
+ ]
252
+ : ditoPackages
253
+ ),
254
+ ...nonEsmDependencies
255
+ ]
256
+ },
257
+ resolve: {
258
+ extensions: [
259
+ '.js', '.mjs', '.jsx', '.ts', '.mts', '.tsx', '.json', '.vue'
260
+ ],
261
+ preserveSymlinks: true,
262
+ alias: [
263
+ {
264
+ find: '@',
265
+ replacement: root
266
+ }
267
+ ]
268
+ }
269
+ },
270
+ config
271
+ )
272
+ )
273
+ }
274
+ }
275
+
276
+ const ditoPackages = [
277
+ '@ditojs/admin',
278
+ '@ditojs/ui',
279
+ '@ditojs/utils'
280
+ ]
281
+
282
+ const nonEsmDependencies = [
283
+ // All non-es modules need to be explicitly included here, and some of
284
+ // them only work due to the use of `createCommonJsPlugin()`.
285
+ '@lk77/vue3-color'
286
+ ]
287
+
288
+ const coreDependencies = [
289
+ ...ditoPackages,
290
+
291
+ // TODO: Figure out a way to generate this automatically for the current
292
+ // dito-admin dependencies, e.g. similar to
293
+ // `getRollupExternalsFromDependencies()`, perhaps as a script to persist to
294
+ // a json file?
295
+
296
+ 'vue',
297
+ '@vue/*',
298
+ '@vueuse/*',
299
+ '@lk77/vue3-color',
300
+ '@kyvg/vue3-notification',
301
+ 'vue-multiselect',
302
+ 'vue-router',
303
+ 'vue-upload-component',
304
+ 'tinycolor2',
305
+ 'focus-trap',
306
+ 'tabbable',
307
+ 'sortablejs',
308
+ 'tippy.js',
309
+ '@tiptap/*',
310
+ 'tiptap-*',
311
+ 'prosemirror-*',
312
+ 'linkifyjs',
313
+ 'codeflask',
314
+ 'nanoid',
315
+ 'punycode',
316
+ 'rope-sequence',
317
+ 'filesize',
318
+ 'filesize-parser',
319
+ 'tslib', // ?
320
+ 'orderedmap',
321
+ 'w3c-keyname'
322
+ ]
@@ -0,0 +1,274 @@
1
+ import { isObject, isArray, asArray } from '@ditojs/utils'
2
+ import { Controller } from './Controller.js'
3
+ import { ControllerError } from '../errors/index.js'
4
+
5
+ // Abstract base class for ModelController and RelationController
6
+ export class CollectionController extends Controller {
7
+ graph = false
8
+ scope = null
9
+ relate = false
10
+ unrelate = false
11
+ modelClass = null // To be defined by sub-classes
12
+ isOneToOne = false
13
+ idParam = null
14
+ idValidator = null
15
+
16
+ // @override
17
+ configure() {
18
+ super.configure()
19
+ this.idParam = this.level ? `id${this.level}` : 'id'
20
+ // Create a dummy model instance to validate the requested id against.
21
+ // eslint-disable-next-line new-cap
22
+ this.idValidator = new this.modelClass()
23
+ }
24
+
25
+ // @override
26
+ setup() {
27
+ this.logController()
28
+ this.setProperty('collection', this.setupActions('collection'))
29
+ this.setProperty(
30
+ 'member',
31
+ this.isOneToOne ? {} : this.setupActions('member')
32
+ )
33
+ this.setProperty('assets', this.setupAssets())
34
+ }
35
+
36
+ // @override
37
+ setupAssets() {
38
+ const { modelClass } = this
39
+ if (this.assets === true) {
40
+ this.assets = modelClass.definition.assets || null
41
+ } else if (isObject(this.assets)) {
42
+ // Merge in the assets definition from the model into the assets config.
43
+ // That way, we can still use `allow` and `authorize` to control the
44
+ // upload access, while keeping the assets definitions in one central
45
+ // location on the model.
46
+ this.assets = {
47
+ ...modelClass.definition.assets,
48
+ ...this.assets
49
+ }
50
+ } else {
51
+ this.assets = null
52
+ }
53
+ // Now call `super.setupAssets()` which performs the usual inheritance /
54
+ // allow / authorize tricks:
55
+ return super.setupAssets()
56
+ }
57
+
58
+ // @override
59
+ getPath(type, path) {
60
+ return type === 'member'
61
+ ? path
62
+ ? `:${this.idParam}/${path}`
63
+ : `:${this.idParam}`
64
+ : path
65
+ }
66
+
67
+ getMemberId(ctx) {
68
+ return this.validateId(ctx.params[this.idParam])
69
+ }
70
+
71
+ getContextWithMemberId(ctx, memberId = this.getMemberId(ctx)) {
72
+ return ctx.extend({ memberId })
73
+ }
74
+
75
+ getModelId(model) {
76
+ const idProperty = this.modelClass.getIdProperty()
77
+ // Handle both composite keys and normal ones.
78
+ return isArray(idProperty)
79
+ ? idProperty.map(property => model[property])
80
+ : model[idProperty]
81
+ }
82
+
83
+ getCollectionIds(ctx) {
84
+ return asArray(ctx.request.body).map(
85
+ model => this.validateId(this.getModelId(model))
86
+ )
87
+ }
88
+
89
+ getIds(ctx) {
90
+ // Returns the model ids that this request concerns, read from the param
91
+ // for member ids, and from the payload for collection ids:
92
+ const { type } = ctx.action
93
+ return type === 'member'
94
+ ? [this.getMemberId(ctx)]
95
+ : type === 'collection'
96
+ ? this.getCollectionIds(ctx)
97
+ : []
98
+ }
99
+
100
+ validateId(id) {
101
+ const reference = this.modelClass.getReference(id)
102
+ // This validates and coerces at the same time, so extract the coerced id
103
+ // from `reference` again afterwards.
104
+ this.idValidator.$validate(reference, {
105
+ coerceTypes: true,
106
+ patch: true
107
+ })
108
+ const values = Object.values(reference)
109
+ return values.length > 1 ? values : values[0]
110
+ }
111
+
112
+ async getMember(
113
+ ctx,
114
+ base = this,
115
+ { query = {}, modify = null, forUpdate = false } = {}
116
+ ) {
117
+ return this.member.get.call(
118
+ this,
119
+ // Extend `ctx` with a new `query` object, while inheriting the route
120
+ // params in `ctx.params`, so fining the member by id still works.
121
+ ctx.extend({ query }),
122
+ (query, trx) => {
123
+ this.setupQuery(query, base)
124
+ query.modify(modify)
125
+ if (forUpdate) {
126
+ if (!trx) {
127
+ throw new ControllerError(
128
+ this,
129
+ 'Using `forUpdate()` without a transaction is invalid'
130
+ )
131
+ }
132
+ query.forUpdate()
133
+ }
134
+ }
135
+ )
136
+ }
137
+
138
+ query(trx) {
139
+ return this.setupQuery(this.modelClass.query(trx))
140
+ }
141
+
142
+ setupQuery(query, base = this) {
143
+ const { scope } = base
144
+ const { allowScope, allowFilter } = this
145
+
146
+ const asAllowArray = value => (value === false ? [] : asArray(value))
147
+
148
+ if (allowScope !== undefined && allowScope !== true) {
149
+ query.allowScope(
150
+ ...asAllowArray(allowScope),
151
+ // Also include the scopes defined by scope so these can pass through.
152
+ ...asArray(scope)
153
+ )
154
+ }
155
+ if (allowFilter !== undefined && allowFilter !== true) {
156
+ query.allowFilter(...asAllowArray(allowFilter))
157
+ }
158
+ if (scope) {
159
+ query.withScope(...asArray(scope))
160
+ }
161
+ return query
162
+ }
163
+
164
+ async execute(/* ctx, execute(query, trx) {} */) {
165
+ // Does nothing in base class.
166
+ // Overrides are in ModelController and RelationController.
167
+ }
168
+
169
+ async executeAndFetch(action, ctx, modify, body = ctx.request.body) {
170
+ const name = `${action}${this.graph ? 'DitoGraph' : ''}AndFetch`
171
+ return this.execute(ctx, (query, trx) =>
172
+ query[name](body).modify(getModify(modify, trx))
173
+ )
174
+ }
175
+
176
+ async executeAndFetchById(action, ctx, modify, body = ctx.request.body) {
177
+ const name = `${action}${this.graph ? 'DitoGraph' : ''}AndFetchById`
178
+ return this.execute(ctx, (query, trx) =>
179
+ query[name](ctx.memberId, body)
180
+ .throwIfNotFound()
181
+ .modify(getModify(modify, trx))
182
+ )
183
+ }
184
+
185
+ collection = this.markAsCoreActions({
186
+ async get(ctx, modify) {
187
+ const result = await this.execute(ctx, (query, trx) => {
188
+ query
189
+ .find(ctx.filteredQuery, this.allowParam)
190
+ .modify(getModify(modify, trx))
191
+ return this.isOneToOne ? query.first() : query
192
+ })
193
+ // This method doesn't always return an array:
194
+ // For RelationControllers where `isOneToOne` is true, it can return
195
+ // `undefined`. Cast to `null` for such cases:
196
+ return result || null
197
+ },
198
+
199
+ async delete(ctx, modify) {
200
+ const count = await this.execute(ctx, (query, trx) =>
201
+ query
202
+ .ignoreScope()
203
+ .find(ctx.filteredQuery, this.allowParam)
204
+ .modify(query => this.isOneToOne && query.throwIfNotFound())
205
+ .modify(getModify(modify, trx))
206
+ .modify(query => (this.unrelate ? query.unrelate() : query.delete()))
207
+ )
208
+ return { count }
209
+ },
210
+
211
+ async post(ctx, modify) {
212
+ const result = this.relate
213
+ ? // Use patchDitoGraphAndFetch() to handle relates for us.
214
+ await this.execute(ctx, (query, trx) =>
215
+ query
216
+ .patchDitoGraphAndFetch(ctx.request.body, { relate: true })
217
+ .modify(getModify(modify, trx))
218
+ )
219
+ : await this.executeAndFetch('insert', ctx, modify)
220
+ ctx.status = 201 // Created
221
+ if (isObject(result)) {
222
+ ctx.set('Location', this.getUrl('collection', this.getModelId(result)))
223
+ }
224
+ return result
225
+ },
226
+
227
+ async put(ctx, modify) {
228
+ return this.executeAndFetch('update', ctx, modify)
229
+ },
230
+
231
+ async patch(ctx, modify) {
232
+ return this.executeAndFetch('patch', ctx, modify)
233
+ }
234
+ })
235
+
236
+ member = this.markAsCoreActions({
237
+ async get(ctx, modify) {
238
+ return this.execute(ctx, (query, trx) =>
239
+ query
240
+ .findById(ctx.memberId)
241
+ .find(ctx.filteredQuery, this.allowParam)
242
+ .throwIfNotFound()
243
+ .modify(getModify(modify, trx))
244
+ )
245
+ },
246
+
247
+ async delete(ctx, modify) {
248
+ const count = await this.execute(ctx, (query, trx) =>
249
+ query
250
+ .ignoreScope()
251
+ .findById(ctx.memberId)
252
+ .find(ctx.filteredQuery, this.allowParam)
253
+ .throwIfNotFound()
254
+ .modify(getModify(modify, trx))
255
+ .modify(query => (this.unrelate ? query.unrelate() : query.delete()))
256
+ )
257
+ return { count }
258
+ },
259
+
260
+ async put(ctx, modify) {
261
+ return this.executeAndFetchById('update', ctx, modify)
262
+ },
263
+
264
+ async patch(ctx, modify) {
265
+ return this.executeAndFetchById('patch', ctx, modify)
266
+ }
267
+ })
268
+ }
269
+
270
+ function getModify(modify, trx) {
271
+ return modify
272
+ ? query => modify(query, trx)
273
+ : null
274
+ }