@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
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// The adapter contract suite (D-02, D-73, D-94): what every adapter does
|
|
2
|
+
// alike, checked through the kernel's public API.
|
|
3
|
+
import type { Adapter } from 'ultimabase'
|
|
4
|
+
import { describe } from 'vitest'
|
|
5
|
+
import { aggregates } from './aggregates.ts'
|
|
6
|
+
import { bulkReturns } from './bulk-returns.ts'
|
|
7
|
+
import { calendarDays } from './days.ts'
|
|
8
|
+
import { cases } from './cases.ts'
|
|
9
|
+
import { cascades } from './cascades.ts'
|
|
10
|
+
import { counts } from './counts.ts'
|
|
11
|
+
import { decimals } from './decimals.ts'
|
|
12
|
+
import { distincts } from './distincts.ts'
|
|
13
|
+
import { feeds } from './feeds.ts'
|
|
14
|
+
import { groups } from './groups.ts'
|
|
15
|
+
import { jsonPaths } from './json-paths.ts'
|
|
16
|
+
import { lists } from './lists.ts'
|
|
17
|
+
import { nested } from './nested.ts'
|
|
18
|
+
import { numbers } from './numbers.ts'
|
|
19
|
+
import { projection } from './projection.ts'
|
|
20
|
+
import { reads } from './reads.ts'
|
|
21
|
+
import { relationFilters } from './relation-filters.ts'
|
|
22
|
+
import { roundTrips } from './round-trips.ts'
|
|
23
|
+
import { searches } from './searches.ts'
|
|
24
|
+
import { sorts } from './sorts.ts'
|
|
25
|
+
import { transactions } from './transactions.ts'
|
|
26
|
+
import { virtuals } from './virtuals.ts'
|
|
27
|
+
import { writes } from './writes.ts'
|
|
28
|
+
|
|
29
|
+
export type Options = {
|
|
30
|
+
/** How many round trips the adapters have made so far, BEGIN and COMMIT included (D-98). */
|
|
31
|
+
readonly roundTrips: () => number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Runs the contract against the adapters `create` returns, one per test, each
|
|
36
|
+
* on an empty database.
|
|
37
|
+
*/
|
|
38
|
+
export function testAdapter(
|
|
39
|
+
name: string,
|
|
40
|
+
create: () => Adapter,
|
|
41
|
+
options: Options,
|
|
42
|
+
): void {
|
|
43
|
+
describe(`the ${name} adapter`, () => {
|
|
44
|
+
reads(create)
|
|
45
|
+
cases(create)
|
|
46
|
+
writes(create)
|
|
47
|
+
cascades(create)
|
|
48
|
+
lists(create)
|
|
49
|
+
jsonPaths(create)
|
|
50
|
+
nested(create)
|
|
51
|
+
numbers(create)
|
|
52
|
+
bulkReturns(create)
|
|
53
|
+
decimals(create)
|
|
54
|
+
calendarDays(create)
|
|
55
|
+
counts(create)
|
|
56
|
+
relationFilters(create)
|
|
57
|
+
virtuals(create)
|
|
58
|
+
aggregates(create)
|
|
59
|
+
groups(create)
|
|
60
|
+
distincts(create)
|
|
61
|
+
sorts(create)
|
|
62
|
+
searches(create)
|
|
63
|
+
projection(create)
|
|
64
|
+
transactions(create)
|
|
65
|
+
feeds(create)
|
|
66
|
+
roundTrips(create, options.roundTrips)
|
|
67
|
+
})
|
|
68
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// An application's tests (D-130): before each test it starts as declared, on
|
|
2
|
+
// a new memory() or its declared database's test tables (D-137) and a free
|
|
3
|
+
// port, without info lines; after each it stops.
|
|
4
|
+
import type { Adapter } from 'ultimabase'
|
|
5
|
+
import { memory } from 'ultimabase'
|
|
6
|
+
import type { Lifecycle } from 'ultimabase/internal'
|
|
7
|
+
import { quietRuns, runningApi, scratchOf } from 'ultimabase/internal'
|
|
8
|
+
import { afterEach, beforeAll, beforeEach } from 'vitest'
|
|
9
|
+
|
|
10
|
+
export type TestApplication = {
|
|
11
|
+
/** The HTTP server's address during a test, such as `http://localhost:54321`. */
|
|
12
|
+
readonly url: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Starts `run` with `PORT` set to `0`, then puts `PORT` back. */
|
|
16
|
+
async function onFreePort(run: Lifecycle): Promise<void> {
|
|
17
|
+
const port = process.env['PORT']
|
|
18
|
+
process.env['PORT'] = '0'
|
|
19
|
+
try {
|
|
20
|
+
await run.start()
|
|
21
|
+
} finally {
|
|
22
|
+
if (port === undefined) delete process.env['PORT']
|
|
23
|
+
else process.env['PORT'] = port
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function urlOf(api: unknown): string | undefined {
|
|
28
|
+
if (typeof api !== 'object' || api === null || !('url' in api)) {
|
|
29
|
+
return undefined
|
|
30
|
+
}
|
|
31
|
+
const { url } = api
|
|
32
|
+
if (typeof url !== 'function') return undefined
|
|
33
|
+
const value: unknown = Reflect.apply(url, api, [])
|
|
34
|
+
return typeof value === 'string' ? value : undefined
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type TestOptions = {
|
|
38
|
+
/**
|
|
39
|
+
* `'declared'` runs the tests on the application's own database, in
|
|
40
|
+
* tables of the file's own, instead of a new `memory()`.
|
|
41
|
+
*/
|
|
42
|
+
readonly database?: 'declared'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The declared database, which gives each test file empty tables. */
|
|
46
|
+
function declared(database: Adapter): () => Adapter {
|
|
47
|
+
// A database that starts empty every time, as memory(), needs nothing.
|
|
48
|
+
if (database.migrations === undefined) return () => database
|
|
49
|
+
const scratch = scratchOf(database)
|
|
50
|
+
if (scratch === undefined) {
|
|
51
|
+
throw new TypeError(
|
|
52
|
+
"testApplication runs on the declared database when it gives a test file tables of its own, as postgres() does; this one does not: remove { database: 'declared' }.",
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
beforeAll(() => scratch().drop)
|
|
56
|
+
return () => database
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Runs `app`, the value `defineApp` returned, around each test of the file
|
|
61
|
+
* that calls it.
|
|
62
|
+
*/
|
|
63
|
+
export function testApplication(
|
|
64
|
+
app: object,
|
|
65
|
+
options: TestOptions = {},
|
|
66
|
+
): TestApplication {
|
|
67
|
+
const runs = quietRuns(app)
|
|
68
|
+
if (runs === undefined) {
|
|
69
|
+
throw new TypeError('testApplication takes the value defineApp returned.')
|
|
70
|
+
}
|
|
71
|
+
const database =
|
|
72
|
+
options.database === 'declared' ? declared(runs.declared) : memory
|
|
73
|
+
let run: Lifecycle | undefined
|
|
74
|
+
beforeEach(async () => {
|
|
75
|
+
run = runs.on(database())
|
|
76
|
+
await onFreePort(run)
|
|
77
|
+
})
|
|
78
|
+
afterEach(async () => {
|
|
79
|
+
await run?.stop()
|
|
80
|
+
run = undefined
|
|
81
|
+
})
|
|
82
|
+
return {
|
|
83
|
+
get url() {
|
|
84
|
+
const url = run === undefined ? undefined : urlOf(runningApi('http'))
|
|
85
|
+
if (url === undefined) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
'url is the address of httpPlugin(), read during a test: add httpPlugin() to the application.',
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
return url
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// The transactions of the contract (D-73): one commit for every write, one
|
|
2
|
+
// rollback for all of them, and nothing seen outside before the commit.
|
|
3
|
+
import type { Adapter } from 'ultimabase'
|
|
4
|
+
import { defineModel, master, transaction } from 'ultimabase'
|
|
5
|
+
import { describe, expect, it } from 'vitest'
|
|
6
|
+
import { started } from './suite.ts'
|
|
7
|
+
|
|
8
|
+
const M = { as: master }
|
|
9
|
+
|
|
10
|
+
const Account = defineModel({
|
|
11
|
+
name: 'Account',
|
|
12
|
+
fields: (f) => ({ name: f.string({ unique: true }) }),
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const open = async (name: string) => Account.create({ data: { name } }, M)
|
|
16
|
+
const names = async (): Promise<string[]> =>
|
|
17
|
+
(await Account.findMany({ orderBy: { name: 'asc' } }, M)).map(
|
|
18
|
+
(account) => account.name,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
export function transactions(create: () => Adapter): void {
|
|
22
|
+
describe('transactions', () => {
|
|
23
|
+
started(create, [Account])
|
|
24
|
+
|
|
25
|
+
it('commits every write together', async () => {
|
|
26
|
+
await transaction(async () => {
|
|
27
|
+
await open('bob')
|
|
28
|
+
await open('cy')
|
|
29
|
+
})
|
|
30
|
+
expect(await names()).toEqual(['bob', 'cy'])
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('reads its own writes', async () => {
|
|
34
|
+
const seen = await transaction(async () => {
|
|
35
|
+
await open('bob')
|
|
36
|
+
return names()
|
|
37
|
+
})
|
|
38
|
+
expect(seen).toEqual(['bob'])
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('rolls every write back when the callback throws', async () => {
|
|
42
|
+
await open('ann')
|
|
43
|
+
const failed = transaction(async () => {
|
|
44
|
+
await open('bob')
|
|
45
|
+
throw new Error('Stop.')
|
|
46
|
+
})
|
|
47
|
+
await expect(failed).rejects.toThrow('Stop.')
|
|
48
|
+
expect(await names()).toEqual(['ann'])
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('rolls back after a failed write, even one the callback catches', async () => {
|
|
52
|
+
const failed = transaction(async () => {
|
|
53
|
+
await open('bob')
|
|
54
|
+
await open('bob').catch(() => undefined)
|
|
55
|
+
})
|
|
56
|
+
await expect(failed).rejects.toMatchObject({ code: 'UNIQUE_VIOLATION' })
|
|
57
|
+
expect(await names()).toEqual([])
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('shows its writes to no one else before the commit', async () => {
|
|
61
|
+
const written = Promise.withResolvers<void>()
|
|
62
|
+
const resumed = Promise.withResolvers<void>()
|
|
63
|
+
const running = transaction(async () => {
|
|
64
|
+
await open('bob')
|
|
65
|
+
written.resolve()
|
|
66
|
+
await resumed.promise
|
|
67
|
+
})
|
|
68
|
+
await written.promise
|
|
69
|
+
// This read runs outside the transaction.
|
|
70
|
+
expect(await names()).toEqual([])
|
|
71
|
+
resumed.resolve()
|
|
72
|
+
await running
|
|
73
|
+
expect(await names()).toEqual(['bob'])
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
}
|
package/src/virtuals.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// Virtual fields in the contract (D-151): the adapter reads the fields a
|
|
2
|
+
// virtual needs though the caller selects none of them, and the rows a
|
|
3
|
+
// virtual returns are read again by id with their relations and counts.
|
|
4
|
+
import type { Adapter, Identity } from 'ultimabase'
|
|
5
|
+
import { allow, defineModel, master } from 'ultimabase'
|
|
6
|
+
import { beforeEach, describe, expect, it } from 'vitest'
|
|
7
|
+
import { started } from './suite.ts'
|
|
8
|
+
|
|
9
|
+
const M: { readonly as: Identity } = { as: master }
|
|
10
|
+
|
|
11
|
+
type Result<T> =
|
|
12
|
+
| { readonly value: T }
|
|
13
|
+
| { readonly issues: readonly { readonly message: string }[] }
|
|
14
|
+
|
|
15
|
+
/** A Standard Schema of what `check` accepts, as zod would type it. */
|
|
16
|
+
function schema<T>(check: (value: unknown) => value is T): {
|
|
17
|
+
readonly '~standard': {
|
|
18
|
+
readonly version: 1
|
|
19
|
+
readonly vendor: string
|
|
20
|
+
readonly validate: (value: unknown) => Result<T>
|
|
21
|
+
readonly types?: { readonly input: T; readonly output: T }
|
|
22
|
+
}
|
|
23
|
+
} {
|
|
24
|
+
const validate = (value: unknown): Result<T> =>
|
|
25
|
+
check(value) ? { value } : { issues: [{ message: 'Invalid.' }] }
|
|
26
|
+
return { '~standard': { version: 1, vendor: 'testing', validate } }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const text = schema((value): value is string => typeof value === 'string')
|
|
30
|
+
const width = schema((value): value is { readonly width: number } =>
|
|
31
|
+
Number.isInteger(Reflect.get(Object(value), 'width')),
|
|
32
|
+
)
|
|
33
|
+
const first = schema((value): value is { readonly first: number } =>
|
|
34
|
+
Number.isInteger(Reflect.get(Object(value), 'first')),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
const Shelf = defineModel({
|
|
38
|
+
name: 'Shelf',
|
|
39
|
+
fields: (f) => ({
|
|
40
|
+
name: f.string(),
|
|
41
|
+
books: f.many(() => Book),
|
|
42
|
+
label: f.virtual(text, { needs: ['name'] }),
|
|
43
|
+
padded: f.virtual(text, { input: width, needs: ['name'] }),
|
|
44
|
+
latest: f.virtual(() => [Book], { input: first }),
|
|
45
|
+
}),
|
|
46
|
+
}).with({
|
|
47
|
+
access: allow.public,
|
|
48
|
+
virtuals: {
|
|
49
|
+
label: { run: ({ record }) => record.name.toUpperCase() },
|
|
50
|
+
padded: {
|
|
51
|
+
run: ({ input, record }) => record.name.padEnd(input.width, '.'),
|
|
52
|
+
},
|
|
53
|
+
latest: {
|
|
54
|
+
runMany: async ({ input, records }) => {
|
|
55
|
+
const ids = records.map((record) => record.id)
|
|
56
|
+
const books = await Book.findMany(
|
|
57
|
+
{
|
|
58
|
+
where: { shelf: { is: { id: { in: ids } } } },
|
|
59
|
+
orderBy: { title: 'desc' },
|
|
60
|
+
include: { shelf: { select: { id: true } } },
|
|
61
|
+
},
|
|
62
|
+
M,
|
|
63
|
+
)
|
|
64
|
+
return records.map((record) =>
|
|
65
|
+
books
|
|
66
|
+
.filter((book) => book.shelf?.id === record.id)
|
|
67
|
+
.slice(0, input.first),
|
|
68
|
+
)
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const Book = defineModel({
|
|
75
|
+
name: 'Book',
|
|
76
|
+
fields: (f) => ({
|
|
77
|
+
title: f.string(),
|
|
78
|
+
shelf: f.one(() => Shelf, { optional: true }),
|
|
79
|
+
tags: f.many(() => Tag),
|
|
80
|
+
}),
|
|
81
|
+
}).with({ access: allow.public })
|
|
82
|
+
|
|
83
|
+
const Tag = defineModel({
|
|
84
|
+
name: 'Tag',
|
|
85
|
+
fields: (f) => ({ name: f.string(), books: f.many(() => Book) }),
|
|
86
|
+
}).with({ access: allow.public })
|
|
87
|
+
|
|
88
|
+
export function virtuals(create: () => Adapter): void {
|
|
89
|
+
describe('virtual fields', () => {
|
|
90
|
+
started(create, [Shelf, Book, Tag])
|
|
91
|
+
|
|
92
|
+
beforeEach(async () => {
|
|
93
|
+
await Shelf.create(
|
|
94
|
+
{
|
|
95
|
+
data: {
|
|
96
|
+
name: 'oak',
|
|
97
|
+
books: {
|
|
98
|
+
create: [
|
|
99
|
+
{ title: 'A', tags: { create: [{ name: 'old' }] } },
|
|
100
|
+
{ title: 'B' },
|
|
101
|
+
{ title: 'C', tags: { create: [{ name: 'new' }] } },
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
M,
|
|
107
|
+
)
|
|
108
|
+
await Shelf.create({ data: { name: 'elm' } }, M)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('reads the fields it needs, which the row leaves out', async () => {
|
|
112
|
+
const rows = await Shelf.findMany(
|
|
113
|
+
{
|
|
114
|
+
orderBy: { name: 'asc' },
|
|
115
|
+
select: { label: true, padded: { width: 5 } },
|
|
116
|
+
},
|
|
117
|
+
M,
|
|
118
|
+
)
|
|
119
|
+
expect(rows).toStrictEqual([
|
|
120
|
+
{ label: 'ELM', padded: 'elm..' },
|
|
121
|
+
{ label: 'OAK', padded: 'oak..' },
|
|
122
|
+
])
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('is in the default row without an input', async () => {
|
|
126
|
+
const row = await Shelf.findFirst({ where: { name: 'oak' } }, M)
|
|
127
|
+
expect(row?.label).toBe('OAK')
|
|
128
|
+
expect(row && 'padded' in row).toBe(false)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('reads the rows it returns with their relations', async () => {
|
|
132
|
+
const rows = await Shelf.findMany(
|
|
133
|
+
{
|
|
134
|
+
orderBy: { name: 'asc' },
|
|
135
|
+
select: {
|
|
136
|
+
name: true,
|
|
137
|
+
latest: {
|
|
138
|
+
first: 2,
|
|
139
|
+
select: {
|
|
140
|
+
title: true,
|
|
141
|
+
shelf: { select: { label: true } },
|
|
142
|
+
tags: { select: { name: true } },
|
|
143
|
+
_count: { select: { tags: true } },
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
M,
|
|
149
|
+
)
|
|
150
|
+
expect(rows).toStrictEqual([
|
|
151
|
+
{ name: 'elm', latest: [] },
|
|
152
|
+
{
|
|
153
|
+
name: 'oak',
|
|
154
|
+
latest: [
|
|
155
|
+
{
|
|
156
|
+
title: 'C',
|
|
157
|
+
shelf: { label: 'OAK' },
|
|
158
|
+
tags: [{ name: 'new' }],
|
|
159
|
+
_count: { tags: 1 },
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
title: 'B',
|
|
163
|
+
shelf: { label: 'OAK' },
|
|
164
|
+
tags: [],
|
|
165
|
+
_count: { tags: 0 },
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
])
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('is read under an included relation', async () => {
|
|
173
|
+
const book = await Book.findFirst(
|
|
174
|
+
{
|
|
175
|
+
where: { title: 'A' },
|
|
176
|
+
include: { shelf: { include: { padded: { width: 4 } } } },
|
|
177
|
+
},
|
|
178
|
+
M,
|
|
179
|
+
)
|
|
180
|
+
expect(book?.shelf?.label).toBe('OAK')
|
|
181
|
+
expect(book?.shelf?.padded).toBe('oak.')
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
}
|