@ditojs/server 2.47.0 → 2.49.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ditojs/server",
3
- "version": "2.47.0",
3
+ "version": "2.49.0",
4
4
  "type": "module",
5
5
  "description": "Dito.js Server – Dito.js is a declarative and modern web framework, based on Objection.js, Koa.js and Vue.js",
6
6
  "repository": "https://github.com/ditojs/dito/tree/master/packages/server",
@@ -25,15 +25,15 @@
25
25
  "node >= 18"
26
26
  ],
27
27
  "dependencies": {
28
- "@ditojs/admin": "^2.47.0",
29
- "@ditojs/build": "^2.47.0",
30
- "@ditojs/router": "^2.47.0",
31
- "@ditojs/utils": "^2.47.0",
28
+ "@ditojs/admin": "^2.49.0",
29
+ "@ditojs/build": "^2.49.0",
30
+ "@ditojs/router": "^2.49.0",
31
+ "@ditojs/utils": "^2.49.0",
32
32
  "@koa/cors": "^5.0.0",
33
33
  "@koa/multer": "^3.1.0",
34
34
  "@originjs/vite-plugin-commonjs": "^1.0.3",
35
35
  "ajv": "^8.17.1",
36
- "ajv-formats": "^2.1.1",
36
+ "ajv-formats": "^3.0.1",
37
37
  "bcryptjs": "^3.0.2",
38
38
  "bytes": "^3.1.2",
39
39
  "data-uri-to-buffer": "^6.0.2",
@@ -66,7 +66,7 @@
66
66
  "pino-pretty": "^13.0.0",
67
67
  "pluralize": "^8.0.0",
68
68
  "repl": "^0.1.3",
69
- "type-fest": "^4.40.1",
69
+ "type-fest": "^4.41.0",
70
70
  "uuid": "^11.1.0"
71
71
  },
72
72
  "peerDependencies": {
@@ -84,11 +84,11 @@
84
84
  "@types/koa-session": "^6.4.5",
85
85
  "@types/koa-static": "^4.0.4",
86
86
  "@types/koa__cors": "^5.0.0",
87
- "@types/node": "^22.15.3",
87
+ "@types/node": "^22.15.17",
88
88
  "knex": "^3.1.0",
89
89
  "objection": "^3.1.5",
90
90
  "typescript": "^5.8.3"
91
91
  },
92
92
  "types": "types",
93
- "gitHead": "9ce26a1dc412e67304e823c0206593ee4a9c8ae4"
93
+ "gitHead": "8276ff1db64a82c03e1569e2403f378b0201ea3c"
94
94
  }
@@ -31,8 +31,7 @@ import {
31
31
  parseDataPath,
32
32
  normalizeDataPath,
33
33
  toPromiseCallback,
34
- mapConcurrently,
35
- deprecate
34
+ mapConcurrently
36
35
  } from '@ditojs/utils'
37
36
  import { Validator } from './Validator.js'
38
37
  import { EventEmitter } from '../lib/index.js'
@@ -879,11 +878,6 @@ export class Application extends Koa {
879
878
  }
880
879
  }
881
880
 
882
- startOrExit() {
883
- deprecate(`app.startOrExit() is deprecated. Use app.execute() instead.`)
884
- return this.execute()
885
- }
886
-
887
881
  // Assets handling
888
882
 
