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 { - constructor() { - super(); - } -} - -export class GetAll extends QueryFor { - readonly route: string = '/api/books'; - readonly queryName: string = 'Library.Inventory.Listing.BookQueries.GetAll'; - readonly validation: QueryValidator = new GetAllValidator(); - readonly treatWarningsAsErrors: boolean = false; - readonly roles: string[] = []; - readonly defaultValue: Book[] = []; - private readonly _sortBy: GetAllSortBy; - private static readonly _sortBy: GetAllSortByWithoutQuery = new GetAllSortByWithoutQuery(); - - constructor() { - super(Book, true); - this._sortBy = new GetAllSortBy(this); - } - - get requiredRequestParameters(): string[] { - return [ - ]; - } - - readonly parameterDescriptors: ParameterDescriptor[] = [ - ]; - - - get sortBy(): GetAllSortBy { - return this._sortBy; - } - - static get sortBy(): GetAllSortByWithoutQuery { - return this._sortBy; - } - - static use(sorting?: Sorting): [QueryResultWithState, PerformQuery, SetSorting] { - return useQuery(GetAll, undefined, sorting); - } - - static useWithPaging(pageSize: number, sorting?: Sorting): [QueryResultWithState, PerformQuery, SetSorting, SetPage, SetPageSize] { - return useQueryWithPaging(GetAll, new Paging(0, pageSize), undefined, sorting); - } - - static useSuspense(sorting?: Sorting): [QueryResultWithState, PerformQuery, SetSorting] { - return useSuspenseQuery(GetAll, undefined, sorting); - } - - static useSuspenseWithPaging(pageSize: number, sorting?: Sorting): [QueryResultWithState, PerformQuery, SetSorting, SetPage, SetPageSize] { - return useSuspenseQueryWithPaging(GetAll, new Paging(0, pageSize), undefined, sorting); - } - - static when(condition: boolean): QueryWhen { - return new QueryWhen(GetAll, condition); - } -} diff --git a/Library/Lending/Inventory/Listing/Listing.cs b/Library/Lending/Inventory/Listing/Listing.cs index ac7345ef..6a644a5b 100644 --- a/Library/Lending/Inventory/Listing/Listing.cs +++ b/Library/Lending/Inventory/Listing/Listing.cs @@ -1,27 +1,36 @@ // 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.Inventory.Adding; +using MongoDB.Driver; namespace Library.Inventory.Listing; -public record Book(ISBN Id, BookTitle Title); +/// +/// Represents a book in the inventory. +/// +/// The ISBN of the book. +/// The title of the book. +[ReadModel] +public record Book(ISBN Id, BookTitle Title) +{ + /// + /// Observes all books in the inventory, pushing updates when the collection changes. + /// + /// The MongoDB collection to observe. + /// A subject that emits the full book list on each change. + public static ISubject> ObserveAll(IMongoCollection collection) => + collection.Observe(); +} +/// +/// Defines the projection from to . +/// public class BookProjection : IProjectionFor { + /// public void Define(IProjectionBuilderFor builder) => builder .AutoMap() .From(); } - -[Route("/api/books")] -public class BookQueries(IEventStore eventStore) : ControllerBase -{ - [HttpGet] - public async Task> GetAll() => - await eventStore.ReadModels.Materialized.GetInstances(); - - [HttpGet("observe")] - public IObservable> ObserveAll() => - eventStore.ReadModels.Materialized.ObserveInstances(); -} diff --git a/Library/Lending/Inventory/Listing/ObserveAll.ts b/Library/Lending/Inventory/Listing/ObserveAll.ts index 69f99e97..d90fee1f 100644 --- a/Library/Lending/Inventory/Listing/ObserveAll.ts +++ b/Library/Lending/Inventory/Listing/ObserveAll.ts @@ -1,47 +1,32 @@ -// @generated by Cratis. Source: Library.Inventory.Listing.BookQueries. Time: 2026-06-05T18:26:20.3046782Z. Hash: 9B9A6E938DA540E3F223A8931BA172039E1E27F50EDF599D8E1F77FDE7D80617 +// @generated by Cratis. Source: Library.Inventory.Listing.Book. Time: 2026-06-07T08:39:08.3578420Z. Hash: A3BF851E0A4DE43F305A50C9A8644CC8C453C9D5869BF3E6CF44821756CA92F0 /*--------------------------------------------------------------------------------------------- * **DO NOT EDIT** - This file is an automatically generated file. *--------------------------------------------------------------------------------------------*/ /* eslint-disable sort-imports */ // eslint-disable-next-line header/header -import { ObservableQueryFor, QueryResultWithState, Sorting, SortingActions, SortingActionsForObservableQuery, Paging, ChangeSet } from '@cratis/arc/queries'; +import { ObservableQueryFor, QueryResultWithState, Sorting, Paging, ChangeSet } from '@cratis/arc/queries'; import { useObservableQuery, useObservableQueryWithPaging, useSuspenseObservableQuery, useSuspenseObservableQueryWithPaging, useChangeStream, SetSorting, SetPage, SetPageSize, ObservableQueryWhen } from '@cratis/arc.react/queries'; import { ParameterDescriptor } from '@cratis/arc/reflection'; import { Book } from './Book'; class ObserveAllSortBy { - private _id: SortingActionsForObservableQuery; - private _title: SortingActionsForObservableQuery; constructor(readonly query: ObserveAll) { - this._id = new SortingActionsForObservableQuery('id', query); - this._title = new SortingActionsForObservableQuery('title', query); } - get id(): SortingActionsForObservableQuery { - return this._id; - } - get title(): SortingActionsForObservableQuery { - return this._title; - } } class ObserveAllSortByWithoutQuery { - private _id: SortingActions = new SortingActions('id'); - private _title: SortingActions = new SortingActions('title'); - get id(): SortingActions { - return this._id; - } - get title(): SortingActions { - return this._title; - } } +/** + * Observes all books in the inventory, pushing updates when the collection changes. + */ export class ObserveAll extends ObservableQueryFor { - readonly route: string = '/api/books/observe'; - readonly queryName: string = 'Library.Inventory.Listing.BookQueries.ObserveAll'; + readonly route: string = '/api/inventory/listing/observe-all'; + readonly queryName: string = 'Library.Inventory.Listing.Book.ObserveAll'; readonly treatWarningsAsErrors: boolean = false; readonly roles: string[] = []; readonly defaultValue: Book[] = []; diff --git a/Library/Lending/Inventory/Listing/index.ts b/Library/Lending/Inventory/Listing/index.ts index c07d09e3..397ffcdd 100644 --- a/Library/Lending/Inventory/Listing/index.ts +++ b/Library/Lending/Inventory/Listing/index.ts @@ -1,10 +1,8 @@ /*--------------------------------------------------------------------------------------------- * **DO NOT EDIT** - This file is an automatically generated file. *--------------------------------------------------------------------------------------------*/ - /* eslint-disable sort-imports */ // eslint-disable-next-line header/header export * from './Book'; -export * from './GetAll'; export * from './Listing'; export * from './ObserveAll'; diff --git a/Library/Lending/Lending.csproj b/Library/Lending/Lending.csproj index aba6287f..09151328 100644 --- a/Library/Lending/Lending.csproj +++ b/Library/Lending/Lending.csproj @@ -14,6 +14,7 @@ + diff --git a/Library/Lending/package.json b/Library/Lending/package.json index 6910d093..cfab4dd0 100644 --- a/Library/Lending/package.json +++ b/Library/Lending/package.json @@ -20,6 +20,7 @@ "@cratis/arc.react": "20.39.1", "@cratis/arc.react.mvvm": "20.39.1", "@cratis/arc.vite": "20.39.1", + "@cratis/components": "^1.10.2", "allotment": "1.20.5", "echarts": "6.0.0", "mobx": "6.15.3", diff --git a/Library/Members/App.tsx b/Library/Members/.frontend/App.tsx similarity index 81% rename from Library/Members/App.tsx rename to Library/Members/.frontend/App.tsx index 6b7964f3..e9c36c57 100644 --- a/Library/Members/App.tsx +++ b/Library/Members/.frontend/App.tsx @@ -3,9 +3,9 @@ import { BrowserRouter, Route, Routes } from "react-router-dom"; import { DialogComponents } from '@cratis/arc.react/dialogs'; -import { MembersHome } from './Home'; -import { MembersList } from './Profiles/Listing/MembersList'; -import { RegisterMember } from './Profiles/Registration/RegisterMemberPanel'; +import { MembersHome } from '../Home'; +import { MembersList } from '../Profiles/Listing/MembersList'; +import { RegisterMember } from '../Profiles/Registration/RegisterMemberPanel'; function App() { return ( diff --git a/Library/Members/index.css b/Library/Members/.frontend/index.css similarity index 100% rename from Library/Members/index.css rename to Library/Members/.frontend/index.css diff --git a/Library/Members/index.html b/Library/Members/.frontend/index.html similarity index 100% rename from Library/Members/index.html rename to Library/Members/.frontend/index.html diff --git a/Library/Lending/index.tsx b/Library/Members/.frontend/index.tsx similarity index 94% rename from Library/Lending/index.tsx rename to Library/Members/.frontend/index.tsx index fce72276..9a15c406 100644 --- a/Library/Lending/index.tsx +++ b/Library/Members/.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/Members/.frontend/tsconfig.json b/Library/Members/.frontend/tsconfig.json index 5dd06b88..20f74b78 100644 --- a/Library/Members/.frontend/tsconfig.json +++ b/Library/Members/.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/Members/.frontend/vite.config.ts b/Library/Members/.frontend/vite.config.ts index 3a63921e..fa30ca22 100644 --- a/Library/Members/.frontend/vite.config.ts +++ b/Library/Members/.frontend/vite.config.ts @@ -36,7 +36,7 @@ export default defineConfig({ pool: 'threads', 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/Members/Members.csproj b/Library/Members/Members.csproj index 23184d36..80e48724 100644 --- a/Library/Members/Members.csproj +++ b/Library/Members/Members.csproj @@ -14,6 +14,7 @@ + diff --git a/Library/Members/Profiles/Listing/Listing.cs b/Library/Members/Profiles/Listing/Listing.cs index ff5e21ae..95c35026 100644 --- a/Library/Members/Profiles/Listing/Listing.cs +++ b/Library/Members/Profiles/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 Members.Profiles.Registration; +using MongoDB.Driver; namespace Members.Profiles.Listing; @@ -14,12 +16,21 @@ namespace Members.Profiles.Listing; [ReadModel] public record MemberProfile(MemberId Id, MemberName Name, string Email) { - public static IObservable> AllMembers(IMaterializedReadModels readModels) => - readModels.ObserveInstances(); + /// + /// Observes all member profiles, pushing updates when the collection changes. + /// + /// The MongoDB collection to observe. + /// A subject that emits the full member profile list on each change. + public static ISubject> ObserveAll(IMongoCollection collection) => + collection.Observe(); } +/// +/// Defines the projection from to . +/// public class MemberProfileProjection : IProjectionFor { + /// public void Define(IProjectionBuilderFor builder) => builder .AutoMap() .From(); diff --git a/Library/Members/Profiles/Listing/AllMembers.ts b/Library/Members/Profiles/Listing/ObserveAll.ts similarity index 59% rename from Library/Members/Profiles/Listing/AllMembers.ts rename to Library/Members/Profiles/Listing/ObserveAll.ts index 6cd11881..f3c26d92 100644 --- a/Library/Members/Profiles/Listing/AllMembers.ts +++ b/Library/Members/Profiles/Listing/ObserveAll.ts @@ -1,4 +1,4 @@ -// @generated by Cratis. Source: Members.Profiles.Listing.MemberProfile. Time: 2026-05-30T06:02:10.4001274Z. Hash: 3E29DC64F1711F2102D8AC356A3DA8FE6FC84AC04994D356A87070C8B4C0FBDA +// @generated by Cratis. Source: Members.Profiles.Listing.MemberProfile. Time: 2026-06-07T08:39:10.9365750Z. Hash: 6FC584707C0B25A46213F8E454F7E69BDE82A953164DAEF9D7FF9814749BF586 /*--------------------------------------------------------------------------------------------- * **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 { MemberProfile } from './MemberProfile'; -class AllMembersSortBy { +class ObserveAllSortBy { - constructor(readonly query: AllMembers) { + constructor(readonly query: ObserveAll) { } } -class AllMembersSortByWithoutQuery { +class ObserveAllSortByWithoutQuery { } -export class AllMembers extends ObservableQueryFor { - readonly route: string = '/api/profiles/listing/all-members'; - readonly queryName: string = 'Members.Profiles.Listing.MemberProfile.AllMembers'; +/** + * Observes all member profiles, pushing updates when the collection changes. + */ +export class ObserveAll extends ObservableQueryFor { + readonly route: string = '/api/profiles/listing/observe-all'; + readonly queryName: string = 'Members.Profiles.Listing.MemberProfile.ObserveAll'; readonly treatWarningsAsErrors: boolean = false; readonly roles: string[] = []; readonly defaultValue: MemberProfile[] = []; - private readonly _sortBy: AllMembersSortBy; - private static readonly _sortBy: AllMembersSortByWithoutQuery = new AllMembersSortByWithoutQuery(); + private readonly _sortBy: ObserveAllSortBy; + private static readonly _sortBy: ObserveAllSortByWithoutQuery = new ObserveAllSortByWithoutQuery(); constructor() { super(MemberProfile, true); - this._sortBy = new AllMembersSortBy(this); + this._sortBy = new ObserveAllSortBy(this); } get requiredRequestParameters(): string[] { @@ -44,35 +47,35 @@ export class AllMembers extends ObservableQueryFor { ]; - get sortBy(): AllMembersSortBy { + get sortBy(): ObserveAllSortBy { return this._sortBy; } - static get sortBy(): AllMembersSortByWithoutQuery { + static get sortBy(): ObserveAllSortByWithoutQuery { return this._sortBy; } static use(sorting?: Sorting): [QueryResultWithState, SetSorting] { - return useObservableQuery(AllMembers, undefined, sorting); + return useObservableQuery(ObserveAll, undefined, sorting); } static useWithPaging(pageSize: number, sorting?: Sorting): [QueryResultWithState, SetSorting, SetPage, SetPageSize] { - return useObservableQueryWithPaging(AllMembers, new Paging(0, pageSize), undefined, sorting); + return useObservableQueryWithPaging(ObserveAll, new Paging(0, pageSize), undefined, sorting); } static useSuspense(sorting?: Sorting): [QueryResultWithState, SetSorting] { - return useSuspenseObservableQuery(AllMembers, undefined, sorting); + return useSuspenseObservableQuery(ObserveAll, undefined, sorting); } static useSuspenseWithPaging(pageSize: number, sorting?: Sorting): [QueryResultWithState, SetSorting, SetPage, SetPageSize] { - return useSuspenseObservableQueryWithPaging(AllMembers, new Paging(0, pageSize), undefined, sorting); + return useSuspenseObservableQueryWithPaging(ObserveAll, new Paging(0, pageSize), undefined, sorting); } static useChangeStream(getKey?: (item: MemberProfile) => unknown, sorting?: Sorting): ChangeSet { - return useChangeStream(AllMembers, undefined, getKey, sorting); + return useChangeStream(ObserveAll, undefined, getKey, sorting); } - static when(condition: boolean): ObservableQueryWhen { - return new ObservableQueryWhen(AllMembers, condition); + static when(condition: boolean): ObservableQueryWhen { + return new ObservableQueryWhen(ObserveAll, condition); } } diff --git a/Library/Members/Profiles/Listing/index.ts b/Library/Members/Profiles/Listing/index.ts index 7d1d3e0c..7ebcb201 100644 --- a/Library/Members/Profiles/Listing/index.ts +++ b/Library/Members/Profiles/Listing/index.ts @@ -1,2 +1,2 @@ -export * from './AllMembers'; export * from './MemberProfile'; +export * from './ObserveAll'; diff --git a/Library/Members/package.json b/Library/Members/package.json index fd87bcfb..5a7ea3be 100644 --- a/Library/Members/package.json +++ b/Library/Members/package.json @@ -20,6 +20,7 @@ "@cratis/arc.react": "20.39.1", "@cratis/arc.react.mvvm": "20.39.1", "@cratis/arc.vite": "20.39.1", + "@cratis/components": "^1.10.2", "mobx": "6.15.3", "mobx-react": "9.2.1", "primeicons": "7.0.0", diff --git a/Library/README.md b/Library/README.md index f6da9e72..c0618221 100644 --- a/Library/README.md +++ b/Library/README.md @@ -4,7 +4,7 @@ This sample shows a full Library system built on the entire Cratis Stack across ## 🏛️ Architecture -``` +```text Library/ ├── Lending/ — Book lending domain (catalogue, loans, reservations) ├── Members/ — Member management domain (registration, profiles) @@ -31,14 +31,13 @@ Both microservices are structured around vertical slices — each feature folder - [.NET 10](https://dot.net) - [Node.js](https://nodejs.org) + [Yarn](https://yarnpkg.com) - [Docker](https://www.docker.com/products/docker-desktop/) or compatible container engine -- [.NET Aspire workload](https://learn.microsoft.com/dotnet/aspire/fundamentals/setup-tooling): `dotnet workload install aspire` ## 🗄️ Database Backends Chronicle supports multiple storage backends. The Composition AppHost and docker-compose files both support the same set of profiles. **MongoDB is the default** when no database type is specified. -| Database | `DATABASE_TYPE` value | Notes | -|----------|----------------------|-------| +| Database | DATABASE_TYPE value | Notes | +|----------|---------------------|-------| | MongoDB | `mongodb` _(default)_ | Uses Chronicle development image (embedded MongoDB) | | PostgreSQL | `postgresql` | Starts a `postgres:16` container | | Microsoft SQL Server | `mssql` | Starts `mcr.microsoft.com/mssql/server:2022-latest` | @@ -75,7 +74,7 @@ Or set `DATABASE_TYPE` directly: DATABASE_TYPE=postgresql dotnet run --project Composition/Composition.csproj ``` -When the AppHost is running, the Aspire dashboard is available at **http://localhost:15888**. +The Aspire dashboard opens automatically at [http://localhost:15888](http://localhost:15888) (no login token required). > **⚠️ Local development only** — The docker-compose files embed hardcoded credentials (PostgreSQL password `chronicle`, SQL Server SA password `Chronicle_Str0ng!`, Vault root token `root`, Keycloak admin `admin`/`admin`). These are intentionally weak and must never be used in any environment beyond your local machine. @@ -115,26 +114,47 @@ cd Members && yarn dev ## 🔗 Service URLs +### Aspire + +| Service | URL | +|---------|-----| +| Lending app (via AuthProxy) | [http://localhost:7000](http://localhost:7000) | +| Members app (via AuthProxy) | [http://localhost:7001](http://localhost:7001) | +| Lending backend Swagger | [http://localhost:5000/swagger](http://localhost:5000/swagger) | +| Members backend Swagger | [http://localhost:5001/swagger](http://localhost:5001/swagger) | +| Lending frontend (direct) | [http://localhost:9000](http://localhost:9000) | +| Members frontend (direct) | [http://localhost:9001](http://localhost:9001) | +| Chronicle Workbench | [http://localhost:8080](http://localhost:8080) | +| HashiCorp Vault | [http://localhost:8200](http://localhost:8200) | +| Keycloak – Lending | [http://localhost:8090](http://localhost:8090) | +| Keycloak – Members | [http://localhost:8091](http://localhost:8091) | +| Aspire Dashboard | [http://localhost:15888](http://localhost:15888) | + +> **Note:** The backend root (`/`) returns 404 because no static files are built in Aspire dev mode — the frontend is served by the Vite dev servers at 9000/9001. Use the `/swagger` path to verify the backend is alive, or access the full app via AuthProxy. + +### docker compose + | Service | URL | |---------|-----| -| Lending (via AuthProxy) | http://localhost:5000 (backend) / http://localhost:9000 (frontend) | -| Members (via AuthProxy) | http://localhost:5001 (backend) / http://localhost:9001 (frontend) | -| Chronicle Workbench | http://localhost:8080 | -| HashiCorp Vault | http://localhost:8200 | -| Keycloak – Lending | http://localhost:8090 | -| Keycloak – Members | http://localhost:8091 | -| Aspire Dashboard | http://localhost:15888 | +| Lending backend | [http://localhost:5000](http://localhost:5000) | +| Members backend | [http://localhost:5001](http://localhost:5001) | +| Lending frontend | [http://localhost:9000](http://localhost:9000) | +| Members frontend | [http://localhost:9001](http://localhost:9001) | +| Chronicle Workbench | [http://localhost:8080](http://localhost:8080) | +| HashiCorp Vault | [http://localhost:8200](http://localhost:8200) | +| Keycloak – Lending | [http://localhost:8090](http://localhost:8090) | +| Keycloak – Members | [http://localhost:8091](http://localhost:8091) | ### Demo users -**Lending realm** (http://localhost:8090/realms/lending) +**Lending realm** ([http://localhost:8090/realms/lending](http://localhost:8090/realms/lending)) | Username | Password | Role | |----------|----------|------| | `librarian` | `librarian` | Librarian | | `borrower` | `borrower` | Borrower | -**Members realm** (http://localhost:8091/realms/members) +**Members realm** ([http://localhost:8091/realms/members](http://localhost:8091/realms/members)) | Username | Password | |----------|----------|