Skip to content

Commit 6c533a5

Browse files
committed
Added first draft of Api Specification
1 parent 4e3d34a commit 6c533a5

File tree

5 files changed

+201
-1
lines changed

5 files changed

+201
-1
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/* eslint-disable @typescript-eslint/no-floating-promises */
2+
import {
3+
getInMemoryEventStore,
4+
type EventStore,
5+
} from '@event-driven-io/emmett';
6+
import {
7+
ApiSpecification,
8+
getApplication,
9+
} from '@event-driven-io/emmett-expressjs';
10+
import { describe, it } from 'node:test';
11+
import { v4 as uuid } from 'uuid';
12+
import type { PricedProductItem, ShoppingCartEvent } from '../events';
13+
import { shoppingCartApi } from './simpleApi';
14+
15+
const getUnitPrice = (_productId: string) => {
16+
return Promise.resolve(100);
17+
};
18+
19+
const given = ApiSpecification.for<ShoppingCartEvent>(
20+
() => getInMemoryEventStore(),
21+
(eventStore: EventStore) =>
22+
getApplication({ apis: [shoppingCartApi(eventStore, getUnitPrice)] }),
23+
);
24+
25+
describe('ShoppingCart', () => {
26+
describe('When empty', () => {
27+
it('should add product item', () => {
28+
const clientId = uuid();
29+
return given([])
30+
.when((request) =>
31+
request
32+
.post(`/clients/${clientId}/shopping-carts/current/product-items`)
33+
.send(productItem),
34+
)
35+
.then([
36+
{
37+
streamName: shoppingCartId,
38+
events: [
39+
{
40+
type: 'ProductItemAddedToShoppingCart',
41+
data: {
42+
shoppingCartId,
43+
productItem,
44+
addedAt: now,
45+
},
46+
},
47+
],
48+
},
49+
]);
50+
});
51+
});
52+
53+
// describe('When opened', () => {
54+
// it('should confirm', () => {
55+
// given({
56+
// type: 'ProductItemAddedToShoppingCart',
57+
// data: {
58+
// shoppingCartId,
59+
// productItem,
60+
// addedAt: oldTime,
61+
// },
62+
// })
63+
// .when({
64+
// type: 'AddProductItemToShoppingCart',
65+
// data: {
66+
// shoppingCartId,
67+
// productItem,
68+
// },
69+
// metadata: { now },
70+
// })
71+
// .then([
72+
// {
73+
// type: 'ProductItemAddedToShoppingCart',
74+
// data: {
75+
// shoppingCartId,
76+
// productItem,
77+
// addedAt: now,
78+
// },
79+
// },
80+
// ]);
81+
// });
82+
// });
83+
84+
// describe('When confirmed', () => {
85+
// it('should not add products', () => {
86+
// given([
87+
// {
88+
// type: 'ProductItemAddedToShoppingCart',
89+
// data: {
90+
// shoppingCartId,
91+
// productItem,
92+
// addedAt: oldTime,
93+
// },
94+
// },
95+
// {
96+
// type: 'ShoppingCartConfirmed',
97+
// data: { shoppingCartId, confirmedAt: oldTime },
98+
// },
99+
// ])
100+
// .when({
101+
// type: 'AddProductItemToShoppingCart',
102+
// data: {
103+
// shoppingCartId,
104+
// productItem,
105+
// },
106+
// metadata: { now },
107+
// })
108+
// .thenThrows(
109+
// (error: Error) => error.message === 'Shopping Cart already closed',
110+
// );
111+
// });
112+
// });
113+
114+
const getRandomProduct = (): PricedProductItem => {
115+
return {
116+
productId: uuid(),
117+
unitPrice: Math.random() * 10,
118+
quantity: Math.random() * 10,
119+
};
120+
};
121+
const oldTime = new Date();
122+
const now = new Date();
123+
const shoppingCartId = uuid();
124+
125+
const productItem = getRandomProduct();
126+
});

packages/emmett-expressjs/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { problemDetailsMiddleware } from './middlewares/problemDetailsMiddleware
1212

1313
export * from './etag';
1414
export * from './handler';
15+
export * from './testing';
1516

1617
export type ErrorToProblemDetailsMapping = (
1718
error: Error,
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import type { Event, EventStore, Flavour } from '@event-driven-io/emmett';
2+
import { type Application } from 'express';
3+
import assert from 'node:assert/strict';
4+
import type { Response, Test } from 'supertest';
5+
import supertest from 'supertest';
6+
import type TestAgent from 'supertest/lib/agent';
7+
8+
export type TestEventStream<EventType extends Event = Event> = {
9+
streamName: string;
10+
events: EventType[];
11+
};
12+
13+
export type ApiSpecification<EventType extends Event = Event> = (
14+
givenStreams: TestEventStream<EventType>[],
15+
) => {
16+
when: (setupRequest: (request: TestAgent<supertest.Test>) => Test) => {
17+
then: (verify: ApiSpecificationAssert<EventType>) => Promise<void>;
18+
};
19+
};
20+
21+
export type ApiSpecificationAssert<EventType extends Event = Event> =
22+
| Flavour<TestEventStream<EventType>[], 'appendedEvents'>
23+
| Flavour<(response: Response) => boolean, 'responseAssert'>
24+
| Flavour<
25+
{
26+
events: TestEventStream<EventType>[];
27+
responseMatches: (response: Response) => boolean;
28+
},
29+
'fullAssert'
30+
>;
31+
32+
export const ApiSpecification = {
33+
for: <EventType extends Event = Event>(
34+
getEventStore: () => EventStore,
35+
getApplication: (eventStore: EventStore) => Application,
36+
): ApiSpecification<EventType> => {
37+
{
38+
return (givenStreams: TestEventStream<EventType>[]) => {
39+
return {
40+
when: (
41+
setupRequest: (request: TestAgent<supertest.Test>) => Test,
42+
) => {
43+
const handle = async () => {
44+
const eventStore = getEventStore();
45+
const application = getApplication(eventStore);
46+
47+
for (const { streamName, events } of givenStreams) {
48+
await eventStore.appendToStream(streamName, events);
49+
}
50+
51+
return setupRequest(supertest(application));
52+
};
53+
54+
return {
55+
then: async (
56+
verify: ApiSpecificationAssert<EventType>,
57+
): Promise<void> => {
58+
const response = await handle();
59+
60+
if (verify.__brand === 'responseAssert') {
61+
assert.ok(verify(response));
62+
} else if (verify.__brand === 'fullAssert') {
63+
assert.ok(verify.responseMatches(response));
64+
}
65+
},
66+
};
67+
},
68+
};
69+
};
70+
}
71+
},
72+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './apiSpecification';

packages/emmett/src/eventStore/eventStore.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export interface EventStore<StreamVersion = DefaultStreamVersionType> {
1414
): Promise<ReadStreamResult<EventType, StreamVersion>>;
1515

1616
appendToStream<EventType extends Event>(
17-
streamId: string,
17+
streamName: string,
1818
events: EventType[],
1919
options?: AppendToStreamOptions<StreamVersion>,
2020
): Promise<AppendToStreamResult<StreamVersion>>;

0 commit comments

Comments
 (0)