889
883
  async createAssets(storage, files, count = 0, transaction = null) {
@@ -3,10 +3,15 @@ import Ajv from 'ajv/dist/2020.js'
3
3
  import addFormats from 'ajv-formats'
4
4
  import { isArray, isObject, clone, isAsync, isPromise } from '@ditojs/utils'
5
5
  import { formatJson } from '../utils/json.js'
6
- import * as schema from '../schema/index.js'
6
+ import {
7
+ keywords as defaultKeywords,
8
+ formats as defaultFormats,
9
+ types as defaultTypes,
10
+ convertSchema
11
+ } from '../schema/index.js'
7
12
 
8
- // Dito does not rely on objection.AjvValidator but instead implements its own
9
- // validator instance that is shared across the whole app and handles schema
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
10
15
  // compilation and caching differently:
11
16
  // It relies on Ajv's addSchema() / getSchema() pattern in conjunction with the
12
17
  // `schemaId: '$id'` option, and each schema is assigned an $id based on the
@@ -14,23 +19,25 @@ import * as schema from '../schema/index.js'
14
19
  // easily validate nested structures.
15
20
 
16
21
  export class Validator extends objection.Validator {
17
- constructor({ options, keywords, formats } = {}) {
22
+ constructor({ options, keywords, formats, types } = {}) {
18
23
  super()
19
24
 
20
25
  this.options = {
21
26
  ...defaultOptions,
22
27
  ...options
23
28
  }
24
-
25
29
  this.keywords = {
26
- ...schema.keywords,
30
+ ...defaultKeywords,
27
31
  ...keywords
28
32
  }
29
-
30
33
  this.formats = {
31
- ...schema.formats,
34
+ ...defaultFormats,
32
35
  ...formats
33
36
  }
37
+ this.types = {
38
+ ...defaultTypes,
39
+ ...types
40
+ }
34
41
 
35
42
  this.schemas = []
36
43
 
@@ -71,6 +78,13 @@ export class Validator extends objection.Validator {
71
78
  })
72
79
  )
73
80
 
81
+ addSchemas(this.types, (type, schema) => {
82
+ ajv.addSchema({
83
+ $id: type,
84
+ ...this.processSchema(convertSchema(schema), options)
85
+ })
86
+ })
87
+
74
88
  // Also add all model schemas that were already compiled so far.
75
89
  for (const schema of this.schemas) {
76
90
  ajv.addSchema(this.processSchema(schema, options))
@@ -32,7 +32,7 @@ export default async function startConsole(app, config) {
32
32
  server.eval = wrapEval(server)
33
33
 
34
34
  server.defineCommand('usage', {
35
- help: 'Detailed Dito Console usage information',
35
+ help: 'Detailed Dito.js Console usage information',
36
36
  action() {
37
37
  displayUsage(app, config, true)
38
38
  this.displayPrompt()
@@ -40,7 +40,7 @@ export default async function startConsole(app, config) {
40
40
  })
41
41
 
42
42
  server.defineCommand('models', {
43
- help: 'Display available Dito models',
43
+ help: 'Display available Dito.js models',
44
44
  action() {
45
45
  console.info(Object.keys(app.models).join(', '))
46
46
  this.displayPrompt()
@@ -59,7 +59,7 @@ export default async function startConsole(app, config) {
59
59
  .slice(0, config.historySize)
60
60
  .filter(line => line.trim())
61
61
  .map(line => server.history.push(line))
62
- } catch (e) {
62
+ } catch {
63
63
  console.info(deindent`
64
64
  Unable to REPL history file at ${historyFile}.
65
65
  A history file will be created on shutdown
@@ -99,13 +99,13 @@ function displayUsage(app, config, details) {
99
99
  console.info(deindent`
100
100
 
101
101
  ------------------------------------------------------------
102
- Dito Console
102
+ Dito.js Console
103
103
 
104
104
  Available references:
105
- - Dito app: ${pico.cyan('app')}
105
+ - Dito.js app: ${pico.cyan('app')}
106
106
  ${
107
107
  modelHandleNames.length > 0
108
- ? ` - Dito models: ${
108
+ ? ` - Dito.js models: ${
109
109
  modelHandleNames.map(m => pico.cyan(m)).join(', ')
110
110
  }`
111
111
  : ''
@@ -182,19 +182,7 @@ export class CollectionController extends Controller {
182
182
  )
183
183
  }
184
184
 
185
- convertToCoreActions(actions) {
186
- // Mark action object and methods as core, so `Controller.processValues()`
187
- // can filter correctly.
188
- for (const action of Object.values(actions)) {
189
- // Mark action functions also, so ControllerAction can use it to determine
190
- // value for `transacted`.
191
- action.core = true
192
- }
193
- actions.$core = true
194
- return actions
195
- }
196
-
197
- collection = this.convertToCoreActions({
185
+ collection = this.markAsCoreActions({
198
186
  async get(ctx, modify) {
199
187
  const result = await this.execute(ctx, (query, trx) => {
200
188
  query
@@ -245,7 +233,7 @@ export class CollectionController extends Controller {
245
233
  }
246
234
  })
247
235
 
248
- member = this.convertToCoreActions({
236
+ member = this.markAsCoreActions({
249
237
  async get(ctx, modify) {
250
238
  return this.execute(ctx, (query, trx) =>
251
239
  query
@@ -32,8 +32,8 @@ export class Controller {
32
32
  name = null
33
33
  path = null
34
34
  url = null
35
- actions = null
36
35
  assets = null
36
+ actions = null
37
37
  transacted = null
38
38
  initialized = false
39
39
 
@@ -80,7 +80,6 @@ export class Controller {
80
80
  // @overridable
81
81
  setup() {
82
82
  this.logController()
83
- this.setProperty('actions', this.actions || this.reflectActionsObject())
84
83
  // Now that the instance fields are reflected in the `controller` object
85
84
  // we can use the normal inheritance mechanism through `setupActions()`:
86
85
  this.setProperty('actions', this.setupActions('actions'))
@@ -142,30 +141,15 @@ export class Controller {
142
141
  return value
143
142
  }
144
143
 
145
- reflectActionsObject() {
146
- // On base controllers, the actions can be defined directly in the class
147
- // instead of inside an actions object, as is done with model and relation
148
- // controllers. But in order to use the same structure for inheritance as
149
- // these other controllers, we reflect these instance fields in a separate
150
- // `actions` object.
151
- const { allow } = this
152
- const actions = allow ? { allow } : {}
153
-
154
- const addAction = key => {
155
- const value = this[key]
156
- // NOTE: Only add instance methods that have a @action() decorator, which
157
- // in turn sets the `method` property on the method, as well as action
158
- // objects which provide the `method` property:
159
- if (value?.method) {
160
- actions[key] = value
161
- }
144
+ markAsCoreActions(actions) {
145
+ // Mark action object and methods as core, so `Controller.processValues()`
146
+ // can filter correctly.
147
+ for (const action of Object.values(actions)) {
148
+ // Mark action functions also, so ControllerAction can use it to determine
149
+ // value for `transacted`.
150
+ action.core = true
162
151
  }
163
- // Use `Object.getOwnPropertyNames()` to get the fields, in order to
164
- // not also receive values from parents (those are fetched later in
165
- // `inheritValues()`, see `getParentValues()`).
166
- const proto = Object.getPrototypeOf(this)
167
- Object.getOwnPropertyNames(proto).forEach(addAction)
168
- Object.getOwnPropertyNames(this).forEach(addAction)
152
+ actions.$core = true
169
153
  return actions
170
154
  }
171
155
 
@@ -190,26 +174,28 @@ export class Controller {
190
174
  values: actions,
191
175
  authorize
192
176
  } = this.processValues(this.inheritValues(type))
193
- for (const [name, action] of Object.entries(actions)) {
194
- // Replace the action object with the converted action handler, so they
195
- // too can benefit from prototypal inheritance:
196
- actions[name] = this.setupAction(
197
- type,
198
- actions,
199
- name,
200
- action,
201
- authorize[name]
202
- )
177
+ if (actions) {
178
+ for (const [name, action] of Object.entries(actions)) {
179
+ // Replace the action object with the converted action handler, so they
180
+ // too can benefit from prototypal inheritance:
181
+ actions[name] = this.setupAction(
182
+ type,
183
+ actions,
184
+ name,
185
+ action,
186
+ authorize[name]
187
+ )
188
+ }
189
+ // Expose a direct reference to the controller on the action object, but
190
+ // also make it inherit from the controller so that all its public fields
191
+ // and functions (`app`, `query()`, `execute()`, etc.) can be accessed
192
+ // directly through `this` from actions.
193
+ // NOTE: Inheritance is also set up by `inheritValues()` so that from the
194
+ // handlers, `super` points to the parent controller's actions object, so
195
+ // that calling `super.patch()` from a patch handler magically works.
196
+ actions.controller = this
197
+ Object.setPrototypeOf(actions, this)
203
198
  }
204
- // Expose a direct reference to the controller on the action object, but
205
- // also make it inherit from the controller so that all its public fields
206
- // and functions (`app`, `query()`, `execute()`, etc.) can be accessed
207
- // directly through `this` from actions.
208
- // NOTE: Inheritance is also set up by `inheritValues()` so that from inside
209
- // the handlers, `super` points to the parent controller's actions object,
210
- // so that calling `super.patch()` from a patch handler magically works.
211
- actions.controller = this
212
- Object.setPrototypeOf(actions, this)
213
199
  return actions
214
200
  }
215
201
 
@@ -7,18 +7,12 @@ export default class ControllerAction {
7
7
  handler,
8
8
  type,
9
9
  name,
10
- _method,
11
- _path,
10
+ method,
11
+ path,
12
12
  _authorize
13
13
  ) {
14
14
  const {
15
15
  core = false,
16
- // Allow decorators on actions to override the predetermined defaults for
17
- // `method`, `path` and `authorize`:
18
- // TODO: `handler.method` and `handler.path` were deprecated in March
19
- // 2022, remove later and only set the valued passed to constructor then.
20
- method = _method,
21
- path = _path,
22
16
  scope,
23
17
  authorize,
24
18
  transacted,
@@ -28,6 +22,7 @@ export default class ControllerAction {
28
22
  ...additional
29
23
  } = handler
30
24
 
25
+ this.app = controller.app
31
26
  this.controller = controller
32
27
  this.actions = actions
33
28
  this.handler = handler
@@ -37,6 +32,8 @@ export default class ControllerAction {
37
32
  this.method = method
38
33
  this.path = path
39
34
  this.scope = scope
35
+ // Allow action handlers to override the predetermined defaults for
36
+ // `authorize`:
40
37
  this.authorize = authorize || _authorize
41
38
  this.transacted = !!(
42
39
  transacted ??
@@ -52,7 +49,6 @@ export default class ControllerAction {
52
49
  )
53
50
  )
54
51
  this.authorization = controller.processAuthorize(this.authorize)
55
- this.app = controller.app
56
52
  this.paramsName = ['post', 'put', 'patch'].includes(this.method)
57
53
  ? 'body'
58
54
  : 'query'
@@ -304,7 +300,7 @@ export default class ControllerAction {
304
300
  if (objectType) {
305
301
  if (value && isString(value)) {
306
302
  if (!/^\{.*\}$/.test(value)) {
307
- // Convert simplified Dito object notation to JSON, supporting:
303
+ // Convert simplified Dito.js object notation to JSON, supporting:
308
304
  // - `"key1":X, "key2":Y` (curly braces are added and parsed through
309
305
  // `JSON.parse()`)
310
306
  // - `key1:X,key2:Y` (a simple parser is applied, splitting into
@@ -333,7 +329,7 @@ export default class ControllerAction {
333
329
  }
334
330
  }
335
331
  if (objectType !== 'object' && isObject(value)) {
336
- // Convert the Pojo to the desired Dito model:
332
+ // Convert the Pojo to the desired Dito.js model:
337
333
  const modelClass = this.app.models[objectType]
338
334
  if (modelClass && !(value instanceof modelClass)) {
339
335
  value = modelClass.fromJson(value, modelOptions)
@@ -87,7 +87,7 @@ export class RelationController extends CollectionController {
87
87
  })
88
88
  }
89
89
 
90
- collection = this.convertToCoreActions({})
90
+ collection = this.markAsCoreActions({})
91
91
 
92
- member = this.convertToCoreActions({})
92
+ member = this.markAsCoreActions({})
93
93
  }
package/src/index.js CHANGED
@@ -6,5 +6,4 @@ export * from './mixins/index.js'
6
6
  export * from './models/index.js'
7
7
  export * from './controllers/index.js'
8
8
  export * from './services/index.js'
9
- export * from './decorators/index.js'
10
9
  export * from './storage/index.js'
@@ -11,7 +11,6 @@ export function handleSession(app, {
11
11
  // Create a ContextStore that resolved the specified model class,
12
12
  // uses it to persist and retrieve the session, and automatically
13
13
  // binds all db operations to `ctx.transaction`, if it is set.
14
- // eslint-disable-next-line new-cap
15
14
  options.ContextStore = createSessionStore(modelClass)
16
15
  }
17
16
  options.autoCommit = false
@@ -343,7 +343,7 @@ export class Model extends objection.Model {
343
343
  },
344
344
  {}
345
345
  ),
346
- additionalProperties: false
346
+ unevaluatedProperties: false
347
347
  },
348
348
  {
349
349
  type: 'object',
@@ -352,7 +352,7 @@ export class Model extends objection.Model {
352
352
  type: 'string'
353
353
  }
354
354
  },
355
- additionalProperties: false
355
+ unevaluatedProperties: false
356
356
  }
357
357
  ]
358
358
  },
@@ -387,13 +387,21 @@ export class Model extends objection.Model {
387
387
  return this._getCached(
388
388
  'jsonSchema',
389
389
  () => {
390
- const schema = convertSchema({
391
- type: 'object',
392
- properties: this.definition.properties
393
- })
390
+ const definitions = {}
391
+ const schema = convertSchema(
392
+ {
393
+ type: 'object',
394
+ properties: this.definition.properties
395
+ },
396
+ { definitions }
397
+ )
394
398
  addRelationSchemas(this, schema.properties)
395
399
  // Merge in root-level schema additions
396
400
  assignDeeply(schema, this.definition.schema)
401
+ // Merge in definitions
402
+ if (Object.keys(definitions).length > 0) {
403
+ schema.definitions = definitions
404
+ }
397
405
  return {
398
406
  $id: this.name,
399
407
  ...schema
@@ -467,7 +475,17 @@ export class Model extends objection.Model {
467
475
  static getAttributes(filter) {
468
476
  const attributes = []
469
477
  const { properties } = this.definition
470
- for (const [name, property] of Object.entries(properties)) {
478
+ const { definitions } = this.jsonSchema
479
+ for (let [name, property] of Object.entries(properties)) {
480
+ // Expand $refs so we can even find properties that uses definitions:
481
+ const { $ref, ...schema } = property
482
+ const definition = $ref && definitions?.[$ref]
483
+ if (definition) {
484
+ property = {
485
+ ...schema,
486
+ ...definition
487
+ }
488
+ }
471
489
  if (filter(property)) {
472
490
  attributes.push(name)
473
491
  }
@@ -814,8 +832,8 @@ export class Model extends objection.Model {
814
832
  // @override
815
833
  static createValidator() {
816
834
  // Use a shared validator per app, so model schema can reference each other.
817
- // NOTE: The Dito Validator class creates and manages this shared Objection
818
- // Validator instance for us, we just need to return it here:
835
+ // NOTE: The Dito.js Validator class creates and manages this shared
836
+ // Objection Validator instance for us, we just need to return it here:
819
837
  return this.app.validator
820
838
  }
821
839
 
@@ -1065,19 +1065,5 @@ const mixinMethods = [
1065
1065
  'havingBetween',
1066
1066
  'havingNotBetween',
1067
1067
  'havingRaw',
1068
- 'havingWrapped',
1069
-
1070
- // deprecated methods that are still supported at the moment.
1071
- // TODO: Remove once we move to Objection 3.0
1072
- 'eager',
1073
- 'joinEager',
1074
- 'naiveEager',
1075
- 'mergeEager',
1076
- 'mergeJoinEager',
1077
- 'mergeNaiveEager',
1078
- 'clearEager',
1079
-
1080
- 'scope',
1081
- 'mergeScope',
1082
- 'clearScope'
1068
+ 'havingWrapped'
1083
1069
  ]
@@ -1,4 +1,5 @@
1
1
  export * as keywords from './keywords/index.js'
2
2
  export * as formats from './formats/index.js'
3
+ export * as types from './types/index.js'
3
4
  export * from './properties.js'
4
5
  export * from './relations.js'
@@ -0,0 +1,31 @@
1
+ import { resolve } from 'url'
2
+ import { MissingRefError } from 'ajv'
3
+ import { clone, mergeDeeply } from '@ditojs/utils'
4
+
5
+ export const $extend = {
6
+ macro(schemas, parentSchema, ctx) {
7
+ const [source, ...patch] = schemas.map(schema => {
8
+ const { $ref } = schema
9
+ if ($ref) {
10
+ const { baseId, self } = ctx
11
+ const id =
12
+ baseId && baseId !== '#'
13
+ ? resolve(baseId, $ref)
14
+ : $ref
15
+ const validate = self.getSchema(id)
16
+ if (!validate) {
17
+ throw new MissingRefError(baseId, $ref)
18
+ }
19
+ schema = validate.schema
20
+ }
21
+ return schema
22
+ })
23
+ return mergeDeeply(clone(source), ...patch)
24
+ },
25
+
26
+ metaSchema: {
27
+ type: 'array',
28
+ items: { type: 'object' },
29
+ minItems: 2
30
+ }
31
+ }
@@ -14,8 +14,8 @@ export const _instanceof = {
14
14
  },
15
15
 
16
16
  validate(schema, data) {
17
- // Support instanceof for basic JS types and Dito models. If `this` is the
18
- // validator's ctx (see passContext), then we can access the models and
17
+ // Support instanceof for basic JS types and Dito.js models. If `this` is
18
+ // the validator's ctx (see passContext), then we can access the models and
19
19
  // check.
20
20
  const models = this?.app?.models
21
21
  for (const type of asArray(schema)) {
@@ -10,3 +10,4 @@ export * from './_instanceof.js'
10
10
  export * from './_validate.js'
11
11
  export * from './_relate.js'
12
12
  export * from './_range.js'
13
+ export * from './_extend.js'
@@ -1,13 +1,17 @@
1
- import { isObject, isArray, isString } from '@ditojs/utils'
1
+ import { isObject, isArray, isString, equals } from '@ditojs/utils'
2
2
 
3
3
  export function convertSchema(schema, options = {}) {
4
4
  if (isArray(schema)) {
5
- // Needed for allOf, anyOf, oneOf, not, items:
5
+ // Needed for allOf, anyOf, oneOf, not, items, see below:
6
6
  schema = schema.map(entry => convertSchema(entry, options))
7
7
  } else if (isObject(schema)) {
8
8
  // Create a shallow clone so we can modify and return:
9
- schema = { ...schema }
10
- const { type } = schema
9
+ // Also collect and propagate the definitions up to the root schema through
10
+ // `options.definitions`, as passed from `Model static get jsonSchema()`:
11
+ const { definitions, ...rest } = schema
12
+ mergeDefinitions(options.definitions, definitions, options)
13
+ schema = rest
14
+ const { $ref, type } = schema
11
15
  if (schema.required === true) {
12
16
  // Our 'required' is not the same as JSON Schema's: Use the 'required'
13
17
  // format instead that only validates if the required value is not empty,
@@ -44,22 +48,31 @@ export function convertSchema(schema, options = {}) {
44
48
  schema.prefixItems &&= convertSchema(schema.prefixItems, options)
45
49
  schema.items &&= convertSchema(schema.items, options)
46
50
 
47
- // Handle nested allOf, anyOf, oneOf, not fields
48
- for (const key of ['allOf', 'anyOf', 'oneOf', 'not']) {
51
+ // Handle nested allOf, anyOf, oneOf & co. fields
52
+ for (const key of ['allOf', 'anyOf', 'oneOf', 'not', '$extend']) {
49
53
  if (key in schema) {
50
54
  schema[key] = convertSchema(schema[key], options)
51
55
  }
52
56
  }
53
57
 
54
- if (isString(type)) {
58
+ if (isString($ref)) {
59
+ // If the $ref is a nested Dito.js definition, convert it to a JSON schema
60
+ // reference. If it is a full URL, use it as is.
61
+ schema.$ref = $ref.startsWith('#')
62
+ ? `#/definitions/${$ref}`
63
+ : $ref
64
+ } else if (isString(type)) {
55
65
  // Convert schema property notation to JSON schema
56
66
  const jsonType = jsonTypes[type]
57
67
  if (jsonType) {
58
68
  schema.type = jsonType
59
- if (hasConvertedProperties && !('additionalProperties' in schema)) {
60
- // Invert the logic of `additionalProperties` so that it needs to be
69
+ if (
70
+ (hasConvertedProperties || schema.discriminator) &&
71
+ !('unevaluatedProperties' in schema)
72
+ ) {
73
+ // Invert the logic of `unevaluatedProperties` so that it needs to be
61
74
  // explicitly set to `true`:
62
- schema.additionalProperties = false
75
+ schema.unevaluatedProperties = false
63
76
  }
64
77
  } else if (['date', 'datetime', 'timestamp'].includes(type)) {
65
78
  // Date properties can be submitted both as a string or a Date object.
@@ -105,7 +118,7 @@ export function convertSchema(schema, options = {}) {
105
118
  return schema
106
119
  }
107
120
 
108
- export function convertProperties(schemaProperties, options) {
121
+ function convertProperties(schemaProperties, options) {
109
122
  const properties = {}
110
123
  const required = []
111
124
  for (const [key, property] of Object.entries(schemaProperties)) {
@@ -117,6 +130,32 @@ export function convertProperties(schemaProperties, options) {
117
130
  return { properties, required }
118
131
  }
119
132
 
133
+ function mergeDefinitions(definitions, defs, options) {
134
+ if (definitions && defs) {
135
+ for (const [key, def] of Object.entries(defs)) {
136
+ if (!key.startsWith('#')) {
137
+ throw new Error(
138
+ `Invalid definition '${
139
+ key
140
+ }', the name of nested Dito.js definitions must start with '#': ${
141
+ JSON.stringify(def)
142
+ }`
143
+ )
144
+ }
145
+ const definition = definitions[key]
146
+ const converted = convertSchema(def, options)
147
+ if (definition && !equals(definition, converted)) {
148
+ throw new Error(
149
+ `Duplicate nested definition for '${key}' with different schema: ${
150
+ JSON.stringify(def)
151
+ }`
152
+ )
153
+ }
154
+ definitions[key] = converted
155
+ }
156
+ }
157
+ }
158
+
120
159
  function addFormat(schema, newFormat) {
121
160
  // Support multiple `format` keywords through `allOf`:
122
161
  const { allOf, format, ...rest } = schema
@@ -30,7 +30,7 @@ describe('convertSchema()', () => {
30
30
  ).toEqual({
31
31
  type: 'object',
32
32
  properties,
33
- additionalProperties: false
33
+ unevaluatedProperties: false
34
34
  })
35
35
  })
36
36
 
@@ -69,7 +69,7 @@ describe('convertSchema()', () => {
69
69
  ).toEqual({
70
70
  type: 'object',
71
71
  properties,
72
- additionalProperties: false
72
+ unevaluatedProperties: false
73
73
  })
74
74
  })
75
75
 
@@ -90,7 +90,7 @@ describe('convertSchema()', () => {
90
90
  type: 'string'
91
91
  }
92
92
  },
93
- additionalProperties: false
93
+ unevaluatedProperties: false
94
94
  })
95
95
  })
96
96
 
@@ -121,7 +121,7 @@ describe('convertSchema()', () => {
121
121
  format: 'required'
122
122
  }
123
123
  },
124
- additionalProperties: false,
124
+ unevaluatedProperties: false,
125
125
  required: ['myString', 'myNumber']
126
126
  })
127
127
  })
@@ -142,7 +142,7 @@ describe('convertSchema()', () => {
142
142
  myString: { type: 'string' },
143
143
  myNumber: { type: 'number' }
144
144
  },
145
- additionalProperties: false,
145
+ unevaluatedProperties: false,
146
146
  required: ['myString', 'myNumber']
147
147
  })
148
148
  })
@@ -163,11 +163,11 @@ describe('convertSchema()', () => {
163
163
  properties: {
164
164
  myText: {
165
165
  type: 'object',
166
- additionalProperties: false,
166
+ unevaluatedProperties: false,
167
167
  properties: {}
168
168
  }
169
169
  },
170
- additionalProperties: false
170
+ unevaluatedProperties: false
171
171
  })
172
172
  })
173
173
 
@@ -178,7 +178,7 @@ describe('convertSchema()', () => {
178
178
  properties: {
179
179
  myText: {
180
180
  type: 'object',
181
- additionalProperties: true,
181
+ unevaluatedProperties: true,
182
182
  properties: {}
183
183
  }
184
184
  }
@@ -188,11 +188,11 @@ describe('convertSchema()', () => {
188
188
  properties: {
189
189
  myText: {
190
190
  type: 'object',
191
- additionalProperties: true,
191
+ unevaluatedProperties: true,
192
192
  properties: {}
193
193
  }
194
194
  },
195
- additionalProperties: false
195
+ unevaluatedProperties: false
196
196
  })
197
197
  })
198
198
 
@@ -223,11 +223,11 @@ describe('convertSchema()', () => {
223
223
  format: 'required'
224
224
  }
225
225
  },
226
- additionalProperties: false,
226
+ unevaluatedProperties: false,
227
227
  required: ['myProperty']
228
228
  }
229
229
  },
230
- additionalProperties: false
230
+ unevaluatedProperties: false
231
231
  })
