@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,405 @@
|
|
|
1
|
+
import objection from 'objection'
|
|
2
|
+
import Ajv from 'ajv/dist/2020.js'
|
|
3
|
+
import addFormats from 'ajv-formats'
|
|
4
|
+
import { isArray, isObject, clone, isAsync, isPromise } from '@ditojs/utils'
|
|
5
|
+
import { formatJson } from '../utils/json.js'
|
|
6
|
+
import {
|
|
7
|
+
keywords as defaultKeywords,
|
|
8
|
+
formats as defaultFormats,
|
|
9
|
+
types as defaultTypes,
|
|
10
|
+
convertSchema
|
|
11
|
+
} from '../schema/index.js'
|
|
12
|
+
|
|
13
|
+
// Dito.js does not rely on objection.AjvValidator but instead implements its
|
|
14
|
+
// own validator instance that is shared across the whole app and handles schema
|
|
15
|
+
// compilation and caching differently:
|
|
16
|
+
// It relies on Ajv's addSchema() / getSchema() pattern in conjunction with the
|
|
17
|
+
// `schemaId: '$id'` option, and each schema is assigned an $id based on the
|
|
18
|
+
// model class name. That way, schemas can reference each other, and they can
|
|
19
|
+
// easily validate nested structures.
|
|
20
|
+
|
|
21
|
+
export class Validator extends objection.Validator {
|
|
22
|
+
constructor({ options, keywords, formats, types } = {}) {
|
|
23
|
+
super()
|
|
24
|
+
|
|
25
|
+
this.options = {
|
|
26
|
+
...defaultOptions,
|
|
27
|
+
...options
|
|
28
|
+
}
|
|
29
|
+
this.keywords = {
|
|
30
|
+
...defaultKeywords,
|
|
31
|
+
...keywords
|
|
32
|
+
}
|
|
33
|
+
this.formats = {
|
|
34
|
+
...defaultFormats,
|
|
35
|
+
...formats
|
|
36
|
+
}
|
|
37
|
+
this.types = {
|
|
38
|
+
...defaultTypes,
|
|
39
|
+
...types
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.schemas = []
|
|
43
|
+
|
|
44
|
+
// Dictionary to hold all created Ajv instances, using the stringified
|
|
45
|
+
// options with which they were created as their keys.
|
|
46
|
+
this.ajvCache = Object.create(null)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
createAjv(options) {
|
|
50
|
+
// Split options into native Ajv options and our additions (async, patch):
|
|
51
|
+
const { async, patch, ...opts } = options
|
|
52
|
+
const ajv = new Ajv({
|
|
53
|
+
...this.options,
|
|
54
|
+
...opts,
|
|
55
|
+
// Patch-validators don't use default values:
|
|
56
|
+
...(patch && { useDefaults: false })
|
|
57
|
+
})
|
|
58
|
+
addFormats(ajv, { mode: 'full' })
|
|
59
|
+
|
|
60
|
+
const addSchemas = (schemas, callback) => {
|
|
61
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
62
|
+
if (schema) {
|
|
63
|
+
// Remove leading '_' to simplify special keywords (e.g. instanceof)
|
|
64
|
+
callback(name.replace(/^_/, ''), schema)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
addSchemas(this.keywords, (keyword, schema) =>
|
|
70
|
+
ajv.addKeyword({
|
|
71
|
+
keyword,
|
|
72
|
+
...schema
|
|
73
|
+
})
|
|
74
|
+
)
|
|
75
|
+
addSchemas(this.formats, (format, schema) =>
|
|
76
|
+
ajv.addFormat(format, {
|
|
77
|
+
...schema
|
|
78
|
+
})
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
addSchemas(this.types, (type, schema) => {
|
|
82
|
+
ajv.addSchema({
|
|
83
|
+
$id: type,
|
|
84
|
+
...this.processSchema(convertSchema(schema), options)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
// Also add all model schemas that were already compiled so far.
|
|
89
|
+
for (const schema of this.schemas) {
|
|
90
|
+
ajv.addSchema(this.processSchema(schema, options))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return ajv
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
getAjv(options = {}) {
|
|
97
|
+
// Cache Ajv instances by keys that represent their options. For improved
|
|
98
|
+
// matching, convert options to a version with all default values missing:
|
|
99
|
+
const filteredOptions = Object.fromEntries(
|
|
100
|
+
Object.entries(options).filter(
|
|
101
|
+
([key, value]) => (
|
|
102
|
+
key in validatorOptions && value !== validatorOptions[key]
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
const cacheKey = formatJson(filteredOptions, false)
|
|
107
|
+
const { ajv } = (this.ajvCache[cacheKey] ??= {
|
|
108
|
+
ajv: this.createAjv(filteredOptions),
|
|
109
|
+
options: filteredOptions
|
|
110
|
+
})
|
|
111
|
+
return ajv
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
compile(jsonSchema, options = {}) {
|
|
115
|
+
const ajv = this.getAjv(options)
|
|
116
|
+
const validator = ajv.compile(this.processSchema(jsonSchema, options))
|
|
117
|
+
// Assume `options.throw = true` as the default.
|
|
118
|
+
const dontThrow = options.throw === false
|
|
119
|
+
return options.async
|
|
120
|
+
? // For async:
|
|
121
|
+
dontThrow
|
|
122
|
+
? async function validate(data) {
|
|
123
|
+
// Emulate `options.throw == false` behavior for async validation:
|
|
124
|
+
// Return `true` or `false`, and store errors on `validate.errors`
|
|
125
|
+
// if validation failed.
|
|
126
|
+
let result
|
|
127
|
+
try {
|
|
128
|
+
// Use `call()` to pass `this` as context to Ajv, see passContext:
|
|
129
|
+
result = await validator.call(this, data)
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (error.errors) {
|
|
132
|
+
validate.errors = error.errors
|
|
133
|
+
result = false
|
|
134
|
+
} else {
|
|
135
|
+
throw error
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return result
|
|
139
|
+
}
|
|
140
|
+
: validator // The default for async is to throw.
|
|
141
|
+
: // For sync:
|
|
142
|
+
dontThrow
|
|
143
|
+
? validator // The default for sync is to not throw.
|
|
144
|
+
: function (data) {
|
|
145
|
+
// Emulate `options.throw == true` behavior for sync validation:
|
|
146
|
+
// Return `true` if successful, throw `Ajv.ValidationError`
|
|
147
|
+
// otherwise. Use `call()` to pass `this` as context to Ajv,
|
|
148
|
+
// see `passContext`:
|
|
149
|
+
const result = validator.call(this, data)
|
|
150
|
+
if (!result) {
|
|
151
|
+
throw new Ajv.ValidationError(validator.errors)
|
|
152
|
+
}
|
|
153
|
+
return result
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
getKeyword(keyword) {
|
|
158
|
+
return this.keywords[keyword]
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
getFormat(format) {
|
|
162
|
+
return this.formats[format]
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
addSchema(jsonSchema) {
|
|
166
|
+
this.schemas.push(jsonSchema)
|
|
167
|
+
// Add schema to all previously created Ajv instances, so they can reference
|
|
168
|
+
// the models.
|
|
169
|
+
for (const { ajv, options } of Object.values(this.ajvCache)) {
|
|
170
|
+
ajv.addSchema(this.processSchema(jsonSchema, options))
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
processSchema(jsonSchema, options = {}) {
|
|
175
|
+
const { patch, async } = options
|
|
176
|
+
const schema = clone(jsonSchema, {
|
|
177
|
+
processValue: value => {
|
|
178
|
+
if (isObject(value)) {
|
|
179
|
+
if (patch) {
|
|
180
|
+
// Remove all required keywords and formats from schema for patch
|
|
181
|
+
// validation.
|
|
182
|
+
delete value.required
|
|
183
|
+
if (value.format === 'required') {
|
|
184
|
+
delete value.format
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Convert async `validate()` keywords to `validateAsync()`:
|
|
188
|
+
if (isAsync(value.validate)) {
|
|
189
|
+
value.validateAsync = value.validate
|
|
190
|
+
delete value.validate
|
|
191
|
+
}
|
|
192
|
+
if (!async) {
|
|
193
|
+
// Remove all async keywords for synchronous validation.
|
|
194
|
+
for (const key in value) {
|
|
195
|
+
const keyword = this.getKeyword(key)
|
|
196
|
+
if (keyword?.async) {
|
|
197
|
+
delete value[key]
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
if (async) {
|
|
205
|
+
schema.$async = true
|
|
206
|
+
for (const definition of Object.values(schema.definitions || {})) {
|
|
207
|
+
definition.$async = true
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return schema
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
parseErrors(errors, options = {}) {
|
|
214
|
+
// Convert from Ajv errors array to Objection-style errorHash,
|
|
215
|
+
// with additional parsing and processing.
|
|
216
|
+
const errorHash = {}
|
|
217
|
+
const duplicates = {}
|
|
218
|
+
for (const error of errors) {
|
|
219
|
+
// Adjust dataPaths to reflect nested validation in Objection.
|
|
220
|
+
// NOTE: As of Ajv 8, `error.dataPath` is now called `error.instancePath`,
|
|
221
|
+
// but we stick to `error.dataPath` in Dito.js, and support both in errors
|
|
222
|
+
// passed in here.
|
|
223
|
+
const instancePath = (error.instancePath ?? error.dataPath) || ''
|
|
224
|
+
const dataPath = `${options?.dataPath || ''}${instancePath}`
|
|
225
|
+
// Unknown properties are reported in `['propertyName']` notation,
|
|
226
|
+
// so replace those with dot-notation, see:
|
|
227
|
+
// https://github.com/epoberezkin/ajv/issues/671
|
|
228
|
+
const key = dataPath.replace(/\['([^']*)'\]/g, '/$1').slice(1)
|
|
229
|
+
const { message, keyword, params } = error
|
|
230
|
+
const definition =
|
|
231
|
+
keyword === 'format'
|
|
232
|
+
? this.getFormat(params.format)
|
|
233
|
+
: this.getKeyword(keyword)
|
|
234
|
+
const identifier = `${key}_${keyword}`
|
|
235
|
+
if (
|
|
236
|
+
// Ajv produces duplicate validation errors sometimes, filter them out.
|
|
237
|
+
!duplicates[identifier] &&
|
|
238
|
+
// Skip keywords that start with $ such as $merge and $patch.
|
|
239
|
+
!/^\$/.test(keyword) &&
|
|
240
|
+
// Skip macro keywords that are only delegating to other keywords.
|
|
241
|
+
!definition?.macro &&
|
|
242
|
+
// Filter out all custom keywords and formats that want to be silent.
|
|
243
|
+
!definition?.silent
|
|
244
|
+
) {
|
|
245
|
+
const array = errorHash[key] || (errorHash[key] = [])
|
|
246
|
+
array.push({
|
|
247
|
+
// Allow custom formats and keywords to override error messages.
|
|
248
|
+
message: definition?.message || message,
|
|
249
|
+
keyword,
|
|
250
|
+
params
|
|
251
|
+
})
|
|
252
|
+
duplicates[identifier] = true
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Now filter through the resulting errors and perform some post-processing:
|
|
256
|
+
for (const [dataPath, errors] of Object.entries(errorHash)) {
|
|
257
|
+
// When we get these errors (caused by `nullable: true`), it means that we
|
|
258
|
+
// have data that isn't null and is causing other errors, and we shouldn't
|
|
259
|
+
// display the 'null' path:
|
|
260
|
+
// [dataPath]: [
|
|
261
|
+
// {
|
|
262
|
+
// message: 'should be null',
|
|
263
|
+
// keyword: 'type',
|
|
264
|
+
// params: { type: 'null' }
|
|
265
|
+
// },
|
|
266
|
+
// {
|
|
267
|
+
// message: 'should match exactly one schema in oneOf',
|
|
268
|
+
// keyword: 'oneOf'
|
|
269
|
+
// }
|
|
270
|
+
// ]
|
|
271
|
+
if (errors.length === 2) {
|
|
272
|
+
const [error1, error2] = errors
|
|
273
|
+
if (
|
|
274
|
+
error1.keyword === 'type' &&
|
|
275
|
+
error1.params.type === 'null' &&
|
|
276
|
+
error2.keyword === 'oneOf'
|
|
277
|
+
) {
|
|
278
|
+
delete errorHash[dataPath]
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return errorHash
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
prefixInstancePaths(errors, instancePathPrefix) {
|
|
286
|
+
// As of Ajv 8, `error.dataPath` is now called `error.instancePath`. In
|
|
287
|
+
// Dito.js we stick to `error.dataPath`, but until the errors pass through
|
|
288
|
+
// `parseErrors()`, we stick to `error.instancePath` for consistency.
|
|
289
|
+
return errors.map(error => ({
|
|
290
|
+
...error,
|
|
291
|
+
instancePath: error.instancePath
|
|
292
|
+
? `${instancePathPrefix}${error.instancePath}`
|
|
293
|
+
: instancePathPrefix
|
|
294
|
+
}))
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// @override
|
|
298
|
+
beforeValidate({ json, model, ctx, options }) {
|
|
299
|
+
// Add validator instance, app and options to context
|
|
300
|
+
ctx.validator = this
|
|
301
|
+
ctx.app = this.app
|
|
302
|
+
ctx.options = options
|
|
303
|
+
ctx.jsonSchema = model.constructor.getJsonSchema()
|
|
304
|
+
const { $beforeValidate } = model
|
|
305
|
+
if ($beforeValidate !== objection.Model.prototype.$beforeValidate) {
|
|
306
|
+
// TODO: Consider adding hooks for 'before:validate' and 'after:validate',
|
|
307
|
+
// and if so, decide what to do about modifying / returning json-schema.
|
|
308
|
+
// Probably we shouldn't allow it there.
|
|
309
|
+
|
|
310
|
+
// Only clone jsonSchema if the handler actually receives it.
|
|
311
|
+
if ($beforeValidate.length > 0) {
|
|
312
|
+
ctx.jsonSchema = clone(ctx.jsonSchema)
|
|
313
|
+
}
|
|
314
|
+
const ret = model.$beforeValidate(ctx.jsonSchema, json, options)
|
|
315
|
+
if (ret !== undefined) {
|
|
316
|
+
ctx.jsonSchema = ret
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// @override
|
|
322
|
+
validate({ json, model, ctx, options }) {
|
|
323
|
+
const { jsonSchema } = ctx
|
|
324
|
+
if (jsonSchema) {
|
|
325
|
+
// We need to clone the input json if we are about to set default values.
|
|
326
|
+
if (
|
|
327
|
+
!options.mutable &&
|
|
328
|
+
!options.patch &&
|
|
329
|
+
hasDefaults(jsonSchema.properties)
|
|
330
|
+
) {
|
|
331
|
+
json = clone(json)
|
|
332
|
+
}
|
|
333
|
+
// Get the right Ajv instance for the given patch and async options
|
|
334
|
+
const validate = this.getAjv(options).getSchema(jsonSchema.$id)
|
|
335
|
+
// Use `call()` to pass ctx as context to Ajv, see passContext:
|
|
336
|
+
const res = validate.call(ctx, json)
|
|
337
|
+
const handleErrors = errors => {
|
|
338
|
+
if (errors) {
|
|
339
|
+
// NOTE: The conversion from Ajv errors to Objection errors happen in
|
|
340
|
+
// Model.createValidationError(), which calls Validator.parseError()
|
|
341
|
+
throw model.constructor.createValidationError({
|
|
342
|
+
type: 'ModelValidation',
|
|
343
|
+
errors,
|
|
344
|
+
options,
|
|
345
|
+
json
|
|
346
|
+
})
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
// Handle both async and sync validation here:
|
|
350
|
+
if (isPromise(res)) {
|
|
351
|
+
return res
|
|
352
|
+
.catch(error => handleErrors(error.errors))
|
|
353
|
+
.then(() => json)
|
|
354
|
+
} else {
|
|
355
|
+
handleErrors(validate.errors)
|
|
356
|
+
return json
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function hasDefaults(obj) {
|
|
363
|
+
if (isArray(obj)) {
|
|
364
|
+
for (const val of obj) {
|
|
365
|
+
if (val && hasDefaults(val)) {
|
|
366
|
+
return true
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
} else if (isObject(obj)) {
|
|
370
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
371
|
+
if (key === 'default' || val && hasDefaults(val)) {
|
|
372
|
+
return true
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return false
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const defaultOptions = {
|
|
380
|
+
strict: false,
|
|
381
|
+
allErrors: true,
|
|
382
|
+
ownProperties: true,
|
|
383
|
+
passContext: true,
|
|
384
|
+
useDefaults: true,
|
|
385
|
+
validateSchema: true
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Options that are allowed to be passed on to the created Ajv instances, with
|
|
389
|
+
// their default settings (some defaults are from Ajv, some from defaultOptions)
|
|
390
|
+
const validatorOptions = {
|
|
391
|
+
// Our extensions:
|
|
392
|
+
async: false,
|
|
393
|
+
patch: false,
|
|
394
|
+
// Ajv Options:
|
|
395
|
+
$data: false,
|
|
396
|
+
$comment: false,
|
|
397
|
+
coerceTypes: false,
|
|
398
|
+
discriminator: true,
|
|
399
|
+
multipleOfPrecision: false,
|
|
400
|
+
ownProperties: true,
|
|
401
|
+
removeAdditional: false,
|
|
402
|
+
uniqueItems: true,
|
|
403
|
+
useDefaults: true,
|
|
404
|
+
verbose: false
|
|
405
|
+
}
|
package/src/app/index.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import repl from 'repl'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import fs from 'fs/promises'
|
|
4
|
+
import pico from 'picocolors'
|
|
5
|
+
import objection from 'objection'
|
|
6
|
+
import { isFunction, deindent } from '@ditojs/utils'
|
|
7
|
+
|
|
8
|
+
let started = false
|
|
9
|
+
let server = null
|
|
10
|
+
|
|
11
|
+
export default async function startConsole(app, config) {
|
|
12
|
+
config = {
|
|
13
|
+
quiet: false,
|
|
14
|
+
prompt: 'dito > ',
|
|
15
|
+
useColors: true,
|
|
16
|
+
ignoreUndefined: true,
|
|
17
|
+
...config
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (started) {
|
|
21
|
+
return server
|
|
22
|
+
}
|
|
23
|
+
started = true
|
|
24
|
+
|
|
25
|
+
server = repl.start({ ...config, prompt: '' })
|
|
26
|
+
server.pause()
|
|
27
|
+
Object.assign(server.context, {
|
|
28
|
+
app,
|
|
29
|
+
objection,
|
|
30
|
+
...app.models
|
|
31
|
+
})
|
|
32
|
+
server.eval = wrapEval(server)
|
|
33
|
+
|
|
34
|
+
server.defineCommand('usage', {
|
|
35
|
+
help: 'Detailed Dito.js Console usage information',
|
|
36
|
+
action() {
|
|
37
|
+
displayUsage(app, config, true)
|
|
38
|
+
this.displayPrompt()
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
server.defineCommand('models', {
|
|
43
|
+
help: 'Display available Dito.js models',
|
|
44
|
+
action() {
|
|
45
|
+
console.info(Object.keys(app.models).join(', '))
|
|
46
|
+
this.displayPrompt()
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// Set up history file
|
|
51
|
+
const historyFile = path.join(process.cwd(), '.console_history')
|
|
52
|
+
try {
|
|
53
|
+
await fs.stat(historyFile)
|
|
54
|
+
const lines = await fs.readFile(historyFile)
|
|
55
|
+
lines
|
|
56
|
+
.toString()
|
|
57
|
+
.split('\n')
|
|
58
|
+
.reverse()
|
|
59
|
+
.slice(0, config.historySize)
|
|
60
|
+
.filter(line => line.trim())
|
|
61
|
+
.map(line => server.history.push(line))
|
|
62
|
+
} catch {
|
|
63
|
+
console.info(deindent`
|
|
64
|
+
Unable to REPL history file at ${historyFile}.
|
|
65
|
+
A history file will be created on shutdown
|
|
66
|
+
`)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await app.start()
|
|
70
|
+
if (!config.quiet) {
|
|
71
|
+
displayUsage(app, config)
|
|
72
|
+
}
|
|
73
|
+
server.setPrompt(config.prompt)
|
|
74
|
+
server.resume()
|
|
75
|
+
server.write('', { name: 'return' })
|
|
76
|
+
return new Promise(resolve => {
|
|
77
|
+
server.once('exit', async () => {
|
|
78
|
+
try {
|
|
79
|
+
await app.stop()
|
|
80
|
+
} catch (err) {
|
|
81
|
+
logError(err)
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const lines = (server.history || [])
|
|
85
|
+
.reverse()
|
|
86
|
+
.filter(line => line.trim())
|
|
87
|
+
.join('\n')
|
|
88
|
+
await fs.writeFile(historyFile, lines)
|
|
89
|
+
} catch (err) {
|
|
90
|
+
logError(err)
|
|
91
|
+
}
|
|
92
|
+
resolve(true)
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function displayUsage(app, config, details) {
|
|
98
|
+
const modelHandleNames = Object.keys(app.models)
|
|
99
|
+
console.info(deindent`
|
|
100
|
+
|
|
101
|
+
------------------------------------------------------------
|
|
102
|
+
Dito.js Console
|
|
103
|
+
|
|
104
|
+
Available references:
|
|
105
|
+
- Dito.js app: ${pico.cyan('app')}
|
|
106
|
+
${
|
|
107
|
+
modelHandleNames.length > 0
|
|
108
|
+
? ` - Dito.js models: ${
|
|
109
|
+
modelHandleNames.map(m => pico.cyan(m)).join(', ')
|
|
110
|
+
}`
|
|
111
|
+
: ''
|
|
112
|
+
}
|
|
113
|
+
`)
|
|
114
|
+
if (details) {
|
|
115
|
+
console.info(deindent`
|
|
116
|
+
Examples:
|
|
117
|
+
|
|
118
|
+
${config.prompt} user = User.where({ lastName: 'Doe' }).first()
|
|
119
|
+
${config.prompt} user.$patch({ firstName: 'Joe' })
|
|
120
|
+
${config.prompt} user.$comments.insert({ ... })
|
|
121
|
+
`)
|
|
122
|
+
}
|
|
123
|
+
console.info('------------------------------------------------------------')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Wraps the default eval with a handler that resolves promises
|
|
127
|
+
function wrapEval({ eval: defaultEval }) {
|
|
128
|
+
return async function (code, context, file, cb) {
|
|
129
|
+
return defaultEval.call(this, code, context, file, async (err, result) => {
|
|
130
|
+
if (err || !(result && isFunction(result.then))) {
|
|
131
|
+
return cb(err, result)
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
const resolved = await result
|
|
135
|
+
// Replace any promises in the REPL context with the resolved promise.
|
|
136
|
+
for (const key in context) {
|
|
137
|
+
if (context[key] === result) {
|
|
138
|
+
context[key] = resolved
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
cb(null, resolved)
|
|
142
|
+
} catch (error) {
|
|
143
|
+
logError(error)
|
|
144
|
+
cb() // Application errors are not REPL errors
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function logError(error) {
|
|
151
|
+
console.info(`\x1b[31m${error}\x1b[0m`)
|
|
152
|
+
}
|