feat: add shared domain contracts and mongo setup

This commit is contained in:
eust-w 2026-03-26 17:24:44 +08:00
parent f41816bbd9
commit 4a3c5a1431
13 changed files with 245 additions and 17 deletions

View File

@ -1,8 +1,10 @@
{ {
"name": "@emboflow/api", "name": "api",
"private": true, "private": true,
"version": "0.1.0", "version": "0.1.0",
"type": "module",
"scripts": { "scripts": {
"dev": "echo 'api app scaffold pending'" "dev": "echo 'api app scaffold pending'",
"test": "node --test --experimental-strip-types"
} }
} }

View File

@ -0,0 +1,20 @@
export type MongoConnectionConfig = {
username: string;
password: string;
host: string;
port: number;
database: string;
authSource?: string;
};
export function createMongoConnectionUri(
config: MongoConnectionConfig,
): string {
const authSource = config.authSource ?? "admin";
return `mongodb://${config.username}:${config.password}@${config.host}:${config.port}/${config.database}?authSource=${authSource}`;
}
export const mongoModuleConfig = {
connectionName: "primary",
defaultAuthSource: "admin",
};

View File

@ -0,0 +1,18 @@
import { ASSET_TYPES } from "../../../../../../packages/contracts/src/domain.ts";
export const ASSET_COLLECTION_NAME = "assets";
export const assetSchemaDefinition = {
workspaceId: { type: "string", required: true },
projectId: { type: "string", required: true },
type: { enum: [...ASSET_TYPES], required: true },
sourceType: { type: "string", required: true },
displayName: { type: "string", required: true },
status: { type: "string", required: true, default: "pending" },
storageRef: { type: "object", required: false, default: null },
detectedFormats: { type: "array", required: false, default: [] },
summary: { type: "object", required: false, default: {} },
createdBy: { type: "string", required: true },
createdAt: { type: "date", required: true },
updatedAt: { type: "date", required: true },
} as const;

View File

@ -0,0 +1,18 @@
export {
WORKSPACE_COLLECTION_NAME,
workspaceSchemaDefinition,
} from "./workspace.schema.ts";
export {
PROJECT_COLLECTION_NAME,
projectSchemaDefinition,
} from "./project.schema.ts";
export {
ASSET_COLLECTION_NAME,
assetSchemaDefinition,
} from "./asset.schema.ts";
export {
WORKFLOW_DEFINITION_COLLECTION_NAME,
WORKFLOW_RUN_COLLECTION_NAME,
workflowDefinitionSchemaDefinition,
workflowRunSchemaDefinition,
} from "./workflow.schema.ts";

View File

@ -0,0 +1,12 @@
export const PROJECT_COLLECTION_NAME = "projects";
export const projectSchemaDefinition = {
workspaceId: { type: "string", required: true },
name: { type: "string", required: true },
slug: { type: "string", required: true },
description: { type: "string", required: false, default: "" },
status: { type: "string", required: true, default: "active" },
createdBy: { type: "string", required: true },
createdAt: { type: "date", required: true },
updatedAt: { type: "date", required: true },
} as const;

View File

@ -0,0 +1,30 @@
import {
WORKFLOW_DEFINITION_STATUSES,
WORKFLOW_RUN_STATUSES,
} from "../../../../../../packages/contracts/src/domain.ts";
export const WORKFLOW_DEFINITION_COLLECTION_NAME = "workflow_definitions";
export const WORKFLOW_RUN_COLLECTION_NAME = "workflow_runs";
export const workflowDefinitionSchemaDefinition = {
workspaceId: { type: "string", required: true },
projectId: { type: "string", required: true },
name: { type: "string", required: true },
slug: { type: "string", required: true },
status: { enum: [...WORKFLOW_DEFINITION_STATUSES], required: true },
latestVersionNumber: { type: "number", required: true, default: 1 },
publishedVersionNumber: { type: "number", required: false, default: null },
createdBy: { type: "string", required: true },
createdAt: { type: "date", required: true },
updatedAt: { type: "date", required: true },
} as const;
export const workflowRunSchemaDefinition = {
workflowDefinitionId: { type: "string", required: true },
workflowVersionId: { type: "string", required: true },
status: { enum: [...WORKFLOW_RUN_STATUSES], required: true },
triggeredBy: { type: "string", required: true },
createdAt: { type: "date", required: true },
startedAt: { type: "date", required: false, default: null },
finishedAt: { type: "date", required: false, default: null },
} as const;