232
232
  })
233
233
 
@@ -256,10 +256,10 @@ describe('convertSchema()', () => {
256
256
  type: 'string'
257
257
  }
258
258
  },
259
- additionalProperties: false
259
+ unevaluatedProperties: false
260
260
  }
261
261
  },
262
- additionalProperties: false
262
+ unevaluatedProperties: false
263
263
  })
264
264
  })
265
265
 
@@ -295,7 +295,7 @@ describe('convertSchema()', () => {
295
295
  format: 'date-time'
296
296
  }
297
297
  },
298
- additionalProperties: false
298
+ unevaluatedProperties: false
299
299
  })
300
300
  })
301
301
 
@@ -316,7 +316,7 @@ describe('convertSchema()', () => {
316
316
  $ref: 'MyModel'
317
317
  }
318
318
  },
319
- additionalProperties: false
319
+ unevaluatedProperties: false
320
320
  })
321
321
  })
322
322
 
@@ -343,7 +343,7 @@ describe('convertSchema()', () => {
343
343
  instanceof: 'MyModel'
344
344
  }
345
345
  },
346
- additionalProperties: false
346
+ unevaluatedProperties: false
347
347
  })
348
348
  })
349
349
 
@@ -366,7 +366,7 @@ describe('convertSchema()', () => {
366
366
  nullable: true
367
367
  }
368
368
  },
