@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,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
+ }
@@ -0,0 +1,2 @@
1
+ export * from './Application.js'
2
+ export * from './Validator.js'
@@ -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
+ }