View File

@ -0,0 +1,14 @@
import { WORKSPACE_TYPES } from "../../../../../../packages/contracts/src/domain.ts";
export const WORKSPACE_COLLECTION_NAME = "workspaces";
export const workspaceSchemaDefinition = {
type: { enum: [...WORKSPACE_TYPES], required: true },
name: { type: "string", required: true },
slug: { type: "string", required: true },
ownerId: { type: "string", required: true },
status: { type: "string", required: true, default: "active" },
settings: { type: "object", required: false, default: {} },
createdAt: { type: "date", required: true },
updatedAt: { type: "date", required: true },
} as const;

View File

@ -0,0 +1,52 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
ASSET_TYPES,
WORKFLOW_RUN_STATUSES,
WORKSPACE_TYPES,
} from "../../../packages/contracts/src/domain.ts";
import { createMongoConnectionUri } from "../src/common/mongo/mongo.module.ts";
import {
ASSET_COLLECTION_NAME,
PROJECT_COLLECTION_NAME,
WORKFLOW_DEFINITION_COLLECTION_NAME,
WORKSPACE_COLLECTION_NAME,
} from "../src/common/mongo/schemas/index.ts";
test("workspace types include personal and team", () => {
assert.deepEqual(WORKSPACE_TYPES, ["personal", "team"]);
});
test("asset types include raw and dataset-oriented sources", () => {
assert.equal(ASSET_TYPES.includes("raw_file"), true);
assert.equal(ASSET_TYPES.includes("standard_dataset"), true);
assert.equal(ASSET_TYPES.includes("rosbag"), true);
});
test("workflow run statuses include the documented execution states", () => {
assert.equal(WORKFLOW_RUN_STATUSES.includes("pending"), true);
assert.equal(WORKFLOW_RUN_STATUSES.includes("running"), true);
assert.equal(WORKFLOW_RUN_STATUSES.includes("success"), true);
assert.equal(WORKFLOW_RUN_STATUSES.includes("failed"), true);
});
test("mongo connection uri builder uses environment-style config", () => {
assert.equal(
createMongoConnectionUri({
username: "emboflow",
password: "emboflow",
host: "localhost",
port: 27017,
database: "emboflow",
}),
"mongodb://emboflow:emboflow@localhost:27017/emboflow?authSource=admin",
);
});
test("schema collection names match the core domain objects", () => {
assert.equal(WORKSPACE_COLLECTION_NAME, "workspaces");
assert.equal(PROJECT_COLLECTION_NAME, "projects");
assert.equal(ASSET_COLLECTION_NAME, "assets");
assert.equal(WORKFLOW_DEFINITION_COLLECTION_NAME, "workflow_definitions");
});

View File

