@afora/core 0.0.6 → 0.0.8
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 +36 -7
- package/dist/core/application/start.d.ts +7 -0
- package/dist/core/application/types/application.interface.d.ts +47 -2
- package/dist/core/command/command.d.ts +11 -4
- package/dist/core/command/types/command.interface.d.ts +26 -0
- package/dist/core/entity/abstract-entity.d.ts +28 -1
- package/dist/core/entity/entity.d.ts +1 -1
- package/dist/core/entity/event.d.ts +13 -2
- package/dist/core/entity/exceptions.d.ts +8 -0
- package/dist/core/entity/types/entity.interface.d.ts +66 -2
- package/dist/core/entity/version.d.ts +13 -0
- package/dist/core/event-policy/event-policy.d.ts +25 -0
- package/dist/core/event-policy/exceptions.d.ts +11 -0
- package/dist/core/event-policy/publish.d.ts +17 -0
- package/dist/core/event-policy/publishers.d.ts +18 -0
- package/dist/core/event-policy/types/event-policy.interface.d.ts +72 -0
- package/dist/core/output/coerce.d.ts +9 -0
- package/dist/core/output/openapi-sink.d.ts +138 -38
- package/dist/core/output/status.d.ts +2 -1
- package/dist/core/parse/parse.d.ts +10 -0
- package/dist/core/parse/strip.d.ts +13 -0
- package/dist/core/property/input.d.ts +20 -0
- package/dist/core/record/plugin.d.ts +6 -2
- package/dist/core/repository/exceptions.d.ts +30 -0
- package/dist/core/repository/in-memory.d.ts +14 -1
- package/dist/core/repository/plan.d.ts +20 -0
- package/dist/core/repository/provider.d.ts +1 -1
- package/dist/core/repository/query.d.ts +9 -2
- package/dist/core/repository/repository.d.ts +40 -8
- package/dist/core/repository/state.d.ts +39 -2
- package/dist/core/repository/types/query.interface.d.ts +106 -5
- package/dist/core/repository/types/repository.interface.d.ts +113 -7
- package/dist/core/use-case/exceptions.d.ts +36 -0
- package/dist/core/use-case/plugin-catalog.d.ts +11 -0
- package/dist/core/use-case/plugin.d.ts +25 -0
- package/dist/core/use-case/steps/types/steps.interface.d.ts +23 -6
- package/dist/core/use-case/target.d.ts +9 -0
- package/dist/core/use-case/types/plugin.interface.d.ts +185 -0
- package/dist/experimental.js +3 -3
- package/dist/{index-3nd9bjnc.js → index-qxfkyd6k.js} +646 -635
- package/dist/{index-gbgc40xe.js → index-t5qwtyj5.js} +2798 -295
- package/dist/index.d.ts +35 -15
- package/dist/index.js +99 -15
- package/dist/std/blueprint-plugin.d.ts +6 -2
- package/dist/std/blueprint.d.ts +3 -0
- package/dist/std/command.d.ts +11 -0
- package/dist/std/entity.d.ts +2 -1
- package/dist/std/event-policies.d.ts +2 -0
- package/dist/std/properties/exceptions.d.ts +11 -0
- package/dist/std/properties/json.d.ts +54 -0
- package/dist/std/properties/password.d.ts +16 -1
- package/dist/std/properties/time.d.ts +4 -0
- package/dist/std/use-cases/auth.d.ts +5 -0
- package/dist/std/use-cases/crud.d.ts +25 -0
- package/dist/std/use-cases/index.d.ts +11 -0
- package/dist/std/use-cases/lifecycle.d.ts +13 -0
- package/dist/std/use-cases/shared.d.ts +88 -0
- package/dist/std/use-cases/types.d.ts +291 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +355 -2
- package/dist/testing/memory-publisher.d.ts +16 -0
- package/dist/testing/provider-contract.d.ts +14 -0
- package/package.json +3 -2
package/dist/testing/index.js
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
|
-
import"../index-
|
|
1
|
+
import"../index-qxfkyd6k.js";
|
|
2
2
|
import {
|
|
3
3
|
RepositoryUniqueViolation2,
|
|
4
4
|
RepositoryEntityNotFound2,
|
|
5
|
+
RepositoryConcurrencyConflict2,
|
|
5
6
|
blueprintOf2,
|
|
6
7
|
entityOf2
|
|
7
|
-
} from "../index-
|
|
8
|
+
} from "../index-t5qwtyj5.js";
|
|
8
9
|
|
|
10
|
+
// src/testing/memory-publisher.ts
|
|
11
|
+
function memoryPublisher() {
|
|
12
|
+
const published = [];
|
|
13
|
+
return {
|
|
14
|
+
published,
|
|
15
|
+
publish(topic, event) {
|
|
16
|
+
published.push({ topic, event });
|
|
17
|
+
},
|
|
18
|
+
clear() {
|
|
19
|
+
published.length = 0;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
9
23
|
// src/testing/provider-contract.ts
|
|
10
24
|
function contextOf(name = "Accounts") {
|
|
11
25
|
class Account extends entityOf2(blueprintOf2((ctx) => ({ email: ctx.string, name: ctx.string }))) {
|
|
@@ -43,6 +57,11 @@ function providerContract(runner, makeProvider, options = {}) {
|
|
|
43
57
|
};
|
|
44
58
|
return { provider, context, seed };
|
|
45
59
|
}
|
|
60
|
+
const gated = (capability) => (name, body) => it(name, async () => {
|
|
61
|
+
const enabled = options[capability] ?? (await makeProvider()).capabilities?.[capability];
|
|
62
|
+
if (enabled === true)
|
|
63
|
+
await body();
|
|
64
|
+
});
|
|
46
65
|
describe("provider contract", () => {
|
|
47
66
|
describe("writes and reads", () => {
|
|
48
67
|
it("inserts and finds by id, empty provider counts zero", async () => {
|
|
@@ -129,6 +148,14 @@ function providerContract(runner, makeProvider, options = {}) {
|
|
|
129
148
|
await provider.update(context, { ...data, name: "Other" });
|
|
130
149
|
expect(await provider.findOne(context, { where: { id: data.id } })).toEqual({ ...data, name: "Other" });
|
|
131
150
|
});
|
|
151
|
+
it("update clears a value that is left out", async () => {
|
|
152
|
+
const { provider, context, seed } = await fixture();
|
|
153
|
+
const data = row({ deletedAt: "2024-01-01T00:00:00.000Z" });
|
|
154
|
+
await seed(data);
|
|
155
|
+
const { deletedAt: _deletedAt, ...alive } = data;
|
|
156
|
+
await provider.update(context, alive);
|
|
157
|
+
expect(await provider.findOne(context, { where: { id: data.id } })).toEqual(alive);
|
|
158
|
+
});
|
|
132
159
|
it("soft-deleted rows keep occupying unique values", async () => {
|
|
133
160
|
const { provider, context, seed } = await fixture();
|
|
134
161
|
await seed(row({
|
|
@@ -460,6 +487,80 @@ function providerContract(runner, makeProvider, options = {}) {
|
|
|
460
487
|
})).toBe(5);
|
|
461
488
|
});
|
|
462
489
|
});
|
|
490
|
+
describe("indexes", () => {
|
|
491
|
+
const indexed = (name, body) => it(name, async () => {
|
|
492
|
+
const enabled = options.indexes ?? (await makeProvider()).capabilities?.indexes;
|
|
493
|
+
if (enabled === true)
|
|
494
|
+
await body();
|
|
495
|
+
});
|
|
496
|
+
const seeded = async () => {
|
|
497
|
+
const f = await fixture();
|
|
498
|
+
const context = {
|
|
499
|
+
...f.context,
|
|
500
|
+
indexes: {
|
|
501
|
+
byGroup: { where: ["group"], orderBy: "n" },
|
|
502
|
+
byName: { where: ["name"] }
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
const rows = [
|
|
506
|
+
row({ n: 3, group: 1, name: "Gil" }),
|
|
507
|
+
row({ group: 1, name: "Érico" }),
|
|
508
|
+
row({ n: 2, group: 2, name: "Ana" }),
|
|
509
|
+
row({ n: 2, group: 1, name: "Ana" }),
|
|
510
|
+
row({ n: 1, group: 1, name: "Bia" }),
|
|
511
|
+
row({ n: 4, group: 2, name: "Zoe" })
|
|
512
|
+
];
|
|
513
|
+
for (const data of rows)
|
|
514
|
+
await f.provider.insert(context, data);
|
|
515
|
+
await f.provider.update(context, {
|
|
516
|
+
...rows[4],
|
|
517
|
+
deletedAt: "2024-01-01T00:00:00.000Z"
|
|
518
|
+
});
|
|
519
|
+
return { ...f, context };
|
|
520
|
+
};
|
|
521
|
+
indexed("an indexed query gives what the scan gives", async () => {
|
|
522
|
+
const { provider, context } = await seeded();
|
|
523
|
+
const queries = [
|
|
524
|
+
["byGroup", { where: { group: 1 } }],
|
|
525
|
+
[
|
|
526
|
+
"byGroup",
|
|
527
|
+
{
|
|
528
|
+
where: { group: 1 },
|
|
529
|
+
orderBy: [{ key: "n" }, { key: "id" }]
|
|
530
|
+
}
|
|
531
|
+
],
|
|
532
|
+
[
|
|
533
|
+
"byGroup",
|
|
534
|
+
{
|
|
535
|
+
where: { group: 1, name: "Ana" },
|
|
536
|
+
orderBy: [{ key: "n", direction: "desc" }, { key: "id" }],
|
|
537
|
+
limit: 2,
|
|
538
|
+
offset: 1
|
|
539
|
+
}
|
|
540
|
+
],
|
|
541
|
+
[
|
|
542
|
+
"byGroup",
|
|
543
|
+
{
|
|
544
|
+
where: { group: 1 },
|
|
545
|
+
orderBy: [{ key: "n" }, { key: "id" }],
|
|
546
|
+
withDeleted: true
|
|
547
|
+
}
|
|
548
|
+
],
|
|
549
|
+
["byName", { where: { name: "Ana" }, orderBy: [{ key: "id" }] }]
|
|
550
|
+
];
|
|
551
|
+
for (const [index, query] of queries) {
|
|
552
|
+
expect(ids(await provider.findMany(context, { ...query, index }))).toEqual(ids(await provider.findMany(context, query)));
|
|
553
|
+
expect(await provider.count(context, {
|
|
554
|
+
...query,
|
|
555
|
+
index
|
|
556
|
+
})).toBe(await provider.count(context, query));
|
|
557
|
+
expect((await provider.findOne(context, {
|
|
558
|
+
...query,
|
|
559
|
+
index
|
|
560
|
+
}))?.id).toBe((await provider.findOne(context, query))?.id);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
});
|
|
463
564
|
const withTransactions = async () => {
|
|
464
565
|
const enabled = options.transactions ?? (await makeProvider()).capabilities?.transactions;
|
|
465
566
|
return enabled === true;
|
|
@@ -584,8 +685,260 @@ function providerContract(runner, makeProvider, options = {}) {
|
|
|
584
685
|
expect(await provider.count(context)).toBe(0);
|
|
585
686
|
});
|
|
586
687
|
});
|
|
688
|
+
describe("versioning", () => {
|
|
689
|
+
const versioned = gated("versioning");
|
|
690
|
+
versioned("update writes with the expected version, and throws RepositoryConcurrencyConflict with a stale one", async () => {
|
|
691
|
+
const { provider, context, seed } = await fixture();
|
|
692
|
+
const data = row({ version: 1 });
|
|
693
|
+
await seed(data);
|
|
694
|
+
await provider.update(context, { ...data, name: "Bia", version: 2 }, { expectedVersion: 1 });
|
|
695
|
+
const stale = await failure(provider.update(context, { ...data, name: "Caio", version: 2 }, { expectedVersion: 1 }));
|
|
696
|
+
expect(stale instanceof RepositoryConcurrencyConflict2).toBe(true);
|
|
697
|
+
const { details } = stale;
|
|
698
|
+
expect([details.id, details.expected]).toEqual([data.id, 1]);
|
|
699
|
+
expect(await provider.findOne(context, { where: { id: data.id } })).toEqual({ ...data, name: "Bia", version: 2 });
|
|
700
|
+
});
|
|
701
|
+
versioned("two updates with the same expected version: exactly one succeeds", async () => {
|
|
702
|
+
const { provider, context, seed } = await fixture();
|
|
703
|
+
const data = row({ version: 1 });
|
|
704
|
+
await seed(data);
|
|
705
|
+
const results = await Promise.allSettled(["Bia", "Caio"].map((name) => provider.update(context, { ...data, name, version: 2 }, { expectedVersion: 1 })));
|
|
706
|
+
const rejected = results.filter((result) => result.status === "rejected");
|
|
707
|
+
expect(rejected.length).toBe(1);
|
|
708
|
+
expect(rejected[0]?.reason instanceof RepositoryConcurrencyConflict2).toBe(true);
|
|
709
|
+
const stored = await provider.findOne(context, {
|
|
710
|
+
where: { id: data.id }
|
|
711
|
+
});
|
|
712
|
+
expect(stored?.version).toBe(2);
|
|
713
|
+
});
|
|
714
|
+
versioned("update without an expected version ignores the stored one", async () => {
|
|
715
|
+
const { provider, context, seed } = await fixture();
|
|
716
|
+
const data = row({ version: 3 });
|
|
717
|
+
await seed(data);
|
|
718
|
+
await provider.update(context, { ...data, name: "Bia", version: 1 });
|
|
719
|
+
expect(await provider.findOne(context, { where: { id: data.id } })).toEqual({ ...data, name: "Bia", version: 1 });
|
|
720
|
+
});
|
|
721
|
+
versioned("an unknown id is still RepositoryEntityNotFound", async () => {
|
|
722
|
+
const { provider, context } = await fixture();
|
|
723
|
+
const error = await failure(provider.update(context, row({ version: 2 }), {
|
|
724
|
+
expectedVersion: 1
|
|
725
|
+
}));
|
|
726
|
+
expect(error instanceof RepositoryEntityNotFound2).toBe(true);
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
describe("atomic", () => {
|
|
730
|
+
const atomic = gated("atomic");
|
|
731
|
+
const increment = (provider, ...args) => provider.increment(...args);
|
|
732
|
+
atomic("increment adds the deltas, stores set and gives the row", async () => {
|
|
733
|
+
const { provider, context, seed } = await fixture();
|
|
734
|
+
const data = row({ visits: 2, age: 30 });
|
|
735
|
+
await seed(data);
|
|
736
|
+
const stored = await increment(provider, context, data.id, { visits: 3, age: -1, n: 5 }, { set: { name: "Bia" } });
|
|
737
|
+
const expected = { ...data, visits: 5, age: 29, n: 5, name: "Bia" };
|
|
738
|
+
expect(stored).toEqual(expected);
|
|
739
|
+
expect(await provider.findOne(context, { where: { id: data.id } })).toEqual(expected);
|
|
740
|
+
});
|
|
741
|
+
atomic("N concurrent increments sum to N", async () => {
|
|
742
|
+
const { provider, context, seed } = await fixture();
|
|
743
|
+
const data = row({ visits: 0 });
|
|
744
|
+
await seed(data);
|
|
745
|
+
await Promise.all(Array.from({ length: 25 }, () => increment(provider, context, data.id, { visits: 1 })));
|
|
746
|
+
const stored = await provider.findOne(context, {
|
|
747
|
+
where: { id: data.id }
|
|
748
|
+
});
|
|
749
|
+
expect(stored?.visits).toBe(25);
|
|
750
|
+
});
|
|
751
|
+
atomic("a missing id is RepositoryEntityNotFound without create, and inserted with it", async () => {
|
|
752
|
+
const { provider, context } = await fixture();
|
|
753
|
+
const data = row({ visits: 1 });
|
|
754
|
+
const missing = await failure(increment(provider, context, data.id, { visits: 1 }));
|
|
755
|
+
expect(missing instanceof RepositoryEntityNotFound2).toBe(true);
|
|
756
|
+
expect(await provider.count(context)).toBe(0);
|
|
757
|
+
const created = await increment(provider, context, data.id, { visits: 1 }, { create: data, set: { name: "Bia" } });
|
|
758
|
+
expect(created).toEqual(data);
|
|
759
|
+
const again = await increment(provider, context, data.id, { visits: 1 }, { create: data });
|
|
760
|
+
expect(again.visits).toBe(2);
|
|
761
|
+
expect(await provider.count(context)).toBe(1);
|
|
762
|
+
});
|
|
763
|
+
atomic("concurrent increments with create lose nothing", async () => {
|
|
764
|
+
const { provider, context } = await fixture();
|
|
765
|
+
const data = row({ visits: 1 });
|
|
766
|
+
await Promise.all(Array.from({ length: 10 }, () => increment(provider, context, data.id, { visits: 1 }, { create: data })));
|
|
767
|
+
const stored = await provider.findOne(context, {
|
|
768
|
+
where: { id: data.id }
|
|
769
|
+
});
|
|
770
|
+
expect(stored?.visits).toBe(10);
|
|
771
|
+
});
|
|
772
|
+
atomic("soft-deleted rows are not incremented", async () => {
|
|
773
|
+
const { provider, context, seed } = await fixture();
|
|
774
|
+
const data = row({ visits: 1, deletedAt: "2024-01-01T00:00:00.000Z" });
|
|
775
|
+
await seed(data);
|
|
776
|
+
const error = await failure(increment(provider, context, data.id, { visits: 1 }));
|
|
777
|
+
expect(error instanceof RepositoryEntityNotFound2).toBe(true);
|
|
778
|
+
const stored = await provider.findOne(context, {
|
|
779
|
+
where: { id: data.id },
|
|
780
|
+
withDeleted: true
|
|
781
|
+
});
|
|
782
|
+
expect(stored?.visits).toBe(1);
|
|
783
|
+
});
|
|
784
|
+
atomic("an increment of version makes an update with the old one conflict", async () => {
|
|
785
|
+
const { provider, context, seed } = await fixture();
|
|
786
|
+
if ((options.versioning ?? provider.capabilities?.versioning) !== true)
|
|
787
|
+
return;
|
|
788
|
+
const data = row({ visits: 0, version: 1 });
|
|
789
|
+
await seed(data);
|
|
790
|
+
const stored = await increment(provider, context, data.id, {
|
|
791
|
+
visits: 1,
|
|
792
|
+
version: 1
|
|
793
|
+
});
|
|
794
|
+
expect(stored.version).toBe(2);
|
|
795
|
+
const error = await failure(provider.update(context, { ...data, name: "Bia", version: 2 }, { expectedVersion: 1 }));
|
|
796
|
+
expect(error instanceof RepositoryConcurrencyConflict2).toBe(true);
|
|
797
|
+
expect((await provider.findOne(context, { where: { id: data.id } }))?.visits).toBe(1);
|
|
798
|
+
});
|
|
799
|
+
atomic("an expired row counts as absent: not found, or replaced by create", async () => {
|
|
800
|
+
const provider = await makeProvider();
|
|
801
|
+
if ((options.expiry ?? provider.capabilities?.expiry) !== true)
|
|
802
|
+
return;
|
|
803
|
+
const now = "2026-06-01T12:00:00.000Z";
|
|
804
|
+
const context = {
|
|
805
|
+
...contextOf(),
|
|
806
|
+
expiry: { key: "expiresAt" }
|
|
807
|
+
};
|
|
808
|
+
const window = row({
|
|
809
|
+
visits: 7,
|
|
810
|
+
expiresAt: "2026-06-01T11:00:00.000Z"
|
|
811
|
+
});
|
|
812
|
+
const live = row({
|
|
813
|
+
visits: 1,
|
|
814
|
+
expiresAt: "2026-06-01T13:00:00.000Z"
|
|
815
|
+
});
|
|
816
|
+
await provider.insert(context, window);
|
|
817
|
+
await provider.insert(context, live);
|
|
818
|
+
const missing = await failure(increment(provider, context, window.id, { visits: 1 }, {
|
|
819
|
+
now
|
|
820
|
+
}));
|
|
821
|
+
expect(missing instanceof RepositoryEntityNotFound2).toBe(true);
|
|
822
|
+
const fresh = {
|
|
823
|
+
id: window.id,
|
|
824
|
+
email: window.email,
|
|
825
|
+
name: "fresh",
|
|
826
|
+
visits: 1,
|
|
827
|
+
expiresAt: "2026-06-01T13:00:00.000Z"
|
|
828
|
+
};
|
|
829
|
+
const created = await increment(provider, context, window.id, { visits: 1 }, { create: fresh, set: { name: "incremented" }, now });
|
|
830
|
+
expect(created).toEqual(fresh);
|
|
831
|
+
const again = await increment(provider, context, window.id, { visits: 1 }, { create: fresh, set: { name: "incremented" }, now });
|
|
832
|
+
expect([again.visits, again.name]).toEqual([2, "incremented"]);
|
|
833
|
+
const counted = await increment(provider, context, live.id, { visits: 1 }, { create: { ...live, visits: 100 }, now });
|
|
834
|
+
expect(counted.visits).toBe(2);
|
|
835
|
+
});
|
|
836
|
+
});
|
|
837
|
+
describe("expiry", () => {
|
|
838
|
+
const expiring = gated("expiry");
|
|
839
|
+
const NOW = "2026-06-01T12:00:00.000Z";
|
|
840
|
+
const seeded = async () => {
|
|
841
|
+
const provider = await makeProvider();
|
|
842
|
+
const context = {
|
|
843
|
+
...contextOf(),
|
|
844
|
+
expiry: { key: "expiresAt" }
|
|
845
|
+
};
|
|
846
|
+
const [past, edge, never, future] = [
|
|
847
|
+
row({ n: 1, expiresAt: "2026-05-01T00:00:00.000Z" }),
|
|
848
|
+
row({ n: 2, expiresAt: NOW }),
|
|
849
|
+
row({ n: 3 }),
|
|
850
|
+
row({ n: 4, expiresAt: "2026-07-01T00:00:00.000Z" })
|
|
851
|
+
];
|
|
852
|
+
for (const data of [past, edge, never, future])
|
|
853
|
+
await provider.insert(context, data);
|
|
854
|
+
return { provider, context, past, edge, never, future };
|
|
855
|
+
};
|
|
856
|
+
expiring("reads leave out rows whose date is at or before now", async () => {
|
|
857
|
+
const { provider, context, past, edge, never, future } = await seeded();
|
|
858
|
+
const query = { now: NOW };
|
|
859
|
+
expect(ids(await provider.findMany(context, query))).toEqual([
|
|
860
|
+
never.id,
|
|
861
|
+
future.id
|
|
862
|
+
]);
|
|
863
|
+
expect(await provider.count(context, query)).toBe(2);
|
|
864
|
+
expect(await provider.findOne(context, {
|
|
865
|
+
...query,
|
|
866
|
+
where: { id: past.id }
|
|
867
|
+
})).toBe(undefined);
|
|
868
|
+
expect(await provider.findOne(context, {
|
|
869
|
+
...query,
|
|
870
|
+
where: { id: edge.id }
|
|
871
|
+
})).toBe(undefined);
|
|
872
|
+
expect((await provider.findOne(context, {
|
|
873
|
+
...query,
|
|
874
|
+
where: { id: future.id }
|
|
875
|
+
}))?.id).toBe(future.id);
|
|
876
|
+
expect(ids(await provider.findMany(context, {
|
|
877
|
+
...query,
|
|
878
|
+
where: { id: { in: [past.id, never.id] } }
|
|
879
|
+
}))).toEqual([never.id]);
|
|
880
|
+
});
|
|
881
|
+
expiring("a row without a date never expires", async () => {
|
|
882
|
+
const { provider, context, never } = await seeded();
|
|
883
|
+
const later = { now: "2999-01-01T00:00:00.000Z" };
|
|
884
|
+
expect(ids(await provider.findMany(context, later))).toEqual([
|
|
885
|
+
never.id
|
|
886
|
+
]);
|
|
887
|
+
expect(await provider.count(context, later)).toBe(1);
|
|
888
|
+
});
|
|
889
|
+
expiring("withExpired gives the expired rows too", async () => {
|
|
890
|
+
const { provider, context, past, edge, never, future } = await seeded();
|
|
891
|
+
const query = { now: NOW, withExpired: true };
|
|
892
|
+
expect(ids(await provider.findMany(context, query))).toEqual([
|
|
893
|
+
past.id,
|
|
894
|
+
edge.id,
|
|
895
|
+
never.id,
|
|
896
|
+
future.id
|
|
897
|
+
]);
|
|
898
|
+
expect(await provider.count(context, query)).toBe(4);
|
|
899
|
+
expect((await provider.findOne(context, {
|
|
900
|
+
...query,
|
|
901
|
+
where: { id: past.id }
|
|
902
|
+
}))?.id).toBe(past.id);
|
|
903
|
+
});
|
|
904
|
+
expiring("leaves expired rows out of a read after a position", async () => {
|
|
905
|
+
const enabled = options.keyset ?? (await makeProvider()).capabilities?.keyset;
|
|
906
|
+
if (enabled !== true)
|
|
907
|
+
return;
|
|
908
|
+
const { provider, context, past, edge, never, future } = await seeded();
|
|
909
|
+
const orderBy = [{ key: "n" }, { key: "id" }];
|
|
910
|
+
expect(ids(await provider.findMany(context, {
|
|
911
|
+
now: NOW,
|
|
912
|
+
orderBy,
|
|
913
|
+
after: [0, ""]
|
|
914
|
+
}))).toEqual([never.id, future.id]);
|
|
915
|
+
expect(ids(await provider.findMany(context, {
|
|
916
|
+
now: NOW,
|
|
917
|
+
orderBy,
|
|
918
|
+
after: [1, past.id],
|
|
919
|
+
withExpired: true
|
|
920
|
+
}))).toEqual([edge.id, never.id, future.id]);
|
|
921
|
+
});
|
|
922
|
+
expiring("unique keys still count expired rows", async () => {
|
|
923
|
+
const { provider, context, past } = await seeded();
|
|
924
|
+
const error = await failure(provider.insert(context, row({ email: past.email })));
|
|
925
|
+
expect(error instanceof RepositoryUniqueViolation2).toBe(true);
|
|
926
|
+
expect(error.details.key).toBe("email");
|
|
927
|
+
expect(await provider.count(context, { now: NOW, withExpired: true })).toBe(4);
|
|
928
|
+
});
|
|
929
|
+
expiring("an entity that does not expire ignores now", async () => {
|
|
930
|
+
const { provider, context, seed } = await fixture();
|
|
931
|
+
const data = row({ expiresAt: "2000-01-01T00:00:00.000Z" });
|
|
932
|
+
await seed(data);
|
|
933
|
+
expect(ids(await provider.findMany(context, { now: NOW }))).toEqual([
|
|
934
|
+
data.id
|
|
935
|
+
]);
|
|
936
|
+
expect(await provider.count(context, { now: NOW })).toBe(1);
|
|
937
|
+
});
|
|
938
|
+
});
|
|
587
939
|
});
|
|
588
940
|
}
|
|
589
941
|
export {
|
|
942
|
+
memoryPublisher,
|
|
590
943
|
providerContract
|
|
591
944
|
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { IEventPublisher, PublishedEvent } from "../core/event-policy/types/event-policy.interface.js";
|
|
2
|
+
/** A publisher that keeps what it is given, for tests. */
|
|
3
|
+
export interface MemoryPublisher extends IEventPublisher {
|
|
4
|
+
/** Each publish, in order. */
|
|
5
|
+
readonly published: readonly {
|
|
6
|
+
readonly topic: string;
|
|
7
|
+
readonly event: PublishedEvent;
|
|
8
|
+
}[];
|
|
9
|
+
/** Forgets what was published. */
|
|
10
|
+
clear(): void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A publisher for `applicationOf({ publishers })` in tests: it keeps each
|
|
14
|
+
* `{ topic, event }` it is given in `published`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function memoryPublisher(): MemoryPublisher;
|
|
@@ -18,6 +18,20 @@ export interface ProviderContractOptions {
|
|
|
18
18
|
* to `provider.capabilities?.keyset`.
|
|
19
19
|
*/
|
|
20
20
|
readonly keyset?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Run the cases of declared indexes (`context.indexes`, `query.index`).
|
|
23
|
+
* Defaults to `provider.capabilities?.indexes`.
|
|
24
|
+
*/
|
|
25
|
+
readonly indexes?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Run the cases of an expected `version` on `update`. Defaults to
|
|
28
|
+
* `provider.capabilities?.versioning`.
|
|
29
|
+
*/
|
|
30
|
+
readonly versioning?: boolean;
|
|
31
|
+
/** Run the cases of `increment`. Defaults to `provider.capabilities?.atomic`. */
|
|
32
|
+
readonly atomic?: boolean;
|
|
33
|
+
/** Run the cases of expired records. Defaults to `provider.capabilities?.expiry`. */
|
|
34
|
+
readonly expiry?: boolean;
|
|
21
35
|
}
|
|
22
36
|
/**
|
|
23
37
|
* 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.
|
|
3
|
+
"version": "0.0.8",
|
|
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",
|
|
@@ -84,8 +84,9 @@
|
|
|
84
84
|
"test:roundtrip": "bun run --cwd packages/codegen test:roundtrip",
|
|
85
85
|
"test:mcp": "bun test --cwd packages/mcp",
|
|
86
86
|
"test:hono": "bun test --cwd packages/hono",
|
|
87
|
+
"test:ws": "bun test --cwd packages/ws",
|
|
87
88
|
"test:drizzle": "bun test --cwd packages/drizzle",
|
|
88
|
-
"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/drizzle build"
|
|
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"
|
|
89
90
|
},
|
|
90
91
|
"dependencies": {
|
|
91
92
|
"slugify": "^1.6.9"
|