@afora/core 0.0.8 → 0.0.9

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 (74) hide show
  1. package/README.md +127 -23
  2. package/dist/adapters.d.ts +25 -0
  3. package/dist/adapters.js +56 -0
  4. package/dist/core/application/exceptions.d.ts +17 -0
  5. package/dist/core/application/migrate.d.ts +19 -0
  6. package/dist/core/application/schema.d.ts +30 -0
  7. package/dist/core/application/types/application.interface.d.ts +37 -0
  8. package/dist/core/auth/jwt.d.ts +30 -0
  9. package/dist/core/auth/types/jwt.interface.d.ts +66 -0
  10. package/dist/core/blueprint/exceptions.d.ts +5 -0
  11. package/dist/core/blueprint/optional.d.ts +3 -1
  12. package/dist/core/blueprint/types/blueprint.interface.d.ts +79 -5
  13. package/dist/core/blueprint/variant.d.ts +32 -0
  14. package/dist/core/entity/abstract-entity.d.ts +5 -0
  15. package/dist/core/entity/entity.d.ts +1 -1
  16. package/dist/core/entity/exceptions.d.ts +9 -0
  17. package/dist/core/entity/trust.d.ts +24 -0
  18. package/dist/core/entity/types/entity.interface.d.ts +30 -0
  19. package/dist/core/output/access.d.ts +16 -0
  20. package/dist/core/output/exceptions.d.ts +16 -0
  21. package/dist/core/output/http.d.ts +36 -0
  22. package/dist/core/output/openapi-sink.d.ts +23 -2
  23. package/dist/core/output/stage.d.ts +7 -2
  24. package/dist/core/output/status.d.ts +1 -1
  25. package/dist/core/output/types/http.interface.d.ts +130 -0
  26. package/dist/core/output/types/output.interface.d.ts +25 -5
  27. package/dist/core/parse/issue.d.ts +28 -3
  28. package/dist/core/parse/parse.d.ts +9 -0
  29. package/dist/core/property/abstract-property.d.ts +9 -0
  30. package/dist/core/property/define-property.d.ts +34 -6
  31. package/dist/core/property/types/property-options.interface.d.ts +10 -0
  32. package/dist/core/property/types/property-type.interface.d.ts +4 -0
  33. package/dist/core/record/abstract-record.d.ts +7 -0
  34. package/dist/core/record/types/record.interface.d.ts +12 -1
  35. package/dist/core/repository/exceptions.d.ts +13 -0
  36. package/dist/core/repository/in-memory.d.ts +2 -2
  37. package/dist/core/repository/migrate.d.ts +20 -0
  38. package/dist/core/repository/state.d.ts +2 -0
  39. package/dist/core/repository/types/repository.interface.d.ts +33 -0
  40. package/dist/core/repository/types/schema.interface.d.ts +126 -0
  41. package/dist/core/repository/upcast.d.ts +64 -0
  42. package/dist/core/schema/schema.d.ts +28 -0
  43. package/dist/core/symbols/kind.d.ts +1 -1
  44. package/dist/core/use-case/access.d.ts +12 -0
  45. package/dist/core/use-case/decorators.d.ts +40 -0
  46. package/dist/core/use-case/route.d.ts +18 -0
  47. package/dist/core/use-case/steps/types/check.interface.d.ts +214 -0
  48. package/dist/core/use-case/types/access.interface.d.ts +39 -0
  49. package/dist/core/use-case/types/route.interface.d.ts +66 -0
  50. package/dist/core/use-case/use-case.d.ts +52 -2
  51. package/dist/experimental.d.ts +13 -1
  52. package/dist/experimental.js +63 -25
  53. package/dist/index-3aae5jfq.js +2997 -0
  54. package/dist/index-hg1z2s2v.js +1055 -0
  55. package/dist/index-ny3as79r.js +4567 -0
  56. package/dist/index-xegv6a1p.js +4291 -0
  57. package/dist/index-y280ew0k.js +396 -0
  58. package/dist/index.d.ts +45 -49
  59. package/dist/index.js +110 -186
  60. package/dist/std/properties/password.d.ts +0 -2
  61. package/dist/std/record-plugins.d.ts +2 -2
  62. package/dist/std/resource.d.ts +31 -0
  63. package/dist/std/runtime.d.ts +6 -0
  64. package/dist/std/use-cases/auth.d.ts +5 -1
  65. package/dist/std/use-cases/index.d.ts +1 -1
  66. package/dist/std/use-cases/shared.d.ts +2 -2
  67. package/dist/std/use-cases/types.d.ts +97 -1
  68. package/dist/testing/http-provider-contract.d.ts +32 -0
  69. package/dist/testing/index.d.ts +2 -0
  70. package/dist/testing/index.js +495 -4
  71. package/dist/testing/provider-contract.d.ts +17 -1
  72. package/package.json +16 -3
  73. package/dist/index-qxfkyd6k.js +0 -1721
  74. package/dist/index-t5qwtyj5.js +0 -8194