@ -6,7 +6,7 @@
**Architecture:** Use a TypeScript monorepo with a React web app, a Node.js API control plane, and a separate Node.js worker. Use MongoDB as the only database, object storage abstraction for cloud storage or MinIO, and a local scheduler with Python and Docker executor contracts. **Architecture:** Use a TypeScript monorepo with a React web app, a Node.js API control plane, and a separate Node.js worker. Use MongoDB as the only database, object storage abstraction for cloud storage or MinIO, and a local scheduler with Python and Docker executor contracts.
**Tech Stack:** pnpm workspace, React, TypeScript, React Flow, NestJS, Mongoose, MongoDB, Docker Compose, Python runtime hooks, unittest/Vitest/Jest-compatible project tests **Tech Stack:** pnpm workspace, React, TypeScript, React Flow, NestJS, Mongoose, MongoDB, Docker Compose, Python runtime hooks, Python unittest, and Node 22 built-in test runner with TypeScript stripping for early package-level tests
--- ---
@ -91,7 +91,7 @@ Create `apps/api/test/domain-contracts.spec.ts` asserting:
Run: Run:
```bash ```bash
pnpm --filter api test domain-contracts.spec.ts pnpm --filter api test test/domain-contracts.spec.ts
``` ```
Expected: FAIL because contracts and schemas are missing. Expected: FAIL because contracts and schemas are missing.
@ -112,7 +112,7 @@ Add a minimal Mongo module in the API app using environment-based connection con
Run: Run:
```bash ```bash
pnpm --filter api test domain-contracts.spec.ts pnpm --filter api test test/domain-contracts.spec.ts
``` ```
Expected: PASS Expected: PASS
@ -149,7 +149,7 @@ Create `apps/api/test/projects.e2e-spec.ts` covering:
Run: Run:
```bash ```bash
pnpm --filter api test projects.e2e-spec.ts pnpm --filter api test test/projects.e2e-spec.ts
``` ```
Expected: FAIL because the modules and endpoints do not exist yet. Expected: FAIL because the modules and endpoints do not exist yet.
@ -170,7 +170,7 @@ Do not build a full production auth stack before the API shape is stable.
Run: Run:
```bash ```bash
pnpm --filter api test projects.e2e-spec.ts pnpm --filter api test test/projects.e2e-spec.ts
``` ```
Expected: PASS Expected: PASS
@ -207,7 +207,7 @@ Create `apps/api/test/assets.e2e-spec.ts` covering:
Run: Run:
```bash ```bash
pnpm --filter api test assets.e2e-spec.ts pnpm --filter api test test/assets.e2e-spec.ts
``` ```
Expected: FAIL because asset ingestion and probe services are missing. Expected: FAIL because asset ingestion and probe services are missing.
@ -229,7 +229,7 @@ Do not build full binary upload optimization yet. First make the metadata contra
Run: Run:
```bash ```bash
pnpm --filter api test assets.e2e-spec.ts pnpm --filter api test test/assets.e2e-spec.ts
``` ```
Expected: PASS Expected: PASS
@ -269,7 +269,7 @@ Create `apps/api/test/workflow-runs.e2e-spec.ts` covering:
Run: Run:
```bash ```bash
pnpm --filter api test workflow-runs.e2e-spec.ts pnpm --filter api test test/workflow-runs.e2e-spec.ts
``` ```
Expected: FAIL because workflow versioning and run creation do not exist yet. Expected: FAIL because workflow versioning and run creation do not exist yet.
@ -291,7 +291,7 @@ Keep V1 graph compilation simple. Support sequential edges first, then one-level
Run: Run:
```bash ```bash
pnpm --filter api test workflow-runs.e2e-spec.ts pnpm --filter api test test/workflow-runs.e2e-spec.ts
``` ```
Expected: PASS Expected: PASS
@ -510,8 +510,8 @@ Create:
Run: Run:
```bash ```bash
pnpm --filter api test artifacts.e2e-spec.ts pnpm --filter api test test/artifacts.e2e-spec.ts
pnpm --filter web test explore-page.test.tsx pnpm --filter web test src/features/explore/explore-page.test.tsx
``` ```
Expected: FAIL because artifact APIs and explore renderers do not exist yet. Expected: FAIL because artifact APIs and explore renderers do not exist yet.
@ -532,8 +532,8 @@ Do not implement the full renderer plugin platform yet. Start with built-ins and
Run: Run:
```bash ```bash
pnpm --filter api test artifacts.e2e-spec.ts pnpm --filter api test test/artifacts.e2e-spec.ts
pnpm --filter web test explore-page.test.tsx pnpm --filter web test src/features/explore/explore-page.test.tsx
``` ```
Expected: PASS Expected: PASS

