@ditojs/server 2.99.1 → 3.0.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.99.1",
3
+ "version": "3.0.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/main/packages/server",
@@ -12,9 +12,6 @@
12
12
  "src/",
13
13
  "types/"
14
14
  ],
15
- "scripts": {
16
- "types": "tsc --noEmit --esModuleInterop ./types/index.d.ts"
17
- },
18
15
  "bin": {
19
16
  "dito": "./src/cli/index.js"
20
17
  },
@@ -25,26 +22,25 @@
25
22
  "node >= 18"
26
23
  ],
27
24
  "dependencies": {
28
- "@ditojs/admin": "^2.99.1",
29
- "@ditojs/build": "^2.99.0",
30
- "@ditojs/router": "^2.99.0",
31
- "@ditojs/utils": "^2.99.0",
25
+ "@ditojs/admin": "^3.0.0",
26
+ "@ditojs/build": "^3.0.0",
27
+ "@ditojs/router": "^3.0.0",
28
+ "@ditojs/utils": "^3.0.0",
32
29
  "@koa/cors": "^5.0.0",
33
30
  "@koa/etag": "^5.0.2",
34
31
  "@koa/multer": "^4.0.0",
35
- "@originjs/vite-plugin-commonjs": "^1.0.3",
36
32
  "ajv": "^8.20.0",
37
33
  "ajv-formats": "^3.0.1",
38
34
  "bcryptjs": "^3.0.3",
39
35
  "bytes": "^3.1.2",
40
36
  "data-uri-to-buffer": "^8.0.0",
41
37
  "eventemitter2": "^6.4.9",
42
- "file-type": "^22.0.1",
43
- "helmet": "^8.1.0",
44
- "koa": "^3.2.0",
38
+ "file-type": "^22.1.1",
39
+ "helmet": "^8.3.0",
40
+ "koa": "^3.2.1",
45
41
  "koa-bodyparser": "^4.4.1",
46
42
  "koa-compose": "^4.1.0",
47
- "koa-compress": "^5.2.1",
43
+ "koa-compress": "^5.2.2",
48
44
  "koa-conditional-get": "^3.0.0",
49
45
  "koa-helmet": "^9.0.0",
50
46
  "koa-mount": "^4.2.0",
@@ -54,20 +50,20 @@
54
50
  "koa-static": "^5.0.0",
55
51
  "leather": "^3.0.3",
56
52
  "mime-types": "^3.0.2",
57
- "multer": "^2.1.1",
53
+ "multer": "^2.4.0",
58
54
  "multer-s3": "github:ditojs/multer-s3#dito",
59
- "nanoid": "^5.1.9",
60
- "parse-duration": "^2.1.6",
55
+ "nanoid": "^6.0.1",
56
+ "parse-duration": "^2.1.9",
61
57
  "passport-local": "^1.0.0",
62
58
  "passthrough-counter": "^1.0.0",
63
59
  "picocolors": "^1.1.1",
64
- "picomatch": "^4.0.4",
65
- "pino": "^10.3.1",
60
+ "picomatch": "^4.0.7",
61
+ "pino": "^10.4.0",
66
62
  "pino-pretty": "^13.1.3",
67
63
  "pluralize": "^8.0.0",
68
64
  "repl": "^0.1.3",
69
- "type-fest": "^5.6.0",
70
- "uuid": "^14.0.0"
65
+ "type-fest": "^5.10.0",
66
+ "uuid": "^14.0.2"
71
67
  },
72
68
  "peerDependencies": {
73
69
  "@aws-sdk/client-s3": "^3.0.0",
@@ -76,19 +72,20 @@
76
72
  "objection": "^3.0.1"
77
73
  },
78
74
  "devDependencies": {
79
- "@aws-sdk/client-s3": "^3.1030.0",
80
- "@aws-sdk/lib-storage": "^3.1030.0",
75
+ "@aws-sdk/client-s3": "^3.1146.0",
76
+ "@aws-sdk/lib-storage": "^3.1146.0",
81
77
  "@types/koa-bodyparser": "^4.3.13",
82
78
  "@types/koa-compress": "^4.0.7",
79
+ "@types/koa-mount": "^4.0.5",
83
80
  "@types/koa-response-time": "^2.1.5",
84
- "@types/koa-session": "^6.4.5",
85
81
  "@types/koa-static": "^4.0.4",
86
82
  "@types/koa__cors": "^5.0.1",
87
83
  "@types/koa__multer": "^2.0.8",
88
- "@types/node": "^25.6.0",
89
- "knex": "^3.2.9",
84
+ "@types/multer-s3": "^3.0.3",
85
+ "@types/node": "^26.6.4",
86
+ "knex": "^3.3.0",
90
87
  "objection": "^3.1.5",
91
88
  "typescript": "^6.0.3"
92
89
  },