@@ -1,12 +1,361 @@
1
- import"../index-qxfkyd6k.js";
2
1
  import {
2
+ exceptionOf2,
3
+ SCHEMA_VERSION_KEY2,
3
4
  RepositoryUniqueViolation2,
4
5
  RepositoryEntityNotFound2,
5
- RepositoryConcurrencyConflict2,
6
+ RepositoryConcurrencyConflict2
7
+ } from "../index-xegv6a1p.js";
8
+ import {
9
+ repositoryOf2,
10
+ useCaseOf2
11
+ } from "../index-3aae5jfq.js";
12
+ import {
13
+ outputOf2,
14
+ applicationOf2,
6
15
  blueprintOf2,
7
- entityOf2
8
- } from "../index-t5qwtyj5.js";
16
+ commandOf2,
17
+ entityOf2,
18
+ recordOf2
19
+ } from "../index-ny3as79r.js";
20
+ import"../index-hg1z2s2v.js";
21
+ import"../index-y280ew0k.js";
22
+
23
+ // src/testing/http-provider-contract.ts
24
+ var Note = recordOf2(blueprintOf2((ctx) => ({ title: ctx.string, secret: ctx.password })));
25
+ var Rejected = exceptionOf2({
26
+ layer: "use-case" /* UseCase */,
27
+ name: "ContractRejected",
28
+ message: "rejected"
29
+ });
30
+ var Echo = blueprintOf2((ctx) => ({
31
+ id: ctx.optional.string,
32
+ page: ctx.optional.integer,
33
+ flag: ctx.optional.boolean,
34
+ locale: ctx.optional.string,
35
+ session: ctx.optional.string,
36
+ title: ctx.optional.string,
37
+ caller: ctx.optional.string,
38
+ tenant: ctx.optional.string
39
+ }));
40
+ function operations() {
41
+
42
+ class Get extends useCaseOf2({
43
+ name: "ContractGet",
44
+ blueprint: blueprintOf2((ctx) => ({
45
+ id: ctx.string,
46
+ page: ctx.optional.integer,
47
+ flag: ctx.optional.boolean
48
+ })),
49
+ response: Echo,
50
+ route: { method: "get", path: "/items/{id}" }
51
+ }) {
52
+ async execute() {
53
+ return { ...this.input };
54
+ }
55
+ }
56
+
57
+ class Change extends useCaseOf2({
58
+ name: "ContractChange",
59
+ blueprint: blueprintOf2((ctx) => ({
60
+ id: ctx.string,
61
+ page: ctx.integer,
62
+ locale: ctx.optional.string,
63
+ session: ctx.optional.string,
64
+ title: ctx.string
65
+ })),
66
+ response: Echo,
67
+ context: blueprintOf2((ctx) => ({ tenant: ctx.optional.string })),
68
+ errors: [Rejected],
69
+ route: {
70
+ method: "put",
71
+ path: "/items/{id}",
72
+ status: 201,
73
+ query: ["page"],
74
+ headers: { locale: "accept-language" },
75
+ cookie: { session: "sid" }
76
+ }
77
+ }) {
78
+ async execute() {
79
+ if (this.input.title === "reject")
80
+ throw new Rejected({ source: "x" });
81
+ if (this.input.title === "crash")
82
+ throw new Error("internal detail");
83
+ return { ...this.input, tenant: this.context.tenant };
84
+ }
85
+ }
9
86
 
87
+ class Remove extends useCaseOf2({
88
+ name: "ContractRemove",
89
+ blueprint: blueprintOf2((ctx) => ({ id: ctx.string })),
90
+ response: blueprintOf2(() => ({})),
91
+ route: { method: "delete", path: "/items/{id}", status: 204 }
92
+ }) {
93
+ async execute() {
94
+ return {};
95
+ }
96
+ }
97
+
98
+ class Reveal extends useCaseOf2({
99
+ name: "ContractReveal",
100
+ blueprint: blueprintOf2(() => ({})),
101
+ response: blueprintOf2(() => ({ note: Note })),
102
+ route: { method: "get", path: "/note" }
103
+ }) {
104
+ async execute() {
105
+ return { note: new Note({ title: "t", secret: await hashed() }) };
106
+ }
107
+ }
108
+
109
+ class Mine extends useCaseOf2({
110
+ name: "ContractMine",
111
+ blueprint: blueprintOf2(() => ({})),
112
+ response: Echo,
113
+ context: blueprintOf2((ctx) => ({ caller: ctx.string })),
114
+ route: { method: "get", path: "/mine" },
115
+ access: { scopes: ["items:read"], context: { caller: "subject" } }
116
+ }) {
117
+ async execute() {
118
+ return { caller: this.context.caller };
119
+ }
120
+ }
121
+
122
+ class Enter extends useCaseOf2({
123
+ name: "ContractEnter",
124
+ blueprint: blueprintOf2((ctx) => ({ title: ctx.string })),
125
+ response: blueprintOf2((ctx) => ({
126
+ title: ctx.string,
127
+ token: ctx.string,
128
+ refreshToken: ctx.string
129
+ })),
130
+ route: {
131
+ method: "post",
132
+ path: "/enter",
133
+ setCookie: {
134
+ token: { name: "sid", secure: false },
135
+ refreshToken: { name: "rid", secure: false, path: "/refresh" }
136
+ }
137
+ }
138
+ }) {
139
+ async execute() {
140
+ return { title: this.input.title, token: "t 1", refreshToken: "" };
141
+ }
142
+ }
143
+ return commandOf2({
144
+ name: "Contract",
145
+ useCases: {
146
+ get: Get,
147
+ change: Change,
148
+ remove: Remove,
149
+ reveal: Reveal,
150
+ mine: Mine,
151
+ enter: Enter
152
+ }
153
+ });
154
+ }
155
+ async function hashed() {
156
+ return "$argon2id$v=19$m=65536,t=2,p=1$c29tZXNhbHRzb21lc2FsdA$4VbQ3OqJ7xk8Y0mG0iZ1c2k1m2Qe0sQeJ7xk8Y0mG0i";
157
+ }
158
+ var authenticator = (request) => {
159
+ const token = request.header("authorization");
160
+ if (token === "Bearer reader")
161
+ return { subject: "u1", scopes: ["items:read"] };
162
+ if (token === "Bearer other")
163
+ return { subject: "u2", scopes: [] };
164
+ return null;
165
+ };
166
+ function httpProviderContract(name, settings, runner) {
167
+ const { describe, it, expect } = runner;
168
+ const origin = "http://contract.test";
169
+ const withServer = async (options, body, hooks = { authenticator }) => {
170
+ const server = outputOf2({
171
+ name: "Contract",
172
+ commands: [operations()],
173
+ statuses: { ContractRejected: 409 },
174
+ pipe: [settings.sink(options, hooks)]
175
+ }).build();
176
+ try {
177
+ await body(async (method, path, init = {}) => {
178
+ const hasBody = init.body !== undefined;
179
+ const response = await settings.fetch(server, new Request(`${origin}${path}`, {
180
+ method,
181
+ headers: {
182
+ ...hasBody ? { "content-type": "application/json" } : {},
183
+ ...init.headers
184
+ },
185
+ ...hasBody ? {
186
+ body: typeof init.body === "string" ? init.body : JSON.stringify(init.body)
187
+ } : {}
188
+ }));
189
+ const text = await response.text();
190
+ let json;
191
+ try {
192
+ json = text === "" ? undefined : JSON.parse(text);
193
+ } catch {
194
+ json = undefined;
195
+ }
196
+ return {
197
+ status: response.status,
198
+ type: (response.headers.get("content-type") ?? "").split(";")[0],
199
+ headers: response.headers,
200
+ text,
201
+ json
202
+ };
203
+ });
204
+ } finally {
205
+ await settings.close?.(server);
206
+ }
207
+ };
208
+ describe(`${name}: HTTP provider contract`, () => {
209
+ it("serves the route a use case declares, path and query coerced", async () => {
210
+ await withServer({ basePath: "/api" }, async (send) => {
211
+ const found = await send("GET", "/api/items/a%20b?page=2&flag=true");
212
+ expect(found.status).toBe(200);
213
+ expect(found.type).toBe("application/json");
214
+ expect(found.json).toEqual({ id: "a b", page: 2, flag: true });
215
+ });
216
+ });
217
+ it("reads each key where the route places it, over the body", async () => {
218
+ await withServer({}, async (send) => {
219
+ const changed = await send("PUT", "/items/p?page=3", {
220
+ headers: { "accept-language": "pt-BR", cookie: "x=1; sid=s%201" },
221
+ body: { id: "from-body", page: 9, title: "T" }
222
+ });
223
+ expect(changed.status).toBe(201);
224
+ expect(changed.json).toEqual({
225
+ id: "p",
226
+ page: 3,
227
+ locale: "pt-BR",
228
+ session: "s 1",
229
+ title: "T"
230
+ });
231
+ });
232
+ });
233
+ it("fills the context from headers", async () => {
234
+ await withServer({ context: { headers: { tenant: "x-tenant" } } }, async (send) => {
235
+ const changed = await send("PUT", "/items/p?page=1", {
236
+ headers: { "x-tenant": "acme" },
237
+ body: { title: "T" }
238
+ });
239
+ expect(changed.json.tenant).toBe("acme");
240
+ });
241
+ });
242
+ it("answers invalid input and invalid JSON with a 400 problem", async () => {
243
+ await withServer({}, async (send) => {
244
+ const invalid = await send("PUT", "/items/p?page=1", {
245
+ body: { title: 1 }
246
+ });
247
+ expect(invalid.status).toBe(400);
248
+ expect(invalid.type).toBe("application/problem+json");
249
+ const problem = invalid.json;
250
+ expect(problem.title).toBe("InvalidUseCaseInput");
251
+ expect(problem.issues.map((issue) => issue.path)).toEqual([["title"]]);
252
+ const notJson = await send("PUT", "/items/p?page=1", {
253
+ body: "{nope"
254
+ });
255
+ expect(notJson.status).toBe(400);
256
+ expect(notJson.json.title).toBe("InvalidRequestBody");
257
+ });
258
+ });
259
+ it("gives errors the status of the output, and hides internal ones", async () => {
260
+ await withServer({}, async (send) => {
261
+ const rejected = await send("PUT", "/items/p?page=1", {
262
+ body: { title: "reject" }
263
+ });
264
+ expect(rejected.status).toBe(409);
265
+ expect(rejected.type).toBe("application/problem+json");
266
+ const crashed = await send("PUT", "/items/p?page=1", {
267
+ body: { title: "crash" }
268
+ });
269
+ expect(crashed.status).toBe(500);
270
+ expect(crashed.text.includes("internal detail")).toBe(false);
271
+ });
272
+ await withServer({ debug: true }, async (send) => {
273
+ const crashed = await send("PUT", "/items/p?page=1", {
274
+ body: { title: "crash" }
275
+ });
276
+ expect(crashed.text.includes("internal detail")).toBe(true);
277
+ });
278
+ });
279
+ it("answers a 204 without a body, and an unknown route with a 404 problem", async () => {
280
+ await withServer({}, async (send) => {
281
+ const removed = await send("DELETE", "/items/p");
282
+ expect(removed.status).toBe(204);
283
+ expect(removed.text).toBe("");
284
+ const unknown = await send("GET", "/nowhere");
285
+ expect(unknown.status).toBe(404);
286
+ expect(unknown.type).toBe("application/problem+json");
287
+ expect(unknown.json.title).toBe("NotFound");
288
+ });
289
+ });
290
+ it("masks sensitive values", async () => {
291
+ await withServer({}, async (send) => {
292
+ const revealed = await send("GET", "/note");
293
+ expect(revealed.json).toEqual({
294
+ note: { title: "t", secret: "********" }
295
+ });
296
+ });
297
+ });
298
+ it("enforces the access of a use case", async () => {
299
+ await withServer({}, async (send) => {
300
+ const anonymous = await send("GET", "/mine");
301
+ expect(anonymous.status).toBe(401);
302
+ expect(anonymous.json.title).toBe("Unauthenticated");
303
+ const other = await send("GET", "/mine", {
304
+ headers: { authorization: "Bearer other" }
305
+ });
306
+ expect(other.status).toBe(403);
307
+ const reader = await send("GET", "/mine", {
308
+ headers: { authorization: "Bearer reader" }
309
+ });
310
+ expect(reader.status).toBe(200);
311
+ expect(reader.json).toEqual({ caller: "u1" });
312
+ });
313
+ await withServer({}, async (send) => {
314
+ const closed = await send("GET", "/mine", {
315
+ headers: { authorization: "Bearer reader" }
316
+ });
317
+ expect(closed.status).toBe(401);
318
+ }, {});
319
+ });
320
+ it("sends the cookies of a route, out of the body", async () => {
321
+ await withServer({}, async (send) => {
322
+ const entered = await send("POST", "/enter", { body: { title: "T" } });
323
+ expect(entered.status).toBe(200);
324
+ expect(entered.json).toEqual({ title: "T" });
325
+ expect(entered.headers.getSetCookie()).toEqual([
326
+ "sid=t%201; Path=/; HttpOnly; SameSite=Lax",
327
+ "rid=; Path=/refresh; HttpOnly; SameSite=Lax; Max-Age=0"
328
+ ]);
329
+ });
330
+ });
331
+ it("puts the request id on successes and problems", async () => {
332
+ await withServer({ requestId: true }, async (send) => {
333
+ const sent = await send("GET", "/items/a", {
334
+ headers: { "x-request-id": "abc-1" }
335
+ });
336
+ expect(sent.headers.get("x-request-id")).toBe("abc-1");
337
+ const problem = await send("GET", "/mine");
338
+ expect((problem.headers.get("x-request-id") ?? "").length).toBe(36);
339
+ });
340
+ });
341
+ it("serves the document of the same routes", async () => {
342
+ await withServer({
343
+ basePath: "/api",
344
+ docs: { path: "/openapi.json", info: { title: "C", version: "1" } }
345
+ }, async (send) => {
346
+ const docs = await send("GET", "/openapi.json");
347
+ expect(docs.status).toBe(200);
348
+ expect(docs.type).toBe("application/json");
349
+ expect(Object.keys(docs.json.paths).sort()).toEqual([
350
+ "/api/enter",
351
+ "/api/items/{id}",
352
+ "/api/mine",
353
+ "/api/note"
354
+ ]);
355
+ });
356
+ });
357
+ });
358
+ }
10
359
  // src/testing/memory-publisher.ts