View File

@ -0,0 +1,9 @@
{
"name": "contracts",
"private": true,
"version": "0.1.0",
"type": "module",
"exports": {
"./domain": "./src/domain.ts"
}
}

View File

@ -0,0 +1,43 @@
export const WORKSPACE_TYPES = ["personal", "team"] as const;
export type WorkspaceType = (typeof WORKSPACE_TYPES)[number];
export const ASSET_TYPES = [
"raw_file",
"archive",
"folder",
"video_collection",
"standard_dataset",
"rosbag",
"hdf5_dataset",
"object_storage_prefix",
] as const;
export type AssetType = (typeof ASSET_TYPES)[number];
export const WORKFLOW_DEFINITION_STATUSES = [
"draft",
"active",
"archived",
] as const;
export type WorkflowDefinitionStatus = (typeof WORKFLOW_DEFINITION_STATUSES)[number];
export const WORKFLOW_RUN_STATUSES = [
"pending",
"queued",
"running",
"success",
"failed",
"cancelled",
"partial_success",
] as const;
export type WorkflowRunStatus = (typeof WORKFLOW_RUN_STATUSES)[number];
export const RUN_TASK_STATUSES = [
"pending",
"queued",
"running",
"success",
"failed",
"cancelled",
"skipped",
] as const;
export type RunTaskStatus = (typeof RUN_TASK_STATUSES)[number];

View File

@ -47,7 +47,7 @@ def run_git(repo: Path, *args: str) -> list[str]:
) )
if result.returncode != 0: if result.returncode != 0:
raise RuntimeError(result.stderr.strip() or "git command failed") raise RuntimeError(result.stderr.strip() or "git command failed")
return [line.strip() for line in result.stdout.splitlines() if line.strip()] return [line.rstrip() for line in result.stdout.splitlines() if line.strip()]
def classify(path_text: str) -> str: def classify(path_text: str) -> str:
@ -117,6 +117,10 @@ def assess_changes(
} }
def extract_status_paths(lines: list[str]) -> list[str]:
return sorted({line[3:] for line in lines if len(line) > 3})
def collect_paths(repo: Path, args: argparse.Namespace) -> list[str]: def collect_paths(repo: Path, args: argparse.Namespace) -> list[str]:
if args.staged: if args.staged:
return run_git(repo, "diff", "--cached", "--name-only", "--diff-filter=ACMR") return run_git(repo, "diff", "--cached", "--name-only", "--diff-filter=ACMR")
@ -128,7 +132,7 @@ def collect_paths(repo: Path, args: argparse.Namespace) -> list[str]:
return run_git(repo, "diff-tree", "--no-commit-id", "--name-only", "-r", args.rev_range) return run_git(repo, "diff-tree", "--no-commit-id", "--name-only", "-r", args.rev_range)
changed = run_git(repo, "status", "--short") changed = run_git(repo, "status", "--short")
return sorted({line[3:] for line in changed if len(line) > 3}) return extract_status_paths(changed)
def parse_args() -> argparse.Namespace: def parse_args() -> argparse.Namespace:

View File

@ -28,6 +28,12 @@ class DocCodeSyncAssessmentTests(unittest.TestCase):
def test_classifies_env_example_as_config(self): def test_classifies_env_example_as_config(self):
self.assertEqual(MODULE.classify(".env.example"), "config") self.assertEqual(MODULE.classify(".env.example"), "config")
def test_extract_status_paths_preserves_first_character(self):
self.assertEqual(
MODULE.extract_status_paths([" M apps/api/package.json"]),
["apps/api/package.json"],
)
def test_strict_mode_blocks_code_without_doc_updates(self): def test_strict_mode_blocks_code_without_doc_updates(self):
assessment = MODULE.assess_changes( assessment = MODULE.assess_changes(
docs=[], docs=[],