369
- additionalProperties: false
369
+ unevaluatedProperties: false
370
370
  })
371
371
  })
372
372
 
@@ -391,7 +391,7 @@ describe('convertSchema()', () => {
391
391
  ]
392
392
  }
393
393
  },
394
- additionalProperties: false
394
+ unevaluatedProperties: false
395
395
  })
396
396
  })
397
397
 
@@ -415,7 +415,7 @@ describe('convertSchema()', () => {
415
415
  nullable: true
416
416
  }
417
417
  },
418
- additionalProperties: false
418
+ unevaluatedProperties: false
419
419
  })
420
420
  })
421
421
 
@@ -440,7 +440,7 @@ describe('convertSchema()', () => {
440
440
  nullable: true
441
441
  }
442
442
  },
443
- additionalProperties: false
443
+ unevaluatedProperties: false
444
444
  })
445
445
  })
446
446
 
@@ -504,7 +504,7 @@ describe('convertSchema()', () => {
504
504
  }
505
505
  },
506
506
  required: ['prop1', 'prop2'],
507
- additionalProperties: false
507
+ unevaluatedProperties: false
508
508
  },
509
509
  {
510
510
  type: 'object',
@@ -519,13 +519,13 @@ describe('convertSchema()', () => {
519
519
  }
520
520
  },
521
521
  required: ['prop3', 'prop4'],
522
- additionalProperties: false
522
+ unevaluatedProperties: false
523
523
  }
