@afora/core 0.0.9 → 0.0.12
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/README.md +62 -189
- package/dist/adapters.js +31 -55
- package/dist/core/application/application.d.ts +30 -0
- package/dist/core/application/application.js +1291 -0
- package/dist/core/application/exceptions.js +85 -0
- package/dist/core/application/migrate.js +67 -0
- package/dist/core/application/providers.js +108 -0
- package/dist/core/application/registry.js +23 -0
- package/dist/core/application/schema.js +398 -0
- package/dist/core/application/start.js +162 -0
- package/dist/core/application/types/application.interface.d.ts +23 -3
- package/dist/core/auth/jwt.js +200 -0
- package/dist/core/auth/types/jwt.interface.d.ts +10 -2
- package/dist/core/blueprint/blueprint-plugin.js +7 -0
- package/dist/core/blueprint/blueprint.js +146 -0
- package/dist/core/blueprint/callable.js +13 -0
- package/dist/core/blueprint/exceptions.js +20 -0
- package/dist/core/blueprint/is-blueprint.js +4 -0
- package/dist/core/blueprint/list.js +19 -0
- package/dist/core/blueprint/optional.js +69 -0
- package/dist/core/blueprint/record-field.js +14 -0
- package/dist/core/blueprint/slice.js +127 -0
- package/dist/core/blueprint/types/blueprint.interface.d.ts +16 -0
- package/dist/core/blueprint/variant.js +72 -0
- package/dist/core/command/command.js +428 -0
- package/dist/core/command/exceptions.js +30 -0
- package/dist/core/command/types/command.interface.d.ts +10 -1
- package/dist/core/entity/abstract-entity.d.ts +19 -0
- package/dist/core/entity/abstract-entity.js +181 -0
- package/dist/core/entity/entity.js +209 -0
- package/dist/core/entity/event.d.ts +39 -0
- package/dist/core/entity/event.js +80 -0
- package/dist/core/entity/exceptions.js +27 -0
- package/dist/core/entity/trust.js +7 -0
- package/dist/core/entity/version.js +13 -0
- package/dist/core/environment/environment.d.ts +26 -3
- package/dist/core/environment/environment.js +71 -0
- package/dist/core/event-policy/event-policy.js +76 -0
- package/dist/core/event-policy/exceptions.js +12 -0
- package/dist/core/event-policy/publish.js +98 -0
- package/dist/core/event-policy/publishers.js +18 -0
- package/dist/core/event-policy/types/event-policy.interface.d.ts +8 -2
- package/dist/core/exception/enums/exception-layers.js +15 -0
- package/dist/core/exception/exception.d.ts +15 -0
- package/dist/core/exception/exception.js +128 -0
- package/dist/core/exception/exported.js +49 -0
- package/dist/core/exception/guard.d.ts +20 -1
- package/dist/core/exception/guard.js +16 -0
- package/dist/core/exception/types/exception.interface.d.ts +13 -0
- package/dist/core/json.js +19 -0
- package/dist/core/logger/exceptions.js +7 -0
- package/dist/core/logger/logger.d.ts +14 -1
- package/dist/core/logger/logger.js +178 -0
- package/dist/core/openapi/exceptions.js +7 -0
- package/dist/core/openapi/openapi.js +132 -0
- package/dist/core/openapi/types/openapi.interface.d.ts +7 -1
- package/dist/core/output/access.js +66 -0
- package/dist/core/output/coerce.js +38 -0
- package/dist/core/output/exceptions.js +22 -0
- package/dist/{index-y280ew0k.js → core/output/http.js} +35 -80
- package/dist/core/output/openapi-sink.d.ts +10 -2
- package/dist/{index-hg1z2s2v.js → core/output/openapi-sink.js} +60 -544
- package/dist/core/output/output.js +192 -0
- package/dist/core/output/problem.js +30 -0
- package/dist/core/output/select.js +28 -0
- package/dist/core/output/stage.js +111 -0
- package/dist/core/output/status.js +52 -0
- package/dist/core/output/types/http.interface.d.ts +5 -1
- package/dist/core/output/types/output.interface.d.ts +24 -0
- package/dist/core/parse/issue.js +31 -0
- package/dist/core/parse/parse.d.ts +8 -1
- package/dist/core/parse/parse.js +323 -0
- package/dist/core/parse/strip.js +48 -0
- package/dist/core/property/abstract-property.js +37 -0
- package/dist/core/property/define-property.js +209 -0
- package/dist/core/property/enums/property-states.js +8 -0
- package/dist/core/property/exceptions.js +12 -0
- package/dist/core/property/input.js +10 -0
- package/dist/core/property/is-property.js +3 -0
- package/dist/core/property/lineage.js +8 -0
- package/dist/core/property/property.d.ts +27 -0
- package/dist/core/property/property.js +46 -0
- package/dist/core/property/refine.js +103 -0
- package/dist/core/record/abstract-record.d.ts +2 -1
- package/dist/core/record/abstract-record.js +307 -0
- package/dist/core/record/exceptions.js +49 -0
- package/dist/core/record/marks.js +7 -0
- package/dist/core/record/mask.js +60 -0
- package/dist/core/record/name.js +23 -0
- package/dist/core/record/plugin-catalog.js +115 -0
- package/dist/core/record/plugin.js +198 -0
- package/dist/core/record/record.js +117 -0
- package/dist/core/record/types/record.interface.d.ts +5 -0
- package/dist/core/repository/cursor.js +83 -0
- package/dist/core/repository/exceptions.js +72 -0
- package/dist/core/repository/in-memory.js +196 -0
- package/dist/core/repository/migrate.js +254 -0
- package/dist/core/repository/plan.js +27 -0
- package/dist/core/repository/provider.js +29 -0
- package/dist/core/repository/query.js +153 -0
- package/dist/core/repository/references.js +196 -0
- package/dist/core/repository/repository.d.ts +22 -2
- package/dist/core/repository/repository.js +720 -0
- package/dist/core/repository/state.js +138 -0
- package/dist/core/repository/types/query.interface.d.ts +13 -0
- package/dist/core/repository/types/repository.interface.d.ts +20 -8
- package/dist/core/repository/types/schema.interface.d.ts +8 -2
- package/dist/core/repository/upcast.js +179 -0
- package/dist/core/schema/schema.d.ts +9 -0
- package/dist/core/schema/schema.js +542 -0
- package/dist/core/standard/standard-schema.js +1 -0
- package/dist/core/standard/standard.js +23 -0
- package/dist/core/symbols/demo.d.ts +4 -0
- package/dist/core/symbols/demo.js +1 -0
- package/dist/core/symbols/kind.js +1 -0
- package/dist/core/symbols/meta.js +1 -0
- package/dist/core/symbols/state.d.ts +5 -0
- package/dist/core/symbols/state.js +1 -0
- package/dist/core/tracing/exceptions.d.ts +4 -0
- package/dist/core/tracing/exceptions.js +7 -0
- package/dist/core/tracing/tracing.d.ts +72 -0
- package/dist/core/tracing/tracing.js +101 -0
- package/dist/core/use-case/access.js +56 -0
- package/dist/core/use-case/decorators.js +65 -0
- package/dist/core/use-case/exceptions.d.ts +5 -1
- package/dist/core/use-case/exceptions.js +48 -0
- package/dist/core/use-case/plugin-catalog.js +68 -0
- package/dist/core/use-case/plugin.js +133 -0
- package/dist/core/use-case/route.js +200 -0
- package/dist/core/use-case/scope.d.ts +5 -0
- package/dist/core/use-case/scope.js +4 -0
- package/dist/core/use-case/steps/describe.d.ts +5 -1
- package/dist/core/use-case/steps/describe.js +24 -0
- package/dist/core/use-case/steps/references.js +105 -0
- package/dist/core/use-case/steps/run.js +139 -0
- package/dist/core/use-case/steps/types/check.interface.d.ts +27 -8
- package/dist/core/use-case/steps/types/steps.interface.d.ts +8 -1
- package/dist/core/use-case/steps/validate.js +368 -0
- package/dist/core/use-case/target.js +100 -0
- package/dist/core/use-case/types/route.interface.d.ts +25 -5
- package/dist/core/use-case/use-case.d.ts +77 -2
- package/dist/core/use-case/use-case.js +493 -0
- package/dist/experimental.d.ts +5 -0
- package/dist/experimental.js +18 -161
- package/dist/index.d.ts +7 -5
- package/dist/index.js +213 -367
- package/dist/std/blueprint-plugin.js +42 -0
- package/dist/std/blueprint.d.ts +21 -0
- package/dist/std/blueprint.js +3 -0
- package/dist/std/command.js +3 -0
- package/dist/std/entity.d.ts +20 -1
- package/dist/std/entity.js +30 -0
- package/dist/std/event-policies.js +3 -0
- package/dist/std/properties/contact.d.ts +3 -0
- package/dist/std/properties/contact.js +29 -0
- package/dist/std/properties/enum.js +26 -0
- package/dist/std/properties/exceptions.js +14 -0
- package/dist/std/properties/headless.js +45 -0
- package/dist/std/properties/identity.js +28 -0
- package/dist/std/properties/json.js +161 -0
- package/dist/std/properties/password.js +79 -0
- package/dist/std/properties/primitives.d.ts +4 -0
- package/dist/std/properties/primitives.js +28 -0
- package/dist/std/properties/time.js +55 -0
- package/dist/std/record-plugins.js +73 -0
- package/dist/std/record.d.ts +20 -1
- package/dist/std/record.js +3 -0
- package/dist/std/resource.js +39 -0
- package/dist/std/runtime.js +193 -0
- package/dist/std/stages.js +4 -0
- package/dist/std/use-cases/auth.js +193 -0
- package/dist/std/use-cases/crud.js +261 -0
- package/dist/std/use-cases/index.js +12 -0
- package/dist/std/use-cases/lifecycle.js +97 -0
- package/dist/std/use-cases/shared.js +232 -0
- package/dist/std/use-cases/types.d.ts +5 -1
- package/dist/std/use-cases/types.js +0 -0
- package/dist/testing/http-provider-contract.js +344 -0
- package/dist/testing/index.js +3 -1435
- package/dist/testing/memory-publisher.js +12 -0
- package/dist/testing/provider-contract.d.ts +12 -3
- package/dist/testing/provider-contract.js +1071 -0
- package/package.json +17 -5
- package/dist/index-3aae5jfq.js +0 -2997
- package/dist/index-ny3as79r.js +0 -4567
- package/dist/index-xegv6a1p.js +0 -4291
|
@@ -0,0 +1,1071 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SCHEMA_VERSION_KEY
|
|
3
|
+
} from "../adapters.js";
|
|
4
|
+
import {
|
|
5
|
+
applicationOf,
|
|
6
|
+
blueprintOf,
|
|
7
|
+
entityOf,
|
|
8
|
+
RepositoryConcurrencyConflict,
|
|
9
|
+
RepositoryEntityNotFound,
|
|
10
|
+
RepositoryUniqueViolation,
|
|
11
|
+
repositoryOf
|
|
12
|
+
} from "../index.js";
|
|
13
|
+
function contextOf(name = "Accounts") {
|
|
14
|
+
|
|
15
|
+
class Account extends entityOf(blueprintOf((ctx) => ({ email: ctx.string, name: ctx.string }))) {
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
entity: Account,
|
|
19
|
+
name,
|
|
20
|
+
unique: ["id", "email"],
|
|
21
|
+
env: {}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
let sequence = 0;
|
|
25
|
+
function row(overrides = {}) {
|
|
26
|
+
sequence += 1;
|
|
27
|
+
const id = `id-${String(sequence).padStart(4, "0")}`;
|
|
28
|
+
return { id, email: `${id}@example.com`, name: id, ...overrides };
|
|
29
|
+
}
|
|
30
|
+
function evolvedRow(overrides = {}) {
|
|
31
|
+
sequence += 1;
|
|
32
|
+
const id = `0190a1b2-c3d4-7e5f-8a9b-${String(sequence).padStart(12, "0")}`;
|
|
33
|
+
return { ...row(), id, email: `${sequence}@example.com`, ...overrides };
|
|
34
|
+
}
|
|
35
|
+
async function failure(promise) {
|
|
36
|
+
try {
|
|
37
|
+
await promise;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
return error;
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const ids = (rows) => rows.map((r) => r.id);
|
|
44
|
+
export function providerContract(runner, makeProvider, options = {}) {
|
|
45
|
+
const { describe, it, expect } = runner;
|
|
46
|
+
async function fixture() {
|
|
47
|
+
const provider = await makeProvider();
|
|
48
|
+
const context = contextOf();
|
|
49
|
+
const seed = async (...rows) => {
|
|
50
|
+
for (const data of rows)
|
|
51
|
+
await provider.insert(context, data);
|
|
52
|
+
};
|
|
53
|
+
return { provider, context, seed };
|
|
54
|
+
}
|
|
55
|
+
const gated = (capability) => (name, body) => it(name, async () => {
|
|
56
|
+
const enabled = options[capability] ?? (await makeProvider()).capabilities?.[capability];
|
|
57
|
+
if (enabled === true)
|
|
58
|
+
await body();
|
|
59
|
+
});
|
|
60
|
+
describe("provider contract", () => {
|
|
61
|
+
describe("writes and reads", () => {
|
|
62
|
+
it("inserts and finds by id, empty provider counts zero", async () => {
|
|
63
|
+
const { provider, context } = await fixture();
|
|
64
|
+
expect(await provider.count(context)).toBe(0);
|
|
65
|
+
expect(await provider.findOne(context)).toBe(undefined);
|
|
66
|
+
expect(await provider.findMany(context)).toEqual([]);
|
|
67
|
+
const data = row({ age: 30 });
|
|
68
|
+
await provider.insert(context, data);
|
|
69
|
+
expect(await provider.count(context)).toBe(1);
|
|
70
|
+
expect(await provider.findOne(context, { where: { id: data.id } })).toEqual(data);
|
|
71
|
+
expect(await provider.findMany(context)).toEqual([data]);
|
|
72
|
+
});
|
|
73
|
+
it("findOne is undefined when nothing matches", async () => {
|
|
74
|
+
const { provider, context, seed } = await fixture();
|
|
75
|
+
await seed(row());
|
|
76
|
+
expect(await provider.findOne(context, { where: { id: "missing" } })).toBe(undefined);
|
|
77
|
+
expect(await provider.findMany(context, { where: { id: "missing" } })).toEqual([]);
|
|
78
|
+
});
|
|
79
|
+
it("update replaces the stored value", async () => {
|
|
80
|
+
const { provider, context, seed } = await fixture();
|
|
81
|
+
const data = row({ name: "Ana" });
|
|
82
|
+
await seed(data);
|
|
83
|
+
await provider.update(context, { ...data, name: "Bia" });
|
|
84
|
+
expect(await provider.findOne(context, { where: { id: data.id } })).toEqual({ ...data, name: "Bia" });
|
|
85
|
+
expect(await provider.count(context)).toBe(1);
|
|
86
|
+
});
|
|
87
|
+
it("remove deletes the row", async () => {
|
|
88
|
+
const { provider, context, seed } = await fixture();
|
|
89
|
+
const [a, b] = [row(), row()];
|
|
90
|
+
await seed(a, b);
|
|
91
|
+
await provider.remove(context, a.id);
|
|
92
|
+
expect(ids(await provider.findMany(context))).toEqual([b.id]);
|
|
93
|
+
expect(await provider.count(context)).toBe(1);
|
|
94
|
+
});
|
|
95
|
+
it("update and remove of an unknown id throw RepositoryEntityNotFound", async () => {
|
|
96
|
+
const { provider, context, seed } = await fixture();
|
|
97
|
+
await seed(row());
|
|
98
|
+
const updated = await failure(provider.update(context, row()));
|
|
99
|
+
expect(updated instanceof RepositoryEntityNotFound).toBe(true);
|
|
100
|
+
const removed = await failure(provider.remove(context, "missing"));
|
|
101
|
+
expect(removed instanceof RepositoryEntityNotFound).toBe(true);
|
|
102
|
+
expect(await provider.count(context)).toBe(1);
|
|
103
|
+
});
|
|
104
|
+
it("keeps tables of different contexts apart", async () => {
|
|
105
|
+
const provider = await makeProvider();
|
|
106
|
+
const [one, two] = [contextOf("One"), contextOf("Two")];
|
|
107
|
+
await provider.insert(one, row());
|
|
108
|
+
expect(await provider.count(one)).toBe(1);
|
|
109
|
+
expect(await provider.count(two)).toBe(0);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
describe("unique keys", () => {
|
|
113
|
+
it("insert throws RepositoryUniqueViolation for a repeated id", async () => {
|
|
114
|
+
const { provider, context, seed } = await fixture();
|
|
115
|
+
const data = row();
|
|
116
|
+
await seed(data);
|
|
117
|
+
const error = await failure(provider.insert(context, { ...row(), id: data.id }));
|
|
118
|
+
expect(error instanceof RepositoryUniqueViolation).toBe(true);
|
|
119
|
+
expect(error.details.key).toBe("id");
|
|
120
|
+
expect(await provider.count(context)).toBe(1);
|
|
121
|
+
});
|
|
122
|
+
it("insert throws for a repeated unique key", async () => {
|
|
123
|
+
const { provider, context, seed } = await fixture();
|
|
124
|
+
await seed(row({ email: "a@example.com" }));
|
|
125
|
+
const error = await failure(provider.insert(context, row({ email: "a@example.com" })));
|
|
126
|
+
expect(error instanceof RepositoryUniqueViolation).toBe(true);
|
|
127
|
+
expect(error.details.key).toBe("email");
|
|
128
|
+
expect(await provider.count(context)).toBe(1);
|
|
129
|
+
});
|
|
130
|
+
it("update throws when taking the unique value of another row", async () => {
|
|
131
|
+
const { provider, context, seed } = await fixture();
|
|
132
|
+
const [a, b] = [row({ email: "a@example.com" }), row()];
|
|
133
|
+
await seed(a, b);
|
|
134
|
+
const error = await failure(provider.update(context, { ...b, email: "a@example.com" }));
|
|
135
|
+
expect(error instanceof RepositoryUniqueViolation).toBe(true);
|
|
136
|
+
expect(error.details.key).toBe("email");
|
|
137
|
+
expect(await provider.findOne(context, { where: { id: b.id } })).toEqual(b);
|
|
138
|
+
});
|
|
139
|
+
it("update may keep the unique values of its own row", async () => {
|
|
140
|
+
const { provider, context, seed } = await fixture();
|
|
141
|
+
const data = row({ email: "a@example.com" });
|
|
142
|
+
await seed(data);
|
|
143
|
+
await provider.update(context, { ...data, name: "Other" });
|
|
144
|
+
expect(await provider.findOne(context, { where: { id: data.id } })).toEqual({ ...data, name: "Other" });
|
|
145
|
+
});
|
|
146
|
+
it("update clears a value that is left out", async () => {
|
|
147
|
+
const { provider, context, seed } = await fixture();
|
|
148
|
+
const data = row({ deletedAt: "2024-01-01T00:00:00.000Z" });
|
|
149
|
+
await seed(data);
|
|
150
|
+
const { deletedAt: _deletedAt, ...alive } = data;
|
|
151
|
+
await provider.update(context, alive);
|
|
152
|
+
expect(await provider.findOne(context, { where: { id: data.id } })).toEqual(alive);
|
|
153
|
+
});
|
|
154
|
+
it("soft-deleted rows keep occupying unique values", async () => {
|
|
155
|
+
const { provider, context, seed } = await fixture();
|
|
156
|
+
await seed(row({
|
|
157
|
+
email: "a@example.com",
|
|
158
|
+
deletedAt: "2024-01-01T00:00:00.000Z"
|
|
159
|
+
}));
|
|
160
|
+
const inserted = await failure(provider.insert(context, row({ email: "a@example.com" })));
|
|
161
|
+
expect(inserted instanceof RepositoryUniqueViolation).toBe(true);
|
|
162
|
+
const other = row();
|
|
163
|
+
await seed(other);
|
|
164
|
+
const updated = await failure(provider.update(context, { ...other, email: "a@example.com" }));
|
|
165
|
+
expect(updated instanceof RepositoryUniqueViolation).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
describe("where", () => {
|
|
169
|
+
it("matches scalars by equality", async () => {
|
|
170
|
+
const { provider, context, seed } = await fixture();
|
|
171
|
+
const [a, b, c] = [
|
|
172
|
+
row({ age: 20, name: "Ana", active: true }),
|
|
173
|
+
row({ age: 30, name: "Bia", active: false }),
|
|
174
|
+
row({ age: 30, name: "Cy", active: true })
|
|
175
|
+
];
|
|
176
|
+
await seed(a, b, c);
|
|
177
|
+
const find = (where) => provider.findMany(context, { where });
|
|
178
|
+
expect(ids(await find({ age: 30 }))).toEqual([b.id, c.id]);
|
|
179
|
+
expect(ids(await find({ name: "Ana" }))).toEqual([a.id]);
|
|
180
|
+
expect(ids(await find({ active: false }))).toEqual([b.id]);
|
|
181
|
+
expect(ids(await find({ age: 31 }))).toEqual([]);
|
|
182
|
+
expect(ids(await find({}))).toEqual([a.id, b.id, c.id]);
|
|
183
|
+
});
|
|
184
|
+
it("matches structured values by structure", async () => {
|
|
185
|
+
const { provider, context, seed } = await fixture();
|
|
186
|
+
const [a, b] = [
|
|
187
|
+
row({ address: { city: "Rio", zip: "1" }, tags: ["x", "y"] }),
|
|
188
|
+
row({ address: { city: "Rio", zip: "2" }, tags: ["y", "x"] })
|
|
189
|
+
];
|
|
190
|
+
await seed(a, b);
|
|
191
|
+
const find = (where) => provider.findMany(context, { where });
|
|
192
|
+
expect(ids(await find({ address: { city: "Rio", zip: "1" } }))).toEqual([a.id]);
|
|
193
|
+
expect(ids(await find({ address: { city: "Rio" } }))).toEqual([]);
|
|
194
|
+
expect(ids(await find({ tags: ["y", "x"] }))).toEqual([b.id]);
|
|
195
|
+
expect(ids(await find({ tags: ["x"] }))).toEqual([]);
|
|
196
|
+
});
|
|
197
|
+
it("matches membership with in", async () => {
|
|
198
|
+
const { provider, context, seed } = await fixture();
|
|
199
|
+
const [a, b, c] = [row({ age: 1 }), row({ age: 2 }), row({ age: 3 })];
|
|
200
|
+
await seed(a, b, c);
|
|
201
|
+
const find = (where) => provider.findMany(context, { where });
|
|
202
|
+
expect(ids(await find({ age: { in: [1, 3] } }))).toEqual([a.id, c.id]);
|
|
203
|
+
expect(ids(await find({ id: { in: [b.id, "missing"] } }))).toEqual([
|
|
204
|
+
b.id
|
|
205
|
+
]);
|
|
206
|
+
expect(ids(await find({ age: { in: [] } }))).toEqual([]);
|
|
207
|
+
expect(await provider.count(context, { where: { age: { in: [] } } })).toBe(0);
|
|
208
|
+
expect(await provider.findOne(context, { where: { age: { in: [] } } })).toBe(undefined);
|
|
209
|
+
});
|
|
210
|
+
it("ANDs conditions on several keys", async () => {
|
|
211
|
+
const { provider, context, seed } = await fixture();
|
|
212
|
+
const [a, b, c] = [
|
|
213
|
+
row({ age: 30, name: "Ana" }),
|
|
214
|
+
row({ age: 30, name: "Bia" }),
|
|
215
|
+
row({ age: 20, name: "Bia" })
|
|
216
|
+
];
|
|
217
|
+
await seed(a, b, c);
|
|
218
|
+
const find = (where) => provider.findMany(context, { where });
|
|
219
|
+
expect(ids(await find({ age: 30, name: "Bia" }))).toEqual([b.id]);
|
|
220
|
+
expect(ids(await find({ age: { in: [20, 30] }, name: { in: ["Bia"] } }))).toEqual([b.id, c.id]);
|
|
221
|
+
expect(ids(await find({ age: 20, name: "Ana" }))).toEqual([]);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
describe("orderBy", () => {
|
|
225
|
+
const order = async (rows, orderBy) => {
|
|
226
|
+
const { provider, context, seed } = await fixture();
|
|
227
|
+
await seed(...rows);
|
|
228
|
+
return ids(await provider.findMany(context, { orderBy }));
|
|
229
|
+
};
|
|
230
|
+
it("is ascending by default and descending on request", async () => {
|
|
231
|
+
const [a, b, c] = [row({ age: 2 }), row({ age: 3 }), row({ age: 1 })];
|
|
232
|
+
expect(await order([a, b, c], [{ key: "age" }])).toEqual([
|
|
233
|
+
c.id,
|
|
234
|
+
a.id,
|
|
235
|
+
b.id
|
|
236
|
+
]);
|
|
237
|
+
expect(await order([a, b, c], [{ key: "age", direction: "asc" }])).toEqual([c.id, a.id, b.id]);
|
|
238
|
+
expect(await order([a, b, c], [{ key: "age", direction: "desc" }])).toEqual([b.id, a.id, c.id]);
|
|
239
|
+
});
|
|
240
|
+
it("sorts numbers numerically", async () => {
|
|
241
|
+
const [a, b, c] = [
|
|
242
|
+
row({ age: 10 }),
|
|
243
|
+
row({ age: 9 }),
|
|
244
|
+
row({ age: 100 })
|
|
245
|
+
];
|
|
246
|
+
expect(await order([a, b, c], [{ key: "age" }])).toEqual([
|
|
247
|
+
b.id,
|
|
248
|
+
a.id,
|
|
249
|
+
c.id
|
|
250
|
+
]);
|
|
251
|
+
});
|
|
252
|
+
it("sorts strings by code point", async () => {
|
|
253
|
+
const [lower, upper, wide, emoji] = [
|
|
254
|
+
row({ name: "a" }),
|
|
255
|
+
row({ name: "Z" }),
|
|
256
|
+
row({ name: "z" }),
|
|
257
|
+
row({ name: "\uD83D\uDE00" })
|
|
258
|
+
];
|
|
259
|
+
expect(await order([emoji, lower, wide, upper], [{ key: "name" }])).toEqual([upper.id, lower.id, wide.id, emoji.id]);
|
|
260
|
+
});
|
|
261
|
+
it("puts missing values first when ascending and last when descending", async () => {
|
|
262
|
+
const [a, b, c] = [row({ age: 2 }), row(), row({ age: 1 })];
|
|
263
|
+
expect(await order([a, b, c], [{ key: "age" }])).toEqual([
|
|
264
|
+
b.id,
|
|
265
|
+
c.id,
|
|
266
|
+
a.id
|
|
267
|
+
]);
|
|
268
|
+
expect(await order([a, b, c], [{ key: "age", direction: "desc" }])).toEqual([a.id, c.id, b.id]);
|
|
269
|
+
});
|
|
270
|
+
it("keeps the insertion order of ties", async () => {
|
|
271
|
+
const [a, b, c, d] = [
|
|
272
|
+
row({ group: 1 }),
|
|
273
|
+
row({ group: 0 }),
|
|
274
|
+
row({ group: 1 }),
|
|
275
|
+
row({ group: 0 })
|
|
276
|
+
];
|
|
277
|
+
expect(await order([a, b, c, d], [{ key: "group" }])).toEqual([
|
|
278
|
+
b.id,
|
|
279
|
+
d.id,
|
|
280
|
+
a.id,
|
|
281
|
+
c.id
|
|
282
|
+
]);
|
|
283
|
+
expect(await order([a, b, c, d], [{ key: "group", direction: "desc" }])).toEqual([a.id, c.id, b.id, d.id]);
|
|
284
|
+
});
|
|
285
|
+
it("sorts by several keys in turn", async () => {
|
|
286
|
+
const [a, b, c, d] = [
|
|
287
|
+
row({ group: 1, name: "b" }),
|
|
288
|
+
row({ group: 0, name: "z" }),
|
|
289
|
+
row({ group: 1, name: "a" }),
|
|
290
|
+
row({ group: 0, name: "y" })
|
|
291
|
+
];
|
|
292
|
+
expect(await order([a, b, c, d], [{ key: "group" }, { key: "name", direction: "desc" }])).toEqual([b.id, d.id, a.id, c.id]);
|
|
293
|
+
expect(await order([a, b, c, d], [{ key: "group", direction: "desc" }, { key: "name" }])).toEqual([c.id, a.id, d.id, b.id]);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
describe("limit and offset", () => {
|
|
297
|
+
const seeded = async () => {
|
|
298
|
+
const f = await fixture();
|
|
299
|
+
const rows = [1, 2, 3, 4, 5].map((n) => row({ n }));
|
|
300
|
+
await f.seed(...rows);
|
|
301
|
+
return { ...f, rows };
|
|
302
|
+
};
|
|
303
|
+
const byN = [{ key: "n" }];
|
|
304
|
+
it("limit takes the first rows, offset skips rows before it", async () => {
|
|
305
|
+
const { provider, context, rows } = await seeded();
|
|
306
|
+
const find = (limit, offset) => provider.findMany(context, { orderBy: byN, limit, offset }).then(ids);
|
|
307
|
+
expect(await find(2)).toEqual([rows[0]?.id, rows[1]?.id]);
|
|
308
|
+
expect(await find(undefined, 3)).toEqual([rows[3]?.id, rows[4]?.id]);
|
|
309
|
+
expect(await find(2, 1)).toEqual([rows[1]?.id, rows[2]?.id]);
|
|
310
|
+
expect(await find(10, 4)).toEqual([rows[4]?.id]);
|
|
311
|
+
expect(await find(2, 5)).toEqual([]);
|
|
312
|
+
});
|
|
313
|
+
it("limit 0 returns nothing", async () => {
|
|
314
|
+
const { provider, context } = await seeded();
|
|
315
|
+
expect(await provider.findMany(context, { limit: 0 })).toEqual([]);
|
|
316
|
+
expect(await provider.findOne(context, { limit: 0 })).toBe(undefined);
|
|
317
|
+
});
|
|
318
|
+
it("findOne returns the first row after the offset", async () => {
|
|
319
|
+
const { provider, context, rows } = await seeded();
|
|
320
|
+
expect((await provider.findOne(context, { orderBy: byN, offset: 2 }))?.id).toBe(rows[2]?.id);
|
|
321
|
+
expect(await provider.findOne(context, { orderBy: byN, offset: 5 })).toBe(undefined);
|
|
322
|
+
});
|
|
323
|
+
it("count ignores order, limit and offset", async () => {
|
|
324
|
+
const { provider, context } = await seeded();
|
|
325
|
+
expect(await provider.count(context, {
|
|
326
|
+
orderBy: [{ key: "n", direction: "desc" }],
|
|
327
|
+
limit: 1,
|
|
328
|
+
offset: 3
|
|
329
|
+
})).toBe(5);
|
|
330
|
+
expect(await provider.count(context, {
|
|
331
|
+
where: { n: { in: [1, 2] } },
|
|
332
|
+
limit: 0,
|
|
333
|
+
offset: 9
|
|
334
|
+
})).toBe(2);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
describe("withDeleted", () => {
|
|
338
|
+
const deletedAt = "2024-01-01T00:00:00.000Z";
|
|
339
|
+
const seeded = async () => {
|
|
340
|
+
const f = await fixture();
|
|
341
|
+
const [live, gone] = [row({ n: 1 }), row({ n: 2, deletedAt })];
|
|
342
|
+
await f.seed(live, gone);
|
|
343
|
+
return { ...f, live, gone };
|
|
344
|
+
};
|
|
345
|
+
it("excludes deleted rows by default", async () => {
|
|
346
|
+
const { provider, context, live, gone } = await seeded();
|
|
347
|
+
expect(ids(await provider.findMany(context))).toEqual([live.id]);
|
|
348
|
+
expect(ids(await provider.findMany(context, { withDeleted: false }))).toEqual([live.id]);
|
|
349
|
+
expect(await provider.count(context)).toBe(1);
|
|
350
|
+
expect(await provider.findOne(context, { where: { id: gone.id } })).toBe(undefined);
|
|
351
|
+
});
|
|
352
|
+
it("includes them with withDeleted", async () => {
|
|
353
|
+
const { provider, context, live, gone } = await seeded();
|
|
354
|
+
const query = { withDeleted: true };
|
|
355
|
+
expect(ids(await provider.findMany(context, query))).toEqual([
|
|
356
|
+
live.id,
|
|
357
|
+
gone.id
|
|
358
|
+
]);
|
|
359
|
+
expect(await provider.count(context, query)).toBe(2);
|
|
360
|
+
expect(await provider.findOne(context, {
|
|
361
|
+
where: { id: gone.id },
|
|
362
|
+
withDeleted: true
|
|
363
|
+
})).toEqual(gone);
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
describe("copies", () => {
|
|
367
|
+
it("does not share data with the caller on write", async () => {
|
|
368
|
+
const { provider, context } = await fixture();
|
|
369
|
+
const data = row({ address: { city: "Rio" }, tags: ["x"] });
|
|
370
|
+
await provider.insert(context, data);
|
|
371
|
+
data.address.city = "SP";
|
|
372
|
+
data.tags.push("y");
|
|
373
|
+
const stored = await provider.findOne(context, {
|
|
374
|
+
where: { id: data.id }
|
|
375
|
+
});
|
|
376
|
+
expect(stored?.address).toEqual({ city: "Rio" });
|
|
377
|
+
expect(stored?.tags).toEqual(["x"]);
|
|
378
|
+
});
|
|
379
|
+
it("does not share data with the caller on read", async () => {
|
|
380
|
+
const { provider, context, seed } = await fixture();
|
|
381
|
+
const data = row({ address: { city: "Rio" }, tags: ["x"] });
|
|
382
|
+
await seed(data);
|
|
383
|
+
const one = await provider.findOne(context, {
|
|
384
|
+
where: { id: data.id }
|
|
385
|
+
});
|
|
386
|
+
one.address.city = "SP";
|
|
387
|
+
one.name = "changed";
|
|
388
|
+
const many = await provider.findMany(context);
|
|
389
|
+
many[0].tags.push("y");
|
|
390
|
+
const stored = await provider.findOne(context, {
|
|
391
|
+
where: { id: data.id }
|
|
392
|
+
});
|
|
393
|
+
expect(stored).toEqual(data);
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
describe("keyset", () => {
|
|
397
|
+
const keyset = (name, body) => it(name, async () => {
|
|
398
|
+
const enabled = options.keyset ?? (await makeProvider()).capabilities?.keyset;
|
|
399
|
+
if (enabled === true)
|
|
400
|
+
await body();
|
|
401
|
+
});
|
|
402
|
+
const seeded = async () => {
|
|
403
|
+
const f = await fixture();
|
|
404
|
+
const [a, b, c, d, e, g] = [
|
|
405
|
+
row({ n: 1, group: 1, name: "Gil" }),
|
|
406
|
+
row({ group: 1, name: "Érico" }),
|
|
407
|
+
row({ n: 2, group: 2, name: "Ana" }),
|
|
408
|
+
row({ n: 2, group: 1, name: "ana" }),
|
|
409
|
+
row({ group: 2, name: "Bia" }),
|
|
410
|
+
row({ n: 3, group: 2, name: "Zoe" })
|
|
411
|
+
];
|
|
412
|
+
await f.seed(a, b, c, d, e, g);
|
|
413
|
+
const find = (query) => f.provider.findMany(f.context, query).then(ids);
|
|
414
|
+
return { ...f, a, b, c, d, e, g, find };
|
|
415
|
+
};
|
|
416
|
+
const by = (key, direction = "asc") => [{ key, direction }, { key: "id" }];
|
|
417
|
+
keyset("gives the rows strictly after the position, ascending", async () => {
|
|
418
|
+
const { find, a, c, d, g } = await seeded();
|
|
419
|
+
const orderBy = by("n");
|
|
420
|
+
expect(await find({ orderBy, after: [1, a.id] })).toEqual([
|
|
421
|
+
c.id,
|
|
422
|
+
d.id,
|
|
423
|
+
g.id
|
|
424
|
+
]);
|
|
425
|
+
expect(await find({ orderBy, after: [2, c.id] })).toEqual([
|
|
426
|
+
d.id,
|
|
427
|
+
g.id
|
|
428
|
+
]);
|
|
429
|
+
expect(await find({ orderBy, after: [3, g.id] })).toEqual([]);
|
|
430
|
+
});
|
|
431
|
+
keyset("follows a descending order", async () => {
|
|
432
|
+
const { find, a, c, d, g } = await seeded();
|
|
433
|
+
const orderBy = by("n", "desc");
|
|
434
|
+
expect(await find({
|
|
435
|
+
orderBy,
|
|
436
|
+
after: [3, g.id],
|
|
437
|
+
where: { n: { in: [1, 2] } }
|
|
438
|
+
})).toEqual([c.id, d.id, a.id]);
|
|
439
|
+
expect(await find({
|
|
440
|
+
orderBy,
|
|
441
|
+
after: [2, d.id],
|
|
442
|
+
where: { n: { in: [1, 2] } }
|
|
443
|
+
})).toEqual([a.id]);
|
|
444
|
+
});
|
|
445
|
+
keyset("missing values are first ascending and last descending", async () => {
|
|
446
|
+
const { find, a, b, c, d, e, g } = await seeded();
|
|
447
|
+
expect(await find({ orderBy: by("n"), after: [undefined, b.id] })).toEqual([e.id, a.id, c.id, d.id, g.id]);
|
|
448
|
+
expect(await find({ orderBy: by("n"), after: [undefined, e.id] })).toEqual([a.id, c.id, d.id, g.id]);
|
|
449
|
+
expect(await find({ orderBy: by("n", "desc"), after: [1, a.id] })).toEqual([b.id, e.id]);
|
|
450
|
+
expect(await find({ orderBy: by("n", "desc"), after: [undefined, b.id] })).toEqual([e.id]);
|
|
451
|
+
expect(await find({ orderBy: by("n", "desc"), after: [undefined, e.id] })).toEqual([]);
|
|
452
|
+
});
|
|
453
|
+
keyset("compares strings by code point", async () => {
|
|
454
|
+
const { find, a, b, c, d, e, g } = await seeded();
|
|
455
|
+
expect(await find({ orderBy: by("name"), after: ["Bia", e.id] })).toEqual([a.id, g.id, d.id, b.id]);
|
|
456
|
+
expect(await find({ orderBy: by("name", "desc"), after: ["ana", d.id] })).toEqual([g.id, a.id, e.id, c.id]);
|
|
457
|
+
});
|
|
458
|
+
keyset("combines with where, withDeleted and limit; count ignores it", async () => {
|
|
459
|
+
const { provider, context, find, a, b, d } = await seeded();
|
|
460
|
+
const orderBy = [{ key: "id" }];
|
|
461
|
+
expect(await find({
|
|
462
|
+
orderBy,
|
|
463
|
+
after: [a.id],
|
|
464
|
+
where: { group: 1 },
|
|
465
|
+
limit: 1
|
|
466
|
+
})).toEqual([b.id]);
|
|
467
|
+
expect(await find({ orderBy, after: [b.id], where: { group: 1 } })).toEqual([d.id]);
|
|
468
|
+
await provider.update(context, {
|
|
469
|
+
...d,
|
|
470
|
+
deletedAt: "2024-01-01T00:00:00.000Z"
|
|
471
|
+
});
|
|
472
|
+
expect(await find({ orderBy, after: [b.id], where: { group: 1 } })).toEqual([]);
|
|
473
|
+
expect(await find({
|
|
474
|
+
orderBy,
|
|
475
|
+
after: [b.id],
|
|
476
|
+
where: { group: 1 },
|
|
477
|
+
withDeleted: true
|
|
478
|
+
})).toEqual([d.id]);
|
|
479
|
+
expect(await provider.count(context, {
|
|
480
|
+
orderBy,
|
|
481
|
+
after: [b.id]
|
|
482
|
+
})).toBe(5);
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
describe("indexes", () => {
|
|
486
|
+
const indexed = (name, body) => it(name, async () => {
|
|
487
|
+
const enabled = options.indexes ?? (await makeProvider()).capabilities?.indexes;
|
|
488
|
+
if (enabled === true)
|
|
489
|
+
await body();
|
|
490
|
+
});
|
|
491
|
+
const seeded = async () => {
|
|
492
|
+
const f = await fixture();
|
|
493
|
+
const context = {
|
|
494
|
+
...f.context,
|
|
495
|
+
indexes: {
|
|
496
|
+
byGroup: { where: ["group"], orderBy: "n" },
|
|
497
|
+
byName: { where: ["name"] }
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
const rows = [
|
|
501
|
+
row({ n: 3, group: 1, name: "Gil" }),
|
|
502
|
+
row({ group: 1, name: "Érico" }),
|
|
503
|
+
row({ n: 2, group: 2, name: "Ana" }),
|
|
504
|
+
row({ n: 2, group: 1, name: "Ana" }),
|
|
505
|
+
row({ n: 1, group: 1, name: "Bia" }),
|
|
506
|
+
row({ n: 4, group: 2, name: "Zoe" })
|
|
507
|
+
];
|
|
508
|
+
for (const data of rows)
|
|
509
|
+
await f.provider.insert(context, data);
|
|
510
|
+
await f.provider.update(context, {
|
|
511
|
+
...rows[4],
|
|
512
|
+
deletedAt: "2024-01-01T00:00:00.000Z"
|
|
513
|
+
});
|
|
514
|
+
return { ...f, context };
|
|
515
|
+
};
|
|
516
|
+
indexed("an indexed query gives what the scan gives", async () => {
|
|
517
|
+
const { provider, context } = await seeded();
|
|
518
|
+
const queries = [
|
|
519
|
+
["byGroup", { where: { group: 1 } }],
|
|
520
|
+
[
|
|
521
|
+
"byGroup",
|
|
522
|
+
{
|
|
523
|
+
where: { group: 1 },
|
|
524
|
+
orderBy: [{ key: "n" }, { key: "id" }]
|
|
525
|
+
}
|
|
526
|
+
],
|
|
527
|
+
[
|
|
528
|
+
"byGroup",
|
|
529
|
+
{
|
|
530
|
+
where: { group: 1, name: "Ana" },
|
|
531
|
+
orderBy: [{ key: "n", direction: "desc" }, { key: "id" }],
|
|
532
|
+
limit: 2,
|
|
533
|
+
offset: 1
|
|
534
|
+
}
|
|
535
|
+
],
|
|
536
|
+
[
|
|
537
|
+
"byGroup",
|
|
538
|
+
{
|
|
539
|
+
where: { group: 1 },
|
|
540
|
+
orderBy: [{ key: "n" }, { key: "id" }],
|
|
541
|
+
withDeleted: true
|
|
542
|
+
}
|
|
543
|
+
],
|
|
544
|
+
["byName", { where: { name: "Ana" }, orderBy: [{ key: "id" }] }]
|
|
545
|
+
];
|
|
546
|
+
for (const [index, query] of queries) {
|
|
547
|
+
expect(ids(await provider.findMany(context, { ...query, index }))).toEqual(ids(await provider.findMany(context, query)));
|
|
548
|
+
expect(await provider.count(context, {
|
|
549
|
+
...query,
|
|
550
|
+
index
|
|
551
|
+
})).toBe(await provider.count(context, query));
|
|
552
|
+
expect((await provider.findOne(context, {
|
|
553
|
+
...query,
|
|
554
|
+
index
|
|
555
|
+
}))?.id).toBe((await provider.findOne(context, query))?.id);
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
const withTransactions = async () => {
|
|
560
|
+
const enabled = options.transactions ?? (await makeProvider()).capabilities?.transactions;
|
|
561
|
+
return enabled === true;
|
|
562
|
+
};
|
|
563
|
+
describe("transactions", () => {
|
|
564
|
+
const transactional = (name, body) => it(name, async () => {
|
|
565
|
+
if (!await withTransactions())
|
|
566
|
+
return;
|
|
567
|
+
await body();
|
|
568
|
+
});
|
|
569
|
+
const open = async () => {
|
|
570
|
+
const f = await fixture();
|
|
571
|
+
const transaction = f.provider.transaction?.bind(f.provider);
|
|
572
|
+
if (!transaction) {
|
|
573
|
+
throw new Error("the provider advertises transactions but has none");
|
|
574
|
+
}
|
|
575
|
+
return { ...f, transaction };
|
|
576
|
+
};
|
|
577
|
+
transactional("commits the writes when the callback resolves", async () => {
|
|
578
|
+
const { provider, context, seed, transaction } = await open();
|
|
579
|
+
const [a, b] = [row({ name: "a" }), row()];
|
|
580
|
+
await seed(a);
|
|
581
|
+
const result = await transaction(async (tx) => {
|
|
582
|
+
await tx.insert(context, b);
|
|
583
|
+
await tx.update(context, { ...a, name: "changed" });
|
|
584
|
+
expect(await tx.count(context)).toBe(2);
|
|
585
|
+
return "done";
|
|
586
|
+
});
|
|
587
|
+
expect(result).toBe("done");
|
|
588
|
+
expect(ids(await provider.findMany(context))).toEqual([a.id, b.id]);
|
|
589
|
+
expect((await provider.findOne(context, { where: { id: a.id } }))?.name).toBe("changed");
|
|
590
|
+
});
|
|
591
|
+
transactional("rolls back every write when the callback rejects", async () => {
|
|
592
|
+
const { provider, context, seed, transaction } = await open();
|
|
593
|
+
const [a, b] = [row({ name: "a" }), row()];
|
|
594
|
+
await seed(a, b);
|
|
595
|
+
const boom = new Error("boom");
|
|
596
|
+
const error = await failure(transaction(async (tx) => {
|
|
597
|
+
await tx.insert(context, row());
|
|
598
|
+
await tx.update(context, { ...a, name: "changed" });
|
|
599
|
+
await tx.remove(context, b.id);
|
|
600
|
+
throw boom;
|
|
601
|
+
}));
|
|
602
|
+
expect(error).toBe(boom);
|
|
603
|
+
expect(await provider.findMany(context)).toEqual([a, b]);
|
|
604
|
+
});
|
|
605
|
+
transactional("rolls back after a failed write inside", async () => {
|
|
606
|
+
const { provider, context, seed, transaction } = await open();
|
|
607
|
+
const a = row({ email: "a@example.com" });
|
|
608
|
+
await seed(a);
|
|
609
|
+
const error = await failure(transaction(async (tx) => {
|
|
610
|
+
await tx.insert(context, row());
|
|
611
|
+
await tx.insert(context, row({ email: "a@example.com" }));
|
|
612
|
+
}));
|
|
613
|
+
expect(error instanceof RepositoryUniqueViolation).toBe(true);
|
|
614
|
+
expect(await provider.findMany(context)).toEqual([a]);
|
|
615
|
+
});
|
|
616
|
+
transactional("does not lose writes made outside meanwhile", async () => {
|
|
617
|
+
const { provider, context, transaction } = await open();
|
|
618
|
+
const [inside, outside] = [row(), row()];
|
|
619
|
+
let release = () => {};
|
|
620
|
+
const gate = new Promise((resolve) => {
|
|
621
|
+
release = resolve;
|
|
622
|
+
});
|
|
623
|
+
let started = () => {};
|
|
624
|
+
const entered = new Promise((resolve) => {
|
|
625
|
+
started = resolve;
|
|
626
|
+
});
|
|
627
|
+
const running = failure(transaction(async (tx) => {
|
|
628
|
+
await tx.insert(context, inside);
|
|
629
|
+
started();
|
|
630
|
+
await gate;
|
|
631
|
+
throw new Error("rollback");
|
|
632
|
+
}));
|
|
633
|
+
await entered;
|
|
634
|
+
const writing = provider.insert(context, outside);
|
|
635
|
+
release();
|
|
636
|
+
await running;
|
|
637
|
+
await writing;
|
|
638
|
+
expect(ids(await provider.findMany(context))).toEqual([outside.id]);
|
|
639
|
+
});
|
|
640
|
+
transactional("rejects the scoped provider after the callback", async () => {
|
|
641
|
+
const { context, transaction } = await open();
|
|
642
|
+
let scoped;
|
|
643
|
+
await transaction(async (tx) => {
|
|
644
|
+
scoped = tx;
|
|
645
|
+
});
|
|
646
|
+
const error = await failure(scoped.insert(context, row()));
|
|
647
|
+
expect(error instanceof Error).toBe(true);
|
|
648
|
+
const read = await failure(scoped.count(context));
|
|
649
|
+
expect(read instanceof Error).toBe(true);
|
|
650
|
+
});
|
|
651
|
+
transactional("treats a nested transaction as a savepoint", async () => {
|
|
652
|
+
const { provider, context, transaction } = await open();
|
|
653
|
+
const [outer, kept, dropped] = [row(), row(), row()];
|
|
654
|
+
await transaction(async (tx) => {
|
|
655
|
+
expect(typeof tx.transaction).toBe("function");
|
|
656
|
+
await tx.insert(context, outer);
|
|
657
|
+
await tx.transaction?.(async (inner) => {
|
|
658
|
+
await inner.insert(context, kept);
|
|
659
|
+
});
|
|
660
|
+
const error = await failure(tx.transaction?.(async (inner) => {
|
|
661
|
+
await inner.insert(context, dropped);
|
|
662
|
+
throw new Error("inner");
|
|
663
|
+
}) ?? Promise.resolve());
|
|
664
|
+
expect(error.message).toBe("inner");
|
|
665
|
+
expect(ids(await tx.findMany(context))).toEqual([outer.id, kept.id]);
|
|
666
|
+
});
|
|
667
|
+
expect(ids(await provider.findMany(context))).toEqual([
|
|
668
|
+
outer.id,
|
|
669
|
+
kept.id
|
|
670
|
+
]);
|
|
671
|
+
});
|
|
672
|
+
transactional("rolls the outer back even if an inner one committed", async () => {
|
|
673
|
+
const { provider, context, transaction } = await open();
|
|
674
|
+
await failure(transaction(async (tx) => {
|
|
675
|
+
await tx.transaction?.(async (inner) => {
|
|
676
|
+
await inner.insert(context, row());
|
|
677
|
+
});
|
|
678
|
+
throw new Error("outer");
|
|
679
|
+
}));
|
|
680
|
+
expect(await provider.count(context)).toBe(0);
|
|
681
|
+
});
|
|
682
|
+
});
|
|
683
|
+
describe("versioning", () => {
|
|
684
|
+
const versioned = gated("versioning");
|
|
685
|
+
versioned("update writes with the expected version, and throws RepositoryConcurrencyConflict with a stale one", async () => {
|
|
686
|
+
const { provider, context, seed } = await fixture();
|
|
687
|
+
const data = row({ version: 1 });
|
|
688
|
+
await seed(data);
|
|
689
|
+
await provider.update(context, { ...data, name: "Bia", version: 2 }, { expectedVersion: 1 });
|
|
690
|
+
const stale = await failure(provider.update(context, { ...data, name: "Caio", version: 2 }, { expectedVersion: 1 }));
|
|
691
|
+
expect(stale instanceof RepositoryConcurrencyConflict).toBe(true);
|
|
692
|
+
const { details } = stale;
|
|
693
|
+
expect([details.id, details.expected]).toEqual([data.id, 1]);
|
|
694
|
+
expect(await provider.findOne(context, { where: { id: data.id } })).toEqual({ ...data, name: "Bia", version: 2 });
|
|
695
|
+
});
|
|
696
|
+
versioned("two updates with the same expected version: exactly one succeeds", async () => {
|
|
697
|
+
const { provider, context, seed } = await fixture();
|
|
698
|
+
const data = row({ version: 1 });
|
|
699
|
+
await seed(data);
|
|
700
|
+
const results = await Promise.allSettled(["Bia", "Caio"].map((name) => provider.update(context, { ...data, name, version: 2 }, { expectedVersion: 1 })));
|
|
701
|
+
const rejected = results.filter((result) => result.status === "rejected");
|
|
702
|
+
expect(rejected.length).toBe(1);
|
|
703
|
+
expect(rejected[0]?.reason instanceof RepositoryConcurrencyConflict).toBe(true);
|
|
704
|
+
const stored = await provider.findOne(context, {
|
|
705
|
+
where: { id: data.id }
|
|
706
|
+
});
|
|
707
|
+
expect(stored?.version).toBe(2);
|
|
708
|
+
});
|
|
709
|
+
versioned("update without an expected version ignores the stored one", async () => {
|
|
710
|
+
const { provider, context, seed } = await fixture();
|
|
711
|
+
const data = row({ version: 3 });
|
|
712
|
+
await seed(data);
|
|
713
|
+
await provider.update(context, { ...data, name: "Bia", version: 1 });
|
|
714
|
+
expect(await provider.findOne(context, { where: { id: data.id } })).toEqual({ ...data, name: "Bia", version: 1 });
|
|
715
|
+
});
|
|
716
|
+
versioned("an unknown id is still RepositoryEntityNotFound", async () => {
|
|
717
|
+
const { provider, context } = await fixture();
|
|
718
|
+
const error = await failure(provider.update(context, row({ version: 2 }), {
|
|
719
|
+
expectedVersion: 1
|
|
720
|
+
}));
|
|
721
|
+
expect(error instanceof RepositoryEntityNotFound).toBe(true);
|
|
722
|
+
});
|
|
723
|
+
});
|
|
724
|
+
describe("atomic", () => {
|
|
725
|
+
const atomic = gated("atomic");
|
|
726
|
+
const increment = (provider, ...args) => provider.increment(...args);
|
|
727
|
+
atomic("increment adds the deltas, stores set and gives the row", async () => {
|
|
728
|
+
const { provider, context, seed } = await fixture();
|
|
729
|
+
const data = row({ visits: 2, age: 30 });
|
|
730
|
+
await seed(data);
|
|
731
|
+
const stored = await increment(provider, context, data.id, { visits: 3, age: -1, n: 5 }, { set: { name: "Bia" } });
|
|
732
|
+
const expected = { ...data, visits: 5, age: 29, n: 5, name: "Bia" };
|
|
733
|
+
expect(stored).toEqual(expected);
|
|
734
|
+
expect(await provider.findOne(context, { where: { id: data.id } })).toEqual(expected);
|
|
735
|
+
});
|
|
736
|
+
atomic("N concurrent increments sum to N", async () => {
|
|
737
|
+
const { provider, context, seed } = await fixture();
|
|
738
|
+
const data = row({ visits: 0 });
|
|
739
|
+
await seed(data);
|
|
740
|
+
await Promise.all(Array.from({ length: 25 }, () => increment(provider, context, data.id, { visits: 1 })));
|
|
741
|
+
const stored = await provider.findOne(context, {
|
|
742
|
+
where: { id: data.id }
|
|
743
|
+
});
|
|
744
|
+
expect(stored?.visits).toBe(25);
|
|
745
|
+
});
|
|
746
|
+
atomic("a missing id is RepositoryEntityNotFound without create, and inserted with it", async () => {
|
|
747
|
+
const { provider, context } = await fixture();
|
|
748
|
+
const data = row({ visits: 1 });
|
|
749
|
+
const missing = await failure(increment(provider, context, data.id, { visits: 1 }));
|
|
750
|
+
expect(missing instanceof RepositoryEntityNotFound).toBe(true);
|
|
751
|
+
expect(await provider.count(context)).toBe(0);
|
|
752
|
+
const created = await increment(provider, context, data.id, { visits: 1 }, { create: data, set: { name: "Bia" } });
|
|
753
|
+
expect(created).toEqual(data);
|
|
754
|
+
const again = await increment(provider, context, data.id, { visits: 1 }, { create: data });
|
|
755
|
+
expect(again.visits).toBe(2);
|
|
756
|
+
expect(await provider.count(context)).toBe(1);
|
|
757
|
+
});
|
|
758
|
+
atomic("concurrent increments with create lose nothing", async () => {
|
|
759
|
+
const { provider, context } = await fixture();
|
|
760
|
+
const data = row({ visits: 1 });
|
|
761
|
+
await Promise.all(Array.from({ length: 10 }, () => increment(provider, context, data.id, { visits: 1 }, { create: data })));
|
|
762
|
+
const stored = await provider.findOne(context, {
|
|
763
|
+
where: { id: data.id }
|
|
764
|
+
});
|
|
765
|
+
expect(stored?.visits).toBe(10);
|
|
766
|
+
});
|
|
767
|
+
atomic("soft-deleted rows are not incremented", async () => {
|
|
768
|
+
const { provider, context, seed } = await fixture();
|
|
769
|
+
const data = row({ visits: 1, deletedAt: "2024-01-01T00:00:00.000Z" });
|
|
770
|
+
await seed(data);
|
|
771
|
+
const error = await failure(increment(provider, context, data.id, { visits: 1 }));
|
|
772
|
+
expect(error instanceof RepositoryEntityNotFound).toBe(true);
|
|
773
|
+
const stored = await provider.findOne(context, {
|
|
774
|
+
where: { id: data.id },
|
|
775
|
+
withDeleted: true
|
|
776
|
+
});
|
|
777
|
+
expect(stored?.visits).toBe(1);
|
|
778
|
+
});
|
|
779
|
+
atomic("an increment of version makes an update with the old one conflict", async () => {
|
|
780
|
+
const { provider, context, seed } = await fixture();
|
|
781
|
+
if ((options.versioning ?? provider.capabilities?.versioning) !== true)
|
|
782
|
+
return;
|
|
783
|
+
const data = row({ visits: 0, version: 1 });
|
|
784
|
+
await seed(data);
|
|
785
|
+
const stored = await increment(provider, context, data.id, {
|
|
786
|
+
visits: 1,
|
|
787
|
+
version: 1
|
|
788
|
+
});
|
|
789
|
+
expect(stored.version).toBe(2);
|
|
790
|
+
const error = await failure(provider.update(context, { ...data, name: "Bia", version: 2 }, { expectedVersion: 1 }));
|
|
791
|
+
expect(error instanceof RepositoryConcurrencyConflict).toBe(true);
|
|
792
|
+
expect((await provider.findOne(context, { where: { id: data.id } }))?.visits).toBe(1);
|
|
793
|
+
});
|
|
794
|
+
atomic("an expired row counts as absent: not found, or replaced by create", async () => {
|
|
795
|
+
const provider = await makeProvider();
|
|
796
|
+
if ((options.expiry ?? provider.capabilities?.expiry) !== true)
|
|
797
|
+
return;
|
|
798
|
+
const now = "2026-06-01T12:00:00.000Z";
|
|
799
|
+
const context = {
|
|
800
|
+
...contextOf(),
|
|
801
|
+
expiry: { key: "expiresAt" }
|
|
802
|
+
};
|
|
803
|
+
const window = row({
|
|
804
|
+
visits: 7,
|
|
805
|
+
expiresAt: "2026-06-01T11:00:00.000Z"
|
|
806
|
+
});
|
|
807
|
+
const live = row({
|
|
808
|
+
visits: 1,
|
|
809
|
+
expiresAt: "2026-06-01T13:00:00.000Z"
|
|
810
|
+
});
|
|
811
|
+
await provider.insert(context, window);
|
|
812
|
+
await provider.insert(context, live);
|
|
813
|
+
const missing = await failure(increment(provider, context, window.id, { visits: 1 }, {
|
|
814
|
+
now
|
|
815
|
+
}));
|
|
816
|
+
expect(missing instanceof RepositoryEntityNotFound).toBe(true);
|
|
817
|
+
const fresh = {
|
|
818
|
+
id: window.id,
|
|
819
|
+
email: window.email,
|
|
820
|
+
name: "fresh",
|
|
821
|
+
visits: 1,
|
|
822
|
+
expiresAt: "2026-06-01T13:00:00.000Z"
|
|
823
|
+
};
|
|
824
|
+
const created = await increment(provider, context, window.id, { visits: 1 }, { create: fresh, set: { name: "incremented" }, now });
|
|
825
|
+
expect(created).toEqual(fresh);
|
|
826
|
+
const again = await increment(provider, context, window.id, { visits: 1 }, { create: fresh, set: { name: "incremented" }, now });
|
|
827
|
+
expect([again.visits, again.name]).toEqual([2, "incremented"]);
|
|
828
|
+
const counted = await increment(provider, context, live.id, { visits: 1 }, { create: { ...live, visits: 100 }, now });
|
|
829
|
+
expect(counted.visits).toBe(2);
|
|
830
|
+
});
|
|
831
|
+
});
|
|
832
|
+
describe("expiry", () => {
|
|
833
|
+
const expiring = gated("expiry");
|
|
834
|
+
const NOW = "2026-06-01T12:00:00.000Z";
|
|
835
|
+
const seeded = async () => {
|
|
836
|
+
const provider = await makeProvider();
|
|
837
|
+
const context = {
|
|
838
|
+
...contextOf(),
|
|
839
|
+
expiry: { key: "expiresAt" }
|
|
840
|
+
};
|
|
841
|
+
const [past, edge, never, future] = [
|
|
842
|
+
row({ n: 1, expiresAt: "2026-05-01T00:00:00.000Z" }),
|
|
843
|
+
row({ n: 2, expiresAt: NOW }),
|
|
844
|
+
row({ n: 3 }),
|
|
845
|
+
row({ n: 4, expiresAt: "2026-07-01T00:00:00.000Z" })
|
|
846
|
+
];
|
|
847
|
+
for (const data of [past, edge, never, future])
|
|
848
|
+
await provider.insert(context, data);
|
|
849
|
+
return { provider, context, past, edge, never, future };
|
|
850
|
+
};
|
|
851
|
+
expiring("reads leave out rows whose date is at or before now", async () => {
|
|
852
|
+
const { provider, context, past, edge, never, future } = await seeded();
|
|
853
|
+
const query = { now: NOW };
|
|
854
|
+
expect(ids(await provider.findMany(context, query))).toEqual([
|
|
855
|
+
never.id,
|
|
856
|
+
future.id
|
|
857
|
+
]);
|
|
858
|
+
expect(await provider.count(context, query)).toBe(2);
|
|
859
|
+
expect(await provider.findOne(context, {
|
|
860
|
+
...query,
|
|
861
|
+
where: { id: past.id }
|
|
862
|
+
})).toBe(undefined);
|
|
863
|
+
expect(await provider.findOne(context, {
|
|
864
|
+
...query,
|
|
865
|
+
where: { id: edge.id }
|
|
866
|
+
})).toBe(undefined);
|
|
867
|
+
expect((await provider.findOne(context, {
|
|
868
|
+
...query,
|
|
869
|
+
where: { id: future.id }
|
|
870
|
+
}))?.id).toBe(future.id);
|
|
871
|
+
expect(ids(await provider.findMany(context, {
|
|
872
|
+
...query,
|
|
873
|
+
where: { id: { in: [past.id, never.id] } }
|
|
874
|
+
}))).toEqual([never.id]);
|
|
875
|
+
});
|
|
876
|
+
expiring("a row without a date never expires", async () => {
|
|
877
|
+
const { provider, context, never } = await seeded();
|
|
878
|
+
const later = { now: "2999-01-01T00:00:00.000Z" };
|
|
879
|
+
expect(ids(await provider.findMany(context, later))).toEqual([
|
|
880
|
+
never.id
|
|
881
|
+
]);
|
|
882
|
+
expect(await provider.count(context, later)).toBe(1);
|
|
883
|
+
});
|
|
884
|
+
expiring("withExpired gives the expired rows too", async () => {
|
|
885
|
+
const { provider, context, past, edge, never, future } = await seeded();
|
|
886
|
+
const query = { now: NOW, withExpired: true };
|
|
887
|
+
expect(ids(await provider.findMany(context, query))).toEqual([
|
|
888
|
+
past.id,
|
|
889
|
+
edge.id,
|
|
890
|
+
never.id,
|
|
891
|
+
future.id
|
|
892
|
+
]);
|
|
893
|
+
expect(await provider.count(context, query)).toBe(4);
|
|
894
|
+
expect((await provider.findOne(context, {
|
|
895
|
+
...query,
|
|
896
|
+
where: { id: past.id }
|
|
897
|
+
}))?.id).toBe(past.id);
|
|
898
|
+
});
|
|
899
|
+
expiring("leaves expired rows out of a read after a position", async () => {
|
|
900
|
+
const enabled = options.keyset ?? (await makeProvider()).capabilities?.keyset;
|
|
901
|
+
if (enabled !== true)
|
|
902
|
+
return;
|
|
903
|
+
const { provider, context, past, edge, never, future } = await seeded();
|
|
904
|
+
const orderBy = [{ key: "n" }, { key: "id" }];
|
|
905
|
+
expect(ids(await provider.findMany(context, {
|
|
906
|
+
now: NOW,
|
|
907
|
+
orderBy,
|
|
908
|
+
after: [0, ""]
|
|
909
|
+
}))).toEqual([never.id, future.id]);
|
|
910
|
+
expect(ids(await provider.findMany(context, {
|
|
911
|
+
now: NOW,
|
|
912
|
+
orderBy,
|
|
913
|
+
after: [1, past.id],
|
|
914
|
+
withExpired: true
|
|
915
|
+
}))).toEqual([edge.id, never.id, future.id]);
|
|
916
|
+
});
|
|
917
|
+
expiring("unique keys still count expired rows", async () => {
|
|
918
|
+
const { provider, context, past } = await seeded();
|
|
919
|
+
const error = await failure(provider.insert(context, row({ email: past.email })));
|
|
920
|
+
expect(error instanceof RepositoryUniqueViolation).toBe(true);
|
|
921
|
+
expect(error.details.key).toBe("email");
|
|
922
|
+
expect(await provider.count(context, { now: NOW, withExpired: true })).toBe(4);
|
|
923
|
+
});
|
|
924
|
+
expiring("an entity that does not expire ignores now", async () => {
|
|
925
|
+
const { provider, context, seed } = await fixture();
|
|
926
|
+
const data = row({ expiresAt: "2000-01-01T00:00:00.000Z" });
|
|
927
|
+
await seed(data);
|
|
928
|
+
expect(ids(await provider.findMany(context, { now: NOW }))).toEqual([
|
|
929
|
+
data.id
|
|
930
|
+
]);
|
|
931
|
+
expect(await provider.count(context, { now: NOW })).toBe(1);
|
|
932
|
+
});
|
|
933
|
+
});
|
|
934
|
+
describe("schema evolution", () => {
|
|
935
|
+
const evolving = (key) => (name, body) => it(name, async () => {
|
|
936
|
+
if (options.schemaEvolution === false)
|
|
937
|
+
return;
|
|
938
|
+
if (key && options.schemaVersionKey === false)
|
|
939
|
+
return;
|
|
940
|
+
await body();
|
|
941
|
+
});
|
|
942
|
+
const always = evolving(false);
|
|
943
|
+
const keyed = evolving(true);
|
|
944
|
+
const contextFor = (entity, name) => ({
|
|
945
|
+
entity,
|
|
946
|
+
name: `${name}Repository`,
|
|
947
|
+
unique: ["id", "email"],
|
|
948
|
+
indexes: {},
|
|
949
|
+
env: {}
|
|
950
|
+
});
|
|
951
|
+
const accountsOf = (options = {}) => class Accounts extends entityOf(blueprintOf((ctx) => ({
|
|
952
|
+
email: ctx.string,
|
|
953
|
+
name: ctx.string,
|
|
954
|
+
age: ctx.integer.with({ fill: 18 }),
|
|
955
|
+
visits: ctx.integer.with({ renamedFrom: "n" }),
|
|
956
|
+
active: ctx.optionalOf(ctx.boolean)
|
|
957
|
+
})), {
|
|
958
|
+
name: "Accounts",
|
|
959
|
+
unique: ["email"],
|
|
960
|
+
...options.schemaVersion === undefined ? {} : {
|
|
961
|
+
schemaVersion: options.schemaVersion,
|
|
962
|
+
upgrade: {
|
|
963
|
+
2: (data) => ({ ...data, active: true })
|
|
964
|
+
}
|
|
965
|
+
},
|
|
966
|
+
...options.previousNames === undefined ? {} : { previousNames: options.previousNames }
|
|
967
|
+
}) {
|
|
968
|
+
};
|
|
969
|
+
async function setup(entity) {
|
|
970
|
+
const provider = await makeProvider();
|
|
971
|
+
const Accounts = repositoryOf({
|
|
972
|
+
entity,
|
|
973
|
+
actions: ["findMany"],
|
|
974
|
+
provider
|
|
975
|
+
});
|
|
976
|
+
const app = applicationOf({
|
|
977
|
+
name: "contract",
|
|
978
|
+
entities: { Accounts: entity },
|
|
979
|
+
repositories: { Accounts }
|
|
980
|
+
});
|
|
981
|
+
const context = contextFor(entity, "Accounts");
|
|
982
|
+
const old = [evolvedRow({ n: 3 }), evolvedRow({ n: 5 })];
|
|
983
|
+
for (const data of old)
|
|
984
|
+
await provider.insert(context, data);
|
|
985
|
+
const read = async () => (await Accounts.findMany()).map((account) => account.toJSON());
|
|
986
|
+
return { provider, app, context, old, read };
|
|
987
|
+
}
|
|
988
|
+
always("reads a row of an older shape back in the current one", async () => {
|
|
989
|
+
const { old, read } = await setup(accountsOf());
|
|
990
|
+
const [first] = await read();
|
|
991
|
+
expect(first?.id).toBe(old[0]?.id);
|
|
992
|
+
expect([first?.age, first?.visits]).toEqual([18, 3]);
|
|
993
|
+
expect(Object.hasOwn(first ?? {}, "n")).toBe(false);
|
|
994
|
+
});
|
|
995
|
+
keyed("reads a row of an older version through upgrade", async () => {
|
|
996
|
+
const { read } = await setup(accountsOf({ schemaVersion: 2 }));
|
|
997
|
+
const [first] = await read();
|
|
998
|
+
expect([first?.age, first?.visits, first?.active]).toEqual([
|
|
999
|
+
18,
|
|
1000
|
+
3,
|
|
1001
|
+
true
|
|
1002
|
+
]);
|
|
1003
|
+
});
|
|
1004
|
+
always("the backfill reports in check and rewrites in apply", async () => {
|
|
1005
|
+
const { provider, app, context, old } = await setup(accountsOf());
|
|
1006
|
+
const check = await app.migrate({ mode: "check", pageSize: 1 });
|
|
1007
|
+
expect([
|
|
1008
|
+
check.entities.Accounts?.scanned,
|
|
1009
|
+
check.entities.Accounts?.upgraded
|
|
1010
|
+
]).toEqual([2, 2]);
|
|
1011
|
+
const untouched = await provider.findOne(context, {
|
|
1012
|
+
where: { id: old[0]?.id }
|
|
1013
|
+
});
|
|
1014
|
+
expect(untouched?.n).toBe(3);
|
|
1015
|
+
await app.migrate({ mode: "apply", pageSize: 1 });
|
|
1016
|
+
const stored = await provider.findOne(context, {
|
|
1017
|
+
where: { id: old[0]?.id }
|
|
1018
|
+
});
|
|
1019
|
+
expect([stored?.age, stored?.visits]).toEqual([18, 3]);
|
|
1020
|
+
expect(stored?.n ?? undefined).toBe(undefined);
|
|
1021
|
+
const again = await app.migrate({ mode: "check" });
|
|
1022
|
+
expect(again.entities.Accounts?.upToDate).toBe(2);
|
|
1023
|
+
});
|
|
1024
|
+
keyed("the backfill stores the schema version", async () => {
|
|
1025
|
+
const { provider, app, context, old } = await setup(accountsOf({ schemaVersion: 2 }));
|
|
1026
|
+
await app.migrate({ mode: "apply" });
|
|
1027
|
+
const stored = await provider.findOne(context, {
|
|
1028
|
+
where: { id: old[1]?.id }
|
|
1029
|
+
});
|
|
1030
|
+
expect([stored?.[SCHEMA_VERSION_KEY], stored?.active]).toEqual([
|
|
1031
|
+
2,
|
|
1032
|
+
true
|
|
1033
|
+
]);
|
|
1034
|
+
const again = await app.migrate({ mode: "check" });
|
|
1035
|
+
expect(again.entities.Accounts?.upToDate).toBe(2);
|
|
1036
|
+
});
|
|
1037
|
+
always("the backfill moves the rows of a previous name", async () => {
|
|
1038
|
+
const entity = accountsOf({ previousNames: ["One"] });
|
|
1039
|
+
const provider = await makeProvider();
|
|
1040
|
+
const Accounts = repositoryOf({
|
|
1041
|
+
entity,
|
|
1042
|
+
actions: ["findMany"],
|
|
1043
|
+
provider
|
|
1044
|
+
});
|
|
1045
|
+
const app = applicationOf({
|
|
1046
|
+
name: "contract",
|
|
1047
|
+
entities: { Accounts: entity },
|
|
1048
|
+
repositories: { Accounts }
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
class One extends entityOf(blueprintOf((ctx) => ({ email: ctx.string, name: ctx.string })), { name: "One" }) {
|
|
1052
|
+
}
|
|
1053
|
+
const previous = contextFor(One, "One");
|
|
1054
|
+
const current = contextFor(entity, "Accounts");
|
|
1055
|
+
const old = [evolvedRow({ n: 1 }), evolvedRow({ n: 2 })];
|
|
1056
|
+
for (const data of old)
|
|
1057
|
+
await provider.insert(previous, data);
|
|
1058
|
+
const check = await app.migrate({ mode: "check" });
|
|
1059
|
+
expect(check.entities.Accounts?.movedFromPreviousNames).toBe(2);
|
|
1060
|
+
expect(await provider.count(current)).toBe(0);
|
|
1061
|
+
await app.migrate({ mode: "apply", pageSize: 1 });
|
|
1062
|
+
expect(await provider.count(current)).toBe(2);
|
|
1063
|
+
expect(await provider.count(previous)).toBe(2);
|
|
1064
|
+
expect((await Accounts.findMany()).map((account) => account.visits)).toEqual([1, 2]);
|
|
1065
|
+
await app.migrate({ mode: "apply", destructive: true, pageSize: 1 });
|
|
1066
|
+
expect(await provider.count(previous)).toBe(0);
|
|
1067
|
+
expect(await provider.count(current)).toBe(2);
|
|
1068
|
+
});
|
|
1069
|
+
});
|
|
1070
|
+
});
|
|
1071
|
+
}
|