Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .markdownlint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"MD013": false,
"MD034": false,
"MD060": false
}
25 changes: 14 additions & 11 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@
<PackageVersion Include="Cratis.Arc.ProxyGenerator.Build" Version="20.39.1" />
<PackageVersion Include="Cratis.Arc.Swagger" Version="20.39.1" />
<PackageVersion Include="Cratis.Arc.Chronicle" Version="20.39.1" />
<PackageVersion Include="Aspire.Hosting.AppHost" Version="13.3.5" />
<PackageVersion Include="Aspire.Hosting.JavaScript" Version="13.3.5" />
<PackageVersion Include="Aspire.Hosting.MongoDB" Version="13.3.5" />
<PackageVersion Include="Aspire.Hosting.PostgreSQL" Version="13.3.5" />
<PackageVersion Include="Aspire.Hosting.SqlServer" Version="13.3.5" />
<PackageVersion Include="Aspire.AppHost.Sdk" Version="13.4.2" />
<PackageVersion Include="Aspire.Hosting.AppHost" Version="13.4.2" />
<PackageVersion Include="Aspire.Hosting.JavaScript" Version="13.4.2" />
<PackageVersion Include="Aspire.Hosting.MongoDB" Version="13.4.2" />
<PackageVersion Include="Aspire.Hosting.PostgreSQL" Version="13.4.2" />
<PackageVersion Include="Aspire.Hosting.SqlServer" Version="13.4.2" />
<PackageVersion Include="Cratis.AuthProxy.Aspire" Version="2.4.0" />
<PackageVersion Include="Cratis.Chronicle" Version="15.33.3" />
<PackageVersion Include="Cratis.Chronicle.AspNetCore" Version="15.33.3" />
Expand All @@ -24,20 +25,22 @@
<PackageVersion Include="Cratis.Chronicle.XUnit.Integration" Version="15.33.3" />
<PackageVersion Include="Cratis.Chronicle.InProcess" Version="15.3.0" />
<!-- Analysis -->
<PackageVersion Include="MongoDB.Driver" Version="3.8.0" />
<PackageVersion Include="MongoDB.Driver" Version="3.9.0" />
<PackageVersion Include="SharpCompress" Version="1.0.0" />
<PackageVersion Include="Snappier" Version="1.3.1" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" NoWarn="NU5104" />
<PackageVersion Include="Roslynator.Analyzers" Version="4.15.0" />
<PackageVersion Include="Meziantou.Analyzer" Version="3.0.77" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.1.2" />
<PackageVersion Include="Terminal.Gui" Version="2.0.0" />
<PackageVersion Include="Meziantou.Analyzer" Version="3.0.101" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.2.1" />
<PackageVersion Include="Terminal.Gui" Version="2.4.4" />
<!-- Testing & Specifications -->
<PackageVersion Include="Cratis.Specifications" Version="3.0.5" />
<PackageVersion Include="Cratis.Specifications.XUnit" Version="3.0.5" />
<PackageVersion Include="Testcontainers" Version="4.12.0" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="NSubstitute" Version="5.3.0" />
<PackageVersion Include="Microsoft.NET.Test.SDK" Version="18.5.1" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.2" />
<PackageVersion Include="Microsoft.NET.Test.SDK" Version="18.6.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.8" />
</ItemGroup>
</Project>
4 changes: 3 additions & 1 deletion Library/Composition/Composition.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Aspire.AppHost.Sdk/13.4.2">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
Expand All @@ -14,6 +14,8 @@
<PackageReference Include="Aspire.Hosting.SqlServer" />
<PackageReference Include="Cratis.AuthProxy.Aspire" />
<PackageReference Include="Cratis.Chronicle.Aspire" />
<PackageReference Include="SharpCompress" />
<PackageReference Include="Snappier" />
</ItemGroup>

