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
25 changes: 25 additions & 0 deletions examples/reconciler/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
# Reconciler

This example deploys two static sites from an ordinary Node.js program. It does not
compile a Notation project or start the Notation CLI.

[`src/index.ts`](./src/index.ts) is the complete program. It defines the desired
resources inline, opens a SQLite state backend, and passes the resources directly to the
reconciler. [`src/static-site.ts`](./src/static-site.ts) defines the local provider
operations used to create, read, update, and delete each site.

Run it from the repository root:

```sh
pnpm --filter reconciler-example demo
```

The generated sites are written to `sites/`, and deployment state is stored in
`sites.db`. Change the resource configuration and run the command again to update the
sites. Remove a resource from the array and run it again to delete that site.

Run the integration test with:

```sh
pnpm --filter reconciler-example test
```
20 changes: 20 additions & 0 deletions examples/reconciler/package.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
{
"private": true,
"name": "reconciler-example",
"type": "module",
"scripts": {
"build": "tsup --clean",
"demo": "pnpm build && node dist/index.js",
"test": "vitest run --root ../.. examples/reconciler/test/reconciler.test.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@notation/reconciler": "workspace:*",
"@notation/resource": "workspace:*",
"@notation/state-sqlite": "workspace:*"
},
"devDependencies": {
"@types/node": "^22.13.4",
"vitest": "^4.1.10"
}
}
33 changes: 33 additions & 0 deletions examples/reconciler/src/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
import { Reconciler, createResourceRegistry } from "@notation/reconciler";
import { SqliteStateBackend } from "@notation/state-sqlite";
import { StaticSite } from "./static-site";

const state = new SqliteStateBackend("sites.db");

const resources = [
new StaticSite({
id: "documentation",
config: {
siteDirectory: "sites/docs",
html: "<h1>Documentation</h1>\n",
},
}),
new StaticSite({
id: "status",
config: {
siteDirectory: "sites/status",
html: "<h1>All systems operational</h1>\n",
},
}),
];

const reconciler = new Reconciler({
state,
registry: createResourceRegistry([StaticSite]),
});

try {
await reconciler.deploy(resources);
} finally {
state.close();
}
63 changes: 63 additions & 0 deletions examples/reconciler/src/static-site.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import { resource } from "@notation/resource";

type StaticSiteApi = {
Key: { siteDirectory: string };
CreateParams: { siteDirectory: string; html: string };
UpdateParams: { siteDirectory: string; html: string };
ReadResult: { html: string };
};

class SiteNotFound extends Error {
readonly name = "SiteNotFound";
}

const staticSite = resource<StaticSiteApi>({ type: "local/site/static" });

export const StaticSite = staticSite
.defineSchema({
siteDirectory: {
propertyType: "param",
presence: "required",
primaryKey: true,
},
html: {
propertyType: "param",
presence: "required",
},
} as const)
.defineOperations({
create: async ({ siteDirectory, html }) => {
await mkdir(siteDirectory, { recursive: true });
await writeFile(path.join(siteDirectory, "index.html"), html, "utf8");
},
read: async ({ siteDirectory }) => {
try {
const html = await readFile(
path.join(siteDirectory, "index.html"),
"utf8",
);
return { html };
} catch (error) {
if (isFileMissing(error)) throw new SiteNotFound(siteDirectory);
throw error;
}
},
update: async (_key, _patch, { siteDirectory, html }) => {
await writeFile(path.join(siteDirectory, "index.html"), html, "utf8");
},
delete: async ({ siteDirectory }) => {
await rm(siteDirectory, { recursive: true });
},
notFoundOnError: [{ name: "SiteNotFound", reason: "site was removed" }],
});

function isFileMissing(error: unknown): boolean {
return (
typeof error === "object" &&
error !== null &&
"code" in error &&
error.code === "ENOENT"
);
}
36 changes: 36 additions & 0 deletions examples/reconciler/test/reconciler.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";

const directories: string[] = [];
const originalWorkingDirectory = process.cwd();

afterEach(async () => {
process.chdir(originalWorkingDirectory);
await Promise.all(
directories
.splice(0)
.map((directory) => rm(directory, { recursive: true, force: true })),
);
});

describe("reconciler example", () => {
it("runs as a self-contained program", async () => {
const workingDirectory = await mkdtemp(
path.join(tmpdir(), "notation-reconciler-test-"),
);
directories.push(workingDirectory);
process.chdir(workingDirectory);

await import("../src/index");

await expect(readFile("sites/docs/index.html", "utf8")).resolves.toBe(
"<h1>Documentation</h1>\n",
);
await expect(readFile("sites/status/index.html", "utf8")).resolves.toBe(
"<h1>All systems operational</h1>\n",
);
await expect(readFile("sites.db")).resolves.not.toHaveLength(0);
});
});
4 changes: 4 additions & 0 deletions examples/reconciler/tsconfig.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
{
"extends": "tsconfig/base.json",
"include": ["src", "test"]
}
8 changes: 8 additions & 0 deletions examples/reconciler/tsup.config.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
import { defineConfig } from "tsup";

export default defineConfig({
entry: ["src/index.ts"],
format: ["esm"],
dts: true,
sourcemap: true,
});
19 changes: 19 additions & 0 deletions pnpm-lock.yaml

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

Loading