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
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,41 @@ here.**

## [Unreleased]

### Fixed

- **A fresh `npm i @metaobjectsdev/cli` no longer reports high-severity advisories.**
`@metaobjectsdev/sdk` declared `zod` `^3.23.0`, which resolves to a release the zod advisory
GHSA-mrhx-c7xc-m3w5 covers (fixed in 4.6.6), and npm installed it beside the `zod` 4 the CLI
already used. The sdk now declares `^4.6.6` and reads its config with the zod 4 API; the
config it accepts is unchanged, and a rejected config still names each offending path, with
the reason now in zod 4's wording.
- **C#: `dotnet meta verify --templates <dir>` fails when `<dir>` does not exist.** It exited 0
with "OK", since nothing in an empty root can drift. It now exits 2 and names the directory.
A bare `dotnet meta verify <metadataDir>` runs the templates gate its help describes, reading
`prompts/` when that directory exists and `templates/` otherwise, where it used to exit 2 asking
for a root. The generated-code gate is `verify --codegen --out <dir>`, as before.
- **C#: the generated create route no longer warns CS8600.** `Author input;` is now `Author? input;`.
The next regeneration changes that one line in every `*Routes.g.cs`.
- **C#: the drift message says `gen` needs `--out`.** "Run 'dotnet meta gen'" on its own exits 2.
- **Java and Kotlin: the stale-code message names a command that runs.** When the generate
configuration sits inside an `<execution>`, `mvn metaobjects:verify` now suggests
`mvn metaobjects:generate@<id>`; a bare `mvn metaobjects:generate` fails there with "No
<loader> element was defined".

### Documentation

- The Java, Kotlin and C# quickstarts in `docs/ports/` work as written on a clean machine against
the published packages: the Java and Kotlin poms carry a Java level, the dependencies the
generated code compiles against and a plugin-level `<configuration>` (Kotlin also its
`<executions>` and `kotlin-maven-plugin`), and the C# page fills the generator list before the
first `gen`, gives the app project, serializes enums as strings, marks fields `@filterable` and
ends in the real drift gate.
- The README opens with a copy-paste quick start, and its Languages table, the port pages and the
Python floor name 1.1.0 / 8.1.0.
- `CONTRIBUTING.md` says one thing about CI: GitHub Actions runs `scripts/ci-local.sh` — the
leak scan on every pull request, the conformance and integration lanes on `main`, nightly
and on release tags.

### Changed

- **Python: `metaobjects gen` and `verify --codegen` run ruff as its own binary.** Every emitted
Expand Down
21 changes: 12 additions & 9 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@ conformance corpora. This guide covers how to propose changes.
1. **Fork** the repo and create a branch from `main`.
2. Make your change following the discipline below — **tests first (TDD)**.
3. Run the gates locally with **`scripts/ci-local.sh --quick`** and make sure it's
green. **This matters:** nothing in `.github/workflows/` runs any more — see the
green. **This matters:** on a pull request GitHub Actions runs only the leak scan
(`hygiene.yml`); the conformance and integration lanes run on `main`, nightly and on
release tags, so your local run is the only check of the rest before merge — see the
*Local CI* section below.
4. Open a **pull request** against `main` and fill out the PR template.
5. A maintainer runs the full gates locally and approves before merge.
5. A maintainer reviews the change and the checks on the pull request before merge.

## The cardinal rule: the metamodel is the spine

Expand Down Expand Up @@ -71,14 +73,15 @@ cd server/python && pytest # Python (in its .venv)
Cross-language persistence / api-contract corpora (Docker + Testcontainers) run via
`scripts/integration-test.sh`.

### Local CI (this IS the CI — run it before opening/merging a PR)
### Local CI (the checks GitHub runs — run it before opening a PR)

GitHub Actions is enabled on this repository. The three check workflows in
`.github/workflows/` run on GitHub as thin wrappers over `scripts/ci-local.sh`,
the single definition of the checks: each wrapper checks out, installs the
toolchains its lane needs, and calls the script, so a local run is the same
check a workflow run would be. Run the script locally before opening or merging
a PR, so nothing red leaves your machine:
GitHub Actions is enabled on this repository. The check workflows in
`.github/workflows/` are thin wrappers over `scripts/ci-local.sh`, the single
definition of the checks: each wrapper checks out, installs the toolchains its
lane needs, and calls the script, so a local run is the same check a workflow
run would be. A pull request runs only the leak scan; the other lanes run on
`main`, nightly and on release tags. Run the script locally before opening or
merging a PR, so nothing red reaches `main`:

