From 4a3c5a14316b3cd5e43afdb46eb476e244d81703 Mon Sep 17 00:00:00 2001 From: eust-w Date: Thu, 26 Mar 2026 17:24:44 +0800 Subject: [PATCH] :sparkles: feat: add shared domain contracts and mongo setup --- apps/api/package.json | 6 ++- apps/api/src/common/mongo/mongo.module.ts | 20 +++++++ .../src/common/mongo/schemas/asset.schema.ts | 18 +++++++ apps/api/src/common/mongo/schemas/index.ts | 18 +++++++ .../common/mongo/schemas/project.schema.ts | 12 +++++ .../common/mongo/schemas/workflow.schema.ts | 30 +++++++++++ .../common/mongo/schemas/workspace.schema.ts | 14 +++++ apps/api/test/domain-contracts.spec.ts | 52 +++++++++++++++++++ ...26-03-26-emboflow-v1-foundation-and-mvp.md | 26 +++++----- packages/contracts/package.json | 9 ++++ packages/contracts/src/domain.ts | 43 +++++++++++++++ scripts/check_doc_code_sync.py | 8 ++- tests/test_doc_code_sync.py | 6 +++ 13 files changed, 245 insertions(+), 17 deletions(-) create mode 100644 apps/api/src/common/mongo/mongo.module.ts create mode 100644 apps/api/src/common/mongo/schemas/asset.schema.ts create mode 100644 apps/api/src/common/mongo/schemas/index.ts create mode 100644 apps/api/src/common/mongo/schemas/project.schema.ts create mode 100644 apps/api/src/common/mongo/schemas/workflow.schema.ts create mode 100644 apps/api/src/common/mongo/schemas/workspace.schema.ts create mode 100644 apps/api/test/domain-contracts.spec.ts create mode 100644 packages/contracts/package.json create mode 100644 packages/contracts/src/domain.ts diff --git a/apps/api/package.json b/apps/api/package.json index 1e6f2a6..14f2620 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,8 +1,10 @@ { - "name": "@emboflow/api", + "name": "api", "private": true, "version": "0.1.0", + "type": "module", "scripts": { - "dev": "echo 'api app scaffold pending'" + "dev": "echo 'api app scaffold pending'", + "test": "node --test --experimental-strip-types" } } diff --git a/apps/api/src/common/mongo/mongo.module.ts b/apps/api/src/common/mongo/mongo.module.ts new file mode 100644 index 0000000..291d96f --- /dev/null +++ b/apps/api/src/common/mongo/mongo.module.ts @@ -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", +}; diff --git a/apps/api/src/common/mongo/schemas/asset.schema.ts b/apps/api/src/common/mongo/schemas/asset.schema.ts new file mode 100644 index 0000000..e5d3ec6 --- /dev/null +++ b/apps/api/src/common/mongo/schemas/asset.schema.ts @@ -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; diff --git a/apps/api/src/common/mongo/schemas/index.ts b/apps/api/src/common/mongo/schemas/index.ts new file mode 100644 index 0000000..53cb863 --- /dev/null +++ b/apps/api/src/common/mongo/schemas/index.ts @@ -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"; diff --git a/apps/api/src/common/mongo/schemas/project.schema.ts b/apps/api/src/common/mongo/schemas/project.schema.ts new file mode 100644 index 0000000..634cac7 --- /dev/null +++ b/apps/api/src/common/mongo/schemas/project.schema.ts @@ -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; diff --git a/apps/api/src/common/mongo/schemas/workflow.schema.ts b/apps/api/src/common/mongo/schemas/workflow.schema.ts new file mode 100644 index 0000000..f1516a8 --- /dev/null +++ b/apps/api/src/common/mongo/schemas/workflow.schema.ts @@ -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; diff --git a/apps/api/src/common/mongo/schemas/workspace.schema.ts b/apps/api/src/common/mongo/schemas/workspace.schema.ts new file mode 100644 index 0000000..fae3f96 --- /dev/null +++ b/apps/api/src/common/mongo/schemas/workspace.schema.ts @@ -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; diff --git a/apps/api/test/domain-contracts.spec.ts b/apps/api/test/domain-contracts.spec.ts new file mode 100644 index 0000000..07522dd --- /dev/null +++ b/apps/api/test/domain-contracts.spec.ts @@ -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"); +}); diff --git a/docs/plans/2026-03-26-emboflow-v1-foundation-and-mvp.md b/docs/plans/2026-03-26-emboflow-v1-foundation-and-mvp.md index fd9f2b0..c955b7f 100644 --- a/docs/plans/2026-03-26-emboflow-v1-foundation-and-mvp.md +++ b/docs/plans/2026-03-26-emboflow-v1-foundation-and-mvp.md @@ -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. -**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: ```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. @@ -112,7 +112,7 @@ Add a minimal Mongo module in the API app using environment-based connection con Run: ```bash -pnpm --filter api test domain-contracts.spec.ts +pnpm --filter api test test/domain-contracts.spec.ts ``` Expected: PASS @@ -149,7 +149,7 @@ Create `apps/api/test/projects.e2e-spec.ts` covering: Run: ```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. @@ -170,7 +170,7 @@ Do not build a full production auth stack before the API shape is stable. Run: ```bash -pnpm --filter api test projects.e2e-spec.ts +pnpm --filter api test test/projects.e2e-spec.ts ``` Expected: PASS @@ -207,7 +207,7 @@ Create `apps/api/test/assets.e2e-spec.ts` covering: Run: ```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. @@ -229,7 +229,7 @@ Do not build full binary upload optimization yet. First make the metadata contra Run: ```bash -pnpm --filter api test assets.e2e-spec.ts +pnpm --filter api test test/assets.e2e-spec.ts ``` Expected: PASS @@ -269,7 +269,7 @@ Create `apps/api/test/workflow-runs.e2e-spec.ts` covering: Run: ```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. @@ -291,7 +291,7 @@ Keep V1 graph compilation simple. Support sequential edges first, then one-level Run: ```bash -pnpm --filter api test workflow-runs.e2e-spec.ts +pnpm --filter api test test/workflow-runs.e2e-spec.ts ``` Expected: PASS @@ -510,8 +510,8 @@ Create: Run: ```bash -pnpm --filter api test artifacts.e2e-spec.ts -pnpm --filter web test explore-page.test.tsx +pnpm --filter api test test/artifacts.e2e-spec.ts +pnpm --filter web test src/features/explore/explore-page.test.tsx ``` 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: ```bash -pnpm --filter api test artifacts.e2e-spec.ts -pnpm --filter web test explore-page.test.tsx +pnpm --filter api test test/artifacts.e2e-spec.ts +pnpm --filter web test src/features/explore/explore-page.test.tsx ``` Expected: PASS diff --git a/packages/contracts/package.json b/packages/contracts/package.json new file mode 100644 index 0000000..54a4e31 --- /dev/null +++ b/packages/contracts/package.json @@ -0,0 +1,9 @@ +{ + "name": "contracts", + "private": true, + "version": "0.1.0", + "type": "module", + "exports": { + "./domain": "./src/domain.ts" + } +} diff --git a/packages/contracts/src/domain.ts b/packages/contracts/src/domain.ts new file mode 100644 index 0000000..7706ce9 --- /dev/null +++ b/packages/contracts/src/domain.ts @@ -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]; diff --git a/scripts/check_doc_code_sync.py b/scripts/check_doc_code_sync.py index 7fdcaec..8502c68 100755 --- a/scripts/check_doc_code_sync.py +++ b/scripts/check_doc_code_sync.py @@ -47,7 +47,7 @@ def run_git(repo: Path, *args: str) -> list[str]: ) if result.returncode != 0: 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: @@ -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]: if args.staged: 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) 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: diff --git a/tests/test_doc_code_sync.py b/tests/test_doc_code_sync.py index 3af7a39..56c071f 100644 --- a/tests/test_doc_code_sync.py +++ b/tests/test_doc_code_sync.py @@ -28,6 +28,12 @@ class DocCodeSyncAssessmentTests(unittest.TestCase): def test_classifies_env_example_as_config(self): 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): assessment = MODULE.assess_changes( docs=[],