93
- "gitHead": "537d4483092604a1bfa055c151bccca4821de19a"
90
+ "gitHead": "399f683a6ca227a03cfc0978fee9055383e6b3d8"
94
91
  }
@@ -3,7 +3,6 @@ import Koa from 'koa'
3
3
  import serve from 'koa-static'
4
4
  import { defineConfig, createServer } from 'vite'
5
5
  import createVuePlugin from '@vitejs/plugin-vue'
6
- import { viteCommonjs as createCommonJsPlugin } from '@originjs/vite-plugin-commonjs'
7
6
  import { testModuleIdentifier, getPostCssConfig } from '@ditojs/build'
8
7
  import { assignDeeply } from '@ditojs/utils'
9
8
  import { Controller } from './Controller.js'
@@ -174,11 +173,10 @@ export class AdminController extends Controller {
174
173
  root,
175
174
  base,
176
175
  mode: this.mode,
177
- envFile: false,
176
+ envDir: false,
178
177
  configFile: false,
179
178
  plugins: [
180
179
  createVuePlugin(),
181
- createCommonJsPlugin(),
182
180
  {
183
181
  // Private plugin to inject script tag above main module that
184
182
  // loads the `dito` object through its own end-point, see:
@@ -250,8 +248,7 @@ export class AdminController extends Controller {
250
248
  'prosemirror-view'
251
249
  ]
252
250
  : ditoPackages
253
- ),
254
- ...nonEsmDependencies
251
+ )
255
252
  ]
256
253
  },
