Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f96a03d
initial commit
aminnairi Nov 23, 2025
182ca81
updated the query example according to the new command addition
aminnairi Nov 23, 2025
2528df4
added a way to define a command
aminnairi Nov 23, 2025
a3f8b6b
using the inmemory command constructor
aminnairi Nov 23, 2025
91236a3
updated the shape of the fact
aminnairi Nov 23, 2025
a2c35d3
updated the shape of the fact
aminnairi Nov 23, 2025
3ae6331
updated the shape of the fact
aminnairi Nov 23, 2025
71e6063
fixed tests according to the recent change in the fact shape
aminnairi Nov 23, 2025
b3fdcd4
now exporting a command class for quickly creating command
aminnairi Nov 23, 2025
3da714c
updated the docs according to the recent command & fact shape change
aminnairi Nov 23, 2025
71eebd9
fixed examples according to the recent changes
aminnairi Nov 24, 2025
44c6a27
better usage documentation
aminnairi Nov 24, 2025
a70dd88
renamed the register method to registerQuery
aminnairi Nov 24, 2025
c333a8f
updated the docs
aminnairi Nov 24, 2025
9d964b1
added changelog for version 2.0.0
aminnairi Nov 24, 2025
cc6a9fc
updated tests to reach 100% code coverage
aminnairi Nov 24, 2025
9e4ca6f
updated package to version 2.0.0
aminnairi Nov 24, 2025
65f6d06
fixed indentation level for headings
aminnairi Nov 24, 2025
dc09967
fixed duplicate heading
aminnairi Nov 24, 2025
243967e
initial commit
aminnairi Nov 24, 2025
f7b0b39
fixed strict tsconfig linter errors
aminnairi Nov 24, 2025
fca9ebe
updated the release date
aminnairi Nov 27, 2025
ae74d33
removed unecessary modules
aminnairi Nov 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 63 additions & 112 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Sourcing & CQRS design patterns while reducing the friction of implementation.
- Ready for deployment in clusters thanks to optimistic locking
- Database agnostic, use files, SQL, NoSQL, IndexedDB, LocalStorage, etc...
- Event Sourcing inspired to prevent data loss and enable smarter analytics
- Query implementation for CQRS applications
- Query & Command implementation for CQRS applications
- Easy initialization of Queries from past events useful after application restart
- No migration script required, evolve your data model as your project evolve

Expand All @@ -40,39 +40,26 @@ Sourcing & CQRS design patterns while reducing the friction of implementation.
### Install the packages

```bash
npm install tsx @aminnairi/facts zod
npm install tsx @aminnairi/facts
```

Note: `zod` is used here and in some examples for robust parsing, but it is entirely optional. You can use any parsing library or method you prefer.
## Create the source file

### Import the package

```typescript
import {
MemoryFactStore,
SqliteFactStore,
ParseError,
FactShape,
Query,
} from "@aminnairi/facts";
import { randomUUID } from "crypto";
import { z } from "zod";
```bash
touch index.ts
```

### Define facts

First, define the structure of your facts using TypeScript interfaces.

```typescript
const streamIdentifier = randomUUID();
import { FactShape } from "@aminnairi/facts";

