@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.
Files changed (119) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +4 -2
  3. package/dist/aggregates.d.ts +3 -0
  4. package/dist/aggregates.d.ts.map +1 -0
  5. package/dist/aggregates.js +99 -0
  6. package/dist/as-user.d.ts +13 -0
  7. package/dist/as-user.d.ts.map +1 -0
  8. package/dist/as-user.js +17 -0
  9. package/dist/bulk-returns.d.ts +3 -0
  10. package/dist/bulk-returns.d.ts.map +1 -0
  11. package/dist/bulk-returns.js +125 -0
  12. package/dist/cascades.d.ts +3 -0
  13. package/dist/cascades.d.ts.map +1 -0
  14. package/dist/cascades.js +269 -0
  15. package/dist/cases.d.ts +3 -0
  16. package/dist/cases.d.ts.map +1 -0
  17. package/dist/cases.js +60 -0
  18. package/dist/counts.d.ts +3 -0
  19. package/dist/counts.d.ts.map +1 -0
  20. package/dist/counts.js +158 -0
  21. package/dist/days.d.ts +3 -0
  22. package/dist/days.d.ts.map +1 -0
  23. package/dist/days.js +74 -0
  24. package/dist/decimals.d.ts +3 -0
  25. package/dist/decimals.d.ts.map +1 -0
  26. package/dist/decimals.js +70 -0
  27. package/dist/distincts.d.ts +3 -0
  28. package/dist/distincts.d.ts.map +1 -0
  29. package/dist/distincts.js +80 -0
  30. package/dist/feeds.d.ts +3 -0
  31. package/dist/feeds.d.ts.map +1 -0
  32. package/dist/feeds.js +164 -0
  33. package/dist/groups.d.ts +3 -0
  34. package/dist/groups.d.ts.map +1 -0
  35. package/dist/groups.js +149 -0
  36. package/dist/index.d.ts +5 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +5 -0
  39. package/dist/json-paths.d.ts +3 -0
  40. package/dist/json-paths.d.ts.map +1 -0
  41. package/dist/json-paths.js +143 -0
  42. package/dist/lists.d.ts +3 -0
  43. package/dist/lists.d.ts.map +1 -0
  44. package/dist/lists.js +234 -0
  45. package/dist/nested.d.ts +3 -0
  46. package/dist/nested.d.ts.map +1 -0
  47. package/dist/nested.js +152 -0
  48. package/dist/numbers.d.ts +3 -0
  49. package/dist/numbers.d.ts.map +1 -0
  50. package/dist/numbers.js +92 -0
  51. package/dist/projection.d.ts +3 -0
  52. package/dist/projection.d.ts.map +1 -0
  53. package/dist/projection.js +53 -0
  54. package/dist/reads.d.ts +3 -0
  55. package/dist/reads.d.ts.map +1 -0
  56. package/dist/reads.js +312 -0
  57. package/dist/relation-filters.d.ts +3 -0
  58. package/dist/relation-filters.d.ts.map +1 -0
  59. package/dist/relation-filters.js +116 -0
  60. package/dist/round-trips.d.ts +3 -0
  61. package/dist/round-trips.d.ts.map +1 -0
  62. package/dist/round-trips.js +284 -0
  63. package/dist/searches.d.ts +3 -0
  64. package/dist/searches.d.ts.map +1 -0
  65. package/dist/searches.js +57 -0
  66. package/dist/silent.d.ts +3 -0
  67. package/dist/silent.d.ts.map +1 -0
  68. package/dist/silent.js +7 -0
  69. package/dist/sorts.d.ts +3 -0
  70. package/dist/sorts.d.ts.map +1 -0
  71. package/dist/sorts.js +108 -0
  72. package/dist/suite.d.ts +7 -0
  73. package/dist/suite.d.ts.map +1 -0
  74. package/dist/suite.js +15 -0
  75. package/dist/test-adapter.d.ts +11 -0
  76. package/dist/test-adapter.d.ts.map +1 -0
  77. package/dist/test-adapter.js +55 -0
  78. package/dist/test-application.d.ts +17 -0
  79. package/dist/test-application.d.ts.map +1 -0
  80. package/dist/test-application.js +68 -0
  81. package/dist/transactions.d.ts +3 -0
  82. package/dist/transactions.d.ts.map +1 -0
  83. package/dist/transactions.js +61 -0
  84. package/dist/virtuals.d.ts +3 -0
  85. package/dist/virtuals.d.ts.map +1 -0
  86. package/dist/virtuals.js +135 -0
  87. package/dist/writes.d.ts +3 -0
  88. package/dist/writes.d.ts.map +1 -0
  89. package/dist/writes.js +219 -0
  90. package/package.json +40 -3
  91. package/src/aggregates.ts +128 -0
  92. package/src/as-user.ts +26 -0
  93. package/src/bulk-returns.ts +151 -0
  94. package/src/cascades.ts +338 -0
  95. package/src/cases.ts +76 -0
  96. package/src/counts.ts +223 -0
  97. package/src/days.ts +107 -0
  98. package/src/decimals.ts +102 -0
  99. package/src/distincts.ts +107 -0
  100. package/src/feeds.ts +197 -0
  101. package/src/groups.ts +185 -0
  102. package/src/index.ts +6 -0
  103. package/src/json-paths.ts +185 -0
  104. package/src/lists.ts +304 -0
  105. package/src/nested.ts +187 -0
  106. package/src/numbers.ts +125 -0
  107. package/src/projection.ts +65 -0
  108. package/src/reads.ts +389 -0
  109. package/src/relation-filters.ts +151 -0
  110. package/src/round-trips.ts +392 -0
  111. package/src/searches.ts +82 -0
  112. package/src/silent.ts +11 -0
  113. package/src/sorts.ts +143 -0
  114. package/src/suite.ts +21 -0
  115. package/src/test-adapter.ts +68 -0
  116. package/src/test-application.ts +93 -0
  117. package/src/transactions.ts +76 -0
  118. package/src/virtuals.ts +184 -0
  119. package/src/writes.ts +315 -0
