✨ feat: add shared domain contracts and mongo setup
This commit is contained in:
parent
f41816bbd9
commit
4a3c5a1431
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
20
apps/api/src/common/mongo/mongo.module.ts
Normal file
20
apps/api/src/common/mongo/mongo.module.ts
Normal 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",
|
||||
};
|
||||
18
apps/api/src/common/mongo/schemas/asset.schema.ts
Normal file
18
apps/api/src/common/mongo/schemas/asset.schema.ts
Normal 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;
|
||||
18
apps/api/src/common/mongo/schemas/index.ts
Normal file
18
apps/api/src/common/mongo/schemas/index.ts
Normal 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";
|
||||
12
apps/api/src/common/mongo/schemas/project.schema.ts
Normal file
12
apps/api/src/common/mongo/schemas/project.schema.ts
Normal 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;
|
||||
30
apps/api/src/common/mongo/schemas/workflow.schema.ts
Normal file
30
apps/api/src/common/mongo/schemas/workflow.schema.ts
Normal 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;
|
||||
14
apps/api/src/common/mongo/schemas/workspace.schema.ts
Normal file
14
apps/api/src/common/mongo/schemas/workspace.schema.ts
Normal 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;
|
||||
52
apps/api/test/domain-contracts.spec.ts
Normal file
52
apps/api/test/domain-contracts.spec.ts
Normal 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");
|
||||
});
|
||||
@ -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
|
||||
|
||||
9
packages/contracts/package.json
Normal file
9
packages/contracts/package.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "contracts",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./domain": "./src/domain.ts"
|
||||
}
|
||||
}
|
||||
43
packages/contracts/src/domain.ts
Normal file
43
packages/contracts/src/domain.ts
Normal 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];
|
||||
@ -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:
|
||||
|
||||
@ -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=[],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user