```bash
scripts/ci-local.sh # full parity: hygiene.yml's leak-scan, all-port
Expand Down
25 changes: 19 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@ at something real, not that the something is correct.
> **Scope.** It protects what the model declares; your hand-written logic is still
> yours.

## Quick start

```bash
npm i @metaobjectsdev/cli && npx meta init # scaffold metaobjects/, the config and agent skills
# declare an entity in metaobjects/ (a 10-line model: metaobjects.dev/getting-started)
npx meta gen --list --probe --format text # what your model can generate
npx meta eject entity queries names barrel # copy them in; add the lines it prints to metaobjects.config.ts
npx meta gen && npx meta verify --codegen # generate, then fail if generated code has drifted
```

Java, Kotlin, C# and Python start the same way from their own package: see
[`docs/ports/`](docs/ports/).

The metamodel is the **durable spine**; generated code is the **disposable
artifact**. Substrate is local-first: typed metadata lives in your repo as YAML or
JSON you own. The entity/model tier of the output is dependency-free, and the optional
Expand Down Expand Up @@ -133,11 +146,11 @@ first-week wedge plan — and `meta init` picks up from there.

| Language | Status | Quickstart | Source |
|---|---|---|---|
| TypeScript | **npm `1.0.13`** — the `@metaobjectsdev/*` packages | [`docs/ports/typescript.md`](docs/ports/typescript.md) | [`server/typescript/`](server/typescript/) · [`client/web/`](client/web/) |
| Java | **Maven Central `8.0.13`** (`com.metaobjects:*` — the JVM major is npm major + 7) — loader + OMDB + render + Maven plugin all shipped; full conformance green | [`docs/ports/java.md`](docs/ports/java.md) | [`server/java/`](server/java/) |
| Kotlin | **Maven Central `8.0.13`** — codegen tier on top of Java: 16 generators (entity, Exposed table, names, relations, repository, output-parser, output-prompt, render-helper, extractor, filter-allowlist, validator, Spring config, storedProc, Spring controller, requirement-tests, api-docs); 24 / 24 persistence-conformance | [`docs/ports/kotlin.md`](docs/ports/kotlin.md) | [`server/java/codegen-kotlin/`](server/java/codegen-kotlin/) · [`server/java/metadata-ktx/`](server/java/metadata-ktx/) |
| C# | **NuGet `1.0.13`** — loader + conformance + EF Core codegen + render engine + `dotnet meta` CLI all shipped | [`docs/ports/csharp.md`](docs/ports/csharp.md) | [`server/csharp/`](server/csharp/) |
| Python | **PyPI `1.0.13`** — loader + conformance + render + entity-model codegen + ObjectManager runtime shipped; schema migrations are TS-owned (ADR-0015) | [`docs/ports/python.md`](docs/ports/python.md) | [`server/python/`](server/python/) |
| TypeScript | **npm `1.1.0`** — the `@metaobjectsdev/*` packages | [`docs/ports/typescript.md`](docs/ports/typescript.md) | [`server/typescript/`](server/typescript/) · [`client/web/`](client/web/) |
| Java | **Maven Central `8.1.0`** (`com.metaobjects:*` — the JVM major is npm major + 7) — loader + OMDB + render + Maven plugin all shipped; full conformance green | [`docs/ports/java.md`](docs/ports/java.md) | [`server/java/`](server/java/) |
| Kotlin | **Maven Central `8.1.0`** — codegen tier on top of Java: 16 generators (entity, Exposed table, names, relations, repository, output-parser, output-prompt, render-helper, extractor, filter-allowlist, validator, Spring config, storedProc, Spring controller, requirement-tests, api-docs); 24 / 24 persistence-conformance | [`docs/ports/kotlin.md`](docs/ports/kotlin.md) | [`server/java/codegen-kotlin/`](server/java/codegen-kotlin/) · [`server/java/metadata-ktx/`](server/java/metadata-ktx/) |
| C# | **NuGet `1.1.0`** — loader + conformance + EF Core codegen + render engine + `dotnet meta` CLI all shipped | [`docs/ports/csharp.md`](docs/ports/csharp.md) | [`server/csharp/`](server/csharp/) |
| Python | **PyPI `1.1.0`** — loader + conformance + render + entity-model codegen + ObjectManager runtime shipped; schema migrations are TS-owned (ADR-0015) | [`docs/ports/python.md`](docs/ports/python.md) | [`server/python/`](server/python/) |

## Capability matrix

Expand Down Expand Up @@ -308,7 +321,7 @@ metaobjects/
|---|---|
| TypeScript | `npm i @metaobjectsdev/cli && npx meta init` → [`docs/ports/typescript.md`](docs/ports/typescript.md) |
| Java | Add `metaobjects-maven-plugin` to your `pom.xml` → [`docs/ports/java.md`](docs/ports/java.md) |
| Kotlin | Add `metaobjects-codegen-kotlin` + `metaobjects-metadata-ktx` → [`docs/ports/kotlin.md`](docs/ports/kotlin.md) |
| Kotlin | Add `metaobjects-metadata-ktx` and the Maven plugin (it bundles the Kotlin generators) → [`docs/ports/kotlin.md`](docs/ports/kotlin.md) |
| C# | `dotnet tool install --global MetaObjects.Cli` → [`docs/ports/csharp.md`](docs/ports/csharp.md) |
| Python | `pip install metaobjects` → [`docs/ports/python.md`](docs/ports/python.md) |

Expand Down
4 changes: 1 addition & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

112 changes: 79 additions & 33 deletions docs/ports/csharp.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,40 @@ Postgres + Npgsql.

## Install

Published to [NuGet](https://www.nuget.org/packages/MetaObjects) at `1.0.13` — four
packages, version-locked to the C# port version:
Published to [NuGet](https://www.nuget.org/packages/MetaObjects) at `1.1.0` — four
packages, version-locked to the C# port version: `MetaObjects` (loader), `MetaObjects.Codegen`,
`MetaObjects.Render` and the `MetaObjects.Cli` tool.

```xml
<!-- YourApp.csproj -->
<ItemGroup>
<PackageReference Include="MetaObjects" Version="1.0.13" />
<PackageReference Include="MetaObjects.Codegen" Version="1.0.13" />
<PackageReference Include="MetaObjects.Render" Version="1.0.13" />
</ItemGroup>
```

Install the CLI as a .NET tool:
Install the CLI as a .NET tool, then scaffold the generator list:

```bash
dotnet tool install --global MetaObjects.Cli
dotnet meta # bare invocation prints the usage banner
dotnet meta init # writes codegen/Generators.cs (the generator list), Program.cs, Codegen.csproj
dotnet meta init # writes codegen/Generators.cs (the generator list), codegen/Program.cs, codegen/Codegen.csproj
```

Your app is a separate project beside `codegen/`. `dotnet meta init` runs from the root of your
repository, and the app's `.csproj` at that root must not compile `codegen/`, which has its own
`Main` (without the `Compile Remove` the build fails with CS0579 and CS8802). This sample uses
SQLite so it runs with nothing else installed; the same generated code runs on Postgres (see
[Use](#use)):

```xml
<!-- App.csproj -->
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MetaObjects.Codegen" Version="1.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.11" />
</ItemGroup>
<ItemGroup>
<Compile Remove="codegen/**" />
</ItemGroup>
</Project>
```

The C# CLI is invoked as `dotnet meta` (command `dotnet-meta`), or run directly
Expand All @@ -33,10 +49,9 @@ canonical Node `meta` CLI, which owns schema + TS codegen (ADR-0015).

## Configure

Drop metadata under `metadata/`:
Drop metadata under `metaobjects/`, for example `metaobjects/meta.blog.json`:

```jsonc
// metadata/meta.blog.json
```json
{ "metadata.root": {
"package": "acme::blog",
"children": [
Expand All @@ -45,8 +60,9 @@ Drop metadata under `metadata/`:
"children": [
{ "source.rdb": { "@table": "authors" } },
{ "field.long": { "name": "id" } },
{ "field.string": { "name": "name", "@required": true, "@maxLength": 200 } },
{ "field.string": { "name": "name", "@required": true, "@maxLength": 200, "@filterable": true } },
{ "field.string": { "name": "bio", "@maxLength": 2000 } },
{ "field.enum": { "name": "role", "@values": ["writer", "editor"], "@required": true, "@filterable": true } },
{ "identity.primary": { "@fields": "id", "@generation": "increment" } }
]
}}
Expand All @@ -68,7 +84,7 @@ var registry = Provider.ComposeRegistry(new IMetaDataTypeProvider[] {
yourProvider, // adds your custom subtype/attrs
});

var loader = MetaDataLoader.FromDirectory("./metadata", registry);
var loader = MetaDataLoader.FromDirectory("./metaobjects", registry);
```

The provider object has the same four-member contract (`Id`, `Dependencies`,
Expand Down Expand Up @@ -111,14 +127,25 @@ using MetaObjects.Codegen;
return CodegenCli.Run(args, Generators.All);
```

Replace the empty list `dotnet meta init` wrote with the one above before the first `gen`: with
an empty list `gen` writes nothing, and `--out` is required.

```bash
# Run the list: EF Core entities + AppDbContext + CRUD minimal-API routes
dotnet meta gen ./metadata --out ./Generated --namespace Acme.Blog
dotnet meta gen ./metaobjects --out ./Generated --namespace Acme.Blog

# Drift-check templates against payloads (FR-004)
dotnet meta verify ./metadata --templates ./prompts
# The drift gate: regenerate into a temp tree and fail (exit 1, naming the files) if the
# committed ./Generated differs. Run it in CI; the namespace is inferred from ./Generated.
dotnet meta verify ./metaobjects --codegen --out ./Generated
```

`verify --codegen` is the gate for generated code. `dotnet meta verify ./metaobjects
--templates ./prompts` is a different gate, for prompt templates against their payloads
(FR-004): it says nothing about generated code, and it fails with exit 2 when the directory it
is given does not exist. A bare `dotnet meta verify ./metaobjects` runs that templates gate
against `prompts/` when it exists, else `templates/`, and fails the same way when neither
exists; pass both subverbs to run both.

`dotnet meta gen --list` is the catalog, and it shows what each generator requires. `routes`
requires `entity`, `db-context` and `filter-allowlist`, because the handlers it emits
reference their output; `gen` warns when one is missing from the list, since the result
Expand Down Expand Up @@ -308,19 +335,38 @@ regenerate. See the "Column naming" section of

```csharp
// Program.cs
using System.Text.Json.Serialization;
using Acme.Blog;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(opts =>
opts.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddDbContext<AppDbContext>(opts => opts.UseSqlite("Data Source=blog.db"));
// Enum fields (role) travel as their symbols ("writer"), not integers: the routes return the
// entity and the host owns serialization, so without this POST {"role":"writer"} is a 400.
builder.Services.ConfigureHttpJsonOptions(o => o.SerializerOptions.Converters.Add(new JsonStringEnumConverter()));

var app = builder.Build();
using (var scope = app.Services.CreateScope())
scope.ServiceProvider.GetRequiredService<AppDbContext>().Database.EnsureCreated(); // the sample creates its own table

app.MapAuthorRoutes(); // generated — GET/POST/PUT/DELETE on /api/authors
app.MapAuthorRoutes(); // generated — GET/POST/PATCH/PUT/DELETE on /api/authors
app.Run();
```

```bash
dotnet run --urls http://127.0.0.1:5077
curl -X POST -H 'content-type: application/json' -d '{"name":"Ada","role":"writer"}' http://127.0.0.1:5077/api/authors # 201
curl -g 'http://127.0.0.1:5077/api/authors?filter[role][eq]=writer' # 200, the row
```

A filter works on a field marked `@filterable` (`name` and `role` above); any other field answers
400 `invalid_filter_field`.

**Postgres.** Swap `UseSqlite(...)` for `UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))`
and add `Npgsql.EntityFrameworkCore.PostgreSQL` instead of the SQLite package. `EnsureCreated`
is for samples; on Postgres create the tables with the Node CLI's migrations
(`meta migrate`, ADR-0015) — the C# port has no migrate surface.

EF Core does the rest — the generated entities and `AppDbContext` are plain EF Core with
no MetaObjects types in them. Note the one exception: generated **routes** emit
`using MetaObjects.Codegen.Runtime;` for the shared filter/sort helpers, so a project that
Expand All @@ -343,10 +389,10 @@ or per-file `DIFFERS`, `verify --codegen` does not treat it as drift, and
has the diff recipe for pulling an upstream fix into it.

**Consumer dependencies.** The generated `AppDbContext` and the `Program.cs`
wiring above use EF Core (`AddDbContext`, `DbContext`, `UseNpgsql`), which
MetaObjects does not pull in for you — add the two EF Core NuGet packages to
the consuming app: `Microsoft.EntityFrameworkCore` and
`Npgsql.EntityFrameworkCore.PostgreSQL`.
wiring above use EF Core (`AddDbContext`, `DbContext`, `UseSqlite`), which
MetaObjects does not pull in for you — add the EF Core provider package for your
database to the consuming app (`Microsoft.EntityFrameworkCore.Sqlite` above,
`Npgsql.EntityFrameworkCore.PostgreSQL` for Postgres).

```csharp
// Optional handwritten service over the generated DbContext
Expand Down Expand Up @@ -465,14 +511,14 @@ contract: `dotnet meta eject requirement-tests` copies it to
are core and are not ejectable.

```bash
# The gate: runs beside whichever drift gate verify was given
dotnet meta verify ./metadata --templates ./prompts
dotnet meta verify ./metadata --templates ./prompts --require-implementers
# The gate: runs on every verify, beside whichever drift gate it was given
dotnet meta verify ./metaobjects --codegen --out ./Generated
dotnet meta verify ./metaobjects --codegen --out ./Generated --require-implementers

# The tests: the same run as everything else codegen/Generators.cs declares. The entry's
# OutDir puts them in the test project, which references xunit; the app's --out does not.
dotnet meta gen ./metadata --out ./Generated
dotnet meta verify ./metadata --codegen --out ./Generated
dotnet meta gen ./metaobjects --out ./Generated
dotnet meta verify ./metaobjects --codegen --out ./Generated
```

Give the entry an `OutDir` in `codegen/Generators.cs` (below): the tests need xunit, which
Expand Down
Loading
Loading