@ditojs/server 2.86.0 → 2.88.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.
@@ -0,0 +1,26 @@
1
+ import { expectTypeOf, describe, it } from 'vitest'
2
+ import type { Application, Model } from '../index.d.ts'
3
+ import type { app } from './fixtures.ts'
4
+
5
+ describe('Application', () => {
6
+ it('models property exists', () => {
7
+ type App = typeof app
8
+ expectTypeOf<App['models']>()
9
+ .not.toBeAny()
10
+ expectTypeOf<App['models']>().toHaveProperty('Item')
11
+ expectTypeOf<App['models']>().toHaveProperty('User')
12
+ })
13
+
14
+ it('start and stop return promises', () => {
15
+ type App = typeof app
16
+ expectTypeOf<ReturnType<App['start']>>()
17
+ .toEqualTypeOf<Promise<void>>()
18
+ expectTypeOf<ReturnType<App['stop']>>()
19
+ .toEqualTypeOf<Promise<void>>()
20
+ })
21
+
22
+ it('addModels accepts model class map', () => {
23
+ type App = typeof app
24
+ expectTypeOf<App['addModels']>().toBeFunction()
25
+ })
26
+ })
@@ -0,0 +1,113 @@
1
+ import { expectTypeOf, assertType, describe, it } from 'vitest'
2
+ import type {
3
+ Controller,
4
+ ModelController,
5
+ CollectionController,
6
+ RelationController,
7
+ QueryBuilder,
8
+ Model,
9
+ KoaContext,
10
+ ModelControllerActionHandler,
11
+ ControllerActionHandler
12
+ } from '../index.d.ts'
13
+ import type { Transaction } from 'objection'
14
+
15
+ describe('Controller', () => {
16
+ it('getMember accepts ctx and returns Promise<Model | null>', () => {
17
+ const ctrl = {} as Controller
18
+ expectTypeOf(ctrl.getMember({} as KoaContext)).toEqualTypeOf<
19
+ Promise<Model | null>
20
+ >()
21
+ })
22
+
23
+ it('setProperty accepts string key and unknown value', () => {
24
+ const ctrl = {} as Controller
25
+ expectTypeOf(ctrl.setProperty).toBeFunction()
26
+ ctrl.setProperty('foo', 42)
27
+ ctrl.setProperty('bar', 'hello')
28
+ })
29
+
30
+ it('action handler this is typed to controller', () => {
31
+ type Handler = ControllerActionHandler<Controller>
32
+ const handler: Handler = function (ctx) {
33
+ expectTypeOf(this).not.toBeAny()
34
+ expectTypeOf(this).toEqualTypeOf<Controller>()
35
+ expectTypeOf(ctx).not.toBeAny()
36
+ expectTypeOf(ctx).toMatchTypeOf<KoaContext>()
37
+ }
38
+ })
39
+ })
40
+
41
+ describe('CollectionController', () => {
42
+ it('getMember returns typed model or null', () => {
43
+ const ctrl = {} as CollectionController<Model>
44
+ const result = ctrl.getMember(
45
+ {} as KoaContext,
46
+ undefined,
47
+ { forUpdate: true }
48
+ )
49
+ expectTypeOf(result).toEqualTypeOf<Promise<Model | null>>()
50
+ })
51
+
52
+ it('executeAndFetch modify receives query and trx', () => {
53
+ const ctrl = {} as CollectionController<Model>
54
+ ctrl.executeAndFetch(
55
+ 'patch',
56
+ {} as KoaContext,
57
+ (query, trx) => {
58
+ expectTypeOf(query).not.toBeAny()
59
+ expectTypeOf(query)
60
+ .toMatchTypeOf<QueryBuilder<Model>>()
61
+ expectTypeOf(trx).not.toBeAny()
62
+ expectTypeOf(trx)
63
+ .toEqualTypeOf<Transaction | undefined>()
64
+ }
65
+ )
66
+ })
67
+
68
+ it('executeAndFetchById modify receives query and trx', () => {
69
+ const ctrl = {} as CollectionController<Model>
70
+ ctrl.executeAndFetchById(
71
+ 'patch',
72
+ {} as KoaContext,
73
+ (query, trx) => {
74
+ expectTypeOf(query).not.toBeAny()
75
+ expectTypeOf(query)
76
+ .toMatchTypeOf<QueryBuilder<Model>>()
77
+ expectTypeOf(trx).not.toBeAny()
78
+ expectTypeOf(trx)
79
+ .toEqualTypeOf<Transaction | undefined>()
80
+ }
81
+ )
82
+ })
83
+ })
84
+
85
+ describe('ModelController', () => {
86
+ it('action handler this is typed to the controller', () => {
87
+ type Handler = ModelControllerActionHandler<ModelController<Model>>
88
+ const handler: Handler = function (ctx) {
89
+ expectTypeOf(this).not.toBeAny()
90
+ expectTypeOf(this)
91
+ .toEqualTypeOf<ModelController<Model>>()
92
+ expectTypeOf(ctx).not.toBeAny()
93
+ expectTypeOf(ctx).toMatchTypeOf<KoaContext>()
94
+ }
95
+ })
96
+ })
97
+
98
+ describe('RelationController', () => {
99
+ it('parent is CollectionController', () => {
100
+ const ctrl = {} as RelationController<Model>
101
+ expectTypeOf(
102
+ ctrl.parent
103
+ ).toMatchTypeOf<CollectionController>()
104
+ })
105
+
106
+ it('has relation-specific properties', () => {
107
+ const ctrl = {} as RelationController<Model>
108
+ expectTypeOf(ctrl.isOneToOne).toBeBoolean()
109
+ expectTypeOf(ctrl.relate).toBeBoolean()
110
+ expectTypeOf(ctrl.unrelate).toBeBoolean()
111
+ expectTypeOf(ctrl.object).toEqualTypeOf<Record<string, unknown>>()
112
+ })
113
+ })
@@ -0,0 +1,53 @@
1
+ import { expectTypeOf, describe, it } from 'vitest'
2
+ import type {
3
+ ResponseError,
4
+ NotFoundError,
5
+ ValidationError,
6
+ DatabaseError,
7
+ ControllerError,
8
+ AuthorizationError,
9
+ AuthenticationError,
10
+ ModelError,
11
+ GraphError
12
+ } from '../index.d.ts'
13
+
14
+ describe('Errors', () => {
15
+ it('ResponseError has status and is an Error', () => {
16
+ const err = {} as ResponseError
17
+ expectTypeOf(err.status).toBeNumber()
18
+ expectTypeOf(err.message).toBeString()
19
+ expectTypeOf(err).toMatchTypeOf<Error>()
20
+ })
21
+
22
+ it('NotFoundError extends ResponseError', () => {
23
+ expectTypeOf<NotFoundError>().toMatchTypeOf<ResponseError>()
24
+ })
25
+
26
+ it('ValidationError extends ResponseError', () => {
27
+ expectTypeOf<ValidationError>().toMatchTypeOf<ResponseError>()
28
+ })
29
+
30
+ it('DatabaseError extends ResponseError', () => {
31
+ expectTypeOf<DatabaseError>().toMatchTypeOf<ResponseError>()
32
+ })
33
+
34
+ it('ControllerError extends ResponseError', () => {
35
+ expectTypeOf<ControllerError>().toMatchTypeOf<ResponseError>()
36
+ })
37
+
38
+ it('AuthorizationError extends ResponseError', () => {
39
+ expectTypeOf<AuthorizationError>().toMatchTypeOf<ResponseError>()
40
+ })
41
+
42
+ it('AuthenticationError extends ResponseError', () => {
43
+ expectTypeOf<AuthenticationError>().toMatchTypeOf<ResponseError>()
44
+ })
45
+
46
+ it('GraphError extends ResponseError', () => {
47
+ expectTypeOf<GraphError>().toMatchTypeOf<ResponseError>()
48
+ })
49
+
50
+ it('ModelError extends ResponseError', () => {
51
+ expectTypeOf<ModelError>().toMatchTypeOf<ResponseError>()
52
+ })
53
+ })
@@ -0,0 +1,19 @@
1
+ import type { Model, Application } from '../index.d.ts'
2
+
3
+ // Test model types that don't narrow `id` from `Id` to avoid
4
+ // variance issues with QueryBuilder's PartialModelObject<M>.
5
+ export interface Item extends Model {
6
+ title: string
7
+ active: boolean
8
+ }
9
+
10
+ export interface User extends Model {
11
+ name: string
12
+ email: string
13
+ items: Item[]
14
+ }
15
+
16
+ export const app = {} as Application<{
17
+ Item: typeof Model
18
+ User: typeof Model
19
+ }>
@@ -0,0 +1,193 @@
1
+ import { expectTypeOf, assertType, describe, it } from 'vitest'
2
+ import type {
3
+ Model,
4
+ QueryBuilder,
5
+ SerializedModel,
6
+ ModelScopes,
7
+ ModelFilters,
8
+ ModelHooks,
9
+ ModelFilterFunction,
10
+ ModelProperty
11
+ } from '../index.d.ts'
12
+ import type { View } from '../../../admin/types/index.d.ts'
13
+ import type { Transaction } from 'objection'
14
+ import type { Item } from './fixtures.ts'
15
+
16
+ describe('Model', () => {
17
+ it('initialize returns void or Promise<void>', () => {
18
+ expectTypeOf<typeof Model.initialize>()
19
+ .returns.toEqualTypeOf<void | Promise<void>>()
20
+ })
21
+
22
+ it('getAttributes requires a filter function', () => {
23
+ expectTypeOf<typeof Model.getAttributes>()
24
+ .toBeCallableWith(
25
+ (prop: ModelProperty) => true
26
+ )
27
+ expectTypeOf<typeof Model.getAttributes>()
28
+ .parameter(0)
29
+ .not.toBeUndefined()
30
+ })
31
+
32
+ it('$patch accepts Date for date fields', () => {
33
+ interface TimestampedItem extends Model {
34
+ title: string
35
+ createdAt: Date
36
+ }
37
+ const item = {} as TimestampedItem
38
+ item.$patch({ title: 'test' })
39
+ item.$patch({ createdAt: new Date() })
40
+ })
41
+
42
+ it('$is accepts Model, null, or undefined', () => {
43
+ const item = {} as Item
44
+ expectTypeOf(item.$is)
45
+ .parameter(0)
46
+ .toEqualTypeOf<Model | null | undefined>()
47
+ assertType<boolean>(item.$is(null))
48
+ assertType<boolean>(item.$is(undefined))
49
+ assertType<boolean>(item.$is({} as Item))
50
+ })
51
+
52
+ it('$transaction handler overload', () => {
53
+ const item = {} as Item
54
+ assertType<Promise<any>>(
55
+ item.$transaction(async trx => {
56
+ expectTypeOf(trx).not.toBeAny()
57
+ expectTypeOf(trx).toMatchTypeOf<Transaction>()
58
+ })
59
+ )
60
+ })
61
+
62
+ it('$transaction trx + handler overload', () => {
63
+ const item = {} as Item
64
+ assertType<Promise<any>>(
65
+ item.$transaction(
66
+ {} as Transaction,
67
+ async trx => {
68
+ expectTypeOf(trx).not.toBeAny()
69
+ expectTypeOf(trx)
70
+ .toMatchTypeOf<Transaction>()
71
+ }
72
+ )
73
+ )
74
+ })
75
+
76
+ it('static transaction overloads exist', () => {
77
+ expectTypeOf<typeof Model.transaction>()
78
+ .toBeCallableWith()
79
+ expectTypeOf<typeof Model.transaction>()
80
+ .toBeCallableWith(async (trx: any) => {})
81
+ })
82
+
83
+ it('scopes handler receives query and applyParentScope', () => {
84
+ const scopes: ModelScopes<Model> = {
85
+ active(query, applyParentScope) {
86
+ expectTypeOf(query).not.toBeAny()
87
+ expectTypeOf(query)
88
+ .toMatchTypeOf<QueryBuilder<Model>>()
89
+ expectTypeOf(applyParentScope)
90
+ .not.toBeAny()
91
+ expectTypeOf(applyParentScope).toEqualTypeOf< (
92
+ query: QueryBuilder<Model>
93
+ ) => QueryBuilder<Model>
94
+ >()
95
+ return applyParentScope(query)
96
+ }
97
+ }
98
+ assertType<ModelScopes<Model>>(scopes)
99
+ })
100
+
101
+ it('filters handler receives typed query builder', () => {
102
+ const filter: ModelFilterFunction<Model> = (
103
+ query,
104
+ ...args
105
+ ) => {
106
+ expectTypeOf(query).not.toBeAny()
107
+ expectTypeOf(query)
108
+ .toMatchTypeOf<QueryBuilder<Model>>()
109
+ return query
110
+ }
111
+ assertType<ModelFilterFunction<Model>>(filter)
112
+ })
113
+
114
+ it('hooks keys match lifecycle patterns', () => {
115
+ const hooks: ModelHooks<Model> = {
116
+ 'before:insert'(args) {
117
+ expectTypeOf(args).not.toBeAny()
118
+ },
119
+ 'after:find'(args) {
120
+ expectTypeOf(args).not.toBeAny()
121
+ },
122
+ 'before:update'(args) {
123
+ expectTypeOf(args).not.toBeAny()
124
+ },
125
+ 'after:delete'(args) {
126
+ expectTypeOf(args).not.toBeAny()
127
+ }
128
+ }
129
+ assertType<ModelHooks<Model>>(hooks)
130
+ })
131
+
132
+ it('QueryBuilderType uses Dito QueryBuilder', () => {
133
+ const model = {} as Model
134
+ expectTypeOf(model.QueryBuilderType).toEqualTypeOf<
135
+ QueryBuilder<Model, Model[]>
136
+ >()
137
+ })
138
+ })
139
+
140
+ describe('SerializedModel', () => {
141
+ it('strips functions, $-prefixed, and internal keys', () => {
142
+ interface TestModel extends Model {
143
+ title: string
144
+ $meta: string
145
+ }
146
+ type Result = SerializedModel<TestModel>
147
+ expectTypeOf<keyof Result>()
148
+ .toEqualTypeOf<'id' | 'title'>()
149
+ })
150
+
151
+ it('converts Date properties to string (JSON serialization)', () => {
152
+ interface TimestampedModel extends Model {
153
+ createdAt: Date
154
+ updatedAt?: Date
155
+ timestamps: Date[]
156
+ }
157
+ type Result = SerializedModel<TimestampedModel>
158
+ expectTypeOf<Result['createdAt']>()
159
+ .toEqualTypeOf<string>()
160
+ expectTypeOf<Result['updatedAt']>()
161
+ .toEqualTypeOf<string | undefined>()
162
+ expectTypeOf<Result['timestamps']>()
163
+ .toEqualTypeOf<string[]>()
164
+ })
165
+
166
+ it('View<SerializedModel<T>> assignable to View<any> through Record', () => {
167
+ interface StreamCheckModel extends Model {
168
+ result: string
169
+ functioning: boolean
170
+ channelId: number
171
+ createdAt: Date
172
+ src: string
173
+ logs?: Record<string, any>[]
174
+ }
175
+ type StreamCheck = SerializedModel<StreamCheckModel>
176
+ const view: View<StreamCheck> = {
177
+ type: 'view',
178
+ components: {
179
+ result: { type: 'text' }
180
+ },
181
+ panels: {
182
+ info: {
183
+ type: 'panel',
184
+ components: {
185
+ src: { type: 'text' }
186
+ }
187
+ }
188
+ }
189
+ }
190
+ const views = { streamChecks: view }
191
+ assertType<Record<string, View<any>>>(views)
192
+ })
193
+ })
@@ -0,0 +1,106 @@
1
+ import { expectTypeOf, assertType, describe, it } from 'vitest'
2
+ import type { QueryBuilder, Model } from '../index.d.ts'
3
+
4
+ describe('QueryBuilder', () => {
5
+ type QB = QueryBuilder<Model, Model[]>
6
+
7
+ it('applyFilter supports string name with args', () => {
8
+ const query = {} as QB
9
+ assertType<QB>(query.applyFilter('active'))
10
+ assertType<QB>(query.applyFilter('recent', 30))
11
+ })
12
+
13
+ it('applyFilter supports object notation', () => {
14
+ const query = {} as QB
15
+ assertType<QB>(
16
+ query.applyFilter({ active: [], recent: [30] })
17
+ )
18
+ })
19
+
20
+ it('applyFilter rejects wrong argument types', () => {
21
+ const query = {} as QB
22
+ // @ts-expect-error - first arg must be string or object
23
+ query.applyFilter(123)
24
+ })
25
+
26
+ it('upsert options are individually optional', () => {
27
+ const query = {} as QB
28
+ assertType<QB>(query.upsert({} as any, { update: true }))
29
+ assertType<QB>(query.upsert({} as any, { fetch: true }))
30
+ assertType<QB>(query.upsert({} as any, {}))
31
+ assertType<QB>(query.upsert({} as any))
32
+ })
33
+
34
+ it('scope methods return this for chaining', () => {
35
+ const query = {} as QB
36
+ assertType<QB>(query.withScope('active'))
37
+ assertType<QB>(query.clearWithScope())
38
+ assertType<QB>(query.ignoreScope('default'))
39
+ assertType<QB>(query.applyScope('active'))
40
+ })
41
+
42
+ it('withGraph returns this for chaining', () => {
43
+ const query = {} as QB
44
+ assertType<QB>(query.withGraph('[items]'))
45
+ assertType<QB>(
46
+ query.withGraph('[items]', { algorithm: 'fetch' })
47
+ )
48
+ })
49
+
50
+ it('find returns this for chaining', () => {
51
+ const query = {} as QB
52
+ assertType<QB>(query.find({}))
53
+ assertType<QB>(
54
+ query.find({}, { scope: true, filter: false })
55
+ )
56
+ })
57
+
58
+ it('DitoGraph methods return this for chaining', () => {
59
+ const query = {} as QB
60
+ assertType<QB>(query.insertDitoGraph({} as any))
61
+ assertType<QB>(query.insertDitoGraphAndFetch({} as any))
62
+ assertType<QB>(query.upsertDitoGraph({} as any))
63
+ assertType<QB>(query.upsertDitoGraphAndFetch({} as any))
64
+ assertType<QB>(query.patchDitoGraph({} as any))
65
+ assertType<QB>(query.patchDitoGraphAndFetch({} as any))
66
+ assertType<QB>(
67
+ query.upsertDitoGraphAndFetchById(1, {} as any)
68
+ )
69
+ assertType<QB>(
70
+ query.updateDitoGraphAndFetchById(1, {} as any)
71
+ )
72
+ assertType<QB>(
73
+ query.patchDitoGraphAndFetchById(1, {} as any)
74
+ )
75
+ })
76
+
77
+ it('truncate accepts optional restart and cascade', () => {
78
+ const query = {} as QB
79
+ assertType<QB>(query.truncate())
80
+ assertType<QB>(
81
+ query.truncate({ restart: true, cascade: true })
82
+ )
83
+ })
84
+
85
+ it('pluck returns this for chaining', () => {
86
+ const query = {} as QB
87
+ assertType<QB>(query.pluck('title'))
88
+ })
89
+
90
+ it('toSQL returns sql and bindings', () => {
91
+ const query = {} as QB
92
+ const result = query.toSQL()
93
+ expectTypeOf(result).not.toBeAny()
94
+ expectTypeOf(result.sql).not.toBeAny()
95
+ expectTypeOf(result.sql).toBeString()
96
+ expectTypeOf(result.bindings)
97
+ .not.toBeAny()
98
+ expectTypeOf(result.bindings)
99
+ .toEqualTypeOf<unknown[]>()
100
+ })
101
+
102
+ it('omit returns void (not chainable)', () => {
103
+ const query = {} as QB
104
+ expectTypeOf(query.omit('id')).toBeVoid()
105
+ })
106
+ })
@@ -0,0 +1,83 @@
1
+ import { assertType, describe, it } from 'vitest'
2
+ import type { ModelRelation, QueryBuilder } from '../index.d.ts'
3
+
4
+ describe('ModelRelation', () => {
5
+ it('accepts basic belongsTo relation', () => {
6
+ assertType<ModelRelation>({
7
+ relation: 'belongsTo',
8
+ from: 'Item.userId',
9
+ to: 'User.id'
10
+ })
11
+ })
12
+
13
+ it('accepts hasMany with scope and filter string', () => {
14
+ assertType<ModelRelation>({
15
+ relation: 'hasMany',
16
+ from: 'User.id',
17
+ to: 'Item.userId',
18
+ scope: 'active',
19
+ filter: 'published'
20
+ })
21
+ })
22
+
23
+ it('accepts filter as object with args', () => {
24
+ assertType<ModelRelation>({
25
+ relation: 'hasMany',
26
+ from: 'User.id',
27
+ to: 'Item.userId',
28
+ filter: { recent: [30], active: [] }
29
+ })
30
+ })
31
+
32
+ it('accepts modify as function', () => {
33
+ assertType<ModelRelation>({
34
+ relation: 'hasMany',
35
+ from: 'User.id',
36
+ to: 'Item.userId',
37
+ modify: query => {
38
+ query.withScope('active')
39
+ }
40
+ })
41
+ })
42
+
43
+ it('accepts modify as find-filter object', () => {
44
+ assertType<ModelRelation>({
45
+ relation: 'hasMany',
46
+ from: 'User.id',
47
+ to: 'Item.userId',
48
+ modify: { active: true }
49
+ })
50
+ })
51
+
52
+ it('accepts through relation with extra', () => {
53
+ assertType<ModelRelation>({
54
+ relation: 'manyToMany',
55
+ from: 'User.id',
56
+ to: 'Tag.id',
57
+ through: {
58
+ from: 'UserTag.userId',
59
+ to: 'UserTag.tagId',
60
+ extra: ['role']
61
+ },
62
+ inverse: true
63
+ })
64
+ })
65
+
66
+ it('accepts owner and nullable options', () => {
67
+ assertType<ModelRelation>({
68
+ relation: 'belongsTo',
69
+ from: 'Item.userId',
70
+ to: 'User.id',
71
+ owner: true,
72
+ nullable: true
73
+ })
74
+ })
75
+
76
+ it('rejects invalid relation type', () => {
77
+ // @ts-expect-error - relation is required
78
+ assertType<ModelRelation>({
79
+ from: 'Item.userId',
80
+ to: 'User.id'
81
+ })
82
+ })
83
+ })