@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/dist/nested.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { defineModel, master } from 'ultimabase';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { started } from './suite.js';
|
|
4
|
+
const M = { as: master };
|
|
5
|
+
const Blogger = defineModel({
|
|
6
|
+
name: 'Blogger',
|
|
7
|
+
fields: (f) => ({
|
|
8
|
+
name: f.string(),
|
|
9
|
+
posts: f.many(() => Post),
|
|
10
|
+
card: f.one(() => Card, { optional: true }),
|
|
11
|
+
}),
|
|
12
|
+
});
|
|
13
|
+
/** Holds the key of its one-to-one with a blogger (D-37). */
|
|
14
|
+
const Card = defineModel({
|
|
15
|
+
name: 'Card',
|
|
16
|
+
fields: (f) => ({
|
|
17
|
+
bio: f.string(),
|
|
18
|
+
blogger: f.one(() => Blogger, { onDelete: 'cascade' }),
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
21
|
+
const Post = defineModel({
|
|
22
|
+
name: 'Post',
|
|
23
|
+
fields: (f) => ({
|
|
24
|
+
title: f.string({ unique: true }),
|
|
25
|
+
blogger: f.one(() => Blogger, { onDelete: 'cascade' }),
|
|
26
|
+
comments: f.many(() => Comment),
|
|
27
|
+
topics: f.many(() => Topic),
|
|
28
|
+
}),
|
|
29
|
+
});
|
|
30
|
+
const Comment = defineModel({
|
|
31
|
+
name: 'Comment',
|
|
32
|
+
fields: (f) => ({
|
|
33
|
+
body: f.string(),
|
|
34
|
+
post: f.one(() => Post, { onDelete: 'cascade' }),
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
const Topic = defineModel({
|
|
38
|
+
name: 'Topic',
|
|
39
|
+
fields: (f) => ({ label: f.string(), posts: f.many(() => Post) }),
|
|
40
|
+
});
|
|
41
|
+
export function nested(create) {
|
|
42
|
+
describe('nested creation', () => {
|
|
43
|
+
started(create, [Blogger, Card, Post, Comment, Topic]);
|
|
44
|
+
it('creates rows three levels deep with the one it writes', async () => {
|
|
45
|
+
const topic = await Topic.create({ data: { label: 'ts' } }, M);
|
|
46
|
+
const blogger = await Blogger.create({
|
|
47
|
+
data: {
|
|
48
|
+
name: 'Ada',
|
|
49
|
+
card: { create: { bio: 'engines' } },
|
|
50
|
+
posts: {
|
|
51
|
+
create: [
|
|
52
|
+
{
|
|
53
|
+
title: 'First',
|
|
54
|
+
comments: { create: [{ body: 'a' }, { body: 'b' }] },
|
|
55
|
+
topics: {
|
|
56
|
+
connect: [{ id: topic.id }],
|
|
57
|
+
create: [{ label: 'go' }],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{ title: 'Second' },
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
include: {
|
|
65
|
+
card: true,
|
|
66
|
+
posts: { include: { comments: true, topics: true } },
|
|
67
|
+
},
|
|
68
|
+
}, M);
|
|
69
|
+
expect(blogger).toMatchObject({
|
|
70
|
+
name: 'Ada',
|
|
71
|
+
card: { bio: 'engines' },
|
|
72
|
+
posts: [
|
|
73
|
+
{
|
|
74
|
+
title: 'First',
|
|
75
|
+
comments: [{ body: 'a' }, { body: 'b' }],
|
|
76
|
+
topics: [{ label: 'ts' }, { label: 'go' }],
|
|
77
|
+
},
|
|
78
|
+
{ title: 'Second', comments: [], topics: [] },
|
|
79
|
+
],
|
|
80
|
+
});
|
|
81
|
+
expect(await Comment.count({}, M)).toBe(2);
|
|
82
|
+
expect(await Topic.count({}, M)).toBe(2);
|
|
83
|
+
});
|
|
84
|
+
it('creates the row a stored one links to', async () => {
|
|
85
|
+
const comment = await Comment.create({
|
|
86
|
+
data: {
|
|
87
|
+
body: 'hello',
|
|
88
|
+
post: {
|
|
89
|
+
create: { title: 'Root', blogger: { create: { name: 'Bo' } } },
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
include: { post: { include: { blogger: true } } },
|
|
93
|
+
}, M);
|
|
94
|
+
expect(comment).toMatchObject({
|
|
95
|
+
post: { title: 'Root', blogger: { name: 'Bo' } },
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
it('creates rows in an update, beside connect and disconnect', async () => {
|
|
99
|
+
const blogger = await Blogger.create({ data: { name: 'Ada' } }, M);
|
|
100
|
+
const old = await Topic.create({ data: { label: 'old' } }, M);
|
|
101
|
+
const post = await Post.create({
|
|
102
|
+
data: {
|
|
103
|
+
title: 'Kept',
|
|
104
|
+
blogger: { connect: { id: blogger.id } },
|
|
105
|
+
topics: { connect: [{ id: old.id }] },
|
|
106
|
+
},
|
|
107
|
+
}, M);
|
|
108
|
+
const updated = await Post.update({
|
|
109
|
+
where: { id: post.id },
|
|
110
|
+
data: {
|
|
111
|
+
comments: { create: [{ body: 'late' }] },
|
|
112
|
+
topics: {
|
|
113
|
+
disconnect: [{ id: old.id }],
|
|
114
|
+
create: [{ label: 'new' }],
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
include: { comments: true, topics: true },
|
|
118
|
+
}, M);
|
|
119
|
+
expect(updated).toMatchObject({
|
|
120
|
+
comments: [{ body: 'late' }],
|
|
121
|
+
topics: [{ label: 'new' }],
|
|
122
|
+
});
|
|
123
|
+
const card = await Blogger.update({
|
|
124
|
+
where: { id: blogger.id },
|
|
125
|
+
data: { card: { create: { bio: 'later' } } },
|
|
126
|
+
include: { card: true },
|
|
127
|
+
}, M);
|
|
128
|
+
expect(card).toMatchObject({ card: { bio: 'later' } });
|
|
129
|
+
});
|
|
130
|
+
it('writes nothing when a row deep down fails', async () => {
|
|
131
|
+
const blogger = await Blogger.create({ data: { name: 'Ada' } }, M);
|
|
132
|
+
await Post.create({
|
|
133
|
+
data: { title: 'Taken', blogger: { connect: { id: blogger.id } } },
|
|
134
|
+
}, M);
|
|
135
|
+
const failed = Blogger.create({
|
|
136
|
+
data: {
|
|
137
|
+
name: 'Bo',
|
|
138
|
+
posts: {
|
|
139
|
+
create: [
|
|
140
|
+
{ title: 'Fresh', comments: { create: [{ body: 'x' }] } },
|
|
141
|
+
{ title: 'Taken' },
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
}, M);
|
|
146
|
+
await expect(failed).rejects.toMatchObject({ code: 'UNIQUE_VIOLATION' });
|
|
147
|
+
expect(await Blogger.count({}, M)).toBe(1);
|
|
148
|
+
expect(await Post.count({}, M)).toBe(1);
|
|
149
|
+
expect(await Comment.count({}, M)).toBe(0);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"numbers.d.ts","sourceRoot":"","sources":["../src/numbers.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAgB,MAAM,YAAY,CAAA;AAgCvD,wBAAgB,OAAO,CAAC,MAAM,EAAE,MAAM,OAAO,GAAG,IAAI,CA0FnD"}
|
package/dist/numbers.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { defineModel, master } from 'ultimabase';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { started } from './suite.js';
|
|
4
|
+
const M = { as: master };
|
|
5
|
+
const Tally = defineModel({
|
|
6
|
+
name: 'Tally',
|
|
7
|
+
fields: (f) => ({
|
|
8
|
+
count: f.number({ integer: true, optional: true }),
|
|
9
|
+
ratio: f.number({ optional: true }),
|
|
10
|
+
points: f.number({ integer: true, list: true }),
|
|
11
|
+
}),
|
|
12
|
+
});
|
|
13
|
+
/** An update hook: `updateMany` locks the rows and groups their writes. */
|
|
14
|
+
const Stock = defineModel({
|
|
15
|
+
name: 'Stock',
|
|
16
|
+
fields: (f) => ({ level: f.number({ integer: true }) }),
|
|
17
|
+
}).with({ hooks: { beforeUpdate: () => { } } });
|
|
18
|
+
const whole = 'Expected a whole number from -2,147,483,648 to 2,147,483,647.';
|
|
19
|
+
const update = async (data, from) => {
|
|
20
|
+
const { id } = await Tally.create({ data: from }, M);
|
|
21
|
+
return Tally.update({ where: { id }, data }, M);
|
|
22
|
+
};
|
|
23
|
+
export function numbers(create) {
|
|
24
|
+
describe('numbers', () => {
|
|
25
|
+
started(create, [Tally, Stock]);
|
|
26
|
+
it('stores and filters whole numbers in 32 bits', async () => {
|
|
27
|
+
const points = [-2_147_483_648, 0, 2_147_483_647];
|
|
28
|
+
await Tally.create({ data: { count: 2_147_483_647, points } }, M);
|
|
29
|
+
await Tally.create({ data: { count: -3, points: [1] } }, M);
|
|
30
|
+
const found = await Tally.findMany({ where: { count: { gt: 0 }, points: { has: 0 } } }, M);
|
|
31
|
+
expect(found).toMatchObject([{ count: 2_147_483_647, points }]);
|
|
32
|
+
expect(await Tally.count({ where: { points: { hasSome: [1, 5] } } }, M)).toBe(1);
|
|
33
|
+
});
|
|
34
|
+
it('computes each operation on the stored number', async () => {
|
|
35
|
+
expect(await update({ count: { increment: 3 } }, { count: 4 })).toMatchObject({ count: 7 });
|
|
36
|
+
expect(await update({ count: { decrement: 2 } }, { count: null })).toMatchObject({ count: -2 });
|
|
37
|
+
expect(await update({ count: { divide: 2 } }, { count: -7 })).toMatchObject({ count: -3 });
|
|
38
|
+
expect(await update({ ratio: { multiply: 1.5 } }, { ratio: 3 })).toMatchObject({ ratio: 4.5 });
|
|
39
|
+
expect(await update({ ratio: { divide: 4 } }, { ratio: 3 })).toMatchObject({ ratio: 0.75 });
|
|
40
|
+
});
|
|
41
|
+
it('computes updateMany on each row, with or without its hooks', async () => {
|
|
42
|
+
for (const count of [1, null, 5]) {
|
|
43
|
+
await Tally.create({ data: { count } }, M);
|
|
44
|
+
}
|
|
45
|
+
const data = { count: { increment: 2 } };
|
|
46
|
+
expect(await Tally.updateMany({ data }, M)).toEqual({ count: 3 });
|
|
47
|
+
const counts = async () => (await Tally.findMany({}, M)).map((row) => row.count);
|
|
48
|
+
expect(await counts()).toEqual([3, 2, 7]);
|
|
49
|
+
await Tally.create({ data: { count: 2_147_483_647 } }, M);
|
|
50
|
+
await expect(Tally.updateMany({ data }, M)).rejects.toMatchObject({
|
|
51
|
+
code: 'VALIDATION_FAILED',
|
|
52
|
+
details: { issues: [{ path: ['count'], message: whole }] },
|
|
53
|
+
});
|
|
54
|
+
expect(await counts()).toEqual([3, 2, 7, 2_147_483_647]);
|
|
55
|
+
for (const level of [1, 2])
|
|
56
|
+
await Stock.create({ data: { level } }, M);
|
|
57
|
+
const doubled = { level: { multiply: 2 } };
|
|
58
|
+
expect(await Stock.updateMany({ data: doubled }, M)).toEqual({
|
|
59
|
+
count: 2,
|
|
60
|
+
});
|
|
61
|
+
const levels = await Stock.findMany({}, M);
|
|
62
|
+
expect(levels.map((row) => row.level)).toEqual([2, 4]);
|
|
63
|
+
});
|
|
64
|
+
it('refuses a result out of bounds, and keeps the row', async () => {
|
|
65
|
+
for (const [data, from, path, message] of [
|
|
66
|
+
[{ count: { increment: 1 } }, { count: 2_147_483_647 }, 'count', whole],
|
|
67
|
+
[
|
|
68
|
+
{ count: { multiply: -2 } },
|
|
69
|
+
{ count: -1_073_741_825 },
|
|
70
|
+
'count',
|
|
71
|
+
whole,
|
|
72
|
+
],
|
|
73
|
+
[
|
|
74
|
+
{ ratio: { multiply: 10 } },
|
|
75
|
+
{ ratio: 1e308 },
|
|
76
|
+
'ratio',
|
|
77
|
+
'Expected a finite number.',
|
|
78
|
+
],
|
|
79
|
+
]) {
|
|
80
|
+
await expect(update(data, from)).rejects.toMatchObject({
|
|
81
|
+
code: 'VALIDATION_FAILED',
|
|
82
|
+
details: { issues: [{ path: [path], message }] },
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
expect((await Tally.findMany({}, M)).map(({ count, ratio }) => [count, ratio])).toEqual([
|
|
86
|
+
[2_147_483_647, null],
|
|
87
|
+
[-1_073_741_825, null],
|
|
88
|
+
[null, 1e308],
|
|
89
|
+
]);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projection.d.ts","sourceRoot":"","sources":["../src/projection.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAqBzC,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,OAAO,GAAG,IAAI,CAyCtD"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { defineModel, master } from 'ultimabase';
|
|
2
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
3
|
+
import { started } from './suite.js';
|
|
4
|
+
const M = { as: master };
|
|
5
|
+
const Writer = defineModel({
|
|
6
|
+
name: 'Writer',
|
|
7
|
+
fields: (f) => ({ name: f.string(), notes: f.many(() => Note) }),
|
|
8
|
+
});
|
|
9
|
+
const Note = defineModel({
|
|
10
|
+
name: 'Note',
|
|
11
|
+
fields: (f) => ({
|
|
12
|
+
title: f.string(),
|
|
13
|
+
body: f.string(),
|
|
14
|
+
writer: f.one(() => Writer),
|
|
15
|
+
}),
|
|
16
|
+
});
|
|
17
|
+
export function projection(create) {
|
|
18
|
+
describe('projection', () => {
|
|
19
|
+
const database = started(create, [Writer, Note]);
|
|
20
|
+
let ada;
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
ada = (await Writer.create({ data: { name: 'Ada' } }, M)).id;
|
|
23
|
+
await Note.create({
|
|
24
|
+
data: {
|
|
25
|
+
title: 'one',
|
|
26
|
+
body: 'long',
|
|
27
|
+
writer: { connect: { id: ada } },
|
|
28
|
+
},
|
|
29
|
+
}, M);
|
|
30
|
+
});
|
|
31
|
+
it('reads the fields a read names, in a relation too', async () => {
|
|
32
|
+
const [note] = await database().find({
|
|
33
|
+
model: 'Note',
|
|
34
|
+
fields: ['title'],
|
|
35
|
+
include: { writer: { fields: ['name'] } },
|
|
36
|
+
});
|
|
37
|
+
expect(note).toEqual({
|
|
38
|
+
id: note?.id,
|
|
39
|
+
title: 'one',
|
|
40
|
+
writer: { id: ada, name: 'Ada' },
|
|
41
|
+
});
|
|
42
|
+
const [whole] = await database().find({ model: 'Note' });
|
|
43
|
+
expect(Object.keys(whole ?? {}).toSorted()).toEqual([
|
|
44
|
+
'body',
|
|
45
|
+
'createdAt',
|
|
46
|
+
'id',
|
|
47
|
+
'title',
|
|
48
|
+
'updatedAt',
|
|
49
|
+
'writer',
|
|
50
|
+
]);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
package/dist/reads.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reads.d.ts","sourceRoot":"","sources":["../src/reads.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAmB,MAAM,YAAY,CAAA;AAyG1D,wBAAgB,KAAK,CAAC,MAAM,EAAE,MAAM,OAAO,GAAG,IAAI,CAyRjD"}
|
package/dist/reads.js
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { defineModel, master } from 'ultimabase';
|
|
2
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
3
|
+
import { started } from './suite.js';
|
|
4
|
+
const M = { as: master };
|
|
5
|
+
const Author = defineModel({
|
|
6
|
+
name: 'Author',
|
|
7
|
+
fields: (f) => ({
|
|
8
|
+
name: f.string({ unique: true }),
|
|
9
|
+
level: f.enumOf(['junior', 'senior'], { default: 'junior' }),
|
|
10
|
+
books: f.many(() => Book),
|
|
11
|
+
profile: f.one(() => Profile, { optional: true }),
|
|
12
|
+
}),
|
|
13
|
+
});
|
|
14
|
+
const Profile = defineModel({
|
|
15
|
+
name: 'Profile',
|
|
16
|
+
fields: (f) => ({ bio: f.string(), author: f.one(() => Author) }),
|
|
17
|
+
});
|
|
18
|
+
const Book = defineModel({
|
|
19
|
+
name: 'Book',
|
|
20
|
+
fields: (f) => ({
|
|
21
|
+
title: f.string(),
|
|
22
|
+
pages: f.number({ optional: true }),
|
|
23
|
+
published: f.boolean({ default: false }),
|
|
24
|
+
meta: f.json({ optional: true }),
|
|
25
|
+
releasedAt: f.dateTime({ optional: true }),
|
|
26
|
+
author: f.one(() => Author),
|
|
27
|
+
tags: f.many(() => Tag),
|
|
28
|
+
}),
|
|
29
|
+
indexes: [{ fields: ['title', 'author'], unique: true }],
|
|
30
|
+
});
|
|
31
|
+
const Tag = defineModel({
|
|
32
|
+
name: 'Tag',
|
|
33
|
+
fields: (f) => ({
|
|
34
|
+
label: f.string({ unique: true }),
|
|
35
|
+
books: f.many(() => Book),
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
38
|
+
// Asymmetric matchers are typed `any`.
|
|
39
|
+
const aDate = expect.any(Date);
|
|
40
|
+
const day = (date) => new Date(Date.UTC(2020, 0, date));
|
|
41
|
+
/** Ada wrote Analysis and Engines and has a profile; Grace wrote Compilers. */
|
|
42
|
+
async function seed() {
|
|
43
|
+
const ada = await Author.create({ data: { name: 'Ada' } }, M);
|
|
44
|
+
const grace = await Author.create({ data: { name: 'Grace', level: 'senior' } }, M);
|
|
45
|
+
await Profile.create({ data: { bio: 'Poet', author: { connect: { id: ada.id } } } }, M);
|
|
46
|
+
const math = await Tag.create({ data: { label: 'math' } }, M);
|
|
47
|
+
const art = await Tag.create({ data: { label: 'art' } }, M);
|
|
48
|
+
const books = [
|
|
49
|
+
{
|
|
50
|
+
title: 'Analysis',
|
|
51
|
+
pages: 300,
|
|
52
|
+
published: true,
|
|
53
|
+
author: ada,
|
|
54
|
+
tags: [math],
|
|
55
|
+
},
|
|
56
|
+
{ title: 'Engines', pages: 120, author: ada, tags: [] },
|
|
57
|
+
{ title: 'Compilers', pages: null, author: grace, tags: [math, art] },
|
|
58
|
+
];
|
|
59
|
+
for (const [index, { author, tags, ...data }] of books.entries()) {
|
|
60
|
+
await Book.create({
|
|
61
|
+
data: {
|
|
62
|
+
...data,
|
|
63
|
+
meta: { rank: index, kind: 'book' },
|
|
64
|
+
releasedAt: day(index + 1),
|
|
65
|
+
author: { connect: { id: author.id } },
|
|
66
|
+
tags: { connect: tags.map(({ id }) => ({ id })) },
|
|
67
|
+
},
|
|
68
|
+
}, M);
|
|
69
|
+
}
|
|
70
|
+
return { ada: ada.id, grace: grace.id };
|
|
71
|
+
}
|
|
72
|
+
const titles = async (where) => (await Book.findMany({ where }, M)).map((book) => book.title);
|
|
73
|
+
const names = async (where) => (await Author.findMany({ where }, M)).map((author) => author.name);
|
|
74
|
+
const ordered = async (orderBy) => (await Book.findMany({ orderBy }, M)).map((book) => book.title);
|
|
75
|
+
const page = async (args) => (await Book.findMany({ orderBy: { title: 'asc' }, ...args }, M)).map((book) => book.title);
|
|
76
|
+
export function reads(create) {
|
|
77
|
+
describe('reads', () => {
|
|
78
|
+
started(create, [Author, Profile, Book, Tag]);
|
|
79
|
+
let authors;
|
|
80
|
+
beforeEach(async () => {
|
|
81
|
+
authors = await seed();
|
|
82
|
+
});
|
|
83
|
+
it('compares strings with every operator, case included', async () => {
|
|
84
|
+
expect(await titles({ title: 'Engines' })).toEqual(['Engines']);
|
|
85
|
+
expect(await titles({ title: { not: 'Engines' } })).toEqual([
|
|
86
|
+
'Analysis',
|
|
87
|
+
'Compilers',
|
|
88
|
+
]);
|
|
89
|
+
expect(await titles({ title: { in: ['Compilers', 'Nope'] } })).toEqual([
|
|
90
|
+
'Compilers',
|
|
91
|
+
]);
|
|
92
|
+
expect(await titles({ title: { notIn: ['Analysis'] } })).toEqual([
|
|
93
|
+
'Engines',
|
|
94
|
+
'Compilers',
|
|
95
|
+
]);
|
|
96
|
+
expect(await titles({ title: { notIn: [] } })).toHaveLength(3);
|
|
97
|
+
expect(await titles({ title: { in: [] } })).toEqual([]);
|
|
98
|
+
expect(await titles({ title: { contains: 'gin' } })).toEqual(['Engines']);
|
|
99
|
+
expect(await titles({ title: { contains: 'GIN' } })).toEqual([]);
|
|
100
|
+
expect(await titles({ title: { startsWith: 'C' } })).toEqual([
|
|
101
|
+
'Compilers',
|
|
102
|
+
]);
|
|
103
|
+
expect(await titles({ title: { endsWith: 'sis' } })).toEqual(['Analysis']);
|
|
104
|
+
// `%` and `_` are characters, not patterns.
|
|
105
|
+
expect(await titles({ title: { contains: '%' } })).toEqual([]);
|
|
106
|
+
expect(await titles({ title: { startsWith: '_' } })).toEqual([]);
|
|
107
|
+
});
|
|
108
|
+
it('matches part of an id as a string', async () => {
|
|
109
|
+
// An id begins with its time, shared by rows created together.
|
|
110
|
+
const id = authors.ada;
|
|
111
|
+
expect(await names({ id: { startsWith: id.slice(0, 30) } })).toEqual([
|
|
112
|
+
'Ada',
|
|
113
|
+
]);
|
|
114
|
+
expect(await names({ id: { contains: id.slice(24, 32) } })).toEqual([
|
|
115
|
+
'Ada',
|
|
116
|
+
]);
|
|
117
|
+
expect(await names({ id: { endsWith: id.slice(24) } })).toEqual(['Ada']);
|
|
118
|
+
expect(await names({ id: { contains: 'Z' } })).toEqual([]);
|
|
119
|
+
});
|
|
120
|
+
it('compares numbers, dates, booleans, enums and json', async () => {
|
|
121
|
+
expect(await titles({ pages: { lt: 300 } })).toEqual(['Engines']);
|
|
122
|
+
expect(await titles({ pages: { lte: 300 } })).toEqual([
|
|
123
|
+
'Analysis',
|
|
124
|
+
'Engines',
|
|
125
|
+
]);
|
|
126
|
+
expect(await titles({ pages: { gt: 120 } })).toEqual(['Analysis']);
|
|
127
|
+
expect(await titles({ pages: { gte: 120 } })).toEqual([
|
|
128
|
+
'Analysis',
|
|
129
|
+
'Engines',
|
|
130
|
+
]);
|
|
131
|
+
expect(await titles({ releasedAt: { gt: day(1) } })).toEqual([
|
|
132
|
+
'Engines',
|
|
133
|
+
'Compilers',
|
|
134
|
+
]);
|
|
135
|
+
expect(await titles({ releasedAt: day(2) })).toEqual(['Engines']);
|
|
136
|
+
expect(await titles({ published: true })).toEqual(['Analysis']);
|
|
137
|
+
expect(await titles({ author: { is: { level: { in: ['senior'] } } } })).toEqual(['Compilers']);
|
|
138
|
+
// A json value equals another of the same content, keys in any order.
|
|
139
|
+
expect(await titles({ meta: { equals: { kind: 'book', rank: 1 } } })).toEqual(['Engines']);
|
|
140
|
+
});
|
|
141
|
+
it('takes -0 for 0 and gives it back as 0, in a number and in json, as PostgreSQL does', async () => {
|
|
142
|
+
const { id } = await Book.create({
|
|
143
|
+
data: {
|
|
144
|
+
title: 'Zero',
|
|
145
|
+
pages: -0,
|
|
146
|
+
meta: { rank: -0, list: [-0, 1] },
|
|
147
|
+
author: { connect: { id: authors.grace } },
|
|
148
|
+
},
|
|
149
|
+
}, M);
|
|
150
|
+
expect(await titles({ pages: 0 })).toEqual(['Zero']);
|
|
151
|
+
expect(await titles({ pages: { in: [0] } })).toEqual(['Zero']);
|
|
152
|
+
expect(await titles({ pages: { not: -0 } })).toEqual([
|
|
153
|
+
'Analysis',
|
|
154
|
+
'Engines',
|
|
155
|
+
]);
|
|
156
|
+
expect(await titles({ meta: { equals: { list: [0, 1], rank: 0 } } })).toEqual(['Zero']);
|
|
157
|
+
// A json array keeps its order and its length.
|
|
158
|
+
expect(await titles({ meta: { equals: { rank: 0, list: [1, 0] } } })).toEqual([]);
|
|
159
|
+
expect(await titles({ meta: { equals: { rank: 0, list: [0] } } })).toEqual([]);
|
|
160
|
+
expect(await titles({ meta: { equals: { kind: 'book', rank: -0 } } })).toEqual(['Analysis']);
|
|
161
|
+
// It comes back as 0, in json as in a number.
|
|
162
|
+
const read = { where: { id }, select: { pages: true, meta: true } };
|
|
163
|
+
expect(await Book.findUnique(read, M)).toStrictEqual({
|
|
164
|
+
pages: 0,
|
|
165
|
+
meta: { rank: 0, list: [0, 1] },
|
|
166
|
+
});
|
|
167
|
+
await Book.update({ where: { id }, data: { meta: -0 } }, M);
|
|
168
|
+
expect(await Book.findUnique(read, M)).toStrictEqual({
|
|
169
|
+
pages: 0,
|
|
170
|
+
meta: 0,
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
it('treats null as a value, and as unknown in a comparison', async () => {
|
|
174
|
+
expect(await titles({ pages: null })).toEqual(['Compilers']);
|
|
175
|
+
expect(await titles({ pages: { not: null } })).toEqual([
|
|
176
|
+
'Analysis',
|
|
177
|
+
'Engines',
|
|
178
|
+
]);
|
|
179
|
+
// `null` is not below 200, nor not below it, as in SQL.
|
|
180
|
+
expect(await titles({ NOT: { pages: { lt: 200 } } })).toEqual([
|
|
181
|
+
'Analysis',
|
|
182
|
+
]);
|
|
183
|
+
expect(await titles({ pages: { not: 300 } })).toEqual(['Engines']);
|
|
184
|
+
});
|
|
185
|
+
it('combines AND, OR and NOT', async () => {
|
|
186
|
+
expect(await titles({
|
|
187
|
+
OR: [{ pages: { lt: 200 } }, { title: 'Analysis' }],
|
|
188
|
+
NOT: [{ title: 'Engines' }],
|
|
189
|
+
})).toEqual(['Analysis']);
|
|
190
|
+
expect(await titles({ AND: [{ pages: { gt: 100 } }, { published: false }] })).toEqual(['Engines']);
|
|
191
|
+
expect(await titles({ OR: [] })).toEqual([]);
|
|
192
|
+
});
|
|
193
|
+
it('filters through a one relation', async () => {
|
|
194
|
+
expect(await titles({ author: { is: { name: 'Grace' } } })).toEqual([
|
|
195
|
+
'Compilers',
|
|
196
|
+
]);
|
|
197
|
+
expect(await titles({ author: { isNot: { name: 'Grace' } } })).toEqual([
|
|
198
|
+
'Analysis',
|
|
199
|
+
'Engines',
|
|
200
|
+
]);
|
|
201
|
+
expect(await names({ profile: { is: null } })).toEqual(['Grace']);
|
|
202
|
+
expect(await names({ profile: { isNot: null } })).toEqual(['Ada']);
|
|
203
|
+
expect(await names({ profile: { is: { bio: 'Poet' } } })).toEqual(['Ada']);
|
|
204
|
+
});
|
|
205
|
+
it('filters through a many relation', async () => {
|
|
206
|
+
expect(await titles({ tags: { some: { label: 'art' } } })).toEqual([
|
|
207
|
+
'Compilers',
|
|
208
|
+
]);
|
|
209
|
+
expect(await titles({ tags: { none: { label: 'math' } } })).toEqual([
|
|
210
|
+
'Engines',
|
|
211
|
+
]);
|
|
212
|
+
// Every tag of a book without tags is art.
|
|
213
|
+
expect(await titles({ tags: { every: { label: 'art' } } })).toEqual([
|
|
214
|
+
'Engines',
|
|
215
|
+
]);
|
|
216
|
+
// Grace's book has no pages: unknown is not false, as in SQL.
|
|
217
|
+
expect(await names({ books: { every: { pages: { gt: 200 } } } })).toEqual(['Grace']);
|
|
218
|
+
});
|
|
219
|
+
it('orders by several fields, nulls last ascending', async () => {
|
|
220
|
+
expect(await ordered({ pages: 'asc' })).toEqual([
|
|
221
|
+
'Engines',
|
|
222
|
+
'Analysis',
|
|
223
|
+
'Compilers',
|
|
224
|
+
]);
|
|
225
|
+
expect(await ordered({ pages: 'desc' })).toEqual([
|
|
226
|
+
'Compilers',
|
|
227
|
+
'Analysis',
|
|
228
|
+
'Engines',
|
|
229
|
+
]);
|
|
230
|
+
expect(await ordered([{ published: 'asc' }, { title: 'desc' }])).toEqual([
|
|
231
|
+
'Engines',
|
|
232
|
+
'Compilers',
|
|
233
|
+
'Analysis',
|
|
234
|
+
]);
|
|
235
|
+
// Without an order, rows come in the order they were created.
|
|
236
|
+
expect(await ordered([])).toEqual(['Analysis', 'Engines', 'Compilers']);
|
|
237
|
+
});
|
|
238
|
+
it('orders strings by code point, as the bytes of their UTF-8', async () => {
|
|
239
|
+
// UTF-16 puts U+FF5A above U+1F600, which is above it as a code point.
|
|
240
|
+
await Tag.create({ data: { label: '😀' } }, M);
|
|
241
|
+
await Tag.create({ data: { label: 'z' } }, M);
|
|
242
|
+
const tags = await Tag.findMany({ orderBy: { label: 'asc' } }, M);
|
|
243
|
+
expect(tags.map(({ label }) => label)).toEqual([
|
|
244
|
+
'art',
|
|
245
|
+
'math',
|
|
246
|
+
'z',
|
|
247
|
+
'😀',
|
|
248
|
+
]);
|
|
249
|
+
});
|
|
250
|
+
it('skips, takes and starts at a cursor, forwards and backwards', async () => {
|
|
251
|
+
expect(await page({ skip: 1, take: 1 })).toEqual(['Compilers']);
|
|
252
|
+
expect(await page({ take: -2 })).toEqual(['Compilers', 'Engines']);
|
|
253
|
+
// A cursor is any unique key, here the index on title and author.
|
|
254
|
+
const cursor = { title: 'Compilers', author: authors.grace };
|
|
255
|
+
expect(await page({ cursor, take: 2 })).toEqual(['Compilers', 'Engines']);
|
|
256
|
+
expect(await page({ cursor, skip: 1 })).toEqual(['Engines']);
|
|
257
|
+
expect(await page({ cursor, take: -2 })).toEqual([
|
|
258
|
+
'Analysis',
|
|
259
|
+
'Compilers',
|
|
260
|
+
]);
|
|
261
|
+
});
|
|
262
|
+
it('includes relations, filtered, ordered and paged, at any depth', async () => {
|
|
263
|
+
const ada = await Author.findUnique({
|
|
264
|
+
where: { name: 'Ada' },
|
|
265
|
+
include: {
|
|
266
|
+
profile: true,
|
|
267
|
+
books: {
|
|
268
|
+
where: { pages: { gt: 100 } },
|
|
269
|
+
orderBy: { pages: 'asc' },
|
|
270
|
+
take: 1,
|
|
271
|
+
include: { tags: { orderBy: { label: 'asc' } } },
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
}, M);
|
|
275
|
+
expect(ada).toMatchObject({
|
|
276
|
+
profile: { bio: 'Poet' },
|
|
277
|
+
// An array matches only an array of the same length.
|
|
278
|
+
books: [{ title: 'Engines', tags: [] }],
|
|
279
|
+
});
|
|
280
|
+
const compilers = await Book.findFirst({
|
|
281
|
+
where: { title: 'Compilers' },
|
|
282
|
+
select: {
|
|
283
|
+
title: true,
|
|
284
|
+
author: { select: { name: true, profile: true } },
|
|
285
|
+
tags: { select: { label: true }, orderBy: { label: 'asc' } },
|
|
286
|
+
},
|
|
287
|
+
}, M);
|
|
288
|
+
expect(compilers).toEqual({
|
|
289
|
+
title: 'Compilers',
|
|
290
|
+
author: { name: 'Grace', profile: null },
|
|
291
|
+
tags: [{ label: 'art' }, { label: 'math' }],
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
it('finds a row by a unique index', async () => {
|
|
295
|
+
const engines = await Book.findUnique({ where: { title: 'Engines', author: authors.ada } }, M);
|
|
296
|
+
expect(engines).toMatchObject({ title: 'Engines', pages: 120 });
|
|
297
|
+
expect(await Book.findUnique({ where: { id: 'missing' } }, M)).toBeNull();
|
|
298
|
+
});
|
|
299
|
+
it('returns each type as written, and counts', async () => {
|
|
300
|
+
const analysis = await Book.findFirst({ where: { title: 'Analysis' } }, M);
|
|
301
|
+
expect(analysis).toMatchObject({
|
|
302
|
+
pages: 300,
|
|
303
|
+
published: true,
|
|
304
|
+
meta: { rank: 0, kind: 'book' },
|
|
305
|
+
releasedAt: day(1),
|
|
306
|
+
createdAt: aDate,
|
|
307
|
+
});
|
|
308
|
+
expect(await Book.count({}, M)).toBe(3);
|
|
309
|
+
expect(await Book.count({ where: { tags: { some: {} } } }, M)).toBe(2);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relation-filters.d.ts","sourceRoot":"","sources":["../src/relation-filters.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAY,MAAM,YAAY,CAAA;AAmDnD,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,OAAO,GAAG,IAAI,CA+F3D"}
|