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