524
524
  ]
525
525
  }
526
526
  }
527
527
  },
528
- additionalProperties: false
528
+ unevaluatedProperties: false
529
529
  })
530
530
  })
531
531
 
@@ -566,11 +566,11 @@ describe('convertSchema()', () => {
566
566
  type: 'number'
567
567
  }
568
568
  },
569
- additionalProperties: false,
569
+ unevaluatedProperties: false,
570
570
  required: ['prop1', 'prop2']
571
571
  }
572
572
  },
573
- additionalProperties: false,
573
+ unevaluatedProperties: false,
574
574
  required: ['myObject']
575
575
  })
576
576
  })
@@ -605,6 +605,7 @@ describe('convertSchema()', () => {
605
605
  ).toEqual({
606
606
  type: 'object',
607
607
  discriminator: { propertyName: 'foo' },
608
+ unevaluatedProperties: false,
608
609
  required: ['foo'],
609
610
  oneOf: [
610
611
  {
@@ -630,4 +631,76 @@ describe('convertSchema()', () => {
630
631
  ]
631
632
  })
632
633
  })
634
+
635
+ it('supports nested Dito.js definitions', () => {
636
+ const definitions = {}
637
+ expect(
638
+ convertSchema(
639
+ {
640
+ type: 'object',
641
+ properties: {
642
+ prop1: {
643
+ $ref: '#type1'
644
+ },
645
+ prop2: {
646
+ type: 'object',
647
+ properties: {
648
+ prop3: {
649
+ $ref: '#type2'
650
+ }
651
+ },
652
+ definitions: {
653
+ '#type2': {
654
+ type: 'object',
655
+ properties: {
656
+ prop3: {
657
+ type: 'string'
658
+ }
659
+ }
660
+ }
661
+ }
662
+ }
663
+ },
664
+
665
+ definitions: {
666
+ '#type1': {
667
+ type: 'integer'
668
+ }
669
+ }
670
+ },
671
+ { definitions }
672
+ )
673
+ ).toEqual({
674
+ type: 'object',
675
+ unevaluatedProperties: false,
676
+ properties: {
677
+ prop1: {
678
+ $ref: '#/definitions/#type1'
679
+ },
680
+ prop2: {
681
+ type: 'object',
682
+ unevaluatedProperties: false,
683
+ properties: {
684
+ prop3: {
685
+ $ref: '#/definitions/#type2'
686
+ }
687
+ }
688
+ }
689
+ }
690
+ })
691
+ expect(definitions).toEqual({
692
+ '#type1': {
693
+ type: 'integer'
694
+ },
695
+ '#type2': {
696
+ type: 'object',
697
+ unevaluatedProperties: false,
698
+ properties: {
699
+ prop3: {
700
+ type: 'string'
701
+ }
702
+ }
703
+ }
704
+ })
705
+ })
633
706
  })
