@ultimabase/testing 0.0.0-stage → 0.1.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/CHANGELOG.md +19 -0
- package/README.md +4 -2
- package/dist/aggregates.d.ts +3 -0
- package/dist/aggregates.d.ts.map +1 -0
- package/dist/aggregates.js +99 -0
- package/dist/as-user.d.ts +13 -0
- package/dist/as-user.d.ts.map +1 -0
- package/dist/as-user.js +17 -0
- package/dist/bulk-returns.d.ts +3 -0
- package/dist/bulk-returns.d.ts.map +1 -0
- package/dist/bulk-returns.js +125 -0
- package/dist/cascades.d.ts +3 -0
- package/dist/cascades.d.ts.map +1 -0
- package/dist/cascades.js +269 -0
- package/dist/cases.d.ts +3 -0
- package/dist/cases.d.ts.map +1 -0
- package/dist/cases.js +60 -0
- package/dist/counts.d.ts +3 -0
- package/dist/counts.d.ts.map +1 -0
- package/dist/counts.js +158 -0
- package/dist/days.d.ts +3 -0
- package/dist/days.d.ts.map +1 -0
- package/dist/days.js +74 -0
- package/dist/decimals.d.ts +3 -0
- package/dist/decimals.d.ts.map +1 -0
- package/dist/decimals.js +70 -0
- package/dist/distincts.d.ts +3 -0
- package/dist/distincts.d.ts.map +1 -0
- package/dist/distincts.js +80 -0
- package/dist/feeds.d.ts +3 -0
- package/dist/feeds.d.ts.map +1 -0
- package/dist/feeds.js +164 -0
- package/dist/groups.d.ts +3 -0
- package/dist/groups.d.ts.map +1 -0
- package/dist/groups.js +149 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/json-paths.d.ts +3 -0
- package/dist/json-paths.d.ts.map +1 -0
- package/dist/json-paths.js +143 -0
- package/dist/lists.d.ts +3 -0
- package/dist/lists.d.ts.map +1 -0
- package/dist/lists.js +234 -0
- package/dist/nested.d.ts +3 -0
- package/dist/nested.d.ts.map +1 -0
- package/dist/nested.js +152 -0
- package/dist/numbers.d.ts +3 -0
- package/dist/numbers.d.ts.map +1 -0
- package/dist/numbers.js +92 -0
- package/dist/projection.d.ts +3 -0
- package/dist/projection.d.ts.map +1 -0
- package/dist/projection.js +53 -0
- package/dist/reads.d.ts +3 -0
- package/dist/reads.d.ts.map +1 -0
- package/dist/reads.js +312 -0
- package/dist/relation-filters.d.ts +3 -0
- package/dist/relation-filters.d.ts.map +1 -0
- package/dist/relation-filters.js +116 -0
- package/dist/round-trips.d.ts +3 -0
- package/dist/round-trips.d.ts.map +1 -0
- package/dist/round-trips.js +284 -0
- package/dist/searches.d.ts +3 -0
- package/dist/searches.d.ts.map +1 -0
- package/dist/searches.js +57 -0
- package/dist/silent.d.ts +3 -0
- package/dist/silent.d.ts.map +1 -0
- package/dist/silent.js +7 -0
- package/dist/sorts.d.ts +3 -0
- package/dist/sorts.d.ts.map +1 -0
- package/dist/sorts.js +108 -0
- package/dist/suite.d.ts +7 -0
- package/dist/suite.d.ts.map +1 -0
- package/dist/suite.js +15 -0
- package/dist/test-adapter.d.ts +11 -0
- package/dist/test-adapter.d.ts.map +1 -0
- package/dist/test-adapter.js +55 -0
- package/dist/test-application.d.ts +17 -0
- package/dist/test-application.d.ts.map +1 -0
- package/dist/test-application.js +68 -0
- package/dist/transactions.d.ts +3 -0
- package/dist/transactions.d.ts.map +1 -0
- package/dist/transactions.js +61 -0
- package/dist/virtuals.d.ts +3 -0
- package/dist/virtuals.d.ts.map +1 -0
- package/dist/virtuals.js +135 -0
- package/dist/writes.d.ts +3 -0
- package/dist/writes.d.ts.map +1 -0
- package/dist/writes.js +219 -0
- package/package.json +40 -3
- package/src/aggregates.ts +128 -0
- package/src/as-user.ts +26 -0
- package/src/bulk-returns.ts +151 -0
- package/src/cascades.ts +338 -0
- package/src/cases.ts +76 -0
- package/src/counts.ts +223 -0
- package/src/days.ts +107 -0
- package/src/decimals.ts +102 -0
- package/src/distincts.ts +107 -0
- package/src/feeds.ts +197 -0
- package/src/groups.ts +185 -0
- package/src/index.ts +6 -0
- package/src/json-paths.ts +185 -0
- package/src/lists.ts +304 -0
- package/src/nested.ts +187 -0
- package/src/numbers.ts +125 -0
- package/src/projection.ts +65 -0
- package/src/reads.ts +389 -0
- package/src/relation-filters.ts +151 -0
- package/src/round-trips.ts +392 -0
- package/src/searches.ts +82 -0
- package/src/silent.ts +11 -0
- package/src/sorts.ts +143 -0
- package/src/suite.ts +21 -0
- package/src/test-adapter.ts +68 -0
- package/src/test-application.ts +93 -0
- package/src/transactions.ts +76 -0
- package/src/virtuals.ts +184 -0
- package/src/writes.ts +315 -0
package/src/reads.ts
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
// The reads of the contract (D-20, D-73): every operator, AND, OR and NOT,
|
|
2
|
+
// filters through relations, order, pages and nested relations.
|
|
3
|
+
import type { Adapter, ArgsOf, WhereOf } from 'ultimabase'
|
|
4
|
+
import { defineModel, master } from 'ultimabase'
|
|
5
|
+
import { beforeEach, describe, expect, it } from 'vitest'
|
|
6
|
+
import { started } from './suite.ts'
|
|
7
|
+
|
|
8
|
+
const M = { as: master }
|
|
9
|
+
|
|
10
|
+
const Author = defineModel({
|
|
11
|
+
name: 'Author',
|
|
12
|
+
fields: (f) => ({
|
|
13
|
+
name: f.string({ unique: true }),
|
|
14
|
+
level: f.enumOf(['junior', 'senior'], { default: 'junior' }),
|
|
15
|
+
books: f.many(() => Book),
|
|
16
|
+
profile: f.one(() => Profile, { optional: true }),
|
|
17
|
+
}),
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const Profile = defineModel({
|
|
21
|
+
name: 'Profile',
|
|
22
|
+
fields: (f) => ({ bio: f.string(), author: f.one(() => Author) }),
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const Book = defineModel({
|
|
26
|
+
name: 'Book',
|
|
27
|
+
fields: (f) => ({
|
|
28
|
+
title: f.string(),
|
|
29
|
+
pages: f.number({ optional: true }),
|
|
30
|
+
published: f.boolean({ default: false }),
|
|
31
|
+
meta: f.json({ optional: true }),
|
|
32
|
+
releasedAt: f.dateTime({ optional: true }),
|
|
33
|
+
author: f.one(() => Author),
|
|
34
|
+
tags: f.many(() => Tag),
|
|
35
|
+
}),
|
|
36
|
+
indexes: [{ fields: ['title', 'author'], unique: true }],
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const Tag = defineModel({
|
|
40
|
+
name: 'Tag',
|
|
41
|
+
fields: (f) => ({
|
|
42
|
+
label: f.string({ unique: true }),
|
|
43
|
+
books: f.many(() => Book),
|
|
44
|
+
}),
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// Asymmetric matchers are typed `any`.
|
|
48
|
+
const aDate: unknown = expect.any(Date)
|
|
49
|
+
|
|
50
|
+
const day = (date: number): Date => new Date(Date.UTC(2020, 0, date))
|
|
51
|
+
|
|
52
|
+
type Authors = { readonly ada: string; readonly grace: string }
|
|
53
|
+
|
|
54
|
+
/** Ada wrote Analysis and Engines and has a profile; Grace wrote Compilers. */
|
|
55
|
+
async function seed(): Promise<Authors> {
|
|
56
|
+
const ada = await Author.create({ data: { name: 'Ada' } }, M)
|
|
57
|
+
const grace = await Author.create(
|
|
58
|
+
{ data: { name: 'Grace', level: 'senior' } },
|
|
59
|
+
M,
|
|
60
|
+
)
|
|
61
|
+
await Profile.create(
|
|
62
|
+
{ data: { bio: 'Poet', author: { connect: { id: ada.id } } } },
|
|
63
|
+
M,
|
|
64
|
+
)
|
|
65
|
+
const math = await Tag.create({ data: { label: 'math' } }, M)
|
|
66
|
+
const art = await Tag.create({ data: { label: 'art' } }, M)
|
|
67
|
+
const books = [
|
|
68
|
+
{
|
|
69
|
+
title: 'Analysis',
|
|
70
|
+
pages: 300,
|
|
71
|
+
published: true,
|
|
72
|
+
author: ada,
|
|
73
|
+
tags: [math],
|
|
74
|
+
},
|
|
75
|
+
{ title: 'Engines', pages: 120, author: ada, tags: [] },
|
|
76
|
+
{ title: 'Compilers', pages: null, author: grace, tags: [math, art] },
|
|
77
|
+
]
|
|
78
|
+
for (const [index, { author, tags, ...data }] of books.entries()) {
|
|
79
|
+
await Book.create(
|
|
80
|
+
{
|
|
81
|
+
data: {
|
|
82
|
+
...data,
|
|
83
|
+
meta: { rank: index, kind: 'book' },
|
|
84
|
+
releasedAt: day(index + 1),
|
|
85
|
+
author: { connect: { id: author.id } },
|
|
86
|
+
tags: { connect: tags.map(({ id }) => ({ id })) },
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
M,
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
return { ada: ada.id, grace: grace.id }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const titles = async (where: WhereOf<typeof Book>): Promise<string[]> =>
|
|
96
|
+
(await Book.findMany({ where }, M)).map((book) => book.title)
|
|
97
|
+
const names = async (where: WhereOf<typeof Author>): Promise<string[]> =>
|
|
98
|
+
(await Author.findMany({ where }, M)).map((author) => author.name)
|
|
99
|
+
const ordered = async (
|
|
100
|
+
orderBy: ArgsOf<typeof Book, 'findMany'>['orderBy'],
|
|
101
|
+
): Promise<string[]> =>
|
|
102
|
+
(await Book.findMany({ orderBy }, M)).map((book) => book.title)
|
|
103
|
+
const page = async (args: ArgsOf<typeof Book, 'findMany'>): Promise<string[]> =>
|
|
104
|
+
(await Book.findMany({ orderBy: { title: 'asc' }, ...args }, M)).map(
|
|
105
|
+
(book) => book.title,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
export function reads(create: () => Adapter): void {
|
|
109
|
+
describe('reads', () => {
|
|
110
|
+
started(create, [Author, Profile, Book, Tag])
|
|
111
|
+
|
|
112
|
+
let authors: Authors
|
|
113
|
+
beforeEach(async () => {
|
|
114
|
+
authors = await seed()
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('compares strings with every operator, case included', async () => {
|
|
118
|
+
expect(await titles({ title: 'Engines' })).toEqual(['Engines'])
|
|
119
|
+
expect(await titles({ title: { not: 'Engines' } })).toEqual([
|
|
120
|
+
'Analysis',
|
|
121
|
+
'Compilers',
|
|
122
|
+
])
|
|
123
|
+
expect(await titles({ title: { in: ['Compilers', 'Nope'] } })).toEqual([
|
|
124
|
+
'Compilers',
|
|
125
|
+
])
|
|
126
|
+
expect(await titles({ title: { notIn: ['Analysis'] } })).toEqual([
|
|
127
|
+
'Engines',
|
|
128
|
+
'Compilers',
|
|
129
|
+
])
|
|
130
|
+
expect(await titles({ title: { notIn: [] } })).toHaveLength(3)
|
|
131
|
+
expect(await titles({ title: { in: [] } })).toEqual([])
|
|
132
|
+
expect(await titles({ title: { contains: 'gin' } })).toEqual(['Engines'])
|
|
133
|
+
expect(await titles({ title: { contains: 'GIN' } })).toEqual([])
|
|
134
|
+
expect(await titles({ title: { startsWith: 'C' } })).toEqual([
|
|
135
|
+
'Compilers',
|
|
136
|
+
])
|
|
137
|
+
expect(await titles({ title: { endsWith: 'sis' } })).toEqual(['Analysis'])
|
|
138
|
+
// `%` and `_` are characters, not patterns.
|
|
139
|
+
expect(await titles({ title: { contains: '%' } })).toEqual([])
|
|
140
|
+
expect(await titles({ title: { startsWith: '_' } })).toEqual([])
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('matches part of an id as a string', async () => {
|
|
144
|
+
// An id begins with its time, shared by rows created together.
|
|
145
|
+
const id = authors.ada
|
|
146
|
+
expect(await names({ id: { startsWith: id.slice(0, 30) } })).toEqual([
|
|
147
|
+
'Ada',
|
|
148
|
+
])
|
|
149
|
+
expect(await names({ id: { contains: id.slice(24, 32) } })).toEqual([
|
|
150
|
+
'Ada',
|
|
151
|
+
])
|
|
152
|
+
expect(await names({ id: { endsWith: id.slice(24) } })).toEqual(['Ada'])
|
|
153
|
+
expect(await names({ id: { contains: 'Z' } })).toEqual([])
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('compares numbers, dates, booleans, enums and json', async () => {
|
|
157
|
+
expect(await titles({ pages: { lt: 300 } })).toEqual(['Engines'])
|
|
158
|
+
expect(await titles({ pages: { lte: 300 } })).toEqual([
|
|
159
|
+
'Analysis',
|
|
160
|
+
'Engines',
|
|
161
|
+
])
|
|
162
|
+
expect(await titles({ pages: { gt: 120 } })).toEqual(['Analysis'])
|
|
163
|
+
expect(await titles({ pages: { gte: 120 } })).toEqual([
|
|
164
|
+
'Analysis',
|
|
165
|
+
'Engines',
|
|
166
|
+
])
|
|
167
|
+
expect(await titles({ releasedAt: { gt: day(1) } })).toEqual([
|
|
168
|
+
'Engines',
|
|
169
|
+
'Compilers',
|
|
170
|
+
])
|
|
171
|
+
expect(await titles({ releasedAt: day(2) })).toEqual(['Engines'])
|
|
172
|
+
expect(await titles({ published: true })).toEqual(['Analysis'])
|
|
173
|
+
expect(
|
|
174
|
+
await titles({ author: { is: { level: { in: ['senior'] } } } }),
|
|
175
|
+
).toEqual(['Compilers'])
|
|
176
|
+
// A json value equals another of the same content, keys in any order.
|
|
177
|
+
expect(
|
|
178
|
+
await titles({ meta: { equals: { kind: 'book', rank: 1 } } }),
|
|
179
|
+
).toEqual(['Engines'])
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('takes -0 for 0 and gives it back as 0, in a number and in json, as PostgreSQL does', async () => {
|
|
183
|
+
const { id } = await Book.create(
|
|
184
|
+
{
|
|
185
|
+
data: {
|
|
186
|
+
title: 'Zero',
|
|
187
|
+
pages: -0,
|
|
188
|
+
meta: { rank: -0, list: [-0, 1] },
|
|
189
|
+
author: { connect: { id: authors.grace } },
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
M,
|
|
193
|
+
)
|
|
194
|
+
expect(await titles({ pages: 0 })).toEqual(['Zero'])
|
|
195
|
+
expect(await titles({ pages: { in: [0] } })).toEqual(['Zero'])
|
|
196
|
+
expect(await titles({ pages: { not: -0 } })).toEqual([
|
|
197
|
+
'Analysis',
|
|
198
|
+
'Engines',
|
|
199
|
+
])
|
|
200
|
+
expect(
|
|
201
|
+
await titles({ meta: { equals: { list: [0, 1], rank: 0 } } }),
|
|
202
|
+
).toEqual(['Zero'])
|
|
203
|
+
// A json array keeps its order and its length.
|
|
204
|
+
expect(
|
|
205
|
+
await titles({ meta: { equals: { rank: 0, list: [1, 0] } } }),
|
|
206
|
+
).toEqual([])
|
|
207
|
+
expect(
|
|
208
|
+
await titles({ meta: { equals: { rank: 0, list: [0] } } }),
|
|
209
|
+
).toEqual([])
|
|
210
|
+
expect(
|
|
211
|
+
await titles({ meta: { equals: { kind: 'book', rank: -0 } } }),
|
|
212
|
+
).toEqual(['Analysis'])
|
|
213
|
+
// It comes back as 0, in json as in a number.
|
|
214
|
+
const read = { where: { id }, select: { pages: true, meta: true } }
|
|
215
|
+
expect(await Book.findUnique(read, M)).toStrictEqual({
|
|
216
|
+
pages: 0,
|
|
217
|
+
meta: { rank: 0, list: [0, 1] },
|
|
218
|
+
})
|
|
219
|
+
await Book.update({ where: { id }, data: { meta: -0 } }, M)
|
|
220
|
+
expect(await Book.findUnique(read, M)).toStrictEqual({
|
|
221
|
+
pages: 0,
|
|
222
|
+
meta: 0,
|
|
223
|
+
})
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('treats null as a value, and as unknown in a comparison', async () => {
|
|
227
|
+
expect(await titles({ pages: null })).toEqual(['Compilers'])
|
|
228
|
+
expect(await titles({ pages: { not: null } })).toEqual([
|
|
229
|
+
'Analysis',
|
|
230
|
+
'Engines',
|
|
231
|
+
])
|
|
232
|
+
// `null` is not below 200, nor not below it, as in SQL.
|
|
233
|
+
expect(await titles({ NOT: { pages: { lt: 200 } } })).toEqual([
|
|
234
|
+
'Analysis',
|
|
235
|
+
])
|
|
236
|
+
expect(await titles({ pages: { not: 300 } })).toEqual(['Engines'])
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('combines AND, OR and NOT', async () => {
|
|
240
|
+
expect(
|
|
241
|
+
await titles({
|
|
242
|
+
OR: [{ pages: { lt: 200 } }, { title: 'Analysis' }],
|
|
243
|
+
NOT: [{ title: 'Engines' }],
|
|
244
|
+
}),
|
|
245
|
+
).toEqual(['Analysis'])
|
|
246
|
+
expect(
|
|
247
|
+
await titles({ AND: [{ pages: { gt: 100 } }, { published: false }] }),
|
|
248
|
+
).toEqual(['Engines'])
|
|
249
|
+
expect(await titles({ OR: [] })).toEqual([])
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('filters through a one relation', async () => {
|
|
253
|
+
expect(await titles({ author: { is: { name: 'Grace' } } })).toEqual([
|
|
254
|
+
'Compilers',
|
|
255
|
+
])
|
|
256
|
+
expect(await titles({ author: { isNot: { name: 'Grace' } } })).toEqual([
|
|
257
|
+
'Analysis',
|
|
258
|
+
'Engines',
|
|
259
|
+
])
|
|
260
|
+
expect(await names({ profile: { is: null } })).toEqual(['Grace'])
|
|
261
|
+
expect(await names({ profile: { isNot: null } })).toEqual(['Ada'])
|
|
262
|
+
expect(await names({ profile: { is: { bio: 'Poet' } } })).toEqual(['Ada'])
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('filters through a many relation', async () => {
|
|
266
|
+
expect(await titles({ tags: { some: { label: 'art' } } })).toEqual([
|
|
267
|
+
'Compilers',
|
|
268
|
+
])
|
|
269
|
+
expect(await titles({ tags: { none: { label: 'math' } } })).toEqual([
|
|
270
|
+
'Engines',
|
|
271
|
+
])
|
|
272
|
+
// Every tag of a book without tags is art.
|
|
273
|
+
expect(await titles({ tags: { every: { label: 'art' } } })).toEqual([
|
|
274
|
+
'Engines',
|
|
275
|
+
])
|
|
276
|
+
// Grace's book has no pages: unknown is not false, as in SQL.
|
|
277
|
+
expect(await names({ books: { every: { pages: { gt: 200 } } } })).toEqual(
|
|
278
|
+
['Grace'],
|
|
279
|
+
)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('orders by several fields, nulls last ascending', async () => {
|
|
283
|
+
expect(await ordered({ pages: 'asc' })).toEqual([
|
|
284
|
+
'Engines',
|
|
285
|
+
'Analysis',
|
|
286
|
+
'Compilers',
|
|
287
|
+
])
|
|
288
|
+
expect(await ordered({ pages: 'desc' })).toEqual([
|
|
289
|
+
'Compilers',
|
|
290
|
+
'Analysis',
|
|
291
|
+
'Engines',
|
|
292
|
+
])
|
|
293
|
+
expect(await ordered([{ published: 'asc' }, { title: 'desc' }])).toEqual([
|
|
294
|
+
'Engines',
|
|
295
|
+
'Compilers',
|
|
296
|
+
'Analysis',
|
|
297
|
+
])
|
|
298
|
+
// Without an order, rows come in the order they were created.
|
|
299
|
+
expect(await ordered([])).toEqual(['Analysis', 'Engines', 'Compilers'])
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it('orders strings by code point, as the bytes of their UTF-8', async () => {
|
|
303
|
+
// UTF-16 puts U+FF5A above U+1F600, which is above it as a code point.
|
|
304
|
+
await Tag.create({ data: { label: '😀' } }, M)
|
|
305
|
+
await Tag.create({ data: { label: 'z' } }, M)
|
|
306
|
+
const tags = await Tag.findMany({ orderBy: { label: 'asc' } }, M)
|
|
307
|
+
expect(tags.map(({ label }) => label)).toEqual([
|
|
308
|
+
'art',
|
|
309
|
+
'math',
|
|
310
|
+
'z',
|
|
311
|
+
'😀',
|
|
312
|
+
])
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it('skips, takes and starts at a cursor, forwards and backwards', async () => {
|
|
316
|
+
expect(await page({ skip: 1, take: 1 })).toEqual(['Compilers'])
|
|
317
|
+
expect(await page({ take: -2 })).toEqual(['Compilers', 'Engines'])
|
|
318
|
+
// A cursor is any unique key, here the index on title and author.
|
|
319
|
+
const cursor = { title: 'Compilers', author: authors.grace }
|
|
320
|
+
expect(await page({ cursor, take: 2 })).toEqual(['Compilers', 'Engines'])
|
|
321
|
+
expect(await page({ cursor, skip: 1 })).toEqual(['Engines'])
|
|
322
|
+
expect(await page({ cursor, take: -2 })).toEqual([
|
|
323
|
+
'Analysis',
|
|
324
|
+
'Compilers',
|
|
325
|
+
])
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
it('includes relations, filtered, ordered and paged, at any depth', async () => {
|
|
329
|
+
const ada = await Author.findUnique(
|
|
330
|
+
{
|
|
331
|
+
where: { name: 'Ada' },
|
|
332
|
+
include: {
|
|
333
|
+
profile: true,
|
|
334
|
+
books: {
|
|
335
|
+
where: { pages: { gt: 100 } },
|
|
336
|
+
orderBy: { pages: 'asc' },
|
|
337
|
+
take: 1,
|
|
338
|
+
include: { tags: { orderBy: { label: 'asc' } } },
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
M,
|
|
343
|
+
)
|
|
344
|
+
expect(ada).toMatchObject({
|
|
345
|
+
profile: { bio: 'Poet' },
|
|
346
|
+
// An array matches only an array of the same length.
|
|
347
|
+
books: [{ title: 'Engines', tags: [] }],
|
|
348
|
+
})
|
|
349
|
+
const compilers = await Book.findFirst(
|
|
350
|
+
{
|
|
351
|
+
where: { title: 'Compilers' },
|
|
352
|
+
select: {
|
|
353
|
+
title: true,
|
|
354
|
+
author: { select: { name: true, profile: true } },
|
|
355
|
+
tags: { select: { label: true }, orderBy: { label: 'asc' } },
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
M,
|
|
359
|
+
)
|
|
360
|
+
expect(compilers).toEqual({
|
|
361
|
+
title: 'Compilers',
|
|
362
|
+
author: { name: 'Grace', profile: null },
|
|
363
|
+
tags: [{ label: 'art' }, { label: 'math' }],
|
|
364
|
+
})
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
it('finds a row by a unique index', async () => {
|
|
368
|
+
const engines = await Book.findUnique(
|
|
369
|
+
{ where: { title: 'Engines', author: authors.ada } },
|
|
370
|
+
M,
|
|
371
|
+
)
|
|
372
|
+
expect(engines).toMatchObject({ title: 'Engines', pages: 120 })
|
|
373
|
+
expect(await Book.findUnique({ where: { id: 'missing' } }, M)).toBeNull()
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it('returns each type as written, and counts', async () => {
|
|
377
|
+
const analysis = await Book.findFirst({ where: { title: 'Analysis' } }, M)
|
|
378
|
+
expect(analysis).toMatchObject({
|
|
379
|
+
pages: 300,
|
|
380
|
+
published: true,
|
|
381
|
+
meta: { rank: 0, kind: 'book' },
|
|
382
|
+
releasedAt: day(1),
|
|
383
|
+
createdAt: aDate,
|
|
384
|
+
})
|
|
385
|
+
expect(await Book.count({}, M)).toBe(3)
|
|
386
|
+
expect(await Book.count({ where: { tags: { some: {} } } }, M)).toBe(2)
|
|
387
|
+
})
|
|
388
|
+
})
|
|
389
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Filtered relations in the contract (D-151): a filter, one-to-many or
|
|
2
|
+
// many-to-many, narrows include, `_count`, a sort by it and some, every and
|
|
3
|
+
// none, in the adapter's one query; a view reads the link of its relation
|
|
4
|
+
// with its own filter, and has no column, key or join of its own.
|
|
5
|
+
import type { Adapter, Identity } from 'ultimabase'
|
|
6
|
+
import { allow, defineModel, master } from 'ultimabase'
|
|
7
|
+
import { beforeEach, describe, expect, it } from 'vitest'
|
|
8
|
+
import { started } from './suite.ts'
|
|
9
|
+
|
|
10
|
+
const M: { readonly as: Identity } = { as: master }
|
|
11
|
+
|
|
12
|
+
const Writer = defineModel({
|
|
13
|
+
name: 'Writer',
|
|
14
|
+
fields: (f) => ({
|
|
15
|
+
name: f.string({ unique: true }),
|
|
16
|
+
notes: f.many(() => Note),
|
|
17
|
+
labels: f.many(() => Label),
|
|
18
|
+
drafts: f.many(() => Note, { from: 'notes' }),
|
|
19
|
+
secrets: f.many(() => Label, { from: 'labels' }),
|
|
20
|
+
}),
|
|
21
|
+
}).with({
|
|
22
|
+
access: allow.public,
|
|
23
|
+
relations: {
|
|
24
|
+
notes: { where: ({ where }) => where({ published: true }) },
|
|
25
|
+
labels: { where: ({ where }) => where({ hidden: false }) },
|
|
26
|
+
drafts: { where: ({ where }) => where({ published: false }) },
|
|
27
|
+
secrets: { where: ({ where }) => where({ hidden: true }) },
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const Note = defineModel({
|
|
32
|
+
name: 'Note',
|
|
33
|
+
fields: (f) => ({
|
|
34
|
+
title: f.string(),
|
|
35
|
+
published: f.boolean({ default: false }),
|
|
36
|
+
writer: f.one(() => Writer, { optional: true }),
|
|
37
|
+
}),
|
|
38
|
+
}).with({ access: allow.public })
|
|
39
|
+
|
|
40
|
+
const Label = defineModel({
|
|
41
|
+
name: 'Label',
|
|
42
|
+
fields: (f) => ({
|
|
43
|
+
name: f.string(),
|
|
44
|
+
hidden: f.boolean({ default: false }),
|
|
45
|
+
writers: f.many(() => Writer),
|
|
46
|
+
}),
|
|
47
|
+
}).with({ access: allow.public })
|
|
48
|
+
|
|
49
|
+
const names = async (where: object): Promise<unknown> =>
|
|
50
|
+
Writer.findMany(
|
|
51
|
+
{ where, orderBy: { name: 'asc' }, select: { name: true } },
|
|
52
|
+
M,
|
|
53
|
+
)
|
|
54
|
+
const named = (...list: readonly string[]) => list.map((name) => ({ name }))
|
|
55
|
+
|
|
56
|
+
export function relationFilters(create: () => Adapter): void {
|
|
57
|
+
describe('filtered relations', () => {
|
|
58
|
+
started(create, [Writer, Note, Label])
|
|
59
|
+
|
|
60
|
+
beforeEach(async () => {
|
|
61
|
+
await Writer.create(
|
|
62
|
+
{
|
|
63
|
+
data: {
|
|
64
|
+
name: 'Ada',
|
|
65
|
+
notes: {
|
|
66
|
+
create: [
|
|
67
|
+
{ title: 'One', published: true },
|
|
68
|
+
{ title: 'Draft' },
|
|
69
|
+
{ title: 'Two', published: true },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
labels: {
|
|
73
|
+
create: [{ name: 'open' }, { name: 'secret', hidden: true }],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
M,
|
|
78
|
+
)
|
|
79
|
+
await Writer.create(
|
|
80
|
+
{ data: { name: 'Bob', notes: { create: [{ title: 'Bob draft' }] } } },
|
|
81
|
+
M,
|
|
82
|
+
)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('includes and counts the rows the filter keeps', async () => {
|
|
86
|
+
const rows = await Writer.findMany(
|
|
87
|
+
{
|
|
88
|
+
orderBy: [{ notes: { _count: 'desc' } }, { name: 'asc' }],
|
|
89
|
+
select: {
|
|
90
|
+
name: true,
|
|
91
|
+
notes: { orderBy: { title: 'asc' }, select: { title: true } },
|
|
92
|
+
labels: { select: { name: true } },
|
|
93
|
+
_count: { select: { notes: true, labels: true } },
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
M,
|
|
97
|
+
)
|
|
98
|
+
expect(rows).toEqual([
|
|
99
|
+
{
|
|
100
|
+
name: 'Ada',
|
|
101
|
+
notes: [{ title: 'One' }, { title: 'Two' }],
|
|
102
|
+
labels: [{ name: 'open' }],
|
|
103
|
+
_count: { notes: 2, labels: 1 },
|
|
104
|
+
},
|
|
105
|
+
{ name: 'Bob', notes: [], labels: [], _count: { notes: 0, labels: 0 } },
|
|
106
|
+
])
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('filters some, every and none', async () => {
|
|
110
|
+
expect(await names({ notes: { some: { title: 'Draft' } } })).toEqual([])
|
|
111
|
+
expect(await names({ labels: { some: { hidden: true } } })).toEqual([])
|
|
112
|
+
expect(await names({ notes: { every: { published: true } } })).toEqual(
|
|
113
|
+
named('Ada', 'Bob'),
|
|
114
|
+
)
|
|
115
|
+
expect(await names({ notes: { none: {} } })).toEqual(named('Bob'))
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('reads a view through the link of its relation', async () => {
|
|
119
|
+
const rows = await Writer.findMany(
|
|
120
|
+
{
|
|
121
|
+
orderBy: [{ secrets: { _count: 'desc' } }, { name: 'asc' }],
|
|
122
|
+
select: {
|
|
123
|
+
name: true,
|
|
124
|
+
drafts: { select: { title: true } },
|
|
125
|
+
secrets: { select: { name: true } },
|
|
126
|
+
_count: { select: { drafts: true, secrets: true } },
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
M,
|
|
130
|
+
)
|
|
131
|
+
expect(rows).toEqual([
|
|
132
|
+
{
|
|
133
|
+
name: 'Ada',
|
|
134
|
+
drafts: [{ title: 'Draft' }],
|
|
135
|
+
secrets: [{ name: 'secret' }],
|
|
136
|
+
_count: { drafts: 1, secrets: 1 },
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: 'Bob',
|
|
140
|
+
drafts: [{ title: 'Bob draft' }],
|
|
141
|
+
secrets: [],
|
|
142
|
+
_count: { drafts: 1, secrets: 0 },
|
|
143
|
+
},
|
|
144
|
+
])
|
|
145
|
+
expect(await names({ secrets: { some: {} } })).toEqual(named('Ada'))
|
|
146
|
+
expect(await names({ drafts: { every: { title: 'Draft' } } })).toEqual(
|
|
147
|
+
named('Ada'),
|
|
148
|
+
)
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
}
|