diff --git a/.markdownlint.json b/.markdownlint.json
new file mode 100644
index 00000000..4164bb5a
--- /dev/null
+++ b/.markdownlint.json
@@ -0,0 +1,5 @@
+{
+ "MD013": false,
+ "MD034": false,
+ "MD060": false
+}
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 93a3cc49..7d1bd1d1 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -11,11 +11,12 @@
-
-
-
-
-
+
+
+
+
+
+
@@ -24,12 +25,14 @@
-
+
+
+
-
-
-
+
+
+
@@ -37,7 +40,7 @@
-
-
+
+
diff --git a/Library/Composition/Composition.csproj b/Library/Composition/Composition.csproj
index 2f002d05..a8deab0d 100644
--- a/Library/Composition/Composition.csproj
+++ b/Library/Composition/Composition.csproj
@@ -1,4 +1,4 @@
-
+
Exe
net10.0
@@ -14,6 +14,8 @@
+
+
diff --git a/Library/Composition/Program.cs b/Library/Composition/Program.cs
index e68cca99..18b71ae0 100644
--- a/Library/Composition/Program.cs
+++ b/Library/Composition/Program.cs
@@ -29,7 +29,7 @@
.WithEnvironment("VAULT_DEV_ROOT_TOKEN_ID", "root")
.WithEnvironment("VAULT_DEV_LISTEN_ADDRESS", "0.0.0.0:8200")
.WithArgs("server", "-dev")
- .WithHttpEndpoint(targetPort: 8200, name: "http");
+ .WithHttpEndpoint(port: 8200, targetPort: 8200, name: "http");
// Chronicle — storage backend is selected dynamically based on DATABASE_TYPE.
// When no configure callback is supplied AddCratisChronicle uses the development image (embedded MongoDB).
@@ -37,12 +37,12 @@
IResourceBuilder chronicle;
if (string.Equals(databaseType, "postgresql", StringComparison.Ordinal))
{
- var db = builder.AddPostgres("postgres").AddDatabase("chronicle");
+ var db = builder.AddPostgres("postgres").AddDatabase("chronicle-db");
chronicle = builder.AddCratisChronicle("chronicle", c => c.WithPostgreSql(db));
}
else if (string.Equals(databaseType, "mssql", StringComparison.Ordinal))
{
- var db = builder.AddSqlServer("mssql").AddDatabase("chronicle");
+ var db = builder.AddSqlServer("mssql").AddDatabase("chronicle-db");
chronicle = builder.AddCratisChronicle("chronicle", c => c.WithMsSql(db));
}
else if (string.Equals(databaseType, "sqlite", StringComparison.Ordinal))
@@ -51,17 +51,21 @@
}
else
{
- // mongodb — use the development image which bundles MongoDB so no extra container is needed
- chronicle = builder.AddCratisChronicle("chronicle");
+ // mongodb — the latest-development image no longer bundles MongoDB, so spin up a dedicated container
+ var mongo = builder.AddMongoDB("mongodb").AddDatabase("chronicle-db");
+ chronicle = builder.AddCratisChronicle("chronicle", c => c.WithMongoDB(mongo));
}
-// Wire Vault compliance key storage into Chronicle
+// Wire Vault compliance key storage into Chronicle and pin to stable host ports.
+// Management (workbench) runs on 8080, gRPC on 35000 — same as the container target ports.
chronicle
.WithEnvironment(
"Cratis__Chronicle__Compliance__KeyStore__Vault__Address",
vault.GetEndpoint("http"))
.WithEnvironment("Cratis__Chronicle__Compliance__KeyStore__Vault__Token", "root")
- .WaitFor(vault);
+ .WaitFor(vault)
+ .WithEndpoint("management", endpoint => endpoint.Port = 8080)
+ .WithEndpoint("grpc", endpoint => endpoint.Port = 35000);
// Keycloak for Lending - pre-seeded with librarian and borrower users
var keycloakLending = builder.AddContainer("keycloak-lending", "quay.io/keycloak/keycloak")
@@ -69,7 +73,7 @@
.WithBindMount("./keycloak/lending", "/opt/keycloak/data/import", isReadOnly: true)
.WithEnvironment("KEYCLOAK_ADMIN", "admin")
.WithEnvironment("KEYCLOAK_ADMIN_PASSWORD", "admin")
- .WithHttpEndpoint(targetPort: 8080, name: "http");
+ .WithHttpEndpoint(port: 8090, targetPort: 8080, name: "http");
// Keycloak for Members - pre-seeded with member users
var keycloakMembers = builder.AddContainer("keycloak-members", "quay.io/keycloak/keycloak")
@@ -77,7 +81,7 @@
.WithBindMount("./keycloak/members", "/opt/keycloak/data/import", isReadOnly: true)
.WithEnvironment("KEYCLOAK_ADMIN", "admin")
.WithEnvironment("KEYCLOAK_ADMIN_PASSWORD", "admin")
- .WithHttpEndpoint(targetPort: 8080, name: "http");
+ .WithHttpEndpoint(port: 8091, targetPort: 8080, name: "http");
// Lending backend - connected to Chronicle; projection sink type flows from DATABASE_TYPE
var lending = builder.AddProject("lending")
@@ -94,12 +98,12 @@
// Lending frontend (Vite dev server via yarn, port 9000)
var lendingFrontend = builder.AddViteApp("lending-frontend", "../Lending")
.WithYarn()
- .WithHttpEndpoint(targetPort: 9000, name: "http");
+ .WithHttpEndpoint(port: 9000, targetPort: 9000, name: "http", isProxied: false);
// Members frontend (Vite dev server via yarn, port 9001)
var membersFrontend = builder.AddViteApp("members-frontend", "../Members")
.WithYarn()
- .WithHttpEndpoint(targetPort: 9001, name: "http");
+ .WithHttpEndpoint(port: 9001, targetPort: 9001, name: "http", isProxied: false);
var keycloakLendingEndpoint = keycloakLending.GetEndpoint("http");
var keycloakMembersEndpoint = keycloakMembers.GetEndpoint("http");
@@ -108,7 +112,7 @@
// WithOidcProvider only accepts a static string for authority; we override it with a
// WithEnvironment callback so Aspire resolves the dynamic Keycloak endpoint at startup.
builder.AddAuthProxy("authproxy-lending")
- .WithHttpEndpoint(targetPort: 8080, name: "http")
+ .WithHttpEndpoint(port: 7000, targetPort: 8080, name: "http")
.WithBackend("main", lending)
.WithFrontend("main", lendingFrontend)
.WithOidcProvider(
@@ -129,7 +133,7 @@
// WithOidcProvider only accepts a static string for authority; we override it with a
// WithEnvironment callback so Aspire resolves the dynamic Keycloak endpoint at startup.
builder.AddAuthProxy("authproxy-members")
- .WithHttpEndpoint(targetPort: 8080, name: "http")
+ .WithHttpEndpoint(port: 7001, targetPort: 8080, name: "http")
.WithBackend("main", members)
.WithFrontend("main", membersFrontend)
.WithOidcProvider(
diff --git a/Library/Lending/App.tsx b/Library/Lending/.frontend/App.tsx
similarity index 79%
rename from Library/Lending/App.tsx
rename to Library/Lending/.frontend/App.tsx
index 5c986fac..29beb51f 100644
--- a/Library/Lending/App.tsx
+++ b/Library/Lending/.frontend/App.tsx
@@ -1,17 +1,17 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
-import { useTheme } from './Utils/useTheme';
+import { useTheme } from '../Utils/useTheme';
import { BrowserRouter, Route, Routes } from "react-router-dom";
-import { LayoutProvider } from './Layout/Default/context/LayoutContext';
+import { LayoutProvider } from '../Layout/Default/context/LayoutContext';
import { DialogComponents } from '@cratis/arc.react/dialogs';
import { BusyIndicatorDialog, ConfirmationDialog } from 'Components/Dialogs';
-import { DefaultLayout } from './Layout/Default/DefaultLayout';
-import { IMenuItemGroup } from './Layout/Default/Sidebar/MenuItem/MenuItem';
-import { Home } from './Home';
+import { DefaultLayout } from '../Layout/Default/DefaultLayout';
+import { IMenuItemGroup } from '../Layout/Default/Sidebar/MenuItem/MenuItem';
+import { Home } from '../Home';
import * as mdIcons from 'react-icons/md';
-import { Authors } from './Authors/Authors';
-import { Inventory } from './Inventory/Inventory';
+import { Authors } from '../Authors/Authors';
+import { Inventory } from '../Inventory/Inventory';
function App() {
useTheme();
diff --git a/Library/Lending/index.css b/Library/Lending/.frontend/index.css
similarity index 100%
rename from Library/Lending/index.css
rename to Library/Lending/.frontend/index.css
diff --git a/Library/Lending/index.html b/Library/Lending/.frontend/index.html
similarity index 100%
rename from Library/Lending/index.html
rename to Library/Lending/.frontend/index.html
diff --git a/Library/Members/index.tsx b/Library/Lending/.frontend/index.tsx
similarity index 94%
rename from Library/Members/index.tsx
rename to Library/Lending/.frontend/index.tsx
index fce72276..9a15c406 100644
--- a/Library/Members/index.tsx
+++ b/Library/Lending/.frontend/index.tsx
@@ -9,7 +9,7 @@ import './index.css';
import React from 'react';
import App from "./App";
import { configure as configureMobx } from 'mobx';
-import { Bindings } from './Bindings';
+import { Bindings } from '../Bindings';
Bindings.initialize();
diff --git a/Library/Lending/.frontend/tsconfig.json b/Library/Lending/.frontend/tsconfig.json
index 25cfcdaa..319298a5 100644
--- a/Library/Lending/.frontend/tsconfig.json
+++ b/Library/Lending/.frontend/tsconfig.json
@@ -1,5 +1,5 @@
{
- "extends": "../../.frontend/tsconfig.base.json",
+ "extends": "../../../.frontend/tsconfig.base.json",
"compilerOptions": {
"allowImportingTsExtensions": true,
"baseUrl": "..",
diff --git a/Library/Lending/.frontend/vite.config.ts b/Library/Lending/.frontend/vite.config.ts
index 9dd0ef18..c2a77e2b 100644
--- a/Library/Lending/.frontend/vite.config.ts
+++ b/Library/Lending/.frontend/vite.config.ts
@@ -50,7 +50,7 @@ export default defineConfig({
},
exclude: ['../dist/**', '../node_modules/**', 'node_modules/**', '../wwwroot/**', 'wwwroot/**', '../**/given/**'],
include: ['../**/for_*/when_*/**/*.ts', '../**/for_*/**/when_*.ts'],
- setupFiles: fileURLToPath(new URL('../../.frontend/vitest.setup.ts', import.meta.url))
+ setupFiles: fileURLToPath(new URL('../../../.frontend/vitest.setup.ts', import.meta.url))
},
plugins: [
react(),
diff --git a/Library/Lending/Authors/Authors.tsx b/Library/Lending/Authors/Authors.tsx
index ed5c2919..b834789b 100644
--- a/Library/Lending/Authors/Authors.tsx
+++ b/Library/Lending/Authors/Authors.tsx
@@ -13,7 +13,7 @@ export const Authors = () => {
return (
- showAddAuthorDialog()} />
+ showAddAuthorDialog()} />
diff --git a/Library/Lending/Authors/Listing/Listing.cs b/Library/Lending/Authors/Listing/Listing.cs
index 8840a411..76f56a88 100644
--- a/Library/Lending/Authors/Listing/Listing.cs
+++ b/Library/Lending/Authors/Listing/Listing.cs
@@ -1,7 +1,9 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+using Cratis.Arc.MongoDB;
using Library.Authors.Registration;
+using MongoDB.Driver;
namespace Library.Authors.Listing;
@@ -13,12 +15,21 @@ namespace Library.Authors.Listing;
[ReadModel]
public record Author(AuthorId Id, AuthorName Name)
{
- public static IObservable> AllAuthors(IMaterializedReadModels readModels) =>
- readModels.ObserveInstances();
+ ///
+ /// Observes all authors, pushing updates when the collection changes.
+ ///
+ /// The MongoDB collection to observe.
+ /// A subject that emits the full author list on each change.
+ public static ISubject> ObserveAll(IMongoCollection collection) =>
+ collection.Observe();
}
+///
+/// Defines the projection from to .
+///
public class AuthorProjection : IProjectionFor
{
+ ///
public void Define(IProjectionBuilderFor builder) => builder
.AutoMap()
.From();
diff --git a/Library/Lending/Authors/Listing/Listing.tsx b/Library/Lending/Authors/Listing/Listing.tsx
index 7037dca6..5aebcbe4 100644
--- a/Library/Lending/Authors/Listing/Listing.tsx
+++ b/Library/Lending/Authors/Listing/Listing.tsx
@@ -1,21 +1,21 @@
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
-import { AllAuthors } from './AllAuthors';
+import { ObserveAll } from './ObserveAll';
const pageSize = 5;
export const Listing = () => {
- const [allAuthorsResult, , setPage] = AllAuthors.useWithPaging(pageSize);
+ const [observeAllResult, , setPage] = ObserveAll.useWithPaging(pageSize);
return (
setPage(e.page ?? 0)}
scrollable
scrollHeight={'flex'}
diff --git a/Library/Lending/Authors/Listing/AllAuthors.ts b/Library/Lending/Authors/Listing/ObserveAll.ts
similarity index 58%
rename from Library/Lending/Authors/Listing/AllAuthors.ts
rename to Library/Lending/Authors/Listing/ObserveAll.ts
index 315dbbf2..d1a5e8b8 100644
--- a/Library/Lending/Authors/Listing/AllAuthors.ts
+++ b/Library/Lending/Authors/Listing/ObserveAll.ts
@@ -1,4 +1,4 @@
-// @generated by Cratis. Source: Library.Authors.Listing.Author. Time: 2026-05-13T07:54:54.4805020Z. Hash: 98FD86C4008F8645846EC4AB47289EBDCB0B783904EB29941696502A4649A884
+// @generated by Cratis. Source: Library.Authors.Listing.Author. Time: 2026-06-07T08:39:08.3578420Z. Hash: BF1C3E5E079CAD421FEDD9AF73E16A95AD45E5D08894D49DD8DC2E2EE7C51796
/*---------------------------------------------------------------------------------------------
* **DO NOT EDIT** - This file is an automatically generated file.
*--------------------------------------------------------------------------------------------*/
@@ -10,29 +10,32 @@ import { useObservableQuery, useObservableQueryWithPaging, useSuspenseObservable
import { ParameterDescriptor } from '@cratis/arc/reflection';
import { Author } from './Author';
-class AllAuthorsSortBy {
+class ObserveAllSortBy {
- constructor(readonly query: AllAuthors) {
+ constructor(readonly query: ObserveAll) {
}
}
-class AllAuthorsSortByWithoutQuery {
+class ObserveAllSortByWithoutQuery {
}
-export class AllAuthors extends ObservableQueryFor {
- readonly route: string = '/api/authors/listing/all-authors';
- readonly queryName: string = 'Library.Authors.Listing.Author.AllAuthors';
+/**
+ * Observes all authors, pushing updates when the collection changes.
+ */
+export class ObserveAll extends ObservableQueryFor {
+ readonly route: string = '/api/authors/listing/observe-all';
+ readonly queryName: string = 'Library.Authors.Listing.Author.ObserveAll';
readonly treatWarningsAsErrors: boolean = false;
readonly roles: string[] = [];
readonly defaultValue: Author[] = [];
- private readonly _sortBy: AllAuthorsSortBy;
- private static readonly _sortBy: AllAuthorsSortByWithoutQuery = new AllAuthorsSortByWithoutQuery();
+ private readonly _sortBy: ObserveAllSortBy;
+ private static readonly _sortBy: ObserveAllSortByWithoutQuery = new ObserveAllSortByWithoutQuery();
constructor() {
super(Author, true);
- this._sortBy = new AllAuthorsSortBy(this);
+ this._sortBy = new ObserveAllSortBy(this);
}
get requiredRequestParameters(): string[] {
@@ -44,35 +47,35 @@ export class AllAuthors extends ObservableQueryFor {
];
- get sortBy(): AllAuthorsSortBy {
+ get sortBy(): ObserveAllSortBy {
return this._sortBy;
}
- static get sortBy(): AllAuthorsSortByWithoutQuery {
+ static get sortBy(): ObserveAllSortByWithoutQuery {
return this._sortBy;
}
static use(sorting?: Sorting): [QueryResultWithState, SetSorting] {
- return useObservableQuery(AllAuthors, undefined, sorting);
+ return useObservableQuery(ObserveAll, undefined, sorting);
}
static useWithPaging(pageSize: number, sorting?: Sorting): [QueryResultWithState, SetSorting, SetPage, SetPageSize] {
- return useObservableQueryWithPaging(AllAuthors, new Paging(0, pageSize), undefined, sorting);
+ return useObservableQueryWithPaging(ObserveAll, new Paging(0, pageSize), undefined, sorting);
}
static useSuspense(sorting?: Sorting): [QueryResultWithState, SetSorting] {
- return useSuspenseObservableQuery(AllAuthors, undefined, sorting);
+ return useSuspenseObservableQuery(ObserveAll, undefined, sorting);
}
static useSuspenseWithPaging(pageSize: number, sorting?: Sorting): [QueryResultWithState, SetSorting, SetPage, SetPageSize] {
- return useSuspenseObservableQueryWithPaging(AllAuthors, new Paging(0, pageSize), undefined, sorting);
+ return useSuspenseObservableQueryWithPaging(ObserveAll, new Paging(0, pageSize), undefined, sorting);
}
static useChangeStream(getKey?: (item: Author) => unknown, sorting?: Sorting): ChangeSet {
- return useChangeStream(AllAuthors, undefined, getKey, sorting);
+ return useChangeStream(ObserveAll, undefined, getKey, sorting);
}
- static when(condition: boolean): ObservableQueryWhen {
- return new ObservableQueryWhen(AllAuthors, condition);
+ static when(condition: boolean): ObservableQueryWhen {
+ return new ObservableQueryWhen(ObserveAll, condition);
}
}
diff --git a/Library/Lending/Authors/Listing/index.ts b/Library/Lending/Authors/Listing/index.ts
index d2d7892c..1d094911 100644
--- a/Library/Lending/Authors/Listing/index.ts
+++ b/Library/Lending/Authors/Listing/index.ts
@@ -1,9 +1,8 @@
/*---------------------------------------------------------------------------------------------
* **DO NOT EDIT** - This file is an automatically generated file.
*--------------------------------------------------------------------------------------------*/
-
/* eslint-disable sort-imports */
// eslint-disable-next-line header/header
-export * from './AllAuthors';
export * from './Author';
export * from './Listing';
+export * from './ObserveAll';
diff --git a/Library/Lending/Inventory/Adding/AddBook.tsx b/Library/Lending/Inventory/Adding/AddBook.tsx
index 0fe507bb..adaf6ba1 100644
--- a/Library/Lending/Inventory/Adding/AddBook.tsx
+++ b/Library/Lending/Inventory/Adding/AddBook.tsx
@@ -2,7 +2,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
import { AddBookTitleToInventory } from './AddBookTitleToInventory';
-import { AllAuthors } from '../../Authors/Listing/AllAuthors';
+import { ObserveAll as AllAuthors } from '../../Authors/Listing/ObserveAll';
import { CommandDialog } from '@cratis/components/CommandDialog';
import { InputTextField, NumberField, DropdownField } from '@cratis/components/CommandForm';
@@ -19,7 +19,7 @@ export const AddBook = () => {
value={c => c.authorId}
title="Author"
- options={authors.data}
+ options={authors.data as unknown as Record[]}
optionValue="id"
optionLabel="name"
placeholder="Select an author"
diff --git a/Library/Lending/Inventory/Adding/Adding.cs b/Library/Lending/Inventory/Adding/Adding.cs
index dc3e4cf0..ab79630a 100644
--- a/Library/Lending/Inventory/Adding/Adding.cs
+++ b/Library/Lending/Inventory/Adding/Adding.cs
@@ -1,10 +1,6 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
-using Cratis.Chronicle.EventSequences;
-using FluentValidation;
-using SharpCompress.Archives;
-
namespace Library.Inventory.Adding;
[Command]
diff --git a/Library/Lending/Inventory/Inventory.tsx b/Library/Lending/Inventory/Inventory.tsx
index 9714633b..14371661 100644
--- a/Library/Lending/Inventory/Inventory.tsx
+++ b/Library/Lending/Inventory/Inventory.tsx
@@ -13,7 +13,7 @@ export const Inventory = () => {
return (
- showAddBookDialog()} />
+ showAddBookDialog()} />
diff --git a/Library/Lending/Inventory/Listing/Book.ts b/Library/Lending/Inventory/Listing/Book.ts
index fabcfd26..f1aec894 100644
--- a/Library/Lending/Inventory/Listing/Book.ts
+++ b/Library/Lending/Inventory/Listing/Book.ts
@@ -1,4 +1,4 @@
-// @generated by Cratis. Source: Library.Inventory.Listing.Book. Time: 2026-02-07T08:07:41.8326730Z. Hash: DCD613C3CAF9889BFF3B860AB89CF70CBD05669AD78B551628BD7767A5DEF785
+// @generated by Cratis. Source: Library.Inventory.Listing.Book. Time: 2026-06-07T08:39:08.3638890Z. Hash: 6C4991C56B036E4B300730D78782BBC7411A4E9C2B9CA1D823E66B13003BABF2
/*---------------------------------------------------------------------------------------------
* **DO NOT EDIT** - This file is an automatically generated file.
*--------------------------------------------------------------------------------------------*/
@@ -7,9 +7,20 @@
// eslint-disable-next-line header/header
import { field } from '@cratis/fundamentals';
+/**
+ * Represents a book in the inventory.
+ */
export class Book {
+
+ /**
+ * The ISBN of the book.
+ */
@field(String)
id!: string;
+
+ /**
+ * The title of the book.
+ */
@field(String)
title!: string;
}
diff --git a/Library/Lending/Inventory/Listing/GetAll.ts b/Library/Lending/Inventory/Listing/GetAll.ts
deleted file mode 100644
index 1454bb74..00000000
--- a/Library/Lending/Inventory/Listing/GetAll.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-// @generated by Cratis. Source: Library.Inventory.Listing.BookQueries. Time: 2026-06-05T18:26:20.2962978Z. Hash: 391EA5A519CCA04FCA7456C55D2192D74535B1F12A1A18BA232BE87220DD85E0
-/*---------------------------------------------------------------------------------------------
- * **DO NOT EDIT** - This file is an automatically generated file.
- *--------------------------------------------------------------------------------------------*/
-
-/* eslint-disable sort-imports */
-// eslint-disable-next-line header/header
-import { QueryFor, QueryResultWithState, QueryValidator, Sorting, SortingActions, SortingActionsForQuery, Paging } from '@cratis/arc/queries';
-import { useQuery, useQueryWithPaging, useSuspenseQuery, useSuspenseQueryWithPaging, PerformQuery, SetSorting, SetPage, SetPageSize, QueryWhen } from '@cratis/arc.react/queries';
-import { ParameterDescriptor } from '@cratis/arc/reflection';
-import { Book } from './Book';
-
-class GetAllSortBy {
- private _id: SortingActionsForQuery;
- private _title: SortingActionsForQuery;
-
- constructor(readonly query: GetAll) {
- this._id = new SortingActionsForQuery('id', query);
- this._title = new SortingActionsForQuery('title', query);
- }
-
- get id(): SortingActionsForQuery {
- return this._id;
- }
- get title(): SortingActionsForQuery {
- return this._title;
- }
-}
-
-class GetAllSortByWithoutQuery {
- private _id: SortingActions = new SortingActions('id');
- private _title: SortingActions = new SortingActions('title');
-
- get id(): SortingActions {
- return this._id;
- }
- get title(): SortingActions {
- return this._title;
- }
-}
-
-export class GetAllValidator extends QueryValidator