11
360
  function memoryPublisher() {
12
361
  const published = [];
@@ -37,6 +386,11 @@ function row(overrides = {}) {
37
386
  const id = `id-${String(sequence).padStart(4, "0")}`;
38
387
  return { id, email: `${id}@example.com`, name: id, ...overrides };
39
388
  }
389
+ function evolvedRow(overrides = {}) {
390
+ sequence += 1;
391
+ const id = `0190a1b2-c3d4-7e5f-8a9b-${String(sequence).padStart(12, "0")}`;
392
+ return { ...row(), id, email: `${sequence}@example.com`, ...overrides };
393
+ }
40
394
  async function failure(promise) {
41
395
  try {
42
396
  await promise;
@@ -936,9 +1290,146 @@ function providerContract(runner, makeProvider, options = {}) {
936
1290
  expect(await provider.count(context, { now: NOW })).toBe(1);
937
1291
  });
938
1292
  });
1293
+ describe("schema evolution", () => {
1294
+ const evolving = (key) => (name, body) => it(name, async () => {
1295
+ if (options.schemaEvolution === false)
1296
+ return;
1297
+ if (key && options.schemaVersionKey === false)
1298
+ return;
1299
+ await body();
1300
+ });
1301
+ const always = evolving(false);
1302
+ const keyed = evolving(true);
1303
+ const contextFor = (entity, name) => ({
1304
+ entity,
1305
+ name: `${name}Repository`,
1306
+ unique: ["id", "email"],
1307
+ indexes: {},
1308
+ env: {}
1309
+ });
1310
+ const accountsOf = (options = {}) => class Accounts extends entityOf2(blueprintOf2((ctx) => ({
1311
+ email: ctx.string,
1312
+ name: ctx.string,
1313
+ age: ctx.integer.with({ fill: 18 }),
1314
+ visits: ctx.integer.with({ renamedFrom: "n" }),
1315
+ active: ctx.optionalOf(ctx.boolean)
1316
+ })), {
1317
+ name: "Accounts",
1318
+ unique: ["email"],
1319
+ ...options.schemaVersion === undefined ? {} : {
1320
+ schemaVersion: options.schemaVersion,
1321
+ upgrade: {
1322
+ 2: (data) => ({ ...data, active: true })
1323
+ }
1324
+ },
1325
+ ...options.previousNames === undefined ? {} : { previousNames: options.previousNames }
1326
+ }) {
1327
+ };
1328
+ async function setup(entity) {
1329
+ const provider = await makeProvider();
1330
+ const Accounts = repositoryOf2({
1331
+ entity,
1332
+ actions: ["findMany"],
1333
+ provider
1334
+ });
1335
+ const app = applicationOf2({
1336
+ name: "contract",
1337
+ entities: { Accounts: entity },
1338
+ repositories: { Accounts }
1339
+ });
1340
+ const context = contextFor(entity, "Accounts");
1341
+ const old = [evolvedRow({ n: 3 }), evolvedRow({ n: 5 })];
1342
+ for (const data of old)
1343
+ await provider.insert(context, data);
1344
+ const read = async () => (await Accounts.findMany()).map((account) => account.toJSON());
1345
+ return { provider, app, context, old, read };
1346
+ }
1347
+ always("reads a row of an older shape back in the current one", async () => {
1348
+ const { old, read } = await setup(accountsOf());
1349
+ const [first] = await read();
1350
+ expect(first?.id).toBe(old[0]?.id);
1351
+ expect([first?.age, first?.visits]).toEqual([18, 3]);
1352
+ expect(Object.hasOwn(first ?? {}, "n")).toBe(false);
1353
+ });
1354
+ keyed("reads a row of an older version through upgrade", async () => {
1355
+ const { read } = await setup(accountsOf({ schemaVersion: 2 }));
1356
+ const [first] = await read();
1357
+ expect([first?.age, first?.visits, first?.active]).toEqual([
1358
+ 18,
1359
+ 3,
1360
+ true
1361
+ ]);
1362
+ });
1363
+ always("the backfill reports in check and rewrites in apply", async () => {
1364
+ const { provider, app, context, old } = await setup(accountsOf());
1365
+ const check = await app.migrate({ mode: "check", pageSize: 1 });
1366
+ expect([
1367
+ check.entities.Accounts?.scanned,
1368
+ check.entities.Accounts?.upgraded
1369
+ ]).toEqual([2, 2]);
1370
+ const untouched = await provider.findOne(context, {
1371
+ where: { id: old[0]?.id }
1372
+ });
1373
+ expect(untouched?.n).toBe(3);
1374
+ await app.migrate({ mode: "apply", pageSize: 1 });
1375
+ const stored = await provider.findOne(context, {
1376
+ where: { id: old[0]?.id }
1377
+ });
1378
+ expect([stored?.age, stored?.visits]).toEqual([18, 3]);
1379
+ expect(stored?.n ?? undefined).toBe(undefined);
1380
+ const again = await app.migrate({ mode: "check" });
1381
+ expect(again.entities.Accounts?.upToDate).toBe(2);
1382
+ });
1383
+ keyed("the backfill stores the schema version", async () => {
1384
+ const { provider, app, context, old } = await setup(accountsOf({ schemaVersion: 2 }));
1385
+ await app.migrate({ mode: "apply" });
1386
+ const stored = await provider.findOne(context, {
1387
+ where: { id: old[1]?.id }
1388
+ });
1389
+ expect([stored?.[SCHEMA_VERSION_KEY2], stored?.active]).toEqual([
1390
+ 2,
1391
+ true
1392
+ ]);
1393
+ const again = await app.migrate({ mode: "check" });
1394
+ expect(again.entities.Accounts?.upToDate).toBe(2);
1395
+ });
1396
+ always("the backfill moves the rows of a previous name", async () => {
1397
+ const entity = accountsOf({ previousNames: ["One"] });
1398
+ const provider = await makeProvider();
1399
+ const Accounts = repositoryOf2({
1400
+ entity,
1401
+ actions: ["findMany"],
1402
+ provider
1403
+ });
1404
+ const app = applicationOf2({
1405
+ name: "contract",
1406
+ entities: { Accounts: entity },
1407
+ repositories: { Accounts }
1408
+ });
1409
+
1410
+ class One extends entityOf2(blueprintOf2((ctx) => ({ email: ctx.string, name: ctx.string })), { name: "One" }) {
1411
+ }
1412
+ const previous = contextFor(One, "One");
1413
+ const current = contextFor(entity, "Accounts");
1414
+ const old = [evolvedRow({ n: 1 }), evolvedRow({ n: 2 })];
1415
+ for (const data of old)
1416
+ await provider.insert(previous, data);
1417
+ const check = await app.migrate({ mode: "check" });
1418
+ expect(check.entities.Accounts?.movedFromPreviousNames).toBe(2);
1419
+ expect(await provider.count(current)).toBe(0);
1420
+ await app.migrate({ mode: "apply", pageSize: 1 });
1421
+ expect(await provider.count(current)).toBe(2);
1422
+ expect(await provider.count(previous)).toBe(2);
1423
+ expect((await Accounts.findMany()).map((account) => account.visits)).toEqual([1, 2]);
1424
+ await app.migrate({ mode: "apply", destructive: true, pageSize: 1 });
1425
+ expect(await provider.count(previous)).toBe(0);
1426
+ expect(await provider.count(current)).toBe(2);
1427
+ });
1428
+ });
939
1429
  });