<ItemGroup>
Expand Down
30 changes: 17 additions & 13 deletions Library/Composition/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,20 @@
.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).
// For any explicit database choice we supply a callback to switch to the production image.
IResourceBuilder<ChronicleResource> 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))
Expand All @@ -51,33 +51,37 @@
}
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")
.WithArgs("start-dev", "--import-realm")
.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")
.WithArgs("start-dev", "--import-realm")
.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<Projects.Lending>("lending")
Expand All @@ -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");
Expand All @@ -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(
Expand All @@ -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(
Expand Down
14 changes: 7 additions & 7 deletions Library/Lending/App.tsx → Library/Lending/.frontend/App.tsx
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
2 changes: 1 addition & 1 deletion Library/Lending/.frontend/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"extends": "../../.frontend/tsconfig.base.json",
"extends": "../../../.frontend/tsconfig.base.json",
"compilerOptions": {
"allowImportingTsExtensions": true,
"baseUrl": "..",
Expand Down
2 changes: 1 addition & 1 deletion Library/Lending/.frontend/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
2 changes: 1 addition & 1 deletion Library/Lending/Authors/Authors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const Authors = () => {
return (
<Page title="Authors" panel>
<Toolbar orientation="horizontal">
<ToolbarButton icon="pi pi-user" text="Add Author" tooltip="Add Author" onClick={() => showAddAuthorDialog()} />
<ToolbarButton icon="pi pi-user" text="Add Author" title="Add Author" onClick={() => showAddAuthorDialog()} />
</Toolbar>
<Listing />

Expand Down
15 changes: 13 additions & 2 deletions Library/Lending/Authors/Listing/Listing.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -13,12 +15,21 @@ namespace Library.Authors.Listing;
[ReadModel]
public record Author(AuthorId Id, AuthorName Name)
{
public static IObservable<IEnumerable<Author>> AllAuthors(IMaterializedReadModels readModels) =>
readModels.ObserveInstances<Author>();
/// <summary>
/// Observes all authors, pushing updates when the collection changes.
/// </summary>
/// <param name="collection">The MongoDB collection to observe.</param>
/// <returns>A subject that emits the full author list on each change.</returns>
public static ISubject<IEnumerable<Author>> ObserveAll(IMongoCollection<Author> collection) =>
collection.Observe();
}

/// <summary>
/// Defines the projection from <see cref="AuthorRegistered"/> to <see cref="Author"/>.
/// </summary>
public class AuthorProjection : IProjectionFor<Author>
{
/// <inheritdoc/>
public void Define(IProjectionBuilderFor<Author> builder) => builder
.AutoMap()
.From<AuthorRegistered>();
Expand Down
10 changes: 5 additions & 5 deletions Library/Lending/Authors/Listing/Listing.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<DataTable
lazy
paginator
value={allAuthorsResult.data}
value={observeAllResult.data}
rows={pageSize}
totalRecords={allAuthorsResult.paging.totalItems}
totalRecords={observeAllResult.paging.totalItems}
alwaysShowPaginator={false}
first={allAuthorsResult.paging.page * pageSize}
first={observeAllResult.paging.page * pageSize}
onPage={(e) => setPage(e.page ?? 0)}
scrollable
scrollHeight={'flex'}
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*--------------------------------------------------------------------------------------------*/
Expand All @@ -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<Author[]> {
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<Author[]> {
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[] {
Expand All @@ -44,35 +47,35 @@ export class AllAuthors extends ObservableQueryFor<Author[]> {
];


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<Author[]>, SetSorting] {
return useObservableQuery<Author[], AllAuthors>(AllAuthors, undefined, sorting);
return useObservableQuery<Author[], ObserveAll>(ObserveAll, undefined, sorting);
}

static useWithPaging(pageSize: number, sorting?: Sorting): [QueryResultWithState<Author[]>, SetSorting, SetPage, SetPageSize] {
return useObservableQueryWithPaging<Author[], AllAuthors>(AllAuthors, new Paging(0, pageSize), undefined, sorting);
return useObservableQueryWithPaging<Author[], ObserveAll>(ObserveAll, new Paging(0, pageSize), undefined, sorting);
}

static useSuspense(sorting?: Sorting): [QueryResultWithState<Author[]>, SetSorting] {
return useSuspenseObservableQuery<Author[], AllAuthors>(AllAuthors, undefined, sorting);
return useSuspenseObservableQuery<Author[], ObserveAll>(ObserveAll, undefined, sorting);
}

static useSuspenseWithPaging(pageSize: number, sorting?: Sorting): [QueryResultWithState<Author[]>, SetSorting, SetPage, SetPageSize] {
return useSuspenseObservableQueryWithPaging<Author[], AllAuthors>(AllAuthors, new Paging(0, pageSize), undefined, sorting);
return useSuspenseObservableQueryWithPaging<Author[], ObserveAll>(ObserveAll, new Paging(0, pageSize), undefined, sorting);
}

static useChangeStream(getKey?: (item: Author) => unknown, sorting?: Sorting): ChangeSet<Author> {
return useChangeStream<Author, AllAuthors>(AllAuthors, undefined, getKey, sorting);
return useChangeStream<Author, ObserveAll>(ObserveAll, undefined, getKey, sorting);
}

static when(condition: boolean): ObservableQueryWhen<AllAuthors, Author[]> {
return new ObservableQueryWhen<AllAuthors, Author[]>(AllAuthors, condition);
static when(condition: boolean): ObservableQueryWhen<ObserveAll, Author[]> {
return new ObservableQueryWhen<ObserveAll, Author[]>(ObserveAll, condition);
}
}
3 changes: 1 addition & 2 deletions Library/Lending/Authors/Listing/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Loading
Loading