@@ -0,0 +1,31 @@
1
+ export const asset = {
2
+ type: 'object',
3
+ properties: {
4
+ key: {
5
+ type: 'string',
6
+ required: true
7
+ },
8
+ name: {
9
+ type: 'string',
10
+ required: true
11
+ },
12
+ type: {
13
+ type: 'string',
14
+ required: true
15
+ },
16
+ size: {
17
+ type: 'integer',
18
+ required: true
19
+ },
20
+ url: {
21
+ type: 'string',
22
+ format: 'uri'
23
+ },
24
+ width: {
25
+ type: 'integer'
26
+ },
27
+ height: {
28
+ type: 'integer'
29
+ }
30
+ }
31
+ }
@@ -0,0 +1 @@
1
+ export * from './_asset.js'
@@ -28,11 +28,6 @@ export class Service {
28
28
  // @overridable
29
29
  async stop() {}
30
30
 
31
- /** @deprecated Use `instance.logger` instead. */
32
- getLogger() {
33
- return this.logger
34
- }
35
-
36
31
  get logger() {
37
32
  const logger = this.app.requestLocals.logger ?? this.app.logger
38
33
  return logger.child({ name: this.#loggerName })
@@ -60,7 +60,6 @@ export class AssetFile {
60
60
  static convert(object, storage) {
61
61
  Object.setPrototypeOf(object, AssetFile.prototype)
62
62
  setHiddenProperty(object, SYMBOL_STORAGE, storage)
63
- return object
64
63
  }
65
64
 
66
65
  static create(options) {
@@ -164,6 +164,6 @@ function getFileTypeFromBuffer(buffer) {
164
164
  try {
165
165
  // Use leather as fall-back for better media file mime type detection.
166
166
  return fileTypeFromBuffer(buffer)?.mime || readMediaAttributes(buffer)?.mime
167
- } catch (err) {}
167
+ } catch {}
168
168
  return null
169
169
  }
@@ -4,7 +4,7 @@ import multer from '@koa/multer'
4
4
  import picomatch from 'picomatch'
5
5
  import { PassThrough } from 'stream'
6
6
  import { readMediaAttributes } from 'leather'
7
- import { hyphenate, toPromiseCallback, deprecate } from '@ditojs/utils'
7
+ import { hyphenate, toPromiseCallback } from '@ditojs/utils'
8
8
  import { AssetFile } from './AssetFile.js'
9
9
 
10
10
  const storageClasses = {}
@@ -77,7 +77,7 @@ export class Storage {
77
77
  }
78
78
 
79
79
  convertAssetFile(file) {
80
- return AssetFile.convert(file, this)
80
+ AssetFile.convert(file, this)
81
81
  }
82
82
 
83
83
  convertStorageFile(storageFile) {
@@ -158,18 +158,8 @@ export class Storage {
158
158
  async _listKeys() {}
159
159
 
160
160
  async _handleUpload(req, file, config) {
161
- if (config.readImageSize) {
162
- deprecate(
163
- `config.readImageSize is deprecated in favour of config.readDimensions`
164
- )
165
- }
166
161
  if (
167
- (
168
- config.readDimensions ||
169
- // TODO: `config.readImageSize` was deprecated in favour of
170
- // `config.readDimensions` in March 2023. Remove in 1 year.
171
- config.readImageSize
172
- ) &&
162
+ config.readDimensions &&
173
163
  /^(image|video)\//.test(file.mimetype)
174
164
  ) {
175
165
  return this._handleMediaFile(req, file)
@@ -1,5 +1,4 @@
1
1
  import { isNumber } from '@ditojs/utils'
2
- // eslint-disable-next-line import/default
3
2
  import parseDuration from 'parse-duration'
4
3
 
5
4
  export function getDuration(duration) {
package/types/index.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // Type definitions for Dito.js server
4
4
  // Project: <https://github.com/ditojs/dito/>
5
5
 
6
- // Export the entire Dito namespace.
6
+ // Export the entire Dito.js namespace.
7
7
 
8
8
  import { ObjectCannedACL, S3ClientConfig } from '@aws-sdk/client-s3'
9
9
  import { DateFormat } from '@ditojs/utils'
@@ -645,14 +645,15 @@ export class Model extends objection.Model {
645
645
  }
646
646
 
647
647
  /**
648
- * Dito automatically adds an `id` property if a model property with the
648
+ * Dito.js automatically adds an `id` property if a model property with the
649
649
  * `primary: true` setting is not already explicitly defined.
650
650
  */
651
651
  readonly id: Id
652
652
 
653
653
  /**
654
- * Dito automatically adds a `foreignKeyId` property if foreign keys occurring
655
- * in relations definitions are not explicitly defined in the properties.
654
+ * Dito.js automatically adds a `foreignKeyId` property if foreign keys
655
+ * occurring in relations definitions are not explicitly defined in the
656
+ * properties.
656
657
  */
657
658
  readonly foreignKeyId: Id
658
659
 
@@ -1442,8 +1443,6 @@ export class Service {
1442
1443
  /** @overridable */
1443
1444
  stop(): Promise<void>
1444
1445
  get logger(): PinoLogger
1445
- /** @deprecated Use `instance.logger` instead. */
1446
- getLogger(ctx: KoaContext): PinoLogger
1447
1446
  }
1448
1447
  export type Services = Record<string, Class<Service> | Service>
1449
1448
 
@@ -1,8 +0,0 @@
1
- import { createDecorator } from '../utils/decorator.js'
2
-
3
- export function action(method, path) {
4
- return createDecorator(value => {
5
- value.method = method
6
- value.path = path
7
- })
8
- }
@@ -1,7 +0,0 @@
1
- import { createDecorator } from '../utils/decorator.js'
2
-
3
- export function authorize(authorize) {
4
- return createDecorator(value => {
5
- value.authorize = authorize
6
- })
7
- }
@@ -1,6 +0,0 @@
1
- export * from './action.js'
2
- export * from './authorize.js'
3
- export * from './parameters.js'
4
- export * from './returns.js'
5
- export * from './scope.js'
6
- export * from './transacted.js'
@@ -1,20 +0,0 @@
1
- import { isArray, isObject } from '@ditojs/utils'
2
- import { createDecorator } from '../utils/decorator.js'
3
-
4
- export function parameters(parameters, options) {
5
- if (!isArray(parameters) && !isObject(parameters)) {
6
- throw new Error(
7
- `@parameters() need to be defined using array or object definitions`
8
- )
9
- }
10
-
11
- return createDecorator(value => {
12
- value.parameters = parameters
13
- // If validation options are provided, expose them through
14
- // `handler.options.parameters`, see ControllerAction
15
- if (options) {
16
- value.options ||= {}
17
- value.options.parameters = options
18
- }
19
- })
20
- }
@@ -1,23 +0,0 @@
1
- import { isObject } from '@ditojs/utils'
2
- import { createDecorator } from '../utils/decorator.js'
3
- import { formatJson } from '../utils/json.js'
4
-
5
- export function returns(returns, options) {
6
- if (!isObject(returns)) {
7
- throw new Error(
8
- `@returns(${
9
- formatJson(returns, false)
10
- }) needs to be defined using an object parameter definition`
11
- )
12
- }
13
-
14
- return createDecorator(value => {
15
- value.returns = returns
16
- // If validation options are provided, expose them through
17
- // `handler.options.returns`, see ControllerAction
18
- if (options) {
19
- value.options ||= {}
20
- value.options.returns = options
21
- }
22
- })
23
- }
@@ -1,8 +0,0 @@
1
- import { createDecorator } from '../utils/decorator.js'
2
-
3
- export function scope(...scopes) {
4
- return createDecorator(value => {
5
- const scope = (value.scope ||= [])
6
- scope.push(...scopes)
7
- })
8
- }
@@ -1,5 +0,0 @@
1
- import { createDecorator } from '../utils/decorator.js'
2
-
3
- export const transacted = createDecorator(value => {
4
- value.transacted = true
5
- })