@ditojs/server 2.93.0 → 2.94.1

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/types/index.d.ts CHANGED
@@ -50,15 +50,31 @@ export type CompiledParametersValidator = {
50
50
  hasModelRefs: boolean
51
51
  }
52
52
 
53
- export type ApplicationConfig = {
53
+ /**
54
+ * Application configuration passed to the Application constructor.
55
+ *
56
+ * Extend via module augmentation for project-specific config:
57
+ *
58
+ * ```ts
59
+ * declare module '@ditojs/server' {
60
+ * interface ApplicationConfig {
61
+ * externalUrls?: {
62
+ * frontend: string
63
+ * backend: string
64
+ * }
65
+ * }
66
+ * }
67
+ * ```
68
+ */
69
+ export interface ApplicationConfig {
54
70
  /** @defaultValue `production` */
55
- env?: 'production' | 'development'
71
+ env?: LiteralUnion<'production' | 'development'>
56
72
  /** The server configuration */
57
73
  server?: {
58
74
  /** The ip address or hostname used to serve requests */
59
75
  host?: string
60
76
  /** The port to listen on for connections */
61
- port?: string
77
+ port?: string | number
62
78
  }
63
79
  /** Logging options */
64
80
  log?: {
@@ -184,7 +200,9 @@ export type ApplicationConfig = {
184
200
  keys?: Koa['keys']
185
201
  }
186
202
  admin?: AdminConfig
187
- knex?: Knex.Config<any> & {
203
+ knex?: Omit<Knex.Config<any>, 'connection'> & {
204
+ /** Knex connection config, or a custom adapter object (e.g. PGlite). */
205
+ connection?: Knex.Config<any>['connection'] | Record<string, unknown>
188
206
  /** @defaultValue `false` */
189
207
  normalizeDbNames?: boolean | Parameters<KnexSnakeCaseMappersFactory>
190
208
  // See https://github.com/brianc/node-pg-types/blob/master/index.d.ts#L67
@@ -268,6 +286,13 @@ export interface DiskStorageConfig extends CommonStorageConfig {
268
286
  * The path to the directory where assets are stored on.
269
287
  */
270
288
  path: string
289
+ /**
290
+ * Whether to store files in a two-level nested folder structure using the
291
+ * first two characters of the file key.
292
+ *
293
+ * @defaultValue true
294
+ */
295
+ nestedFolders?: boolean
271
296
  }
272
297
 
273
298
  export type StorageConfig = S3StorageConfig | DiskStorageConfig
@@ -384,14 +409,20 @@ export class Application<$Models extends Models = Models> {
384
409
  add(
385
410
  method: string,
386
411
  path: string,
387
- handler: Function
412
+ handler: (
413
+ ctx: KoaContext,
414
+ next: () => Promise<void>
415
+ ) => OrPromiseOf<void>
388
416
  ): this
389
417
  find(
390
418
  method: string,
391
419
  path: string
392
420
  ): {
393
421
  status: number
394
- handler?: Function
422
+ handler?: (
423
+ ctx: KoaContext,
424
+ next: () => Promise<void>
425
+ ) => OrPromiseOf<void>
395
426
  params?: Record<string, string>
396
427
  allowed: string[]
397
428
  }
@@ -429,9 +460,9 @@ export class Application<$Models extends Models = Models> {
429
460
  /** The schema validator instance. */
430
461
  validator: Validator
431
462
  /** Registered storage instances by name. */
432
- storages: Record<string, Storage>
463
+ storages: ResolvedStorages
433
464
  /** Registered service instances by name. */
434
- services: Record<string, Service>
465
+ services: ResolvedServices
435
466
  /** Registered controller instances by name. */
436
467
  controllers: Record<string, Controller>
437
468
  models: $Models
@@ -461,7 +492,11 @@ export class Application<$Models extends Models = Models> {
461
492
  addStorages(storages: StorageConfigs): void
462
493
  setupStorages(): Promise<void>
463
494
  /** Returns a storage by name, or `null` if not found. */
464
- getStorage(name: string): Storage | null
495
+ getStorage<K extends string>(
496
+ name: K
497
+ ): K extends keyof ResolvedStorages
498
+ ? ResolvedStorages[K]
499
+ : Storage | null
465
500
 
466
501
  addService(
467
502
  service: Service | Class<Service>,
@@ -471,7 +506,12 @@ export class Application<$Models extends Models = Models> {
471
506
  addServices(services: Services): void
472
507
  setupServices(): Promise<void>
473
508
  /** Returns a service by name. */
474
- getService(name: string): Service | null
509
+ getService<K extends string>(
510
+ name: K
511
+ ): K extends keyof ResolvedServices
512
+ ? ResolvedServices[K]
513
+ : Service | null
514
+
475
515
  /** Finds a service matching the given predicate. */
476
516
  findService(
477
517
  callback: (service: Service) => boolean
@@ -620,7 +660,11 @@ export class Application<$Models extends Models = Models> {
620
660
  method: HTTPMethod,
621
661
  path: string,
622
662
  transacted: boolean,
623
- middlewares: OrArrayOf<(ctx: KoaContext, next: Function) => void>,
663
+ middlewares: OrArrayOf< (
664
+ ctx: KoaContext,
665
+ next: () => Promise<void>
666
+ ) => OrPromiseOf<void>
667
+ >,
624
668
  controller?: Controller | null,
625
669
  action?: any
626
670
  ): void
@@ -658,6 +702,7 @@ export interface Application
658
702
 
659
703
  export type SchemaType = LiteralUnion<
660
704
  | 'string'
705
+ | 'text'
661
706
  | 'number'
662
707
  | 'integer'
663
708
  | 'boolean'
@@ -669,27 +714,105 @@ export type SchemaType = LiteralUnion<
669
714
  | 'timestamp'
670
715
  >
671
716
 
672
- export interface ModelRelation {
717
+ type RelatedModel<T> = T extends (infer U)[]
718
+ ? U extends Model
719
+ ? U
720
+ : never
721
+ : T extends Model
722
+ ? T
723
+ : never
724
+
725
+ type ModelName<T extends Model, $Models, R = ResolveModels<$Models>> = {
726
+ [K in keyof R]: R[K] extends Class<T> ? K : never
727
+ }[keyof R]
728
+
729
+ type ResolveModels<$Models> = $Models extends Promise<infer M> ? M : $Models
730
+
731
+ type OnlyModels<$Models, R = ResolveModels<$Models>> = {
732
+ [K in keyof R as R[K] extends ModelClass ? K : never]: R[K]
733
+ }
734
+
735
+ type HasModels<$Models> = keyof OnlyModels<$Models> extends never ? false : true
736
+
737
+ type ModelRef<T extends Model, $Models> =
738
+ HasModels<$Models> extends true
739
+ ? `${ModelName<T, $Models> & string}.${keyof SerializedModel<T> & string}`
740
+ : string
741
+
742
+ type AnyModelRef<$Models> =
743
+ HasModels<$Models> extends true
744
+ ? `${string}.${string}`
745
+ : string
746
+
747
+ /**
748
+ * The supported relation types for model associations.
749
+ *
750
+ * - `'belongsTo'` — single model, foreign key on this table
751
+ * - `'hasOne'` — single model, foreign key on the other table
752
+ * - `'hasMany'` — array of models, foreign key on the other table
753
+ * - `'manyToMany'` — array of models, via a join table
754
+ * - `'hasOneThrough'` — single model, via a join table
755
+ *
756
+ * @see {@link https://github.com/ditojs/dito/blob/main/docs/model-relations.md#relation-types|Relation Types}
757
+ */
758
+ export type RelationType =
759
+ | 'belongsTo'
760
+ | 'hasMany'
761
+ | 'hasOne'
762
+ | 'manyToMany'
763
+ | 'hasOneThrough'
764
+
765
+ type RelationTypeForProperty<T> = T extends Model[]
766
+ ? 'hasMany' | 'manyToMany'
767
+ : T extends Model
768
+ ? 'belongsTo' | 'hasOne' | 'hasOneThrough'
769
+ : RelationType
770
+
771
+ /**
772
+ * A model relation definition describing how two models are associated.
773
+ *
774
+ * When used with type arguments, provides type-safe `from`/`to` strings
775
+ * and narrows `relation` based on the property type (array vs single
776
+ * model).
777
+ *
778
+ * @example
779
+ * ```ts
780
+ * // Untyped — accepts any relation definition:
781
+ * const rel: ModelRelation = {
782
+ * relation: 'hasMany',
783
+ * from: 'Tag.id',
784
+ * to: 'Task.tagId'
785
+ * }
786
+ * ```
787
+ *
788
+ * @see {@link https://github.com/ditojs/dito/blob/main/docs/model-relations.md|Model Relations}
789
+ */
790
+ export interface ModelRelation<
791
+ $Owner extends Model = Model,
792
+ $Related extends Model = Model,
793
+ $Models = unknown,
794
+ $PropertyType = unknown
795
+ > {
673
796
  /**
674
797
  * The type of relation
675
798
  *
676
799
  * @see {@link https://github.com/ditojs/dito/blob/main/docs/model-relations.md#relation-types|Relation Types}
677
800
  */
678
- relation: LiteralUnion<
679
- 'belongsTo' | 'hasMany' | 'hasOne' | 'manyToMany' | 'hasOneThrough'
680
- >
801
+ relation: unknown extends $PropertyType
802
+ ? RelationType
803
+ : RelationTypeForProperty<$PropertyType>
681
804
  /**
682
805
  * The model and property name from which the relation is to be built, as a
683
806
  * string with both identifiers separated by '.', e.g.:
684
807
  * 'FromModelClass.fromPropertyName'
685
808
  */
686
- from: string
809
+ from: ModelRef<$Owner, $Models>
687
810
  /**
688
811
  * The model and property name to which the relation is to be built, as a
689
812
  * string with both identifiers separated by '.', e.g.:
690
813
  * 'ToModelClass.toPropertyName'
691
814
  */
692
- to: string
815
+ to: ModelRef<$Related, $Models>
693
816
  /**
694
817
  * When set to true the join model class and table is to be built
695
818
  * automatically, or allows to specify an existing one manually.
@@ -705,14 +828,14 @@ export interface ModelRelation {
705
828
  * be built, as a string with both identifiers separated by '.', e.g.:
706
829
  * 'FromModelClass.fromPropertyName'
707
830
  */
708
- from: string
831
+ from: AnyModelRef<$Models>
709
832
  /**
710
833
  * The model and property name or table and column name of an existing
711
834
  * join model class or join table to which the through relation is to be
712
835
  * built, as a string with both identifiers separated by '.', e.g.:
713
836
  * 'toModelClass.toPropertyName'
714
837
  */
715
- to: string
838
+ to: AnyModelRef<$Models>
716
839
  /**
717
840
  * List additional columns to be added to the related model.
718
841
  *
@@ -733,11 +856,17 @@ export interface ModelRelation {
733
856
  */
734
857
  inverse?: boolean
735
858
  /**
736
- * Optionally, a scope can be defined to be applied when loading the
737
- * relation's models. The scope needs to be defined in the related model
738
- * class' scopes definitions.
859
+ * Optionally overrides the related model class derived from
860
+ * the `to` property. Can be specified as the name the model
861
+ * was registered with or the model class itself.
862
+ */
863
+ modelClass?: string | Class<$Related>
864
+ /**
865
+ * Optionally, one or more scopes can be defined to be applied when
866
+ * loading the relation's models. The scopes need to be defined in the
867
+ * related model class' scopes definitions.
739
868
  */
740
- scope?: string
869
+ scope?: OrArrayOf<string>
741
870
  /**
742
871
  * Optionally, a filter to apply when loading the relation's models.
743
872
  * Accepts a Dito.js filter name/object (resolved via the related model's
@@ -818,7 +947,7 @@ export type ModelProperty<T = any> = Schema<T> & {
818
947
  */
819
948
  computed?: boolean
820
949
  /**
821
- * Marks the property has hidden, so that it does not show up in data
950
+ * Marks the property as hidden, so that it does not show up in data
822
951
  * converted to JSON.
823
952
  *
824
953
  * This can be used for sensitive data.
@@ -850,6 +979,24 @@ export type ModelScopes<$Model extends Model = Model> = Record<
850
979
  ModelScope<$Model>
851
980
  >
852
981
 
982
+ /**
983
+ * A query modifier function that transforms a query builder.
984
+ * Modifiers are reusable query fragments applied via
985
+ * `.modify('name')` on queries.
986
+ */
987
+ export type ModelModifier<$Model extends Model = Model> = (
988
+ query: QueryBuilder<$Model>,
989
+ ...args: any[]
990
+ ) => void
991
+
992
+ /**
993
+ * Map of modifier names to modifier functions.
994
+ */
995
+ export type ModelModifiers<$Model extends Model = Model> = Record<
996
+ string,
997
+ ModelModifier<$Model>
998
+ >
999
+
853
1000
  /**
854
1001
  * A filter handler function that modifies a query builder
855
1002
  * based on external parameters (e.g. from URL query strings).
@@ -859,6 +1006,45 @@ export type ModelFilterFunction<$Model extends Model = Model> = (
859
1006
  ...args: any[]
860
1007
  ) => void
861
1008
 
1009
+ /**
1010
+ * Registry of known filter type names for use with
1011
+ * `{ filter: '...', properties: [...] }` in model filter definitions.
1012
+ *
1013
+ * Filter types are reusable query filters registered via
1014
+ * `QueryFilters.register()`. Dito.js ships with two built-in types:
1015
+ * - `'text'` — text search with operators (contains, equals, etc.)
1016
+ * - `'date-range'` — date range filtering with from/to parameters
1017
+ *
1018
+ * Register custom filter types at runtime with
1019
+ * `QueryFilters.register()`, then declare them for type safety
1020
+ * via module augmentation:
1021
+ *
1022
+ * ```ts
1023
+ * // Register at runtime:
1024
+ * QueryFilters.register({
1025
+ * 'country': {
1026
+ * parameters: { code: { type: 'string' } },
1027
+ * handler(query, property, { code }) {
1028
+ * query.where(property, code)
1029
+ * }
1030
+ * }
1031
+ * })
1032
+ *
1033
+ * // Declare for type safety:
1034
+ * declare module '@ditojs/server' {
1035
+ * interface QueryFilterTypes {
1036
+ * 'country': true
1037
+ * }
1038
+ * }
1039
+ * ```
1040
+ *
1041
+ * @see {@link https://github.com/ditojs/dito/blob/main/docs/model-filters.md|Model Filters}
1042
+ */
1043
+ export interface QueryFilterTypes {
1044
+ 'text': true
1045
+ 'date-range': true
1046
+ }
1047
+
862
1048
  /**
863
1049
  * A model filter definition. Can be one of:
864
1050
  *
@@ -873,7 +1059,7 @@ export type ModelFilterFunction<$Model extends Model = Model> = (
873
1059
  */
874
1060
  export type ModelFilter<$Model extends Model = Model> =
875
1061
  | {
876
- filter: LiteralUnion<'text' | 'date-range'>
1062
+ filter: keyof QueryFilterTypes
877
1063
  properties?: string[]
878
1064
  }
879
1065
  | {
@@ -949,6 +1135,11 @@ export type ModelHooks<$Model extends Model = Model> = {
949
1135
  }
950
1136
 
951
1137
  export class Model extends objection.Model {
1138
+ static query<M extends Model>(
1139
+ this: Constructor<M>,
1140
+ trxOrKnex?: objection.TransactionOrKnex
1141
+ ): QueryBuilder<M, M[]>
1142
+
952
1143
  constructor(json?: Record<string, any>)
953
1144
 
954
1145
  /** @see {@link https://github.com/ditojs/dito/blob/main/docs/model-properties.md|Model Properties} */
@@ -963,6 +1154,47 @@ export class Model extends objection.Model {
963
1154
  /** @see {@link https://github.com/ditojs/dito/blob/main/docs/model-filters.md|Model Filters} */
964
1155
  static filters: ModelFilters<Model>
965
1156
 
1157
+ /**
1158
+ * Reusable named query fragments applied via
1159
+ * `.modify('name')` on query builders. Commonly used
1160
+ * to define shared select sets or filter conditions.
1161
+ *
1162
+ * @example
1163
+ * ```ts
1164
+ * // Define modifiers on a model:
1165
+ * static modifiers = {
1166
+ * sharedSelects: query =>
1167
+ * query.select('id', 'name', 'status'),
1168
+ * whereActive: query =>
1169
+ * query.where('active', true)
1170
+ * }
1171
+ *
1172
+ * // Use them on queries via QueryBuilder.modify():
1173
+ * query.modify('sharedSelects').modify('whereActive')
1174
+ * ```
1175
+ */
1176
+ static modifiers: ModelModifiers<Model>
1177
+
1178
+ /**
1179
+ * Additional JSON Schema properties that are deep-merged
1180
+ * into the model's compiled schema (built from
1181
+ * {@link Model.properties}). Used for example to add
1182
+ * a custom `validate` function for cross-field validation.
1183
+ *
1184
+ * @example
1185
+ * ```ts
1186
+ * static schema: Schema = {
1187
+ * validate({ data, options }) {
1188
+ * if (!data.name) {
1189
+ * throw new Error('Name is required')
1190
+ * }
1191
+ * return true
1192
+ * }
1193
+ * }
1194
+ * ```
1195
+ */
1196
+ static schema: Schema
1197
+
966
1198
  static hooks: ModelHooks<Model>
967
1199
 
968
1200
  static assets: ModelAssets
@@ -976,15 +1208,8 @@ export class Model extends objection.Model {
976
1208
  hooks: ModelHooks
977
1209
  assets: ModelAssets
978
1210
  options: Record<string, any>
979
- modifiers: Record<
980
- string,
981
- (
982
- builder: objection.QueryBuilder<any, any>,
983
- ...args: any[]
984
- ) => void
985
- >
986
- schema: Record<string, any>
987
- [key: string]: any
1211
+ modifiers: ModelModifiers
1212
+ schema: Schema
988
1213
  }
989
1214
 
990
1215
  /** Derived from class name (removes 'Model' suffix). */
@@ -1068,13 +1293,7 @@ export class Model extends objection.Model {
1068
1293
  /** Returns the named property definition, if found. */
1069
1294
  static getProperty(name: string): ModelProperty | null
1070
1295
  /** Returns the model's query modifiers. */
1071
- static getModifiers(): Record<
1072
- string,
1073
- (
1074
- builder: objection.QueryBuilder<any, any>,
1075
- ...args: any[]
1076
- ) => void
1077
- >
1296
+ static getModifiers(): ModelModifiers
1078
1297
 
1079
1298
  /**
1080
1299
  * Returns property names matching the given filter
@@ -1268,20 +1487,6 @@ export class Model extends objection.Model {
1268
1487
  trx?: objection.Transaction
1269
1488
  ): Promise<Model | Model[]>
1270
1489
 
1271
- /**
1272
- * Dito.js automatically adds an `id` property if a model
1273
- * property with the `primary: true` setting is not
1274
- * already explicitly defined.
1275
- */
1276
- readonly id: Id
1277
-
1278
- /**
1279
- * Dito.js automatically adds a `foreignKeyId` property
1280
- * if foreign keys occurring in relations definitions are
1281
- * not explicitly defined in the properties.
1282
- */
1283
- readonly foreignKeyId: Id
1284
-
1285
1490
  QueryBuilderType: QueryBuilder<this, this[]>
1286
1491
 
1287
1492
  $app: Application<Models>
@@ -1506,20 +1711,112 @@ export interface Model extends KnexHelper {}
1506
1711
 
1507
1712
  export type ModelClass = Class<Model>
1508
1713
 
1509
- export type ModelRelations = Record<string, ModelRelation>
1714
+ type ModelRelationKey<T, K extends keyof T> = K extends
1715
+ | 'QueryBuilderType'
1716
+ | `$${string}`
1717
+ ? never
1718
+ : T[K] extends (...args: any[]) => any
1719
+ ? never
1720
+ : T[K] extends Model | Model[] | undefined
1721
+ ? K
1722
+ : never
1723
+
1724
+ /**
1725
+ * A map of relation definitions for a model class.
1726
+ *
1727
+ * When used with a type argument, restricts keys to declared
1728
+ * `Model` or `Model[]` properties on the class.
1729
+ *
1730
+ * @example
1731
+ * ```ts
1732
+ * // Type-safe keys only:
1733
+ * class Tag extends Model {
1734
+ * declare tasks: Task[]
1735
+ *
1736
+ * static override relations: ModelRelations<Tag> = {
1737
+ * tasks: {
1738
+ * relation: 'manyToMany',
1739
+ * from: 'Tag.id',
1740
+ * to: 'Task.id'
1741
+ * }
1742
+ * }
1743
+ * }
1744
+ *
1745
+ * // With typed from/to via typeof import():
1746
+ * class Tag extends Model {
1747
+ * declare tasks: Task[]
1748
+ *
1749
+ * static override relations:
1750
+ * ModelRelations<Tag, typeof import('./models')> = {
1751
+ * tasks: {
1752
+ * relation: 'manyToMany',
1753
+ * from: 'Tag.id', // validated against Tag props
1754
+ * to: 'Task.id' // validated against Task props
1755
+ * }
1756
+ * }
1757
+ * }
1758
+ * ```
1759
+ */
1760
+ export type ModelRelations<$Model extends Model = Model, $Models = unknown> = [
1761
+ keyof SerializedModel<$Model>
1762
+ ] extends [never]
1763
+ ? Record<string, ModelRelation>
1764
+ : {
1765
+ [K in keyof $Model as ModelRelationKey<$Model, K>]?: ModelRelation<
1766
+ $Model,
1767
+ RelatedModel<$Model[K]>,
1768
+ $Models,
1769
+ $Model[K]
1770
+ >
1771
+ }
1510
1772
 
1511
- export type ModelProperties = Record<string, ModelProperty>
1773
+ export type ModelProperties<$Model extends Model = Model> = [
1774
+ keyof SerializedModel<$Model>
1775
+ ] extends [never]
1776
+ ? Record<string, ModelProperty>
1777
+ : {
1778
+ [K in keyof $Model as ModelDataKey<$Model, K>]?: ModelProperty<$Model[K]>
1779
+ }
1512
1780
 
1513
1781
  /**
1514
- * A controller action definition. Either an options object
1515
- * with `handler`, `method`, `path`, `authorize`, etc., or a
1516
- * bare handler function. The HTTP method and path are
1517
- * derived from the action name key (e.g. `'postImport'`
1518
- * maps to `POST /import`).
1782
+ * A controller action definition. The HTTP method and route
1783
+ * path are derived from the action name key (e.g.
1784
+ * `'post import'` `POST /import`, `'get'` `GET /`).
1785
+ *
1786
+ * Can be either a bare handler function or an options
1787
+ * object with `handler`, `authorize`, `parameters`, etc.
1788
+ *
1789
+ * @example
1790
+ * ```ts
1791
+ * // Bare handler function:
1792
+ * actions = {
1793
+ * 'post import'(ctx) {
1794
+ * return this.importData(ctx.request.body)
1795
+ * }
1796
+ * }
1797
+ *
1798
+ * // Options object:
1799
+ * actions = {
1800
+ * 'post import': {
1801
+ * handler(ctx) { ... },
1802
+ * authorize: 'admin',
1803
+ * transacted: true
1804
+ * }
1805
+ * }
1806
+ *
1807
+ * // With typed parameters:
1808
+ * const action: ControllerAction<this, { query: string }> = {
1809
+ * parameters: { query: { type: 'string' } },
1810
+ * handler(ctx, { query }) { ... } // query: string
1811
+ * }
1812
+ * ```
1519
1813
  */
1520
- export type ControllerAction<$Controller extends Controller = Controller> =
1521
- | ControllerActionOptions<$Controller>
1522
- | ControllerActionHandler<$Controller>
1814
+ export type ControllerAction<
1815
+ $Controller extends Controller = Controller,
1816
+ $Params = Record<string, any>
1817
+ > =
1818
+ | ControllerActionOptions<$Controller, $Params>
1819
+ | ControllerActionHandler<$Controller, $Params>
1523
1820
 
1524
1821
  export class Controller {
1525
1822
  app: Application
@@ -1562,9 +1859,12 @@ export class Controller {
1562
1859
  logRoutes: boolean
1563
1860
 
1564
1861
  /**
1565
- * A list of allowed actions. If provided, only the
1566
- * action names listed here as strings will be mapped to
1567
- * routes, everything else will be omitted.
1862
+ * Which inherited actions to enable, e.g.
1863
+ * `['get', 'post', 'get stats']`. Actions defined
1864
+ * directly on the same object are always enabled.
1865
+ * When set, it replaces any parent controller's
1866
+ * `allow` list. When omitted, the parent's `allow`
1867
+ * list is inherited.
1568
1868
  */
1569
1869
  allow?: OrReadOnly<ControllerActionName[]>
1570
1870
 
@@ -1620,15 +1920,25 @@ export class Controller {
1620
1920
  transacted: boolean,
1621
1921
  authorize: Authorize,
1622
1922
  action: $ControllerAction,
1623
- handlers: ((ctx: KoaContext, next: Function) => void)[]
1923
+ handlers: ((
1924
+ ctx: KoaContext,
1925
+ next: () => Promise<void>
1926
+ ) => OrPromiseOf<void>)[]
1927
+ ): void
1928
+
1929
+ setupActions(
1930
+ type: 'actions' | 'collection' | 'member'
1931
+ ): Record<string, unknown> | undefined
1932
+
1933
+ setupActionRoute(
1934
+ type: 'actions' | 'collection' | 'member',
1935
+ action: ControllerAction
1624
1936
  ): void
1625
1937
 
1626
- setupActions(type: string): any
1627
- setupActionRoute(type: string, action: ControllerAction): void
1628
- setupAssets(): any
1938
+ setupAssets(): Record<string, unknown> | undefined
1629
1939
  setupAssetRoute(
1630
1940
  dataPath: OrArrayOf<string>,
1631
- config: any,
1941
+ config: Record<string, unknown>,
1632
1942
  authorize: Authorize
1633
1943
  ): void
1634
1944
 
@@ -1649,11 +1959,11 @@ export class Controller {
1649
1959
  }
1650
1960
 
1651
1961
  emitHook(
1652
- type: string,
1962
+ type: `${'before' | 'after'}:${string}`,
1653
1963
  handleResult: boolean,
1654
1964
  ctx: KoaContext,
1655
- ...args: any[]
1656
- ): Promise<any>
1965
+ ...args: unknown[]
1966
+ ): Promise<unknown>
1657
1967
 
1658
1968
  processAuthorize(
1659
1969
  authorize: Authorize
@@ -1680,9 +1990,11 @@ export type ActionParameter = Schema & { name: string }
1680
1990
  * the Koa context and any resolved action parameters. `this`
1681
1991
  * is bound to the controller instance.
1682
1992
  */
1683
- export type ModelControllerActionHandler<
1684
- $ModelController extends ModelController = ModelController
1685
- > = (this: $ModelController, ctx: KoaContext, ...args: any[]) => any
1993
+ export type ModelControllerActionHandler<$ModelController = ModelController> = (
1994
+ this: $ModelController,
1995
+ ctx: KoaContext,
1996
+ ...args: any[]
1997
+ ) => any
1686
1998
 
1687
1999
  /**
1688
2000
  * Handler function for a controller action. Receives the Koa
@@ -1690,12 +2002,14 @@ export type ModelControllerActionHandler<
1690
2002
  * bound to the controller instance.
1691
2003
  */
1692
2004
  export type ControllerActionHandler<
1693
- $Controller extends Controller = Controller
1694
- > = (this: $Controller, ctx: KoaContext, ...args: any[]) => any
2005
+ $Controller extends Controller = Controller,
2006
+ $Params = Record<string, any>
2007
+ > = keyof $Params extends never
2008
+ ? (this: $Controller, ctx: KoaContext) => any
2009
+ : (this: $Controller, ctx: KoaContext, params: $Params) => any
1695
2010
 
1696
2011
  type ModelDataKey<T, K extends keyof T> = K extends
1697
2012
  | 'QueryBuilderType'
1698
- | 'foreignKeyId'
1699
2013
  | `$${string}`
1700
2014
  ? never
1701
2015
  : T[K] extends (...args: any[]) => any
@@ -1720,14 +2034,14 @@ type ModelDataProperties<$Model extends Model = Model> = {
1720
2034
  * - Any other `string`: Checked as a role via
1721
2035
  * `UserModel.$hasRole()`.
1722
2036
  * - `function`: Dynamically resolves to any of the above.
1723
- * - `Record<HTTPMethod, string | string[]>`: Per-method
2037
+ * - `Partial<Record<HTTPMethod, string | string[]>>`: Per-method
1724
2038
  * role authorization.
1725
2039
  */
1726
2040
  export type Authorize =
1727
2041
  | boolean
1728
2042
  | OrArrayOf<LiteralUnion<'$self' | '$owner'>>
1729
2043
  | ((ctx: KoaContext) => OrPromiseOf<Authorize>)
1730
- | Record<HTTPMethod, string | string[]>
2044
+ | Partial<Record<HTTPMethod, string | string[]>>
1731
2045
 
1732
2046
  export type BaseControllerActionOptions = {
1733
2047
  /**
@@ -1756,13 +2070,6 @@ export type BaseControllerActionOptions = {
1756
2070
  * overridable `UserModel.hasRole()` method.
1757
2071
  */
1758
2072
  authorize?: Authorize
1759
- /**
1760
- * Validates action parameters and maps them to Koa's `ctx.query` object
1761
- * passed to the action handler.
1762
- *
1763
- * @see {@link https://github.com/ditojs/dito/blob/main/docs/model-properties.md Model Properties}
1764
- */
1765
- parameters?: { [key: string]: Schema }
1766
2073
  /**
1767
2074
  * Provides a schema for the value returned from the action handler and
1768
2075
  * optionally maps the value to a key inside a returned object when it
@@ -1786,17 +2093,41 @@ export type BaseControllerActionOptions = {
1786
2093
  }
1787
2094
 
1788
2095
  export type ControllerActionOptions<
1789
- $Controller extends Controller = Controller
2096
+ $Controller extends Controller = Controller,
2097
+ $Params = Record<string, any>
1790
2098
  > = BaseControllerActionOptions & {
1791
- handler: ControllerActionHandler<$Controller>
2099
+ /**
2100
+ * Defines validated parameters for the action handler.
2101
+ * The handler receives a single parameter object with
2102
+ * the same keys:
2103
+ * ```ts
2104
+ * parameters: { cart: { type: 'object' } },
2105
+ * handler(ctx, { cart }) { ... }
2106
+ * ```
2107
+ *
2108
+ * For strongly-typed parameters, pass a params map as
2109
+ * the second type argument to {@link ControllerActions}:
2110
+ * ```ts
2111
+ * actions: ControllerActions<this, {
2112
+ * 'get search': { query: string }
2113
+ * }>
2114
+ * ```
2115
+ */
2116
+ parameters?: { [K in keyof $Params]: Schema<$Params[K]> }
2117
+ handler: ControllerActionHandler<$Controller, $Params>
1792
2118
  }
1793
2119
 
1794
- export type ModelControllerActionOptions<
1795
- $ModelController extends ModelController = ModelController
1796
- > = BaseControllerActionOptions & {
1797
- /** The function to be called when the action route is requested. */
1798
- handler: ModelControllerActionHandler<$ModelController>
1799
- }
2120
+ export type ModelControllerActionOptions<$ModelController = ModelController> =
2121
+ BaseControllerActionOptions & {
2122
+ /**
2123
+ * Defines validated parameters for the action handler.
2124
+ * The handler receives a single parameter object with
2125
+ * the same keys.
2126
+ */
2127
+ parameters?: { [key: string]: Schema }
2128
+ /** The function to be called when the action route is requested. */
2129
+ handler: ModelControllerActionHandler<$ModelController>
2130
+ }
1800
2131
 
1801
2132
  export type MemberActionParameter<$Model extends Model = Model> =
1802
2133
  | Schema
@@ -1825,28 +2156,22 @@ export type MemberActionParameter<$Model extends Model = Model> =
1825
2156
  * A model controller action: either an options object with
1826
2157
  * `handler` or a bare handler function.
1827
2158
  */
1828
- export type ModelControllerAction<
1829
- $ModelController extends ModelController = ModelController
1830
- > =
2159
+ export type ModelControllerAction<$ModelController = ModelController> =
1831
2160
  | ModelControllerActionOptions<$ModelController>
1832
2161
  | ModelControllerActionHandler<$ModelController>
1833
2162
 
1834
2163
  /**
1835
2164
  * Map of action names to action definitions for a model
1836
- * controller's collection-level actions (e.g. `getList`,
1837
- * `postCreate`).
2165
+ * controller's collection-level actions (e.g. `'get'`,
2166
+ * `'post'`, `'post login'`).
1838
2167
  */
1839
- export type ModelControllerActions<
1840
- $ModelController extends ModelController = ModelController
1841
- > = {
2168
+ export type ModelControllerActions<$ModelController = ModelController> = {
1842
2169
  [name: ControllerActionName]: ModelControllerAction<$ModelController>
1843
2170
  allow?: OrReadOnly<ControllerActionName[]>
1844
2171
  authorize?: Authorize
1845
2172
  }
1846
2173
 
1847
- type ModelControllerMemberAction<
1848
- $ModelController extends ModelController = ModelController
1849
- > =
2174
+ type ModelControllerMemberAction<$ModelController = ModelController> =
1850
2175
  | (Omit<ModelControllerActionOptions<$ModelController>, 'parameters'> & {
1851
2176
  parameters?: {
1852
2177
  [key: string]: MemberActionParameter<
@@ -1862,28 +2187,66 @@ type ModelControllerMemberAction<
1862
2187
  * `delete`). Member actions can use `{ from: 'member' }`
1863
2188
  * parameters to receive the resolved member model.
1864
2189
  */
1865
- export type ModelControllerMemberActions<
1866
- $ModelController extends ModelController = ModelController
1867
- > = {
2190
+ export type ModelControllerMemberActions<$ModelController = ModelController> = {
1868
2191
  [name: ControllerActionName]: ModelControllerMemberAction<$ModelController>
1869
2192
  allow?: OrReadOnly<ControllerActionName[]>
1870
2193
  authorize?: Authorize
1871
2194
  }
1872
2195
 
1873
2196
  /**
1874
- * Action name pattern: an HTTP method followed by an
1875
- * optional suffix, e.g. `'getStats'`, `'postImport'`,
1876
- * `'deleteAll'`.
2197
+ * Action name: an HTTP method, optionally followed by a
2198
+ * space and a name (e.g. `'get'`, `'post login'`,
2199
+ * `'get session'`). The method and name are split on the
2200
+ * first space to determine the HTTP method and route path.
2201
+ * Bare names without an HTTP method prefix (e.g. `'login'`)
2202
+ * are not supported and will throw at runtime.
1877
2203
  */
1878
2204
  export type ControllerActionName = `${HTTPMethod}${string}`
1879
2205
 
1880
2206
  /**
1881
2207
  * Map of action names to action definitions for a
1882
- * controller. Supports `allow` to whitelist specific
1883
- * actions and `authorize` for group-level authorization.
2208
+ * controller. Action names follow the pattern
2209
+ * `'{method} {path}'` (e.g. `'post import'`) or just
2210
+ * `'{method}'` for default CRUD actions (e.g. `'get'`).
2211
+ *
2212
+ * Use `allow` to whitelist specific actions and
2213
+ * `authorize` for group-level authorization.
2214
+ *
2215
+ * Pass a `$ParamsMap` as the second type argument to
2216
+ * strongly type handler parameters per action:
2217
+ *
2218
+ * @example
2219
+ * ```ts
2220
+ * type Params = {
2221
+ * 'get search': { query: string }
2222
+ * 'post create': { name: string }
2223
+ * }
2224
+ *
2225
+ * actions: ControllerActions<this, Params> = {
2226
+ * allow: ['get search', 'post create'],
2227
+ * 'get search': {
2228
+ * parameters: { query: { type: 'string' } },
2229
+ * handler(ctx, { query }) { ... } // query: string
2230
+ * },
2231
+ * 'post create': {
2232
+ * parameters: { name: { type: 'string' } },
2233
+ * handler(ctx, { name }) { ... } // name: string
2234
+ * }
2235
+ * }
2236
+ * ```
2237
+ *
2238
+ * @see {@link ControllerAction}
1884
2239
  */
1885
- export type ControllerActions<$Controller extends Controller = Controller> = {
1886
- [name: ControllerActionName]: ControllerAction<$Controller>
2240
+ export type ControllerActions<
2241
+ $Controller extends Controller = Controller,
2242
+ $ParamsMap extends Record<string, any> = {}
2243
+ > = {
2244
+ [K in keyof $ParamsMap & ControllerActionName]?: ControllerAction<
2245
+ $Controller,
2246
+ $ParamsMap[K]
2247
+ >
2248
+ } & {
2249
+ [name: ControllerActionName]: ControllerAction<$Controller, any>
1887
2250
  allow?: OrReadOnly<ControllerActionName[]>
1888
2251
  authorize?: Authorize
1889
2252
  }
@@ -1916,66 +2279,65 @@ export class AdminController extends Controller {
1916
2279
  defineViteConfig(config?: UserConfig): UserConfig
1917
2280
  }
1918
2281
  type ModelControllerHookType = 'collection' | 'member'
1919
- type ModelControllerHookKeys<
1920
- $Keys extends string,
1921
- $ModelControllerHookType extends string
1922
- > = `${
2282
+ type ModelControllerHookKey = `${
1923
2283
  | 'before'
1924
2284
  | 'after'
1925
2285
  | '*'
1926
2286
  }:${
1927
- | $ModelControllerHookType
2287
+ | ModelControllerHookType
1928
2288
  | '*'
1929
2289
  }:${
1930
- | Exclude<$Keys, 'allow'>
1931
2290
  | ControllerActionName
1932
2291
  | '*'
1933
2292
  }`
1934
- type ModelControllerHook<
1935
- $ModelController extends ModelController = ModelController
1936
- > = (
1937
- ctx: KoaContext,
1938
- result: objection.Page<ModelFromModelController<$ModelController>>
1939
- ) => any
1940
-
1941
- type HookKeysFromController<$ModelController extends ModelController> =
1942
- | ModelControllerHookKeys<
1943
- Exclude<
1944
- keyof Exclude<$ModelController['collection'], undefined>,
1945
- symbol | number
1946
- >,
1947
- 'collection'
1948
- >
1949
- | ModelControllerHookKeys<
1950
- Exclude<
1951
- keyof Exclude<$ModelController['member'], undefined>,
1952
- symbol | number
1953
- >,
1954
- 'member'
1955
- >
1956
2293
 
1957
- type HandlerFromHookKey<
1958
- $ModelController extends ModelController,
1959
- K extends HookKeysFromController<$ModelController>
1960
- > = K extends `${
1961
- | 'before'
1962
- | 'after'
1963
- | '*'
1964
- }:${
1965
- | 'collection'
1966
- | 'member'
1967
- | '*'
1968
- }:${string}`
1969
- ? (this: $ModelController, ctx: KoaContext, ...args: any[]) => any
1970
- : never
1971
-
1972
- type ModelControllerHooks<
1973
- $ModelController extends ModelController = ModelController
1974
- > = {
1975
- [$Key in HookKeysFromController<$ModelController>]?: HandlerFromHookKey<
1976
- $ModelController,
1977
- $Key
1978
- >
2294
+ type CrudActionName = 'get' | 'post' | 'put' | 'patch' | 'delete'
2295
+ type NonDeleteCrudActionName = Exclude<CrudActionName, 'delete'>
2296
+
2297
+ type BeforeHookKey =
2298
+ `${'before' | '*'}:${ModelControllerHookType | '*'}:${ControllerActionName | '*'}`
2299
+ type AfterDeleteHookKey = `after:${ModelControllerHookType | '*'}:delete`
2300
+ type AfterItemHookKey =
2301
+ | `after:member:${NonDeleteCrudActionName}`
2302
+ | `after:collection:${'post' | 'put' | 'patch'}`
2303
+ type AfterCollectionGetHookKey = `after:collection:get`
2304
+ // Catch-all for custom actions and wildcard scopes;
2305
+ // CRUD hooks get strongly-typed results via the specific key types above.
2306
+ type AfterCustomHookKey =
2307
+ `after:${ModelControllerHookType | '*'}:${ControllerActionName | '*'}`
2308
+
2309
+ export type ModelControllerHooks<$ModelController = ModelController> = {
2310
+ [$Key in BeforeHookKey]?: (
2311
+ this: $ModelController,
2312
+ ctx: KoaContext,
2313
+ params?: Record<string, any>
2314
+ ) => void
2315
+ } & {
2316
+ [$Key in AfterDeleteHookKey]?: (
2317
+ this: $ModelController,
2318
+ ctx: KoaContext,
2319
+ result: { count: number }
2320
+ ) => any
2321
+ } & {
2322
+ [$Key in AfterItemHookKey]?: (
2323
+ this: $ModelController,
2324
+ ctx: KoaContext,
2325
+ item: ModelFromModelController<$ModelController>
2326
+ ) => any
2327
+ } & {
2328
+ [$Key in AfterCollectionGetHookKey]?: (
2329
+ this: $ModelController,
2330
+ ctx: KoaContext,
2331
+ result:
2332
+ | ModelFromModelController<$ModelController>[]
2333
+ | Page<ModelFromModelController<$ModelController>>
2334
+ ) => any
2335
+ } & {
2336
+ [$Key in AfterCustomHookKey]?: (
2337
+ this: $ModelController,
2338
+ ctx: KoaContext,
2339
+ result: any
2340
+ ) => any
1979
2341
  }
1980
2342
 
1981
2343
  /**
@@ -2026,12 +2388,12 @@ export class CollectionController<
2026
2388
  * The controller's collection actions with built-in CRUD
2027
2389
  * defaults.
2028
2390
  */
2029
- collection?: ModelControllerActions<CollectionController<$Model>>
2391
+ collection?: ModelControllerActions<this>
2030
2392
  /**
2031
2393
  * The controller's member actions with built-in CRUD
2032
2394
  * defaults.
2033
2395
  */
2034
- member?: ModelControllerMemberActions<CollectionController<$Model>>
2396
+ member?: ModelControllerMemberActions<this>
2035
2397
 
2036
2398
  /** Creates a query builder for this controller's model. */
2037
2399
  query(trx?: objection.Transaction): QueryBuilder<$Model>
@@ -2071,13 +2433,13 @@ export class CollectionController<
2071
2433
  * Executes a controller action within a transaction
2072
2434
  * context.
2073
2435
  */
2074
- execute(
2436
+ execute<R>(
2075
2437
  ctx: KoaContext,
2076
2438
  execute: (
2077
2439
  query: QueryBuilder<$Model>,
2078
2440
  trx?: objection.Transaction
2079
- ) => any
2080
- ): Promise<any>
2441
+ ) => R
2442
+ ): Promise<Awaited<R>>
2081
2443
 
2082
2444
  /**
2083
2445
  * Extracts model IDs from the request body collection,
@@ -2152,12 +2514,12 @@ export class ModelController<
2152
2514
  * The controller's collection actions. Wrap actions in
2153
2515
  * this object to assign them to the collection.
2154
2516
  */
2155
- collection?: ModelControllerActions<ModelController<$Model>>
2517
+ collection?: ModelControllerActions<this>
2156
2518
  /**
2157
2519
  * The controller's member actions. Wrap actions in this
2158
2520
  * object to assign them to the member.
2159
2521
  */
2160
- member?: ModelControllerMemberActions<ModelController<$Model>>
2522
+ member?: ModelControllerMemberActions<this>
2161
2523
  assets?:
2162
2524
  | boolean
2163
2525
  | {
@@ -2166,10 +2528,55 @@ export class ModelController<
2166
2528
  }
2167
2529
 
2168
2530
  /**
2169
- * When nothing is returned from a hook, the standard
2170
- * action result is used.
2531
+ * Lifecycle hooks that run before or after controller
2532
+ * actions. Keys follow the pattern
2533
+ * `'{before|after|*}:{collection|member|*}:{actionName|*}'`.
2534
+ * Use `'*'` in any segment to match all values.
2535
+ *
2536
+ * @example
2537
+ * ```ts
2538
+ * override hooks: ModelControllerHooks<MyController> = {
2539
+ * // Guard: prevent patching locked records
2540
+ * 'before:member:patch'(ctx) {
2541
+ * if (ctx.request.body.locked) {
2542
+ * throw new ResponseError('Locked records cannot be modified', {
2543
+ * status: 403
2544
+ * })
2545
+ * }
2546
+ * },
2547
+ * // Enrich: attach computed data to the member
2548
+ * async 'after:member:get'(ctx, item) {
2549
+ * const isProcessing = await checkJobStatus(item.id)
2550
+ * item.$set({ isProcessing })
2551
+ * return item
2552
+ * },
2553
+ * // Side effect: log after a successful deletion
2554
+ * 'after:member:delete'(ctx, result) {
2555
+ * const name = this.modelClass.name
2556
+ * logger.info(`Deleted ${result.count} ${name} record(s)`)
2557
+ * },
2558
+ * // Custom action: runs after a custom 'get export' action
2559
+ * 'after:collection:get export'(ctx, items) {
2560
+ * analytics.track('export', { count: items.length })
2561
+ * },
2562
+ * // Custom action: receives the resolved parameters
2563
+ * 'before:member:post import'(ctx, params) {
2564
+ * logger.info(`Importing ${params?.repoFamily?.name}`)
2565
+ * }
2566
+ * }
2567
+ * ```
2568
+ *
2569
+ * `before:` CRUD hooks receive only `(ctx)`. Custom
2570
+ * action hooks also receive the resolved `params` object.
2571
+ *
2572
+ * `after:` hooks receive `(ctx, result)`. Returning a
2573
+ * value from an `after:` hook replaces the action result.
2574
+ *
2575
+ * @see {@link ModelControllerActions}
2576
+ * @see {@link ModelControllerMemberActions}
2577
+ * @see {@link QueryParameterOptions} for pagination parameters
2171
2578
  */
2172
- hooks?: ModelControllerHooks<ModelController<$Model>>
2579
+ hooks?: ModelControllerHooks<this>
2173
2580
  /** Map of relation name to RelationController instance. */
2174
2581
  relations?: Record<string, RelationController>
2175
2582
  }
@@ -2502,7 +2909,25 @@ export type QueryParameterOptions = {
2502
2909
  }
2503
2910
  export type QueryParameterOptionKey = keyof QueryParameterOptions
2504
2911
 
2505
- export class Service {
2912
+ /**
2913
+ * Base class for application services.
2914
+ *
2915
+ * @typeParam $Config - The shape of the service's configuration
2916
+ * object, accessed via `this.config` in service methods.
2917
+ *
2918
+ * ```ts
2919
+ * class MailService extends Service<{
2920
+ * smtp: { host: string; port: number }
2921
+ * from: string
2922
+ * }> {
2923
+ * async send(to: string, body: string) {
2924
+ * const { host, port } = this.config!.smtp
2925
+ * // ...
2926
+ * }
2927
+ * }
2928
+ * ```
2929
+ */
2930
+ export class Service<$Config extends object = Record<string, unknown>> {
2506
2931
  constructor(app: Application<Models>, name?: string)
2507
2932
 
2508
2933
  /** The application instance. */
@@ -2510,11 +2935,11 @@ export class Service {
2510
2935
  /** The camelized service name. */
2511
2936
  name: string
2512
2937
  /** The service configuration. */
2513
- config: Record<string, unknown> | null
2938
+ config: $Config | null
2514
2939
  /** Whether this service has been initialized. */
2515
2940
  initialized: boolean
2516
2941
 
2517
- setup(config: Record<string, unknown>): void
2942
+ setup(config: $Config): void
2518
2943
 
2519
2944
  /**
2520
2945
  * Override in sub-classes if the service needs async
@@ -2533,6 +2958,46 @@ export class Service {
2533
2958
  }
2534
2959
  export type Services = Record<string, Class<Service> | Service>
2535
2960
 
2961
+ /**
2962
+ * Registry of application service instances by name.
2963
+ *
2964
+ * Extend via module augmentation for typed `app.services` access:
2965
+ *
2966
+ * ```ts
2967
+ * declare module '@ditojs/server' {
2968
+ * interface ApplicationServices {
2969
+ * mail: MailService
2970
+ * jobs: JobService
2971
+ * }
2972
+ * }
2973
+ * ```
2974
+ */
2975
+ export interface ApplicationServices {}
2976
+
2977
+ type ResolvedServices = keyof ApplicationServices extends never
2978
+ ? Record<string, Service>
2979
+ : ApplicationServices
2980
+
2981
+ /**
2982
+ * Registry of application storage instances by name.
2983
+ *
2984
+ * Extend via module augmentation for typed `app.storages` access:
2985
+ *
2986
+ * ```ts
2987
+ * declare module '@ditojs/server' {
2988
+ * interface ApplicationStorages {
2989
+ * s3: Storage
2990
+ * local: Storage
2991
+ * }
2992
+ * }
2993
+ * ```
2994
+ */
2995
+ export interface ApplicationStorages {}
2996
+
2997
+ type ResolvedStorages = keyof ApplicationStorages extends never
2998
+ ? Record<string, Storage>
2999
+ : ApplicationStorages
3000
+
2536
3001
  export class QueryBuilder<
2537
3002
  M extends Model,
2538
3003
  R = M[]
@@ -2908,6 +3373,19 @@ export class Storage {
2908
3373
  */
2909
3374
  convertAssetFile(file: AssetFileObject): void
2910
3375
 
3376
+ /**
3377
+ * Signs an asset file by setting its `signature` property
3378
+ * to an HMAC derived from its storage key.
3379
+ */
3380
+ signAssetFile(file: AssetFile): void
3381
+
3382
+ /**
3383
+ * Verifies that an asset file's signature matches its
3384
+ * storage key. Returns false if the signature is missing
3385
+ * or invalid.
3386
+ */
3387
+ verifyAssetFile(file: AssetFile): boolean
3388
+
2911
3389
  /** Registers a storage subclass by type name. */
2912
3390
  static register(storageClass: Class<Storage>): void
2913
3391
  /** Retrieves a registered storage class by type name. */
@@ -3140,11 +3618,22 @@ export function addRelationSchemas(
3140
3618
  properties: Record<string, ModelProperty>
3141
3619
  ): void
3142
3620
 
3143
- export type Keyword =
3621
+ export type Keyword = (
3144
3622
  | SetOptional<Ajv.MacroKeywordDefinition, 'keyword'>
3145
3623
  | SetOptional<Ajv.CodeKeywordDefinition, 'keyword'>
3146
3624
  | SetOptional<Ajv.FuncKeywordDefinition, 'keyword'>
3147
- export type Format = Ajv.ValidateFunction | Ajv.FormatDefinition<string>
3625
+ ) & {
3626
+ /** Custom error message shown when validation fails. */
3627
+ message?: string
3628
+ /** When true, validation errors for this keyword are suppressed. */
3629
+ silent?: boolean
3630
+ }
3631
+ export type Format = (Ajv.ValidateFunction | Ajv.FormatDefinition<string>) & {
3632
+ /** Custom error message shown when validation fails. */
3633
+ message?: string
3634
+ /** When true, validation errors for this format are suppressed. */
3635
+ silent?: boolean
3636
+ }
3148
3637
 
3149
3638
  /** Built-in AJV keyword definitions. */
3150
3639
  export const keywords: {
@@ -3175,14 +3664,34 @@ export const types: {
3175
3664
  color: Record<string, any>
3176
3665
  }
3177
3666
  export type Id = string | number
3178
- export type KoaContext<$State = any> = Koa.ParameterizedContext<
3667
+ /**
3668
+ * Koa context state available as `ctx.state`.
3669
+ *
3670
+ * Extend via module augmentation for typed state access:
3671
+ *
3672
+ * ```ts
3673
+ * declare module '@ditojs/server' {
3674
+ * interface KoaContextState {
3675
+ * foo: string
3676
+ * }
3677
+ * }
3678
+ * ```
3679
+ */
3680
+ export interface KoaContextState {
3681
+ user: InstanceType<typeof UserModel>
3682
+ [key: string]: unknown
3683
+ }
3684
+
3685
+ export type KoaContext<$State = KoaContextState> = Koa.ParameterizedContext<
3179
3686
  $State,
3180
3687
  {
3181
3688
  transaction: objection.Transaction
3182
- session: koaSession.ContextSession & { state: { user: any } }
3689
+ session: koaSession.ContextSession
3183
3690
  logger: PinoLogger
3184
3691
  }
3185
- >
3692
+ > & {
3693
+ app: Application
3694
+ }
3186
3695
 
3187
3696
  type LiteralUnion<T extends U, U = string> = T | (U & Record<never, never>)
3188
3697
 
@@ -3192,8 +3701,9 @@ type OrReadOnly<T> = Readonly<T> | T
3192
3701
 
3193
3702
  type OrPromiseOf<T> = Promise<T> | T
3194
3703
 
3195
- type ModelFromModelController<$ModelController extends ModelController> =
3196
- InstanceType<Exclude<$ModelController['modelClass'], undefined>>
3704
+ type ModelFromModelController<
3705
+ $ModelController extends { modelClass?: Class<any> }
3706
+ > = InstanceType<Exclude<$ModelController['modelClass'], undefined>>
3197
3707
 
3198
3708
  type SerializeModelPropertyValue<T> = T extends (infer U)[]
3199
3709
  ? SerializeModelPropertyValue<U>[]
@@ -3244,10 +3754,10 @@ export type SelectModelPropertyKeys<T extends Model> = keyof SerializedModel<T>
3244
3754
  * }
3245
3755
  * ```
3246
3756
  */
3247
- export type Schema<T = any> = JSONSchemaType<T> & {
3757
+ export type Schema<$Value = any> = JSONSchemaType<$Value> & {
3248
3758
  // keywords/_validate.js
3249
3759
  validate?: (params: {
3250
- data: unknown
3760
+ data: $Value
3251
3761
  parentData: object | unknown[]
3252
3762
  rootData: object | unknown[]
3253
3763
  dataPath: string
@@ -3260,7 +3770,7 @@ export type Schema<T = any> = JSONSchemaType<T> & {
3260
3770
 
3261
3771
  // keywords/_validate.js
3262
3772
  validateAsync?: (params: {
3263
- data: unknown
3773
+ data: $Value
3264
3774
  parentData: object | unknown[]
3265
3775
  rootData: object | unknown[]
3266
3776
  dataPath: string