interface TodoAddedV1Fact extends FactShape {
name: "todo-added";
version: 1;
stream: {
name: "todo";
identifier: string;
};
streamName: "todo";
payload: {
name: string;
done: boolean;
Expand All @@ -82,92 +69,30 @@ interface TodoAddedV1Fact extends FactShape {
interface TodoRemovedV1Fact extends FactShape {
name: "todo-removed";
version: 1;
stream: {
name: "todo";
identifier: string;
};
streamName: "todo";
payload: null;
}

type TodoFact = TodoAddedV1Fact | TodoRemovedV1Fact;
```

### Define a fact schema (optional, but recommended)

For robust parsing, especially when using a persistent store like SQLite, it's highly recommended to define a schema for your facts using a library like `zod`. (Note: `zod` is used here as an example; any other parsing library or custom parsing logic can be used.)

```typescript
const TodoAddedV1FactSchema = z.object({
identifier: z.string().uuid(),
name: z.literal("todo-added"),
version: z.literal(1),
date: z.date(),
position: z.number().int().min(0),
stream: z.object({
name: z.literal("todo"),
identifier: z.string().uuid(),
}),
payload: z.object({
name: z.string(),
done: z.boolean(),
}),
});

const TodoRemovedV1FactSchema = z.object({
identifier: z.string().uuid(),
name: z.literal("todo-removed"),
version: z.literal(1),
date: z.date(),
position: z.number().int().min(0),
stream: z.object({
name: z.literal("todo"),
identifier: z.string().uuid(),
}),
payload: z.null(),
});

const TodoFactSchema = z.union([
TodoAddedV1FactSchema,
TodoRemovedV1FactSchema,
]);
```

### Initialize the store

You can use the in-memory store for development and testing, or the SQLite store for production.

```typescript
// In-memory store
const factStore = new MemoryFactStore<TodoFact>();

// SQLite store
const factParser = (fact: unknown): TodoFact | ParseError => {
const json = JSON.parse(fact as string, (key, value) => {
if (key === "date" && typeof value === "string") {
return new Date(value);
}
return value;
});

const result = TodoFactSchema.safeParse(json);
if (!result.success) {
return new ParseError();
}
return result.data as TodoFact;
};

const sqliteFactStore = SqliteFactStore.for<TodoFact>("./todo.sqlite", {
parser: factParser,
});
import { FactShape, MemoryFactStore } from "@aminnairi/facts";

// process.on("exit", () => sqliteFactStore.close());
const factStore = new MemoryFactStore<TodoFact>();
```

### Define a query (optional)
### Define a query

Queries are used to build read models from your facts.

```typescript
import { Query, match } from "@aminnairi/facts";

interface Todo {
identifier: string;
name: string;
Expand All @@ -178,17 +103,18 @@ class MemoryTodosQuery implements Query<TodoFact, Todo[]> {
public constructor(private readonly todos: Map<string, Todo> = new Map()) {}

public async handle(fact: TodoFact): Promise<void> {
if (fact.name === "todo-added") {
this.todos.set(fact.stream.identifier, {
identifier: fact.stream.identifier,
name: fact.payload.name,
done: fact.payload.done,
});
}

if (fact.name === "todo-removed") {
this.todos.delete(fact.stream.identifier);
}
match(fact, {
"tood-added": (todoAddedFact) => {
this.todos.set(fact.stream.identifier, {
identifier: fact.stream.identifier,
name: fact.payload.name,
done: fact.payload.done,
});
},
"todo-removed": (todoRemovedFact) => {
this.todos.delete(fact.stream.identifier);
},
});
}

public async fetch(): Promise<Todo[]> {
Expand All @@ -197,11 +123,29 @@ class MemoryTodosQuery implements Query<TodoFact, Todo[]> {
}

const todosQuery = new MemoryTodosQuery();
```

### Define commands

```typescript
import { MemoryCommand } from "@aminnairi/facts";

const addTodoCommand = new MemoryCommand<TodoAddedV1Fact>();
const removeTodoCommand = new MemoryCommand<todoRemovedFact>();
```

### Initialize the store

```typescript
factStore.register(todosQuery);

const initResult = await factStore.initialize();
if (initResult instanceof Error) {
console.error("Failed to initialize query:", initResult);
factStore.registerCommand(addTodoCommand);
factStore.registerCommand(removeTodoCommand);

const error = await factStore.initialize();

if (error instanceof Error) {
console.error("Failed to initialize queries:", error);
}
```

Expand All @@ -210,7 +154,7 @@ if (initResult instanceof Error) {
Save facts to the store. The `position` property is used for optimistic locking.

```typescript
await factStore.save({
await addTodoCommand.run({
identifier: randomUUID(),
name: "todo-added",
version: 1,
Expand All @@ -226,7 +170,7 @@ await factStore.save({
},
});

await factStore.save({
await removeTodoCommand.run({
identifier: randomUUID(),
name: "todo-removed",
version: 1,
Expand All @@ -245,20 +189,21 @@ await factStore.save({
You can retrieve facts from the store using `find` and `findFromLast`.

```typescript
const result = await factStore.find(
(fact) => fact.stream.identifier === streamIdentifier,
);
const result = await factStore.find((fact) => {
fact.stream.identifier === streamIdentifier;
});

if (result instanceof Error) {
console.error("Failed to find facts:", result);
} else {
for (const fact of result) {
console.log(fact.name, fact.payload);
}
process.exit(1);
}

for (const fact of result) {
console.log(fact.name, fact.payload);
}
```

### Fetch data (optional)
### Fetch data

Fetch the read model from your query.

Expand All @@ -270,6 +215,12 @@ for (const todo of todos) {
}
```

## Run the script

```bash
npx tsx index.ts
```

## ✍️ Examples

- [Fact store using the provided SQLite implementation](packages/examples/sqlite/store.ts)
Expand Down
69 changes: 69 additions & 0 deletions packages/examples/memory/command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { FactShape, MemoryFactStore, MemoryCommand } from "@aminnairi/facts"
import { randomUUID } from "crypto"

interface TodoAddedV1Fact extends FactShape {
name: "todo-added"
version: 1
streamName: "todo"
payload: {
name: string
done: boolean
}
}

interface TodoRemovedV1Fact extends FactShape {
name: "todo-removed"
version: 1
streamName: "todo"
payload: null
}

type TodoFact =
| TodoAddedV1Fact
| TodoRemovedV1Fact

const addTodoCommand = new MemoryCommand<TodoAddedV1Fact>
const removeTodoCommand = new MemoryCommand<TodoRemovedV1Fact>
const factStore = new MemoryFactStore<TodoFact>

factStore.registerCommand(addTodoCommand)
factStore.registerCommand(removeTodoCommand)

let error = await addTodoCommand.send({
name: "todo-added",
date: new Date(),
identifier: randomUUID(),
position: 0,
version: 1,
streamName: "todo",
streamIdentifier: randomUUID(),
payload: {
name: "Do the dishes",
done: true
}
})

if (error instanceof Error) {
console.error(error)
process.exit(1)
}

error = await removeTodoCommand.send({
name: "todo-removed",
date: new Date(),
identifier: randomUUID(),
position: 0,
version: 1,
streamName: "todo",
streamIdentifier: randomUUID(),
payload: null
})

if (error instanceof Error) {
console.error(error)
process.exit(1)
}

const facts = await factStore.find()

console.log(facts)
Loading