@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.
- package/package.json +8 -7
- package/src/app/Application.js +5 -2
- package/src/controllers/Controller.js +6 -2
- package/src/mixins/AssetMixin.js +4 -2
- package/src/models/Model.js +3 -3
- package/src/storage/AssetFile.js +2 -0
- package/src/storage/Storage.js +50 -4
- package/src/storage/Storage.test.js +58 -0
- package/src/utils/asset.js +7 -0
- package/types/index.d.ts +1712 -349
- package/types/tests/application.test-d.ts +26 -0
- package/types/tests/controller.test-d.ts +113 -0
- package/types/tests/errors.test-d.ts +53 -0
- package/types/tests/fixtures.ts +19 -0
- package/types/tests/model.test-d.ts +193 -0
- package/types/tests/query-builder.test-d.ts +106 -0
- package/types/tests/relation.test-d.ts +83 -0
- package/types/tests/storage.test-d.ts +113 -0
|
@@ -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
|
+
})
|