257
254
  resolve: {
@@ -279,12 +276,6 @@ const ditoPackages = [
279
276
  '@ditojs/utils'
280
277
  ]
281
278
 
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
279
  const coreDependencies = [
289
280
  ...ditoPackages,
290
281
 
@@ -296,12 +287,11 @@ const coreDependencies = [
296
287
  'vue',
297
288
  '@vue/*',
298
289
  '@vueuse/*',
299
- '@lk77/vue3-color',
290
+ 'vue-color',
300
291
  '@kyvg/vue3-notification',
301
292
  'vue-multiselect',
302
293
  'vue-router',
303
294
  'vue-upload-component',
304
- 'tinycolor2',
305
295
  'focus-trap',
306
296
  'tabbable',
307
297
  'sortablejs',
@@ -314,8 +304,6 @@ const coreDependencies = [
314
304
  'nanoid',
315
305
  'punycode',
316
306
  'rope-sequence',
317
- 'filesize',
318
- 'filesize-parser',
319
307
  'tslib', // ?
320
308
  'orderedmap',
321
309
  'w3c-keyname'
@@ -16,7 +16,9 @@ import {
16
16
  setValueAtDataPath,
17
17
  mapConcurrently,
18
18
  assignDeeply,
19
- deprecate
19
+ deprecate,
20
+ formatPlainDate,
21
+ parsePlainDate
20
22
  } from '@ditojs/utils'
21
23
  import { QueryBuilder } from '../query/index.js'
22
24
  import { EventEmitter, KnexHelper } from '../lib/index.js'
@@ -443,10 +445,21 @@ export class Model extends objection.Model {
443
445
  static get dateAttributes() {
444
446
  return this._getCached(
445
447
  'jsonSchema:dateAttributes',
448
+ () =>
449
+ this.getAttributes(
450
+ ({ type, computed }) => !computed && type === 'date'
451
+ ),
452
+ []
453
+ )
454
+ }
455
+
456
+ static get datetimeAttributes() {
457
+ return this._getCached(
458
+ 'jsonSchema:datetimeAttributes',
446
459
  () =>
447
460
  this.getAttributes(
448
461
  ({ type, computed }) => (
449
- !computed && ['date', 'datetime', 'timestamp'].includes(type)
462
+ !computed && ['datetime', 'timestamp'].includes(type)
450
463
  )
451
464
  ),
452
465
  []
@@ -590,12 +603,20 @@ export class Model extends objection.Model {
590
603
  // @override
591
604
  $formatDatabaseJson(json) {
592
605
  const { constructor } = this
593
- for (const key of constructor.dateAttributes) {
606
+ for (const key of constructor.datetimeAttributes) {
594
607
  const date = json[key]
595
608
  if (date?.toISOString) {
596
609
  json[key] = date.toISOString()
597
610
  }
598
611
  }
612
+ for (const key of constructor.dateAttributes) {
613
+ const date = json[key]
614
+ if (date?.toISOString) {
615
+ // Store dates as their plain date, as the database ignores the
616
+ // time zone when casting a timestamp to a date.
617
+ json[key] = formatPlainDate(date)
618
+ }
619
+ }
599
620
  if (constructor.isSQLite()) {
600
621
  // SQLite does not support boolean natively and needs conversion...
601
622
  for (const key of constructor.booleanAttributes) {
@@ -636,10 +657,17 @@ export class Model extends objection.Model {
636
657
  // @override
637
658
  $parseJson(json, { trusted = false } = {}) {
638
659
  const { constructor } = this
639
- for (const key of constructor.dateAttributes) {
660
+ for (const key of [
661
+ ...constructor.dateAttributes,
662
+ ...constructor.datetimeAttributes
663
+ ]) {
640
664
  const date = json[key]
641
665
  if (date !== undefined) {
642
- json[key] = isString(date) ? new Date(date) : date
666
+ // Parse plain dates like `2026-05-14` as local midnight, which is
667
+ // also how Postgres reads them for timestamps.
668
+ json[key] = isString(date)
669
+ ? parsePlainDate(date) ?? new Date(date)
670
+ : date
643
671
  }
644
672
  }
645
673
  // Convert plain asset files objects to AssetFile instances with references
@@ -652,13 +680,21 @@ export class Model extends objection.Model {
652
680
 
653
681
  // @override
654
682
  $formatJson(json) {
683
+ const { constructor } = this
655
684
  // Remove hidden attributes.
656
- for (const key of this.constructor.hiddenAttributes) {
685
+ for (const key of constructor.hiddenAttributes) {
657
686
  delete json[key]
658
687
  }
688
+ // Return dates as their plain date, see `$formatDatabaseJson()`.
689
+ for (const key of constructor.dateAttributes) {
690
+ const date = json[key]
691
+ if (date?.toISOString) {
692
+ json[key] = formatPlainDate(date)
693
+ }
694
+ }
659
695
  // Sign asset files so clients can send them back with valid signatures.
660
696
  // Clone file objects to avoid mutating the model's internals.
661
- this.constructor._signAssetFiles(json)
697
+ constructor._signAssetFiles(json)
662
698
  return json
663
699
  }
664
700
 
package/types/index.d.ts CHANGED
@@ -19,7 +19,7 @@ import { Options as KoaBodyParserOptions } from 'koa-bodyparser'
19
19
  import { CompressOptions } from 'koa-compress'
20
20
  import koaMount from 'koa-mount'
21
21
  import koaResponseTime from 'koa-response-time'
22
- import koaSession from 'koa-session'
22
+ import { CreateSessionOptions as KoaSessionOptions } from 'koa-session'
23
23
  import multer from '@koa/multer'
24
24
  import multerS3 from 'multer-s3'
25
25
  import * as objection from 'objection'
@@ -185,7 +185,7 @@ export interface ApplicationConfig {
185
185
  * @defaultValue `false`
186
186
  * @see https://github.com/koajs/session
187
187
  */
188
- session?: boolean | (koaSession.opts & { modelClass: string })
188
+ session?: boolean | (KoaSessionOptions & { modelClass: string })
189
189
  /**
190
190
  * Enable passport authentication middleware
191
191
  *
@@ -413,7 +413,7 @@ export class Application<$Models extends Models = Models> {
413
413
  ctx: KoaContext,
414
414
  next: () => Promise<void>
415
415
  ) => OrPromiseOf<void>
416
- ): this
416
+ ): unknown
417
417
  find(
418
418
  method: string,
419
419
  path: string
@@ -660,7 +660,8 @@ export class Application<$Models extends Models = Models> {
660
660
  method: HTTPMethod,
661
661
  path: string,
662
662
  transacted: boolean,
663
- middlewares: OrArrayOf< (
663
+ middlewares: OrArrayOf<
664
+ (
664
665
  ctx: KoaContext,
665
666
  next: () => Promise<void>
666
667
  ) => OrPromiseOf<void>
@@ -683,21 +684,21 @@ export class Application<$Models extends Models = Models> {
683
684
 
684
685
  export interface Application
685
686
  extends Omit<
686
- Koa,
687
- | 'setMaxListeners'
688
- | 'removeListener'
689
- | 'removeAllListeners'
690
- | 'prependOnceListener'
691
- | 'prependListener'
692
- | 'once'
693
- | 'on'
694
- | 'off'
695
- | 'listeners'
696
- | 'addListener'
697
- | 'listenerCount'
698
- | 'emit'
699
- | 'eventNames'
700
- >,
687
+ Koa,
688
+ | 'setMaxListeners'
689
+ | 'removeListener'
690
+ | 'removeAllListeners'
691
+ | 'prependOnceListener'
692
+ | 'prependListener'
693
+ | 'once'
694
+ | 'on'
695
+ | 'off'
696
+ | 'listeners'
697
+ | 'addListener'
698
+ | 'listenerCount'
699
+ | 'emit'
700
+ | 'eventNames'
701
+ >,
701
702
  EventEmitter {}
702
703
 
703
704
  export type SchemaType = LiteralUnion<
@@ -875,7 +876,7 @@ export interface ModelRelation<
875
876
  filter?:
876
877
  | string
877
878
  | { [name: string]: unknown[] }
878
- | ((query: QueryBuilder) => void)
879
+ | ((query: QueryBuilder<Model>) => void)
879
880
  | Record<string, unknown>
880
881
  /**
881
882
  * Controls whether the auto-inserted foreign key property should be marked as
@@ -898,7 +899,7 @@ export interface ModelRelation<
898
899
  * As an object: `modify: { active: true }` (converted to a find-filter)
899
900
  */
900
901
  modify?:
901
- | ((query: QueryBuilder) => void)
902
+ | ((query: QueryBuilder<Model>) => void)
902
903
  | Record<string, unknown>
903
904
  }
904
905
 
@@ -962,10 +963,15 @@ export type ModelProperty<T = any> = Schema<T> & {
962
963
  *
963
964
  * @see {@link https://github.com/ditojs/dito/blob/main/docs/model-scopes.md|Model Scopes}
964
965
  */
965
- export type ModelScope<$Model extends Model = Model> = (
966
- query: QueryBuilder<$Model>,
967
- applyParentScope: (query: QueryBuilder<$Model>) => QueryBuilder<$Model>
968
- ) => QueryBuilder<$Model, any> | void
966
+ // Scopes, filters and modifiers are declared through methods, to make their
967
+ // parameters bivariant, so that models can type them with their own query
968
+ // builders, e.g. `QueryBuilder<MyModel>` instead of `QueryBuilder<Model>`.
969
+ export type ModelScope<$Model extends Model = Model> = {
970
+ bivarianceHack(
971
+ query: QueryBuilder<$Model>,
972
+ applyParentScope: (query: QueryBuilder<$Model>) => QueryBuilder<$Model>
973
+ ): QueryBuilder<$Model, any> | void
974
+ }['bivarianceHack']
969
975
 
970
976
  /**
971
977
  * Map of scope names to scope functions. Scopes can be
@@ -984,10 +990,9 @@ export type ModelScopes<$Model extends Model = Model> = Record<
984
990
  * Modifiers are reusable query fragments applied via
985
991
  * `.modify('name')` on queries.
986
992
  */
987
- export type ModelModifier<$Model extends Model = Model> = (
988
- query: QueryBuilder<$Model>,
989
- ...args: any[]
990
- ) => void
993
+ export type ModelModifier<$Model extends Model = Model> = {
994
+ bivarianceHack(query: QueryBuilder<$Model>, ...args: any[]): void
995
+ }['bivarianceHack']
991
996
 
992
997
  /**
993
998
  * Map of modifier names to modifier functions.
@@ -1001,10 +1006,9 @@ export type ModelModifiers<$Model extends Model = Model> = Record<
1001
1006
  * A filter handler function that modifies a query builder
1002
1007
  * based on external parameters (e.g. from URL query strings).
1003
1008
  */
1004
- export type ModelFilterFunction<$Model extends Model = Model> = (
1005
- queryBuilder: QueryBuilder<$Model>,
1006
- ...args: any[]
1007
- ) => void
1009
+ export type ModelFilterFunction<$Model extends Model = Model> = {
1010
+ bivarianceHack(queryBuilder: QueryBuilder<$Model>, ...args: any[]): void
1011
+ }['bivarianceHack']
1008
1012
 
1009
1013
  /**
1010
1014
  * Registry of known filter type names for use with
@@ -1098,6 +1102,30 @@ export interface ModelAsset {
1098
1102
  /** Map of property names to their asset configurations. */
1099
1103
  export type ModelAssets = Record<string, ModelAsset>
1100
1104
 
1105
+ /**
1106
+ * Configuration for a controller's asset upload route, merged over the model's
1107
+ * asset definition of the same data path.
1108
+ */
1109
+ export type ControllerAsset = ModelAsset & {
1110
+ transacted?: boolean
1111
+ }
1112
+
1113
+ /**
1114
+ * Map of data paths, with support for `*` and `**` wildcards, to their asset
1115
+ * upload route configurations.
1116
+ */
1117
+ export type ControllerAssets = {
1118
+ [dataPath: string]:
1119
+ | ControllerAsset
1120
+ | OrReadOnly<string[]>
1121
+ | ControllerAssetsAuthorize
1122
+ | undefined
1123
+ allow?: OrReadOnly<string[]>
1124
+ authorize?: ControllerAssetsAuthorize
1125
+ }
1126
+
1127
+ type ControllerAssetsAuthorize = Authorize | Record<string, Authorize>
1128
+
1101
1129
  export interface ModelOptions extends objection.ModelOptions {
1102
1130
  graph?: boolean
1103
1131
  async?: boolean
@@ -1134,7 +1162,20 @@ export type ModelHooks<$Model extends Model = Model> = {
1134
1162
  }`]?: ModelHookFunction<$Model>
1135
1163
  }
1136
1164
 
1137
- export class Model extends objection.Model {
1165
+ // objection's static side, minus the members that dito types with its own
1166
+ // `Model` and `QueryBuilder`. objection's versions use its own types, which
1167
+ // these can't be compatible with, e.g. modifiers receive objection's
1168
+ // QueryBuilder, while they always receive dito's at runtime.
1169
+ interface ObjectionModelStatic
1170
+ extends Omit<
1171
+ typeof objection.Model,
1172
+ 'modifiers' | 'query' | 'fromJson' | 'createNotFoundError'
1173
+ > {
1174
+ new (): objection.Model
1175
+ }
1176
+ declare const ObjectionModel: ObjectionModelStatic
1177
+
1178
+ export class Model extends ObjectionModel {
1138
1179
  static query<M extends Model>(
1139
1180
  this: Constructor<M>,
1140
1181
  trxOrKnex?: objection.TransactionOrKnex
@@ -1232,7 +1273,10 @@ export class Model extends objection.Model {
1232
1273
 
1233
1274
  static get jsonAttributes(): string[]
1234
1275
  static get booleanAttributes(): string[]
1276
+ /** The `date` attributes, stored and returned as plain dates. */
1235
1277
  static get dateAttributes(): string[]
1278
+ /** The `datetime` and `timestamp` attributes. */
1279
+ static get datetimeAttributes(): string[]
1236
1280
  static get computedAttributes(): string[]
1237
1281
  static get hiddenAttributes(): string[]
1238
1282
 
@@ -1240,8 +1284,6 @@ export class Model extends objection.Model {
1240
1284
  static app: Application<Models>
1241
1285
  /** Whether the model has been initialized by the application. */
1242
1286
  static initialized: boolean
1243
- /** The QueryBuilder class used by this model. */
1244
- static QueryBuilder: typeof QueryBuilder
1245
1287
  /** Whether to deep-clone object attributes on read. */
1246
1288
  static cloneObjectAttributes: boolean
1247
1289
  /**
@@ -1499,6 +1541,8 @@ export class Model extends objection.Model {
1499
1541
  $is(model: Model | null | undefined): boolean
1500
1542
  /** Returns `true` if all named properties are defined. */
1501
1543
  $has(...properties: string[]): boolean
1544
+ /** Returns the knex instance, as in objection. */
1545
+ $transaction(): Knex
1502
1546
  /** Runs a callback within a transaction. */
1503
1547
  $transaction(
1504
1548
  handler: (trx: objection.Transaction) => Promise<any>
@@ -1885,7 +1929,7 @@ export class Controller {
1885
1929
  base?: any,
1886
1930
  options?: {
1887
1931
  query?: Record<string, any>
1888
- modify?: (query: QueryBuilder<Model>) => QueryBuilder<Model>
1932
+ modify?: (query: QueryBuilder<any>) => QueryBuilder<any>
1889
1933
  forUpdate?: boolean
1890
1934
  }
1891
1935
  ): Promise<Model | null>
@@ -1985,16 +2029,20 @@ export interface Controller extends EventEmitter {}
1985
2029
  /** A named action parameter with a JSON Schema definition. */
1986
2030
  export type ActionParameter = Schema & { name: string }
1987
2031
 
2032
+ // Handlers are declared through a method, to make their parameters, including
2033
+ // `this`, bivariant, so that controller subclasses remain assignable to their
2034
+ // base classes, e.g. in `ApplicationControllers`.
2035
+ type ControllerHandler<$Controller, $Args extends any[], $Result = any> = {
2036
+ bivarianceHack(this: $Controller, ...args: $Args): $Result
2037
+ }['bivarianceHack']
2038
+
1988
2039
  /**
1989
2040
  * Handler function for a model controller action. Receives
1990
2041
  * the Koa context and any resolved action parameters. `this`
1991
2042
  * is bound to the controller instance.
1992
2043
  */
1993
- export type ModelControllerActionHandler<$ModelController = ModelController> = (
1994
- this: $ModelController,
1995
- ctx: KoaContext,
1996
- ...args: any[]
1997
- ) => any
2044
+ export type ModelControllerActionHandler<$ModelController = ModelController> =
2045
+ ControllerHandler<$ModelController, [ctx: KoaContext, ...args: any[]]>
1998
2046
 
1999
2047
  /**
2000
2048
  * Handler function for a controller action. Receives the Koa
@@ -2005,8 +2053,8 @@ export type ControllerActionHandler<
2005
2053
  $Controller extends Controller = Controller,
2006
2054
  $Params = Record<string, any>
2007
2055
  > = keyof $Params extends never
2008
- ? (this: $Controller, ctx: KoaContext) => any
2009
- : (this: $Controller, ctx: KoaContext, params: $Params) => any
2056
+ ? ControllerHandler<$Controller, [ctx: KoaContext]>
2057
+ : ControllerHandler<$Controller, [ctx: KoaContext, params: $Params]>
2010
2058
 
2011
2059
  type ModelDataKey<T, K extends keyof T> = K extends
2012
2060
  | 'QueryBuilderType'
@@ -2171,7 +2219,9 @@ export type ModelControllerActions<$ModelController = ModelController> = {
2171
2219
  authorize?: Authorize
2172
2220
  }
2173
2221
 
2174
- type ModelControllerMemberAction<$ModelController = ModelController> =
2222
+ type ModelControllerMemberAction<
2223
+ $ModelController extends ModelControllerLike = ModelController
2224
+ > =
2175
2225
  | (Omit<ModelControllerActionOptions<$ModelController>, 'parameters'> & {
2176
2226
  parameters?: {
2177
2227
  [key: string]: MemberActionParameter<
@@ -2187,7 +2237,9 @@ type ModelControllerMemberAction<$ModelController = ModelController> =
2187
2237
  * `delete`). Member actions can use `{ from: 'member' }`
2188
2238
  * parameters to receive the resolved member model.
2189
2239
  */
2190
- export type ModelControllerMemberActions<$ModelController = ModelController> = {
2240
+ export type ModelControllerMemberActions<
2241
+ $ModelController extends ModelControllerLike = ModelController
2242
+ > = {
2191
2243
  [name: ControllerActionName]: ModelControllerMemberAction<$ModelController>
2192
2244
  allow?: OrReadOnly<ControllerActionName[]>
2193
2245
  authorize?: Authorize
@@ -2306,38 +2358,39 @@ type AfterCollectionGetHookKey = `after:collection:get`
2306
2358
  type AfterCustomHookKey =
2307
2359
  `after:${ModelControllerHookType | '*'}:${ControllerActionName | '*'}`
2308
2360
 
2309
- export type ModelControllerHooks<$ModelController = ModelController> = {
2310
- [$Key in BeforeHookKey]?: (
2311
- this: $ModelController,
2312
- ctx: KoaContext,
2313
- params?: Record<string, any>
2314
- ) => void
2361
+ export type ModelControllerHooks<
2362
+ $ModelController extends ModelControllerLike = ModelController
2363
+ > = {
2364
+ [$Key in BeforeHookKey]?: ControllerHandler<
2365
+ $ModelController,
2366
+ [ctx: KoaContext, params?: Record<string, any>],
2367
+ void
2368
+ >
2315
2369
  } & {
2316
- [$Key in AfterDeleteHookKey]?: (
2317
- this: $ModelController,
2318
- ctx: KoaContext,
2319
- result: { count: number }
2320
- ) => any
2370
+ [$Key in AfterDeleteHookKey]?: ControllerHandler<
2371
+ $ModelController,
2372
+ [ctx: KoaContext, result: { count: number }]
2373
+ >
2321
2374
  } & {
2322
- [$Key in AfterItemHookKey]?: (
2323
- this: $ModelController,
2324
- ctx: KoaContext,
2325
- item: ModelFromModelController<$ModelController>
2326
- ) => any
2375
+ [$Key in AfterItemHookKey]?: ControllerHandler<
2376
+ $ModelController,
2377
+ [ctx: KoaContext, item: ModelFromModelController<$ModelController>]
2378
+ >
2327
2379
  } & {
2328
- [$Key in AfterCollectionGetHookKey]?: (
2329
- this: $ModelController,
2330
- ctx: KoaContext,
2331
- result:
2332
- | ModelFromModelController<$ModelController>[]
2333
- | Page<ModelFromModelController<$ModelController>>
2334
- ) => any
2380
+ [$Key in AfterCollectionGetHookKey]?: ControllerHandler<
2381
+ $ModelController,
2382
+ [
2383
+ ctx: KoaContext,
2384
+ result:
2385
+ | ModelFromModelController<$ModelController>[]
2386
+ | Page<ModelFromModelController<$ModelController>>
2387
+ ]
2388
+ >
2335
2389
  } & {
2336
- [$Key in AfterCustomHookKey]?: (
2337
- this: $ModelController,
2338
- ctx: KoaContext,
2339
- result: any
2340
- ) => any
2390
+ [$Key in AfterCustomHookKey]?: ControllerHandler<
2391
+ $ModelController,
2392
+ [ctx: KoaContext, result: any]
2393
+ >
2341
2394
  }
2342
2395
 
2343
2396
  /**
@@ -2520,12 +2573,11 @@ export class ModelController<
2520
2573
  * object to assign them to the member.
2521
2574
  */
2522
2575
  member?: ModelControllerMemberActions<this>
2523
- assets?:
2524
- | boolean
2525
- | {
2526
- allow?: OrArrayOf<string>
2527
- authorize: Record<string, OrArrayOf<string>>
2528
- }
2576
+ /**
2577
+ * The asset upload routes, by data path, or `true` to use the model's
2578
+ * asset definitions.
2579
+ */
2580
+ assets?: boolean | ControllerAssets
2529
2581
 
2530
2582
  /**
2531
2583
  * Lifecycle hooks that run before or after controller
@@ -2577,8 +2629,31 @@ export class ModelController<
2577
2629
  * @see {@link QueryParameterOptions} for pagination parameters
2578
2630
  */
2579
2631
  hooks?: ModelControllerHooks<this>
2580
- /** Map of relation name to RelationController instance. */
2581
- relations?: Record<string, RelationController>
2632
+ /**
2633
+ * The relations to expose as nested routes, by relation name. After setup,
2634
+ * each configuration is replaced with its RelationController instance.
2635
+ */
2636
+ relations?: Record<string, ModelControllerRelation | RelationController>
2637
+ }
2638
+
2639
+ /**
2640
+ * Configuration of a relation route on a model controller, from which the
2641
+ * {@link RelationController} is created. The relation's collection actions are
2642
+ * provided as `relation`, to make sense for both one- and many-relations.
2643
+ */
2644
+ export type ModelControllerRelation = Partial<
2645
+ Pick<
2646
+ RelationController,
2647
+ | 'graph'
2648
+ | 'transacted'
2649
+ | 'scope'
2650
+ | 'allowScope'
2651
+ | 'allowFilter'
2652
+ | 'allowParam'
2653
+ >
2654
+ > & {
2655
+ relation?: ModelControllerActions<RelationController>
2656
+ member?: ModelControllerMemberActions<RelationController>
2582
2657
  }
2583
2658
 
2584
2659
  /**
@@ -3003,7 +3078,7 @@ export class QueryBuilder<
3003
3078
  R = M[]
3004
3079
  > extends objection.QueryBuilder<M, R> {
3005
3080
  /** Clones the query with scope/filter state. */
3006
- clone(): QueryBuilder<M, R>
3081
+ clone(): this
3007
3082
  /**
3008
3083
  * Inherits scopes from a parent query.
3009
3084
  * @override
@@ -3142,7 +3217,11 @@ export class QueryBuilder<
3142
3217
  options?: DitoGraphOptions
3143
3218
  ): this
3144
3219
 
3145
- truncate(options?: { restart?: boolean; cascade?: boolean }): this
3220
+ // Returns the query builder, which objection declares as `Promise<void>`.
3221
+ truncate(options?: {
3222
+ restart?: boolean
3223
+ cascade?: boolean
3224
+ }): this & Promise<void>
3146
3225
 
3147
3226
  ArrayQueryBuilderType: QueryBuilder<M, M[]>
3148
3227
  SingleQueryBuilderType: QueryBuilder<M, M>
@@ -3212,8 +3291,8 @@ export type PartialModelObject<T extends Model> = {
3212
3291
  export type PartialDitoModelGraph<M extends Partial<Model>> = {
3213
3292
  [K in objection.NonFunctionPropertyNames<M>]?: objection.Defined<
3214
3293
  M[K]
3215
- > extends Model
3216
- ? PartialDitoModelGraph<M[K]>
3294
+ > extends infer D extends Model
3295
+ ? PartialDitoModelGraph<D>
3217
3296
  : objection.Defined<M[K]> extends Array<infer I>
3218
3297
  ? I extends Partial<Model>
3219
3298
  ? PartialDitoModelGraph<I>[]
@@ -3606,11 +3685,11 @@ export function convertRelation(
3606
3685
  ): Record<string, any>
3607
3686
 
3608
3687
  export function getRelationClass(
3609
- relation: string | typeof objection.Relation
3610
- ): typeof objection.Relation | null
3688
+ relation: string | objection.RelationType
3689
+ ): objection.RelationType | null
3611
3690
 
3612
3691
  export function isThroughRelationClass(
3613
- relationClass: typeof objection.Relation
3692
+ relationClass: objection.RelationType
3614
3693
  ): boolean
3615
3694
 
3616
3695
  export function addRelationSchemas(
@@ -3682,11 +3761,25 @@ export interface KoaContextState {
3682
3761
  [key: string]: unknown
3683
3762
  }
3684
3763
 
3764
+ /**
3765
+ * The session on `ctx.session`, mirroring koa-session's `Session` class, which
3766
+ * it doesn't export.
3767
+ */
3768
+ export type KoaSession = Record<string, any> & {
3769
+ isNew: boolean
3770
+ readonly length: number
3771
+ readonly populated: boolean
3772
+ maxAge: number
3773
+ save(callback?: (error?: Error) => void): Promise<void> | undefined
3774
+ regenerate(callback?: (error?: Error) => void): Promise<void> | undefined
3775
+ manuallyCommit(): Promise<void> | undefined
3776
+ }
3777
+
3685
3778
  export type KoaContext<$State = KoaContextState> = Koa.ParameterizedContext<
3686
3779
  $State,
3687
3780
  {
3688
3781
  transaction: objection.Transaction
3689
- session: koaSession.ContextSession
3782
+ session: KoaSession | null
3690
3783
  logger: PinoLogger
3691
3784
  }
3692
3785
  > & {
@@ -3701,9 +3794,10 @@ type OrReadOnly<T> = Readonly<T> | T
3701
3794
 
3702
3795
  type OrPromiseOf<T> = Promise<T> | T
3703
3796
 
3704
- type ModelFromModelController<
3705
- $ModelController extends { modelClass?: Class<any> }
3706
- > = InstanceType<Exclude<$ModelController['modelClass'], undefined>>
3797
+ type ModelControllerLike = { modelClass?: Class<Model> }
3798
+
3799
+ type ModelFromModelController<$ModelController extends ModelControllerLike> =
3800
+ InstanceType<Exclude<$ModelController['modelClass'], undefined>>
3707
3801
 
3708
3802
  type SerializeModelPropertyValue<T> = T extends (infer U)[]
3709
3803
  ? SerializeModelPropertyValue<U>[]