package/dist/groups.js ADDED
@@ -0,0 +1,149 @@
1
+ import { defineModel, master } from 'ultimabase';
2
+ import { beforeEach, describe, expect, it } from 'vitest';
3
+ import { suiteUser } from './as-user.js';
4
+ import { started } from './suite.js';
5
+ const M = { as: master };
6
+ const user = { as: suiteUser('00000000-0000-4000-8000-000000000000') };
7
+ /** A user reads the teams that are not hidden. */
8
+ const Team = defineModel({
9
+ name: 'Team',
10
+ fields: (f) => ({
11
+ hidden: f.boolean({ default: false }),
12
+ deals: f.many(() => Deal),
13
+ }),
14
+ }).with({ access: { read: ({ where }) => where({ hidden: false }) } });
15
+ const Deal = defineModel({
16
+ name: 'Deal',
17
+ fields: (f) => ({
18
+ stage: f.enumOf(['b', 'a', 'c']),
19
+ amount: f.decimal({ precision: 8, scale: 2 }),
20
+ units: f.number({ integer: true, optional: true }),
21
+ on: f.dateTime({ optional: true }),
22
+ region: f.string({ optional: true }),
23
+ team: f.one(() => Team, { optional: true }),
24
+ }),
25
+ }).with({ access: { read: () => true } });
26
+ const day = (n) => new Date(Date.UTC(2026, 4, n, 12, 30));
27
+ const stages = async (kept) => (await kept).map((group) => group.stage);
28
+ export function groups(create) {
29
+ describe('groups', () => {
30
+ started(create, [Team, Deal]);
31
+ let red = '';
32
+ let blue = '';
33
+ beforeEach(async () => {
34
+ red = (await Team.create({ data: {} }, M)).id;
35
+ blue = (await Team.create({ data: { hidden: true } }, M)).id;
36
+ const rows = [
37
+ { stage: 'a', amount: '1.10', units: 2, on: day(1), region: 'n' },
38
+ { stage: 'a', amount: '2.20', units: 3, on: day(1), region: 'n' },
39
+ { stage: 'b', amount: '5', on: day(2), region: 's' },
40
+ { stage: 'c', amount: '10', units: 1 },
41
+ { stage: 'b', amount: '1', units: 4, on: day(2), region: 's' },
42
+ ];
43
+ const teams = [red, blue, red, undefined, red];
44
+ for (const [at, row] of rows.entries()) {
45
+ const id = teams[at];
46
+ const data = id === undefined ? row : { ...row, team: { connect: { id } } };
47
+ await Deal.create({ data }, M);
48
+ }
49
+ });
50
+ it('groups by scalars and dates, the rows with no value together', async () => {
51
+ const result = await Deal.groupBy({ by: ['region', 'on'], _count: true, _sum: { amount: true } }, M);
52
+ expect(result).toStrictEqual([
53
+ { region: 'n', on: day(1), _count: 2, _sum: { amount: '3.30' } },
54
+ { region: 's', on: day(2), _count: 2, _sum: { amount: '6.00' } },
55
+ { region: null, on: null, _count: 1, _sum: { amount: '10.00' } },
56
+ ]);
57
+ const regions = await Deal.groupBy({ by: ['region'], orderBy: { region: 'desc' } }, M);
58
+ expect(regions).toStrictEqual([
59
+ { region: null },
60
+ { region: 's' },
61
+ { region: 'n' },
62
+ ]);
63
+ });
64
+ it('sorts by an aggregate, then by the keys, and pages', async () => {
65
+ const counted = await Deal.groupBy({ by: ['stage'], orderBy: { _count: 'desc' }, _max: { units: true } }, M);
66
+ expect(counted).toStrictEqual([
67
+ { stage: 'a', _max: { units: 3 } },
68
+ { stage: 'b', _max: { units: 4 } },
69
+ { stage: 'c', _max: { units: 1 } },
70
+ ]);
71
+ const summed = { _sum: { amount: 'desc' } };
72
+ const sorted = await Deal.groupBy({ by: ['stage'], orderBy: summed }, M);
73
+ expect(sorted).toStrictEqual([
74
+ { stage: 'c' },
75
+ { stage: 'b' },
76
+ { stage: 'a' },
77
+ ]);
78
+ const paged = await Deal.groupBy({ by: ['stage'], orderBy: summed, skip: 1, take: 1 }, M);
79
+ expect(paged).toStrictEqual([{ stage: 'b' }]);
80
+ });
81
+ it('groups by a relation, null when unset or unread', async () => {
82
+ const args = {
83
+ by: ['team'],
84
+ orderBy: { _count: 'desc' },
85
+ _count: true,
86
+ };
87
+ expect(await Deal.groupBy(args, M)).toStrictEqual([
88
+ { team: { id: red }, _count: 3 },
89
+ { team: { id: blue }, _count: 1 },
90
+ { team: null, _count: 1 },
91
+ ]);
92
+ expect(await Deal.groupBy(args, user)).toStrictEqual([
93
+ { team: { id: red }, _count: 3 },
94
+ { team: null, _count: 2 },
95
+ ]);
96
+ });
97
+ it('keeps the groups having passes, by their values and aggregates', async () => {
98
+ const by = ['stage'];
99
+ const sum = { amount: { _sum: { gt: '5' } } };
100
+ expect(await stages(Deal.groupBy({ by, having: sum }, M))).toEqual([
101
+ 'b',
102
+ 'c',
103
+ ]);
104
+ const counts = {
105
+ stage: { in: ['a', 'b'] },
106
+ amount: { _count: 2 },
107
+ };
108
+ expect(await stages(Deal.groupBy({ by, having: counts }, M))).toEqual([
109
+ 'a',
110
+ 'b',
111
+ ]);
112
+ const average = { units: { _avg: { lt: 3 } } };
113
+ expect(await stages(Deal.groupBy({ by, having: average }, M))).toEqual([
114
+ 'a',
115
+ 'c',
116
+ ]);
117
+ const least = { on: { _min: { gte: day(2) } } };
118
+ expect(await stages(Deal.groupBy({ by, having: least }, M))).toEqual([
119
+ 'b',
120
+ ]);
121
+ const either = {
122
+ OR: [{ stage: 'c' }, { units: { _max: { gte: 4 } } }],
123
+ };
124
+ expect(await stages(Deal.groupBy({ by, having: either }, M))).toEqual([
125
+ 'b',
126
+ 'c',
127
+ ]);
128
+ const dated = { NOT: { on: { _min: { equals: null } } } };
129
+ expect(await stages(Deal.groupBy({ by, having: dated }, M))).toEqual([
130
+ 'a',
131
+ 'b',
132
+ ]);
133
+ const counted = await Deal.groupBy({
134
+ by: ['stage', 'region'],
135
+ _count: true,
136
+ having: { region: { not: null }, amount: { _count: { gt: 1 } } },
137
+ orderBy: { _sum: { amount: 'desc' } },
138
+ }, M);
139
+ expect(counted).toStrictEqual([
140
+ { stage: 'b', region: 's', _count: 2 },
141
+ { stage: 'a', region: 'n', _count: 2 },
142
+ ]);
143
+ });
144
+ it('gives no groups without rows', async () => {
145
+ const where = { region: 'w' };
146
+ expect(await Deal.groupBy({ by: ['stage'], where, _count: true }, M)).toStrictEqual([]);
147
+ });
148
+ });
149
+ }
@@ -0,0 +1,5 @@
1
+ export { asUser } from './as-user.ts';
2
+ export { testAdapter } from './test-adapter.ts';
3
+ export type { TestApplication, TestOptions } from './test-application.ts';
4
+ export { testApplication } from './test-application.ts';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AACrC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACzE,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ // `@ultimabase/testing` (D-40): an application's tests (D-130), the adapter
2
+ // contract suite (D-94) and a test's user caller (D-80).
3
+ export { asUser } from './as-user.js';
4
+ export { testAdapter } from './test-adapter.js';
5
+ export { testApplication } from './test-application.js';
@@ -0,0 +1,3 @@
1
+ import type { Adapter } from 'ultimabase';
2
+ export declare function jsonPaths(create: () => Adapter): void;
3
+ //# sourceMappingURL=json-paths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-paths.d.ts","sourceRoot":"","sources":["../src/json-paths.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAoC,MAAM,YAAY,CAAA;AAsD3E,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,OAAO,GAAG,IAAI,CAgIrD"}
@@ -0,0 +1,143 @@
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 Doc = defineModel({
6
+ name: 'Doc',
7
+ fields: (f) => ({
8
+ name: f.string({ unique: true }),
9
+ meta: f.json({ optional: true }),
10
+ }),
11
+ });
12
+ const rows = [
13
+ {
14
+ name: 'ada',
15
+ meta: {
16
+ author: { name: 'Ada', age: 36 },
17
+ tags: ['x', 'y'],
18
+ items: [{ id: 1, kind: 'a' }, { id: 2 }],
19
+ note: null,
20
+ score: 5,
21
+ },
22
+ },
23
+ {
24
+ name: 'bob',
25
+ meta: { author: { name: 'bob' }, tags: ['y'], items: [], score: 'high' },
26
+ },
27
+ { name: 'cy', meta: [1, 2, 3] },
28
+ { name: 'nil' },
29
+ { name: 'str', meta: '50%_off' },
30
+ ];
31
+ const names = async (where) => (await Doc.findMany({ where, orderBy: { name: 'asc' } }, M)).map((doc) => doc.name);
32
+ /** The rows the path filter keeps, and the rows its `NOT` keeps. */
33
+ const kept = async (meta) => [
34
+ await names({ meta }),
35
+ await names({ NOT: { meta } }),
36
+ ];
37
+ const contains = (path, arrayContains) => names({ meta: { path, arrayContains } });
38
+ const at = (path, equals) => names({ meta: { path, equals } });
39
+ export function jsonPaths(create) {
40
+ describe('json paths', () => {
41
+ started(create, [Doc]);
42
+ const seeded = async () => {
43
+ for (const data of rows)
44
+ await Doc.create({ data }, M);
45
+ };
46
+ it('tests equality at a path, unknown where the path is missing', async () => {
47
+ await seeded();
48
+ const name = ['author', 'name'];
49
+ expect(await kept({ path: name, equals: 'Ada' })).toEqual([
50
+ ['ada'],
51
+ ['bob'],
52
+ ]);
53
+ expect(await kept({ path: name, not: 'Ada' })).toEqual([['bob'], ['ada']]);
54
+ expect(await names({ meta: { path: ['note'], equals: null } })).toEqual([
55
+ 'ada',
56
+ ]);
57
+ expect(await names({ meta: { path: [], equals: [1, 2, 3] } })).toEqual([
58
+ 'cy',
59
+ ]);
60
+ expect(await names({ meta: { path: [], equals: '50%_off' } })).toEqual([
61
+ 'str',
62
+ ]);
63
+ const author = { path: ['author'], equals: { age: 36, name: 'Ada' } };
64
+ expect(await names({ meta: author })).toEqual(['ada']);
65
+ });
66
+ it('compares numbers by value and strings by code point', async () => {
67
+ await seeded();
68
+ const score = ['score'];
69
+ expect(await kept({ path: score, gt: 3 })).toEqual([['ada'], ['bob']]);
70
+ expect(await kept({ path: score, lt: 10 })).toEqual([['ada'], ['bob']]);
71
+ expect(await kept({ path: score, gte: 'high' })).toEqual([
72
+ ['bob'],
73
+ ['ada'],
74
+ ]);
75
+ expect(await names({ meta: { path: score, lte: 5 } })).toEqual(['ada']);
76
+ const name = ['author', 'name'];
77
+ expect(await kept({ path: name, gt: 'Z' })).toEqual([['bob'], ['ada']]);
78
+ });
79
+ it('matches strings literally, and insensitively with mode', async () => {
80
+ await seeded();
81
+ const name = ['author', 'name'];
82
+ expect(await names({ meta: { path: [], stringContains: '%_' } })).toEqual(['str']);
83
+ expect(await names({ meta: { path: name, stringContains: '%' } })).toEqual([]);
84
+ expect(await names({ meta: { path: name, stringStartsWith: 'a' } })).toEqual([]);
85
+ const insensitive = { mode: 'insensitive' };
86
+ expect(await names({
87
+ meta: { path: name, stringStartsWith: 'a', ...insensitive },
88
+ })).toEqual(['ada']);
89
+ expect(await names({
90
+ meta: { path: name, stringEndsWith: 'B', ...insensitive },
91
+ })).toEqual(['bob']);
92
+ expect(await kept({ path: ['score'], stringContains: 'h' })).toEqual([
93
+ ['bob'],
94
+ ['ada'],
95
+ ]);
96
+ });
97
+ it('tests arrays with arrayContains, arrayStartsWith and arrayEndsWith', async () => {
98
+ await seeded();
99
+ const tags = ['tags'];
100
+ expect(await contains(tags, 'y')).toEqual(['ada', 'bob']);
101
+ expect(await contains(tags, ['y', 'x'])).toEqual(['ada']);
102
+ expect(await contains(tags, [])).toEqual(['ada', 'bob']);
103
+ expect(await contains(['items'], { id: 1 })).toEqual(['ada']);
104
+ expect(await contains([], 2)).toEqual(['cy']);
105
+ expect(await contains([], [[1]])).toEqual([]);
106
+ expect(await kept({ path: ['author'], arrayContains: 'Ada' })).toEqual([
107
+ [],
108
+ ['ada', 'bob'],
109
+ ]);
110
+ expect(await names({ meta: { path: tags, arrayStartsWith: 'x' } })).toEqual(['ada']);
111
+ expect(await names({ meta: { path: tags, arrayEndsWith: 'y' } })).toEqual(['ada', 'bob']);
112
+ const first = { path: ['items'], arrayStartsWith: { kind: 'a', id: 1 } };
113
+ expect(await kept(first)).toEqual([['ada'], ['bob']]);
114
+ expect(await kept({ path: ['items'], arrayEndsWith: { id: 2 } })).toEqual([['ada'], ['bob']]);
115
+ });
116
+ it('reads an array index as PostgreSQL does', async () => {
117
+ await seeded();
118
+ expect(await at(['items', '0', 'id'], 1)).toEqual(['ada']);
119
+ expect(await at(['items', '-1', 'id'], 2)).toEqual(['ada']);
120
+ expect(await at(['0'], 1)).toEqual(['cy']);
121
+ expect(await at(['-3'], 1)).toEqual(['cy']);
122
+ expect(await at(['-0'], 1)).toEqual(['cy']);
123
+ expect(await at([' 1'], 2)).toEqual(['cy']);
124
+ expect(await at(['+1'], 2)).toEqual(['cy']);
125
+ for (const key of ['1 ', '-4', '3', '99999999999', 'one']) {
126
+ expect(await kept({ path: [key], equals: 2 })).toEqual([[], []]);
127
+ }
128
+ expect(await at(['tags', '0', 'x'], 'x')).toEqual([]);
129
+ });
130
+ it('filters nothing with a path and no operator', async () => {
131
+ await seeded();
132
+ const all = ['ada', 'bob', 'cy', 'nil', 'str'];
133
+ expect(await names({ meta: { path: ['author'] } })).toEqual(all);
134
+ expect(await names({ meta: { path: ['author'], mode: 'insensitive' } })).toEqual(all);
135
+ const both = {
136
+ path: ['author', 'name'],
137
+ stringStartsWith: 'A',
138
+ not: 'Ada',
139
+ };
140
+ expect(await names({ meta: both })).toEqual([]);
141
+ });
142
+ });
143
+ }
@@ -0,0 +1,3 @@
1
+ import type { Adapter } from 'ultimabase';
2
+ export declare function lists(create: () => Adapter): void;
3
+ //# sourceMappingURL=lists.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lists.d.ts","sourceRoot":"","sources":["../src/lists.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAyB,MAAM,YAAY,CAAA;AAgChE,wBAAgB,KAAK,CAAC,MAAM,EAAE,MAAM,OAAO,GAAG,IAAI,CA2QjD"}
package/dist/lists.js ADDED
@@ -0,0 +1,234 @@
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 Article = defineModel({
6
+ name: 'Article',
7
+ fields: (f) => ({
8
+ title: f.string({ unique: true }),
9
+ tags: f.string({ list: true }),
10
+ labels: f.string({ list: true, default: ['new'] }),
11
+ scores: f.number({ list: true }),
12
+ sizes: f.enumOf(['S', 'M', 'L'], { list: true, default: ['M'] }),
13
+ }),
14
+ indexes: [
15
+ { fields: ['tags'] },
16
+ { fields: ['scores'] },
17
+ { fields: ['sizes'] },
18
+ ],
19
+ });
20
+ /** An update hook: `updateMany` locks the rows and groups their writes. */
21
+ const Shelf = defineModel({
22
+ name: 'Shelf',
23
+ fields: (f) => ({ tags: f.string({ list: true }) }),
24
+ }).with({ hooks: { beforeUpdate: () => { } } });
25
+ const titles = async (where) => (await Article.findMany({ where }, M)).map((article) => article.title);
26
+ export function lists(create) {
27
+ describe('lists', () => {
28
+ started(create, [Article, Shelf]);
29
+ it('stores a list, [] when unwritten, and replaces it whole', async () => {
30
+ const ts = await Article.create({ data: { title: 'ts', tags: ['ts', 'node'] } }, M);
31
+ expect(ts).toMatchObject({ tags: ['ts', 'node'], labels: ['new'] });
32
+ expect(await Article.create({ data: { title: 'none', labels: [] } }, M)).toMatchObject({ tags: [], labels: [] });
33
+ await Article.update({ where: { id: ts.id }, data: { tags: ['go', 'go'] } }, M);
34
+ expect(await Article.findUnique({ where: { title: 'ts' } }, M)).toMatchObject({ tags: ['go', 'go'], labels: ['new'] });
35
+ });
36
+ it('filters a list with has, hasSome and NOT', async () => {
37
+ for (const [title, tags] of [
38
+ ['a', ['ts', 'node']],
39
+ ['b', ['go']],
40
+ ['c', []],
41
+ ]) {
42
+ await Article.create({ data: { title, tags } }, M);
43
+ }
44
+ expect(await titles({ tags: { has: 'ts' } })).toEqual(['a']);
45
+ expect(await titles({ tags: { has: 'rust' } })).toEqual([]);
46
+ expect(await titles({ tags: { hasSome: ['go', 'node'] } })).toEqual([
47
+ 'a',
48
+ 'b',
49
+ ]);
50
+ expect(await titles({ tags: { hasSome: [] } })).toEqual([]);
51
+ expect(await titles({ NOT: { tags: { has: 'ts' } } })).toEqual(['b', 'c']);
52
+ expect(await titles({ tags: { has: 'node', hasSome: ['go', 'ts'] } })).toEqual(['a']);
53
+ });
54
+ it('filters a list with hasEvery and isEmpty', async () => {
55
+ for (const [title, tags, scores, sizes] of [
56
+ ['a', ['ts', 'node', 'ts'], [1, 2], ['S', 'L']],
57
+ ['b', ['ts'], [2], []],
58
+ ['c', [], [], ['L']],
59
+ ]) {
60
+ await Article.create({ data: { title, tags, scores, sizes } }, M);
61
+ }
62
+ expect(await titles({ tags: { hasEvery: ['node', 'ts'] } })).toEqual([
63
+ 'a',
64
+ ]);
65
+ expect(await titles({ tags: { hasEvery: ['ts', 'ts'] } })).toEqual([
66
+ 'a',
67
+ 'b',
68
+ ]);
69
+ expect(await titles({ tags: { hasEvery: [] } })).toEqual(['a', 'b', 'c']);
70
+ expect(await titles({ scores: { hasEvery: [2] } })).toEqual(['a', 'b']);
71
+ expect(await titles({ sizes: { hasEvery: ['L', 'S'] } })).toEqual(['a']);
72
+ expect(await titles({ tags: { isEmpty: true } })).toEqual(['c']);
73
+ expect(await titles({ sizes: { isEmpty: false } })).toEqual(['a', 'c']);
74
+ expect(await titles({ NOT: { scores: { isEmpty: true } } })).toEqual([
75
+ 'a',
76
+ 'b',
77
+ ]);
78
+ expect(await titles({
79
+ tags: { hasEvery: ['ts'], isEmpty: false, has: 'node' },
80
+ })).toEqual(['a']);
81
+ });
82
+ it('updates a list with set, push, pushUnique and remove', async () => {
83
+ const { id } = await Article.create({ data: { title: 'a', tags: ['a', 'a'], scores: [1, 2, 1] } }, M);
84
+ const updated = (data) => Article.update({ where: { id }, data }, M);
85
+ const tags = async (data) => (await updated({ tags: data })).tags;
86
+ expect(await tags({ pushUnique: 'b' })).toEqual(['a', 'a', 'b']);
87
+ expect(await tags({ pushUnique: ['a', 'c', 'c', 'd'] })).toEqual([
88
+ 'a',
89
+ 'a',
90
+ 'b',
91
+ 'c',
92
+ 'd',
93
+ ]);
94
+ expect(await tags({ push: ['b', 'b'] })).toEqual([
95
+ 'a',
96
+ 'a',
97
+ 'b',
98
+ 'c',
99
+ 'd',
100
+ 'b',
101
+ 'b',
102
+ ]);
103
+ expect(await tags({ remove: ['a', 'z'] })).toEqual([
104
+ 'b',
105
+ 'c',
106
+ 'd',
107
+ 'b',
108
+ 'b',
109
+ ]);
110
+ expect(await tags({ remove: 'b' })).toEqual(['c', 'd']);
111
+ expect(await tags({ push: ['"', 'NULL', '{}'] })).toEqual([
112
+ 'c',
113
+ 'd',
114
+ '"',
115
+ 'NULL',
116
+ '{}',
117
+ ]);
118
+ expect(await tags({ remove: ['NULL', 'c'] })).toEqual(['d', '"', '{}']);
119
+ expect(await tags({ push: [] })).toEqual(['d', '"', '{}']);
120
+ expect(await tags({ push: undefined })).toEqual(['d', '"', '{}']);
121
+ expect(await tags({ set: 'x' })).toEqual(['x']);
122
+ expect(await updated({
123
+ scores: { remove: 1 },
124
+ sizes: { pushUnique: ['L', 'M', 'L'] },
125
+ labels: { set: [] },
126
+ })).toMatchObject({ scores: [2], sizes: ['M', 'L'], labels: [] });
127
+ const upserted = await Article.upsert({
128
+ where: { id },
129
+ create: { title: 'b' },
130
+ update: { tags: { pushUnique: ['x', 'y'] } },
131
+ }, M);
132
+ expect(upserted.tags).toEqual(['x', 'y']);
133
+ });
134
+ it('updates the lists of many rows, with or without their hooks', async () => {
135
+ for (const [title, tags] of [
136
+ ['a', ['x', 'y', 'x']],
137
+ ['b', []],
138
+ ]) {
139
+ await Article.create({ data: { title, tags } }, M);
140
+ await Shelf.create({ data: { tags } }, M);
141
+ }
142
+ for (const model of [Article, Shelf]) {
143
+ expect(await model.updateMany({ data: { tags: { remove: 'x' } } }, M)).toEqual({ count: 2 });
144
+ expect(await model.updateMany({ data: { tags: { pushUnique: 'z' } } }, M)).toEqual({ count: 2 });
145
+ }
146
+ expect((await Article.findMany({}, M)).map((row) => row.tags)).toEqual([
147
+ ['y', 'z'],
148
+ ['z'],
149
+ ]);
150
+ expect((await Shelf.findMany({}, M)).map((row) => row.tags)).toEqual([
151
+ ['y', 'z'],
152
+ ['z'],
153
+ ]);
154
+ });
155
+ it('refuses a list operation that is not one, or a bad element', async () => {
156
+ const { id } = await Article.create({ data: { title: 'a', tags: ['a'] } }, M);
157
+ const takes = 'data.tags takes a list or one of set, push, pushUnique, remove.';
158
+ const refusals = [
159
+ [{}, 'INVALID_QUERY', takes],
160
+ [{ push: 'b', remove: 'a' }, 'INVALID_QUERY', takes],
161
+ [{ append: 'b' }, 'INVALID_QUERY', takes],
162
+ [{ push: '' }, 'VALIDATION_FAILED', 'Invalid data.'],
163
+ [{ pushUnique: ['b', '\0'] }, 'VALIDATION_FAILED', 'Invalid data.'],
164
+ ];
165
+ for (const [tags, code, message] of refusals) {
166
+ await expect(
167
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- the test sends what the types refuse.
168
+ Article.update({ where: { id }, data: { tags } }, M)).rejects.toMatchObject({ code, message });
169
+ }
170
+ await expect(Article.update({ where: { id }, data: { tags: { push: '' } } }, M)).rejects.toMatchObject({
171
+ details: {
172
+ issues: [
173
+ {
174
+ path: ['tags'],
175
+ message: 'Expected a list of non-empty strings without a NUL or a lone surrogate.',
176
+ },
177
+ ],
178
+ },
179
+ });
180
+ const pushed = { push: 'a' };
181
+ const data = { title: 'b', tags: pushed };
182
+ await expect(Article.create({ data }, M)).rejects.toMatchObject({
183
+ code: 'VALIDATION_FAILED',
184
+ });
185
+ expect(await Article.findUnique({ where: { id } }, M)).toMatchObject({
186
+ tags: ['a'],
187
+ });
188
+ });
189
+ it('stores and filters numbers and enum values', async () => {
190
+ const a = await Article.create({ data: { title: 'a', scores: [1.5, -2, 0, 1.5], sizes: ['S', 'L'] } }, M);
191
+ expect(a).toMatchObject({ scores: [1.5, -2, 0, 1.5], sizes: ['S', 'L'] });
192
+ expect(await Article.create({ data: { title: 'b', scores: [1e300] } }, M)).toMatchObject({ scores: [1e300], sizes: ['M'] });
193
+ await Article.create({ data: { title: 'c', sizes: [] } }, M);
194
+ expect(await titles({ scores: { has: -2 } })).toEqual(['a']);
195
+ expect(await titles({ scores: { has: 2 } })).toEqual([]);
196
+ expect(await titles({ scores: { hasSome: [0, 1e300] } })).toEqual([
197
+ 'a',
198
+ 'b',
199
+ ]);
200
+ expect(await titles({ sizes: { has: 'M' } })).toEqual(['b']);
201
+ expect(await titles({ sizes: { hasSome: ['S', 'M'] } })).toEqual([
202
+ 'a',
203
+ 'b',
204
+ ]);
205
+ expect(await titles({ NOT: { sizes: { has: 'S' } } })).toEqual(['b', 'c']);
206
+ await Article.update({ where: { id: a.id }, data: { scores: [], sizes: ['L', 'L'] } }, M);
207
+ expect(await Article.findUnique({ where: { title: 'a' } }, M)).toMatchObject({ scores: [], sizes: ['L', 'L'] });
208
+ });
209
+ it('stores any well-formed text as an element', async () => {
210
+ const tags = ['"', '\\', 'a,b', '{}', ' ', 'NULL', 'é😀'];
211
+ await Article.create({ data: { title: 'odd', tags } }, M);
212
+ expect(await Article.findUnique({ where: { title: 'odd' } }, M)).toMatchObject({ tags });
213
+ for (const tag of tags) {
214
+ expect(await titles({ tags: { has: tag } })).toEqual(['odd']);
215
+ }
216
+ expect(await titles({ tags: { hasSome: ['null'] } })).toEqual([]);
217
+ });
218
+ it('refuses an empty element, a NUL or a lone surrogate', async () => {
219
+ for (const tags of [[''], ['a\0'], ['\uD800']]) {
220
+ await expect(Article.create({ data: { title: 'bad', tags } }, M)).rejects.toMatchObject({ code: 'VALIDATION_FAILED' });
221
+ }
222
+ for (const where of [
223
+ { tags: { has: '' } },
224
+ { tags: { hasSome: ['\0'] } },
225
+ { tags: { hasEvery: [''] } },
226
+ ]) {
227
+ await expect(Article.findMany({ where }, M)).rejects.toMatchObject({
228
+ code: 'INVALID_QUERY',
229
+ });
230
+ }
231
+ expect(await Article.count({}, M)).toBe(0);
232
+ });
233
+ });
234
+ }
@@ -0,0 +1,3 @@
1
+ import type { Adapter } from 'ultimabase';
2
+ export declare function nested(create: () => Adapter): void;
3
+ //# sourceMappingURL=nested.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nested.d.ts","sourceRoot":"","sources":["../src/nested.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAgDzC,wBAAgB,MAAM,CAAC,MAAM,EAAE,MAAM,OAAO,GAAG,IAAI,CAwIlD"}