940
1430
  }
941
1431
  export {
1432
+ httpProviderContract,
942
1433
  memoryPublisher,
943
1434
  providerContract
944
1435
  };
@@ -1,4 +1,4 @@
1
- import { type IRepositoryProvider } from "../index.js";
1
+ import { type IRepositoryProvider } from "../adapters.js";
2
2
  /** The parts of a test runner the kit uses; `bun:test`, vitest and jest fit. */
3
3
  export interface ContractRunner {
4
4
  describe(name: string, body: () => void): void;
@@ -32,6 +32,22 @@ export interface ProviderContractOptions {
32
32
  readonly atomic?: boolean;
33
33
  /** Run the cases of expired records. Defaults to `provider.capabilities?.expiry`. */
34
34
  readonly expiry?: boolean;
35
+ /**
36
+ * Run the cases of schema evolution, through repositories and
37
+ * `app.migrate` over the basic methods: an old row read back upgraded,
38
+ * the backfill in check and apply, rows moved from a previous name.
39
+ * Default `true`. Their entities are named `Accounts` (and `One`, the
40
+ * previous name), with the keys of the other cases; `createdAt` and
41
+ * `updatedAt` are never written.
42
+ */
43
+ readonly schemaEvolution?: boolean;
44
+ /**
45
+ * Run the schema evolution cases that store the version key
46
+ * (`SCHEMA_VERSION_KEY`, `"$v"`) in rows, for an entity declared with
47
+ * `schemaVersion`. Default `true`; a provider with a fixed set of
48
+ * columns needs one for it (or maps `context.schema.key` to its own).
49
+ */
50
+ readonly schemaVersionKey?: boolean;
35
51
  }
36
52
  /**
37
53
  * Registers the behavior every `IRepositoryProvider` v2 must have. Pass the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afora/core",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "Domain modeling on TypeBox: entities with invariants, repositories, use cases, sensitive data masking and layered exceptions, usable by any Standard Schema consumer.",
5
5
  "keywords": [
6
6
  "domain-driven-design",
@@ -40,6 +40,11 @@
40
40
  "import": "./dist/index.js",
41
41
  "default": "./dist/index.js"
42
42
  },
43
+ "./adapters": {
44
+ "types": "./dist/adapters.d.ts",
45
+ "import": "./dist/adapters.js",
46
+ "default": "./dist/adapters.js"
47
+ },
43
48
  "./experimental": {
44
49
  "types": "./dist/experimental.d.ts",
45
50
  "import": "./dist/experimental.js",
@@ -68,9 +73,11 @@
68
73
  "lint": "biome lint .",
69
74
  "fmt": "biome check --write .",
70
75
  "knip": "knip",
71
- "build": "rm -rf dist && bun build ./src/index.ts ./src/experimental.ts ./src/testing/index.ts --outdir ./dist --target node --format esm --splitting --packages external && tsc -p tsconfig.build.json",
76
+ "build": "rm -rf dist && bun build ./src/index.ts ./src/adapters.ts ./src/experimental.ts ./src/testing/index.ts --outdir ./dist --target node --format esm --splitting --packages external && tsc -p tsconfig.build.json",
72
77
  "postbuild": "bun scripts/add-dts-extensions.ts",
73
78
  "bench": "cd bench && bun install && bun run.ts",
79
+ "bench:guard": "cd bench && bun install && bun run.ts --guard",
80
+ "bench:check": "cd bench && bun install && bun run.ts --check",
74
81
  "smoke": "bun run build && node scripts/smoke.mjs",
75
82
  "test:types": "bun run build && bun tests/types-compat/run.ts",
76
83
  "test:type-perf": "bun run build && bun test ./tests/type-perf",
@@ -78,15 +85,21 @@
78
85
  "docs:links": "bun scripts/check-doc-links.ts",
79
86
  "test:interop": "cd examples/interop && bun install && bun test",
80
87
  "test:blog": "cd examples/blog && bun install && bun test",
88
+ "test:agent-built": "cd examples/agent-built && bun install && bun check && bun test",
89
+ "eval:agent-built": "cd examples/agent-built && bun install && bun run eval:all",
81
90
  "prepare": "husky",
82
91
  "prepublishOnly": "bun run typecheck && bun test && bun run smoke",
83
92
  "test:codegen": "bun run --cwd packages/codegen test",
84
93
  "test:roundtrip": "bun run --cwd packages/codegen test:roundtrip",
85
94
  "test:mcp": "bun test --cwd packages/mcp",
86
95
  "test:hono": "bun test --cwd packages/hono",
96
+ "test:fastify": "bun test --cwd packages/fastify",
97
+ "test:express": "bun test --cwd packages/express",
98
+ "test:elysia": "bun test --cwd packages/elysia",
87
99
  "test:ws": "bun test --cwd packages/ws",
88
100
  "test:drizzle": "bun test --cwd packages/drizzle",
89
- "build:packages": "bun run build && bun run --cwd packages/codegen build && bun run --cwd packages/mcp build && bun run --cwd packages/hono build && bun run --cwd packages/ws build && bun run --cwd packages/drizzle build"
101
+ "test:dynamodb": "bun test --cwd packages/dynamodb",
102
+ "build:packages": "bun run build && bun run --cwd packages/codegen build && bun run --cwd packages/mcp build && bun run --cwd packages/hono build && bun run --cwd packages/fastify build && bun run --cwd packages/express build && bun run --cwd packages/elysia build && bun run --cwd packages/ws build && bun run --cwd packages/drizzle build && bun run --cwd packages/dynamodb build"
90
103
  },
91
104
  "dependencies": {
92
105
  "slugify": "^1.6.9"