@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
@@ -0,0 +1,55 @@
1
+ import { describe } from 'vitest';
2
+ import { aggregates } from './aggregates.js';
3
+ import { bulkReturns } from './bulk-returns.js';
4
+ import { calendarDays } from './days.js';
5
+ import { cases } from './cases.js';
6
+ import { cascades } from './cascades.js';
7
+ import { counts } from './counts.js';
8
+ import { decimals } from './decimals.js';
9
+ import { distincts } from './distincts.js';
10
+ import { feeds } from './feeds.js';
11
+ import { groups } from './groups.js';
12
+ import { jsonPaths } from './json-paths.js';
13
+ import { lists } from './lists.js';
14
+ import { nested } from './nested.js';
15
+ import { numbers } from './numbers.js';
16
+ import { projection } from './projection.js';
17
+ import { reads } from './reads.js';
18
+ import { relationFilters } from './relation-filters.js';
19
+ import { roundTrips } from './round-trips.js';
20
+ import { searches } from './searches.js';
21
+ import { sorts } from './sorts.js';
22
+ import { transactions } from './transactions.js';
23
+ import { virtuals } from './virtuals.js';
24
+ import { writes } from './writes.js';
25
+ /**
26
+ * Runs the contract against the adapters `create` returns, one per test, each
27
+ * on an empty database.
28
+ */
29
+ export function testAdapter(name, create, options) {
30
+ describe(`the ${name} adapter`, () => {
31
+ reads(create);
32
+ cases(create);
33
+ writes(create);
34
+ cascades(create);
35
+ lists(create);
36
+ jsonPaths(create);
37
+ nested(create);
38
+ numbers(create);
39
+ bulkReturns(create);
40
+ decimals(create);
41
+ calendarDays(create);
42
+ counts(create);
43
+ relationFilters(create);
44
+ virtuals(create);
45
+ aggregates(create);
46
+ groups(create);
47
+ distincts(create);
48
+ sorts(create);
49
+ searches(create);
50
+ projection(create);
51
+ transactions(create);
52
+ feeds(create);
53
+ roundTrips(create, options.roundTrips);
54
+ });
55
+ }
@@ -0,0 +1,17 @@
1
+ export type TestApplication = {
2
+ /** The HTTP server's address during a test, such as `http://localhost:54321`. */
3
+ readonly url: string;
4
+ };
5
+ export type TestOptions = {
6
+ /**
7
+ * `'declared'` runs the tests on the application's own database, in
8
+ * tables of the file's own, instead of a new `memory()`.
9
+ */
10
+ readonly database?: 'declared';
11
+ };
12
+ /**
13
+ * Runs `app`, the value `defineApp` returned, around each test of the file
14
+ * that calls it.
15
+ */
16
+ export declare function testApplication(app: object, options?: TestOptions): TestApplication;
17
+ //# sourceMappingURL=test-application.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-application.d.ts","sourceRoot":"","sources":["../src/test-application.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,eAAe,GAAG;IAC7B,iFAAiF;IACjF,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CACpB,CAAA;AAwBD,MAAM,MAAM,WAAW,GAAG;IACzB;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,UAAU,CAAA;CAC9B,CAAA;AAgBD;;;GAGG;AACH,wBAAgB,eAAe,CAC9B,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,WAAgB,GACvB,eAAe,CA2BjB"}
@@ -0,0 +1,68 @@
1
+ import { memory } from 'ultimabase';
2
+ import { quietRuns, runningApi, scratchOf } from 'ultimabase/internal';
3
+ import { afterEach, beforeAll, beforeEach } from 'vitest';
4
+ /** Starts `run` with `PORT` set to `0`, then puts `PORT` back. */
5
+ async function onFreePort(run) {
6
+ const port = process.env['PORT'];
7
+ process.env['PORT'] = '0';
8
+ try {
9
+ await run.start();
10
+ }
11
+ finally {
12
+ if (port === undefined)
13
+ delete process.env['PORT'];
14
+ else
15
+ process.env['PORT'] = port;
16
+ }
17
+ }
18
+ function urlOf(api) {
19
+ if (typeof api !== 'object' || api === null || !('url' in api)) {
20
+ return undefined;
21
+ }
22
+ const { url } = api;
23
+ if (typeof url !== 'function')
24
+ return undefined;
25
+ const value = Reflect.apply(url, api, []);
26
+ return typeof value === 'string' ? value : undefined;
27
+ }
28
+ /** The declared database, which gives each test file empty tables. */
29
+ function declared(database) {
30
+ // A database that starts empty every time, as memory(), needs nothing.
31
+ if (database.migrations === undefined)
32
+ return () => database;
33
+ const scratch = scratchOf(database);
34
+ if (scratch === undefined) {
35
+ throw new TypeError("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' }.");
36
+ }
37
+ beforeAll(() => scratch().drop);
38
+ return () => database;
39
+ }
40
+ /**
41
+ * Runs `app`, the value `defineApp` returned, around each test of the file
42
+ * that calls it.
43
+ */
44
+ export function testApplication(app, options = {}) {
45
+ const runs = quietRuns(app);
46
+ if (runs === undefined) {
47
+ throw new TypeError('testApplication takes the value defineApp returned.');
48
+ }
49
+ const database = options.database === 'declared' ? declared(runs.declared) : memory;
50
+ let run;
51
+ beforeEach(async () => {
52
+ run = runs.on(database());
53
+ await onFreePort(run);
54
+ });
55
+ afterEach(async () => {
56
+ await run?.stop();
57
+ run = undefined;
58
+ });
59
+ return {
60
+ get url() {
61
+ const url = run === undefined ? undefined : urlOf(runningApi('http'));
62
+ if (url === undefined) {
63
+ throw new Error('url is the address of httpPlugin(), read during a test: add httpPlugin() to the application.');
64
+ }
65
+ return url;
66
+ },
67
+ };
68
+ }
@@ -0,0 +1,3 @@
1
+ import type { Adapter } from 'ultimabase';
2
+ export declare function transactions(create: () => Adapter): void;
3
+ //# sourceMappingURL=transactions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transactions.d.ts","sourceRoot":"","sources":["../src/transactions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAkBzC,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,OAAO,GAAG,IAAI,CAuDxD"}
@@ -0,0 +1,61 @@
1
+ import { defineModel, master, transaction } from 'ultimabase';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { started } from './suite.js';
4
+ const M = { as: master };
5
+ const Account = defineModel({
6
+ name: 'Account',
7
+ fields: (f) => ({ name: f.string({ unique: true }) }),
8
+ });
9
+ const open = async (name) => Account.create({ data: { name } }, M);
10
+ const names = async () => (await Account.findMany({ orderBy: { name: 'asc' } }, M)).map((account) => account.name);
11
+ export function transactions(create) {
12
+ describe('transactions', () => {
13
+ started(create, [Account]);
14
+ it('commits every write together', async () => {
15
+ await transaction(async () => {
16
+ await open('bob');
17
+ await open('cy');
18
+ });
19
+ expect(await names()).toEqual(['bob', 'cy']);
20
+ });
21
+ it('reads its own writes', async () => {
22
+ const seen = await transaction(async () => {
23
+ await open('bob');
24
+ return names();
25
+ });
26
+ expect(seen).toEqual(['bob']);
27
+ });
28
+ it('rolls every write back when the callback throws', async () => {
29
+ await open('ann');
30
+ const failed = transaction(async () => {
31
+ await open('bob');
32
+ throw new Error('Stop.');
33
+ });
34
+ await expect(failed).rejects.toThrow('Stop.');
35
+ expect(await names()).toEqual(['ann']);
36
+ });
37
+ it('rolls back after a failed write, even one the callback catches', async () => {
38
+ const failed = transaction(async () => {
39
+ await open('bob');
40
+ await open('bob').catch(() => undefined);
41
+ });
42
+ await expect(failed).rejects.toMatchObject({ code: 'UNIQUE_VIOLATION' });
43
+ expect(await names()).toEqual([]);
44
+ });
45
+ it('shows its writes to no one else before the commit', async () => {
46
+ const written = Promise.withResolvers();
47
+ const resumed = Promise.withResolvers();
48
+ const running = transaction(async () => {
49
+ await open('bob');
50
+ written.resolve();
51
+ await resumed.promise;
52
+ });
53
+ await written.promise;
54
+ // This read runs outside the transaction.
55
+ expect(await names()).toEqual([]);
56
+ resumed.resolve();
57
+ await running;
58
+ expect(await names()).toEqual(['bob']);
59
+ });
60
+ });
61
+ }
@@ -0,0 +1,3 @@
1
+ import type { Adapter } from 'ultimabase';
2
+ export declare function virtuals(create: () => Adapter): void;
3
+ //# sourceMappingURL=virtuals.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"virtuals.d.ts","sourceRoot":"","sources":["../src/virtuals.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAY,MAAM,YAAY,CAAA;AAoFnD,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,OAAO,GAAG,IAAI,CAgGpD"}
@@ -0,0 +1,135 @@
1
+ import { allow, defineModel, master } from 'ultimabase';
2
+ import { beforeEach, describe, expect, it } from 'vitest';
3
+ import { started } from './suite.js';
4
+ const M = { as: master };
5
+ /** A Standard Schema of what `check` accepts, as zod would type it. */
6
+ function schema(check) {
7
+ const validate = (value) => check(value) ? { value } : { issues: [{ message: 'Invalid.' }] };
8
+ return { '~standard': { version: 1, vendor: 'testing', validate } };
9
+ }
10
+ const text = schema((value) => typeof value === 'string');
11
+ const width = schema((value) => Number.isInteger(Reflect.get(Object(value), 'width')));
12
+ const first = schema((value) => Number.isInteger(Reflect.get(Object(value), 'first')));
13
+ const Shelf = defineModel({
14
+ name: 'Shelf',
15
+ fields: (f) => ({
16
+ name: f.string(),
17
+ books: f.many(() => Book),
18
+ label: f.virtual(text, { needs: ['name'] }),
19
+ padded: f.virtual(text, { input: width, needs: ['name'] }),
20
+ latest: f.virtual(() => [Book], { input: first }),
21
+ }),
22
+ }).with({
23
+ access: allow.public,
24
+ virtuals: {
25
+ label: { run: ({ record }) => record.name.toUpperCase() },
26
+ padded: {
27
+ run: ({ input, record }) => record.name.padEnd(input.width, '.'),
28
+ },
29
+ latest: {
30
+ runMany: async ({ input, records }) => {
31
+ const ids = records.map((record) => record.id);
32
+ const books = await Book.findMany({
33
+ where: { shelf: { is: { id: { in: ids } } } },
34
+ orderBy: { title: 'desc' },
35
+ include: { shelf: { select: { id: true } } },
36
+ }, M);
37
+ return records.map((record) => books
38
+ .filter((book) => book.shelf?.id === record.id)
39
+ .slice(0, input.first));
40
+ },
41
+ },
42
+ },
43
+ });
44
+ const Book = defineModel({
45
+ name: 'Book',
46
+ fields: (f) => ({
47
+ title: f.string(),
48
+ shelf: f.one(() => Shelf, { optional: true }),
49
+ tags: f.many(() => Tag),
50
+ }),
51
+ }).with({ access: allow.public });
52
+ const Tag = defineModel({
53
+ name: 'Tag',
54
+ fields: (f) => ({ name: f.string(), books: f.many(() => Book) }),
55
+ }).with({ access: allow.public });
56
+ export function virtuals(create) {
57
+ describe('virtual fields', () => {
58
+ started(create, [Shelf, Book, Tag]);
59
+ beforeEach(async () => {
60
+ await Shelf.create({
61
+ data: {
62
+ name: 'oak',
63
+ books: {
64
+ create: [
65
+ { title: 'A', tags: { create: [{ name: 'old' }] } },
66
+ { title: 'B' },
67
+ { title: 'C', tags: { create: [{ name: 'new' }] } },
68
+ ],
69
+ },
70
+ },
71
+ }, M);
72
+ await Shelf.create({ data: { name: 'elm' } }, M);
73
+ });
74
+ it('reads the fields it needs, which the row leaves out', async () => {
75
+ const rows = await Shelf.findMany({
76
+ orderBy: { name: 'asc' },
77
+ select: { label: true, padded: { width: 5 } },
78
+ }, M);
79
+ expect(rows).toStrictEqual([
80
+ { label: 'ELM', padded: 'elm..' },
81
+ { label: 'OAK', padded: 'oak..' },
82
+ ]);
83
+ });
84
+ it('is in the default row without an input', async () => {
85
+ const row = await Shelf.findFirst({ where: { name: 'oak' } }, M);
86
+ expect(row?.label).toBe('OAK');
87
+ expect(row && 'padded' in row).toBe(false);
88
+ });
89
+ it('reads the rows it returns with their relations', async () => {
90
+ const rows = await Shelf.findMany({
91
+ orderBy: { name: 'asc' },
92
+ select: {
93
+ name: true,
94
+ latest: {
95
+ first: 2,
96
+ select: {
97
+ title: true,
98
+ shelf: { select: { label: true } },
99
+ tags: { select: { name: true } },
100
+ _count: { select: { tags: true } },
101
+ },
102
+ },
103
+ },
104
+ }, M);
105
+ expect(rows).toStrictEqual([
106
+ { name: 'elm', latest: [] },
107
+ {
108
+ name: 'oak',
109
+ latest: [
110
+ {
111
+ title: 'C',
112
+ shelf: { label: 'OAK' },
113
+ tags: [{ name: 'new' }],
114
+ _count: { tags: 1 },
115
+ },
116
+ {
117
+ title: 'B',
118
+ shelf: { label: 'OAK' },
119
+ tags: [],
120
+ _count: { tags: 0 },
121
+ },
122
+ ],
123
+ },
124
+ ]);
125
+ });
126
+ it('is read under an included relation', async () => {
127
+ const book = await Book.findFirst({
128
+ where: { title: 'A' },
129
+ include: { shelf: { include: { padded: { width: 4 } } } },
130
+ }, M);
131
+ expect(book?.shelf?.label).toBe('OAK');
132
+ expect(book?.shelf?.padded).toBe('oak.');
133
+ });
134
+ });
135
+ }
@@ -0,0 +1,3 @@
1
+ import type { Adapter } from 'ultimabase';
2
+ export declare function writes(create: () => Adapter): void;
3
+ //# sourceMappingURL=writes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"writes.d.ts","sourceRoot":"","sources":["../src/writes.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAqDzC,wBAAgB,MAAM,CAAC,MAAM,EAAE,MAAM,OAAO,GAAG,IAAI,CAkQlD"}
package/dist/writes.js ADDED
@@ -0,0 +1,219 @@
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 Team = defineModel({
6
+ name: 'Team',
7
+ fields: (f) => ({
8
+ name: f.string({ unique: true }),
9
+ players: f.many(() => Player),
10
+ }),
11
+ });
12
+ const Player = defineModel({
13
+ name: 'Player',
14
+ fields: (f) => ({
15
+ name: f.string(),
16
+ score: f.number({ default: 0 }),
17
+ active: f.boolean({ default: true }),
18
+ bio: f.string({ optional: true }),
19
+ stats: f.json({ optional: true }),
20
+ bornAt: f.dateTime({ optional: true }),
21
+ team: f.one(() => Team, { optional: true }),
22
+ badge: f.one(() => Badge, { optional: true }),
23
+ skills: f.many(() => Skill),
24
+ }),
25
+ indexes: [{ fields: ['name', 'team'], unique: true }],
26
+ });
27
+ const Badge = defineModel({
28
+ name: 'Badge',
29
+ fields: (f) => ({ code: f.string(), player: f.one(() => Player) }),
30
+ });
31
+ const Skill = defineModel({
32
+ name: 'Skill',
33
+ fields: (f) => ({
34
+ label: f.string({ unique: true }),
35
+ players: f.many(() => Player),
36
+ }),
37
+ });
38
+ const skillsOf = async (id) => (await Skill.findMany({ where: { players: { some: { id } } }, orderBy: { label: 'asc' } }, M)).map((skill) => skill.label);
39
+ const skill = async (label) => Skill.create({ data: { label } }, M);
40
+ export function writes(create) {
41
+ describe('writes', () => {
42
+ const database = started(create, [Team, Player, Badge, Skill]);
43
+ it('creates a row and returns it as stored', async () => {
44
+ const bornAt = new Date(Date.UTC(1990, 4, 17, 8, 30, 15, 250));
45
+ const created = await Player.create({ data: { name: 'Ada', stats: { goals: [1, 2], ok: true }, bornAt } }, M);
46
+ expect(created).toMatchObject({
47
+ name: 'Ada',
48
+ score: 0,
49
+ active: true,
50
+ bio: null,
51
+ stats: { goals: [1, 2], ok: true },
52
+ bornAt,
53
+ });
54
+ expect(created.createdAt).toBeInstanceOf(Date);
55
+ expect(await Player.findUnique({ where: { id: created.id } }, M)).toEqual(created);
56
+ });
57
+ it('updates values, to null too, and moves updatedAt', async () => {
58
+ const ada = await Player.create({ data: { name: 'Ada', bio: 'Poet', score: 1.5 } }, M);
59
+ const updated = await Player.update({
60
+ where: { id: ada.id },
61
+ data: { bio: null, score: -2, active: false },
62
+ }, M);
63
+ expect(updated).toMatchObject({ bio: null, score: -2, active: false });
64
+ expect(updated.createdAt).toEqual(ada.createdAt);
65
+ expect(updated.updatedAt.getTime()).toBeGreaterThanOrEqual(ada.updatedAt.getTime());
66
+ expect(await Player.findUnique({ where: { id: ada.id } }, M)).toEqual(updated);
67
+ });
68
+ it('deletes a row and returns it', async () => {
69
+ const ada = await Player.create({ data: { name: 'Ada' } }, M);
70
+ expect(await Player.delete({ where: { id: ada.id } }, M)).toEqual(ada);
71
+ expect(await Player.count({}, M)).toBe(0);
72
+ await expect(Player.delete({ where: { id: ada.id } }, M)).rejects.toMatchObject({ code: 'NOT_FOUND' });
73
+ });
74
+ it('reads, with before, the rows its delete removed, as they were', async () => {
75
+ const ada = await Player.create({ data: { name: 'Ada' } }, M);
76
+ await Player.create({ data: { name: 'Bob' } }, M);
77
+ const read = { model: 'Player', where: {} };
78
+ const where = { name: { equals: 'Ada' } };
79
+ const removed = await database().write({
80
+ changes: [{ kind: 'delete', model: 'Player', where }],
81
+ read,
82
+ before: true,
83
+ });
84
+ expect(removed).toMatchObject([{ id: ada.id, name: 'Ada' }]);
85
+ // Nothing left to remove, or no delete at all: no row (D-104).
86
+ const again = await database().write({
87
+ changes: [{ kind: 'delete', model: 'Player', where }],
88
+ read,
89
+ before: true,
90
+ });
91
+ expect(again).toEqual([]);
92
+ expect(await database().write({ changes: [], read, before: true })).toEqual([]);
93
+ expect(await Player.count({}, M)).toBe(1);
94
+ });
95
+ it('reads, with before and no delete, the rows its update changed', async () => {
96
+ const ada = await Player.create({ data: { name: 'Ada' } }, M);
97
+ await Player.create({ data: { name: 'Bob' } }, M);
98
+ const changed = await database().write({
99
+ changes: [
100
+ {
101
+ kind: 'update',
102
+ model: 'Player',
103
+ where: { name: { equals: 'Ada' } },
104
+ set: { name: 'Eve' },
105
+ },
106
+ ],
107
+ read: { model: 'Player', where: {} },
108
+ before: true,
109
+ });
110
+ // As the update left them (D-133).
111
+ expect(changed).toMatchObject([{ id: ada.id, name: 'Eve' }]);
112
+ });
113
+ it('connects and disconnects a one-to-many', async () => {
114
+ const team = await Team.create({ data: { name: 'Reds' } }, M);
115
+ const ada = await Player.create({ data: { name: 'Ada', team: { connect: { id: team.id } } } }, M);
116
+ const reds = await Team.findUnique({ where: { id: team.id }, include: { players: true } }, M);
117
+ expect(reds).toMatchObject({ players: [{ id: ada.id }] });
118
+ await Player.update({ where: { id: ada.id }, data: { team: { disconnect: true } } }, M);
119
+ expect(await Player.count({ where: { team: { is: null } } }, M)).toBe(1);
120
+ });
121
+ it('connects a one-to-one from either side', async () => {
122
+ const ada = await Player.create({ data: { name: 'Ada' } }, M);
123
+ const badge = await Badge.create({ data: { code: 'A1', player: { connect: { id: ada.id } } } }, M);
124
+ const found = await Player.findUnique({ where: { id: ada.id }, include: { badge: true } }, M);
125
+ expect(found).toMatchObject({ badge: { id: badge.id } });
126
+ const grace = await Player.create({ data: { name: 'Grace', badge: { connect: { id: badge.id } } } }, M);
127
+ const moved = await Badge.findUnique({ where: { id: badge.id }, include: { player: true } }, M);
128
+ expect(moved).toMatchObject({ player: { id: grace.id } });
129
+ });
130
+ it('links and unlinks a many-to-many, and unlinks a deleted row', async () => {
131
+ const math = await skill('math');
132
+ const art = await skill('art');
133
+ const chess = await skill('chess');
134
+ const ada = await Player.create({
135
+ data: {
136
+ name: 'Ada',
137
+ skills: {
138
+ connect: [{ id: math.id }, { id: art.id }],
139
+ },
140
+ },
141
+ }, M);
142
+ expect(await skillsOf(ada.id)).toEqual(['art', 'math']);
143
+ await Player.update({
144
+ where: { id: ada.id },
145
+ data: {
146
+ skills: {
147
+ connect: [{ id: chess.id }],
148
+ disconnect: [{ id: math.id }],
149
+ },
150
+ },
151
+ }, M);
152
+ expect(await skillsOf(ada.id)).toEqual(['art', 'chess']);
153
+ await Skill.delete({ where: { id: art.id } }, M);
154
+ expect(await skillsOf(ada.id)).toEqual(['chess']);
155
+ await Player.delete({ where: { id: ada.id } }, M);
156
+ expect(await Skill.count({ where: { players: { some: {} } } }, M)).toBe(0);
157
+ });
158
+ it('refuses a second row with a unique field or index, and keeps the first', async () => {
159
+ const team = await Team.create({ data: { name: 'Reds' } }, M);
160
+ await expect(Team.create({ data: { name: 'Reds' } }, M)).rejects.toMatchObject({
161
+ code: 'UNIQUE_VIOLATION',
162
+ details: { model: 'Team', fields: ['name'] },
163
+ });
164
+ const data = { name: 'Ada', team: { connect: { id: team.id } } };
165
+ await Player.create({ data }, M);
166
+ await expect(Player.create({ data }, M)).rejects.toMatchObject({
167
+ code: 'UNIQUE_VIOLATION',
168
+ details: { model: 'Player', fields: ['name', 'team'] },
169
+ });
170
+ const grace = await Player.create({ data: { name: 'Grace' } }, M);
171
+ await expect(Player.update({ where: { id: grace.id }, data: { name: 'Ada', team: data.team } }, M)).rejects.toMatchObject({ code: 'UNIQUE_VIOLATION' });
172
+ expect(await Team.count({}, M)).toBe(1);
173
+ expect(await Player.count({ where: { name: 'Grace', team: { is: null } } }, M)).toBe(1);
174
+ });
175
+ it('stores any well-formed text and the dates of years 1 to 9999', async () => {
176
+ const first = new Date('0001-01-01T00:00:00.000Z');
177
+ const last = new Date('9999-12-31T23:59:59.999Z');
178
+ const ada = await Player.create({ data: { name: '😀 Ada', stats: { 'é😀': ['z'] }, bornAt: first } }, M);
179
+ await Player.create({ data: { name: 'Grace', bornAt: last } }, M);
180
+ expect(await Player.findMany({ orderBy: { bornAt: 'asc' } }, M)).toMatchObject([
181
+ { name: '😀 Ada', stats: { 'é😀': ['z'] }, bornAt: first },
182
+ { name: 'Grace', bornAt: last },
183
+ ]);
184
+ expect(await Player.count({ where: { id: ada.id, bornAt: first } }, M)).toBe(1);
185
+ });
186
+ it('refuses a NUL, a lone surrogate or a year outside 1 to 9999', async () => {
187
+ const invalid = { code: 'VALIDATION_FAILED' };
188
+ for (const data of [
189
+ { name: 'A\0da' },
190
+ { name: '\uD800' },
191
+ { name: 'Ada', stats: { 'k\0': 1 } },
192
+ { name: 'Ada', stats: ['\uDC00'] },
193
+ { name: 'Ada', bornAt: new Date('0000-12-31T23:59:59.999Z') },
194
+ { name: 'Ada', bornAt: new Date('+010000-01-01T00:00:00.000Z') },
195
+ ]) {
196
+ await expect(Player.create({ data }, M)).rejects.toMatchObject(invalid);
197
+ }
198
+ const query = { code: 'INVALID_QUERY' };
199
+ const late = new Date('+010000-01-01T00:00:00.000Z');
200
+ for (const where of [
201
+ { name: 'A\0' },
202
+ { name: { contains: '\uD800' } },
203
+ { id: { in: ['\0'] } },
204
+ { bornAt: { lt: late } },
205
+ { stats: { equals: { a: '\0' } } },
206
+ ]) {
207
+ await expect(Player.findMany({ where }, M)).rejects.toMatchObject(query);
208
+ }
209
+ const team = { connect: { id: '\0' } };
210
+ await expect(Player.create({ data: { name: 'Ada', team } }, M)).rejects.toMatchObject(query);
211
+ expect(await Player.count({}, M)).toBe(0);
212
+ });
213
+ it('refuses to connect a missing row, and writes nothing', async () => {
214
+ const created = Player.create({ data: { name: 'Ada', team: { connect: { id: 'missing' } } } }, M);
215
+ await expect(created).rejects.toMatchObject({ code: 'NOT_FOUND' });
216
+ expect(await Player.count({}, M)).toBe(0);
217
+ });
218
+ });
219
+ }
package/package.json CHANGED
@@ -1,6 +1,43 @@
1
1
  {
2
2
  "name": "@ultimabase/testing",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.1",
4
+ "description": "Test helpers for UltimaBase applications, plugins and adapters.",
5
+ "type": "module",
6
+ "license": "Apache-2.0",
7
+ "homepage": "https://ultimabase.dev/docs/testing",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/MrEkinox/ultimabase.git",
11
+ "directory": "packages/testing"
12
+ },
13
+ "engines": {
14
+ "node": ">=24.7.0"
15
+ },
16
+ "exports": {
17
+ ".": "./dist/index.js"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "src",
22
+ "!src/**/*.test.ts",
23
+ "!src/test"
24
+ ],
25
+ "scripts": {
26
+ "prepack": "rm -rf dist && yarn run -T tsc --project tsconfig.build.json"
27
+ },
28
+ "publishConfig": {
29
+ "exports": {
30
+ ".": "./dist/index.js"
31
+ }
32
+ },
33
+ "peerDependencies": {
34
+ "ultimabase": "^0.1.1",
35
+ "vitest": "^5.0.5"
36
+ },
37
+ "devDependencies": {
38
+ "@ultimabase/http": "0.1.1",
39
+ "ultimabase": "0.1.1",
40
+ "vite": "8.3.4",
41
+ "vitest": "5.0.5"
42
+ }
6
43
  }