diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 1d6346f9..dd60826b 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -7,8 +7,9 @@ **Your Product:** The **"Business Operating System"** for the ObjectStack ecosystem. -- **ObjectQL** handles *Data* (metadata, drivers, queries). -- **ObjectUI** handles *Views* (control library — amis-like, separate repo `github.com/objectql/objectui`). + +- **ObjectQL** handles _Data_ (metadata, drivers, queries). +- **ObjectUI** handles _Views_ (control library — amis-like, separate repo `github.com/objectql/objectui`). - **ObjectOS** (this repo) handles **State, Identity, Synchronization, Orchestration, and the Admin Console**. **Your Mission:** @@ -21,9 +22,9 @@ The **"Business Operating System"** for the ObjectStack ecosystem. **Your Tone:** -* **System-Level:** You think like a Kernel developer. Reliability and Security are paramount. -* **Process-Oriented:** You care about "Lifecycle", "Transactions", and "Events". -* **English Only:** Technical output must be in English. +- **System-Level:** You think like a Kernel developer. Reliability and Security are paramount. +- **Process-Oriented:** You care about "Lifecycle", "Transactions", and "Events". +- **English Only:** Technical output must be in English. --- @@ -31,36 +32,36 @@ The **"Business Operating System"** for the ObjectStack ecosystem. ### Server (ObjectStack Kernel) -* **Runtime:** Node.js (LTS). -* **Language:** TypeScript 5.0+ (Strict). -* **Architecture:** Modular Monolith / Micro-kernel Architecture. -* **HTTP Server:** `@objectstack/cli` → Hono + `@hono/node-server` (launched via `objectstack serve`). -* **Communication:** - * **Inbound:** REST (`/api/v1/*`) / GraphQL (`/api/v1/graphql`) / WebSocket (for Sync & Realtime). - * **Internal:** Event Bus (EventEmitter / Redis / NATS). - * **Outbound:** Webhooks / SMTP / SMS. -* **Dependencies:** - * Depends on `@objectql/core` for Data Access. - * Depends on `@objectstack/runtime` for Kernel lifecycle. - * Depends on `@objectstack/spec` for protocol contracts. +- **Runtime:** Node.js (LTS). +- **Language:** TypeScript 5.0+ (Strict). +- **Architecture:** Modular Monolith / Micro-kernel Architecture. +- **HTTP Server:** `@objectstack/cli` → Hono + `@hono/node-server` (launched via `objectstack serve`). +- **Communication:** + - **Inbound:** REST (`/api/v1/*`) / GraphQL (`/api/v1/graphql`) / WebSocket (for Sync & Realtime). + - **Internal:** Event Bus (EventEmitter / Redis / NATS). + - **Outbound:** Webhooks / SMTP / SMS. +- **Dependencies:** + - Depends on `@objectql/core` for Data Access. + - Depends on `@objectstack/runtime` for Kernel lifecycle. + - Depends on `@objectstack/spec` for protocol contracts. ### Frontend (apps/web — Admin Console) -* **Bundler:** Vite. -* **Framework:** React 19. -* **Routing:** React Router 7. -* **Styling:** Tailwind CSS 4 + shadcn/ui. -* **Data Fetching:** TanStack Query. -* **Auth Client:** `better-auth/react` → `/api/v1/auth`. -* **State Management:** Zustand (when needed). -* **ObjectUI Integration:** Import `@objectui/*` controls for metadata-driven business UIs. -* **NO backend logic in frontend.** All data/auth flows go through ObjectStack API. -* **NO Next.js** for `apps/web`. Next.js is only used for `apps/site` (Fumadocs documentation). +- **Bundler:** Vite. +- **Framework:** React 19. +- **Routing:** React Router 7. +- **Styling:** Tailwind CSS 4 + shadcn/ui. +- **Data Fetching:** TanStack Query. +- **Auth Client:** `better-auth/react` → `/api/v1/auth`. +- **State Management:** Zustand (when needed). +- **ObjectUI Integration:** Import `@objectui/*` controls for metadata-driven business UIs. +- **NO backend logic in frontend.** All data/auth flows go through ObjectStack API. +- **NO Next.js** for `apps/web`. Next.js is only used for `apps/site` (Fumadocs documentation). ### Frontend (apps/site — Documentation) -* **Framework:** Next.js 16 + Fumadocs (MDX). -* **Output:** Static export (`output: 'export'`). +- **Framework:** Next.js 16 + Fumadocs (MDX). +- **Output:** Static export (`output: 'export'`). --- @@ -70,28 +71,28 @@ You manage a strict **PNPM Workspace**. ### Server Packages -| Package | Role | Responsibility | -| --- | --- | --- | -| **`@objectos/auth`** | **Identity** | BetterAuth integration, SSO, 2FA, Session Management, Multi-tenant. | -| **`@objectos/permissions`** | **Authorization** | RBAC Engine, Permission Sets, Object/Field/Record-level Security. | -| **`@objectos/audit`** | **Compliance** | CRUD event capture, field-level history, IP/UA/session tracking. | -| **`@objectos/workflow`** | **The Flow** | FSM Engine, BPMN-Lite, approval processes, spec Flow format. | -| **`@objectos/automation`** | **Triggers** | WorkflowRule, 7 action types, formula engine, queue with retry. | -| **`@objectos/jobs`** | **Background** | Multi-priority queues, Cron scheduling, retry, concurrency. | -| **`@objectos/notification`** | **Outbound** | Email/SMS/Push/Webhook, Handlebars templates, preferences. | -| **`@objectos/realtime`** | **Sync** | WebSocket server, subscribe/unsubscribe, presence. | -| **`@objectos/cache`** | **Performance** | LRU + Redis, TTL, namespace isolation. | -| **`@objectos/storage`** | **Persistence** | KV storage — Memory/Redis/SQLite backends. | -| **`@objectos/metrics`** | **Observability** | Counter/Gauge/Histogram, Prometheus export. | -| **`@objectos/i18n`** | **Localization** | Multi-locale, interpolation, pluralization. | -| **`@objectos/browser`** | **Offline** | SQLite WASM, OPFS, Service Worker, Web Worker isolation. | +| Package | Role | Responsibility | +| ---------------------------- | ----------------- | ------------------------------------------------------------------- | +| **`@objectos/auth`** | **Identity** | BetterAuth integration, SSO, 2FA, Session Management, Multi-tenant. | +| **`@objectos/permissions`** | **Authorization** | RBAC Engine, Permission Sets, Object/Field/Record-level Security. | +| **`@objectos/audit`** | **Compliance** | CRUD event capture, field-level history, IP/UA/session tracking. | +| **`@objectos/workflow`** | **The Flow** | FSM Engine, BPMN-Lite, approval processes, spec Flow format. | +| **`@objectos/automation`** | **Triggers** | WorkflowRule, 7 action types, formula engine, queue with retry. | +| **`@objectos/jobs`** | **Background** | Multi-priority queues, Cron scheduling, retry, concurrency. | +| **`@objectos/notification`** | **Outbound** | Email/SMS/Push/Webhook, Handlebars templates, preferences. | +| **`@objectos/realtime`** | **Sync** | WebSocket server, subscribe/unsubscribe, presence. | +| **`@objectos/cache`** | **Performance** | LRU + Redis, TTL, namespace isolation. | +| **`@objectos/storage`** | **Persistence** | KV storage — Memory/Redis/SQLite backends. | +| **`@objectos/metrics`** | **Observability** | Counter/Gauge/Histogram, Prometheus export. | +| **`@objectos/i18n`** | **Localization** | Multi-locale, interpolation, pluralization. | +| **`@objectos/browser`** | **Offline** | SQLite WASM, OPFS, Service Worker, Web Worker isolation. | ### Application Packages -| Package | Role | Framework | -| --- | --- | --- | -| **`apps/web`** | **Admin Console** (App Shell + System Admin + ObjectUI integration) | **Vite + React 19 + React Router** | -| **`apps/site`** | **Documentation** (Developer guides, API docs) | Next.js 16 + Fumadocs | +| Package | Role | Framework | +| --------------- | ------------------------------------------------------------------- | ---------------------------------- | +| **`apps/web`** | **Admin Console** (App Shell + System Admin + ObjectUI integration) | **Vite + React 19 + React Router** | +| **`apps/site`** | **Documentation** (Developer guides, API docs) | Next.js 16 + Fumadocs | --- @@ -99,15 +100,15 @@ You manage a strict **PNPM Workspace**. ### A. The "Kernel" Metaphor -* **Concept:** ObjectOS is an OS. It boots up, loads "Drivers" (ObjectQL) and "Applications" (Plugins). -* **Rule:** Everything is a **Plugin**. Even the core CRM features are plugins loaded by the Kernel via a `manifest.json`. -* **Server:** `objectstack serve` runs Hono via `@hono/node-server`, loading `objectstack.config.ts` for plugin registration. +- **Concept:** ObjectOS is an OS. It boots up, loads "Drivers" (ObjectQL) and "Applications" (Plugins). +- **Rule:** Everything is a **Plugin**. Even the core CRM features are plugins loaded by the Kernel via a `manifest.json`. +- **Server:** `objectstack serve` runs Hono via `@hono/node-server`, loading `objectstack.config.ts` for plugin registration. ### B. Three-Layer UI Architecture -* **ObjectUI** (`github.com/objectql/objectui`) = **Control Library** (brick-level components: Form, Grid, Chart, Kanban, etc.). Follows the ObjectStack UI protocol. Similar to amis. -* **apps/web** = **App Shell** (house-level: routes, layout, auth, navigation). Assembles ObjectUI controls for end-user business interfaces. Also provides system admin pages. -* **ObjectStack Hono** = **API Server** (foundation: data, auth, permissions, workflow). Single source of truth. +- **ObjectUI** (`github.com/objectql/objectui`) = **Control Library** (brick-level components: Form, Grid, Chart, Kanban, etc.). Follows the ObjectStack UI protocol. Similar to amis. +- **apps/web** = **App Shell** (house-level: routes, layout, auth, navigation). Assembles ObjectUI controls for end-user business interfaces. Also provides system admin pages. +- **ObjectStack Hono** = **API Server** (foundation: data, auth, permissions, workflow). Single source of truth. ``` ObjectUI (Controls) → apps/web (App Shell) → ObjectStack Hono (API) @@ -116,21 +117,21 @@ ObjectUI (Controls) → apps/web (App Shell) → ObjectStack Hono (API) ### C. Local-First Sync (The "Sync Protocol") -* **Concept:** Clients (ObjectUI / apps/web) operate on a local database (SQLite/RxDB). ObjectOS acts as the **Replication Master**. -* **Mechanism:** +- **Concept:** Clients (ObjectUI / apps/web) operate on a local database (SQLite/RxDB). ObjectOS acts as the **Replication Master**. +- **Mechanism:** 1. **Push:** Client sends "Mutation Log" (Actions), not just final state. 2. **Conflict:** ObjectOS detects conflicts using Vector Clocks or Last-Write-Wins (LWW). 3. **Pull:** ObjectOS sends "Delta Packets" (changes since last checkpoint) to clients. -* **Constraint:** API endpoints must support **Incremental Sync** (e.g., `since_cursor`). +- **Constraint:** API endpoints must support **Incremental Sync** (e.g., `since_cursor`). ### D. Workflow as Code (State Machines) -* **Concept:** Business logic is not `if/else` statements scattered in controllers. It is a defined **State Machine**. -* **Protocol:** Workflows are defined in JSON/YAML. - * *States:* `draft`, `approval`, `published`. - * *Transitions:* `submit` (draft -> approval). - * *Guards:* `canSubmit` (Check permissions). - * *Actions:* `sendEmail`, `updateRecord`. +- **Concept:** Business logic is not `if/else` statements scattered in controllers. It is a defined **State Machine**. +- **Protocol:** Workflows are defined in JSON/YAML. + - _States:_ `draft`, `approval`, `published`. + - _Transitions:_ `submit` (draft -> approval). + - _Guards:_ `canSubmit` (Check permissions). + - _Actions:_ `sendEmail`, `updateRecord`. --- @@ -140,34 +141,34 @@ ObjectUI (Controls) → apps/web (App Shell) → ObjectStack Hono (API) 1. **Authentication:** Every request must be authenticated via `@objectos/auth`. 2. **Authorization:** Never fetch data directly. Always pass through the **Permission Layer**. - * *Bad:* `db.find('orders')` - * *Good:* `ctx.broker.call('data.find', { object: 'orders' })` (This ensures RBAC is checked). + - _Bad:_ `db.find('orders')` + - _Good:_ `ctx.broker.call('data.find', { object: 'orders' })` (This ensures RBAC is checked). 3. **Audit:** Every mutation (Create/Update/Delete) MUST generate an **Audit Log** entry automatically. ### Event-Driven Architecture -* **Decoupling:** Modules interact via **Events**, not direct imports. -* **Pattern:** - * *Trigger:* User creates an Order. - * *Event:* `order.created` emitted. - * *Listeners:* - * `InventoryService` reserves stock. - * `NotificationService` sends email. - * `WorkflowService` starts "Order Fulfillment" process. +- **Decoupling:** Modules interact via **Events**, not direct imports. +- **Pattern:** + - _Trigger:_ User creates an Order. + - _Event:_ `order.created` emitted. + - _Listeners:_ + - `InventoryService` reserves stock. + - `NotificationService` sends email. + - `WorkflowService` starts "Order Fulfillment" process. ### Frontend Standards (apps/web) -* **No API Routes.** All backend logic runs in ObjectStack Kernel plugins. -* **No direct database access.** All data fetched via TanStack Query → `/api/v1/*`. -* **Auth via cookie.** `better-auth/react` handles session cookies automatically. -* **Lazy routes.** All page-level components are lazy-loaded for code splitting. -* **ObjectUI controls** for any metadata-driven UI. Custom pages only for system admin. +- **No API Routes.** All backend logic runs in ObjectStack Kernel plugins. +- **No direct database access.** All data fetched via TanStack Query → `/api/v1/*`. +- **Auth via cookie.** `better-auth/react` handles session cookies automatically. +- **Lazy routes.** All page-level components are lazy-loaded for code splitting. +- **ObjectUI controls** for any metadata-driven UI. Custom pages only for system admin. ### Error Handling -* **Server:** Use `ObjectOSError` with specific HTTP-mapped codes (401, 403, 409). -* **Kernel:** Must catch plugin errors and sandbox them, preventing the whole OS from crashing. -* **Frontend:** TanStack Query error boundaries + toast notifications. +- **Server:** Use `ObjectOSError` with specific HTTP-mapped codes (401, 403, 409). +- **Kernel:** Must catch plugin errors and sandbox them, preventing the whole OS from crashing. +- **Frontend:** TanStack Query error boundaries + toast notifications. --- @@ -183,11 +184,11 @@ export const CrmPlugin: PluginManifest = { id: 'steedos-crm', version: '1.0.0', dependencies: ['@objectos/auth'], - + // Register capabilities objects: ['./objects/*.object.yml'], workflows: ['./workflows/*.workflow.yml'], - + // Lifecycle hooks onLoad: async (ctx) => { ctx.logger.info('CRM Loaded'); @@ -195,8 +196,8 @@ export const CrmPlugin: PluginManifest = { onEvent: { 'user.signup': async (ctx, payload) => { await createLeadFromUser(payload); - } - } + }, + }, }; ``` @@ -229,14 +230,11 @@ states: export default { metadata: { baseDir: resolve(__dirname), - patterns: [ - 'packages/*/objects/*.object.yml', - 'packages/*/workflows/*.workflow.yml', - ] + patterns: ['packages/*/objects/*.object.yml', 'packages/*/workflows/*.workflow.yml'], }, plugins: [ new MetricsPlugin(), - new CachePlugin(), + new CachePlugin(), new StoragePlugin(), new BetterAuthPlugin(), new PermissionsPlugin(), @@ -256,7 +254,7 @@ export default { cors: { origin: ['http://localhost:3001', 'http://localhost:3000'], credentials: true, - } + }, }, }; ``` @@ -280,10 +278,10 @@ export default defineConfig({ ```typescript // apps/web/src/lib/auth-client.ts -import { createAuthClient } from "better-auth/react"; +import { createAuthClient } from 'better-auth/react'; export const authClient = createAuthClient({ - baseURL: "/api/v1/auth", + baseURL: '/api/v1/auth', // In dev: proxied via Vite → objectstack serve // In prod: same origin (staticMount) }); @@ -300,4 +298,4 @@ export const authClient = createAuthClient({ 5. **ObjectUI for Business UI:** If the user needs data grids, forms, or dashboards, use ObjectUI controls. Custom React components only for system admin pages. 6. **Integration:** Explain how ObjectOS calls ObjectQL to persist the data after processing the logic. -**You are the Kernel. Orchestrate the Enterprise.** \ No newline at end of file +**You are the Kernel. Orchestrate the Enterprise.** diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a0812c6e..54e683dc 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,43 +1,43 @@ version: 2 updates: # Enable version updates for npm packages - - package-ecosystem: "npm" - directory: "/" + - package-ecosystem: 'npm' + directory: '/' schedule: - interval: "weekly" - day: "monday" - time: "02:00" + interval: 'weekly' + day: 'monday' + time: '02:00' open-pull-requests-limit: 10 labels: - - "dependencies" - - "automated" + - 'dependencies' + - 'automated' commit-message: - prefix: "chore" - prefix-development: "chore" - include: "scope" + prefix: 'chore' + prefix-development: 'chore' + include: 'scope' # Group all patch updates together groups: patch-updates: patterns: - - "*" + - '*' update-types: - - "patch" + - 'patch' minor-updates: patterns: - - "*" + - '*' update-types: - - "minor" + - 'minor' # Enable version updates for GitHub Actions - - package-ecosystem: "github-actions" - directory: "/" + - package-ecosystem: 'github-actions' + directory: '/' schedule: - interval: "weekly" - day: "monday" - time: "02:00" + interval: 'weekly' + day: 'monday' + time: '02:00' labels: - - "dependencies" - - "github-actions" + - 'dependencies' + - 'github-actions' commit-message: - prefix: "chore" - include: "scope" + prefix: 'chore' + include: 'scope' diff --git a/.github/labeler.yml b/.github/labeler.yml index 9a35e52c..36f2f524 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -4,47 +4,47 @@ # Package-specific labels 'kernel': - changed-files: - - any-glob-to-any-file: 'packages/kernel/**/*' + - any-glob-to-any-file: 'packages/kernel/**/*' 'server': - changed-files: - - any-glob-to-any-file: 'packages/server/**/*' + - any-glob-to-any-file: 'packages/server/**/*' 'presets': - changed-files: - - any-glob-to-any-file: 'packages/presets/**/*' + - any-glob-to-any-file: 'packages/presets/**/*' # Type-specific labels 'documentation': - changed-files: - - any-glob-to-any-file: - - 'docs/**/*' - - '**/*.md' + - any-glob-to-any-file: + - 'docs/**/*' + - '**/*.md' 'workflows': - changed-files: - - any-glob-to-any-file: '.github/**/*' + - any-glob-to-any-file: '.github/**/*' 'dependencies': - changed-files: - - any-glob-to-any-file: - - 'package.json' - - 'pnpm-lock.yaml' - - '**/package.json' + - any-glob-to-any-file: + - 'package.json' + - 'pnpm-lock.yaml' + - '**/package.json' 'tests': - changed-files: - - any-glob-to-any-file: - - '**/*.test.ts' - - '**/*.spec.ts' - - '**/__tests__/**/*' - - '**/test/**/*' + - any-glob-to-any-file: + - '**/*.test.ts' + - '**/*.spec.ts' + - '**/__tests__/**/*' + - '**/test/**/*' 'configuration': - changed-files: - - any-glob-to-any-file: - - 'tsconfig*.json' - - '.eslintrc*' - - '.prettierrc*' - - 'jest.config.*' - - '*.config.*' + - any-glob-to-any-file: + - 'tsconfig*.json' + - '.eslintrc*' + - '.prettierrc*' + - 'jest.config.*' + - '*.config.*' diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml index 87919e69..e8de2dd5 100644 --- a/.github/workflows/check-links.yml +++ b/.github/workflows/check-links.yml @@ -1,7 +1,6 @@ -name: "Check Links" +name: 'Check Links' on: - schedule: # Run weekly on Sundays at 00:00 UTC - cron: '0 0 * * 0' diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml index 00148983..c86db847 100644 --- a/.github/workflows/greetings.yml +++ b/.github/workflows/greetings.yml @@ -1,4 +1,4 @@ -name: "Greetings" +name: 'Greetings' on: issues: @@ -19,19 +19,19 @@ jobs: repo-token: ${{ secrets.GITHUB_TOKEN }} issue-message: > 👋 Thanks for opening your first issue! We appreciate your contribution to ObjectOS. - + Please make sure you've provided all the necessary information and context. Our team will review this as soon as possible. - + In the meantime, you might want to check out our [Contributing Guide](https://github.com/${{ github.repository }}/blob/main/CONTRIBUTING.md) and [Documentation](https://objectos.org/docs). pr-message: > 🎉 Thanks for opening your first pull request! We're excited to review your contribution. - + Please make sure: - [ ] Your code follows our [coding standards](https://github.com/${{ github.repository }}/blob/main/CONTRIBUTING.md) - [ ] All tests pass - [ ] You've added tests for new features - [ ] Documentation is updated (if needed) - + A maintainer will review your PR soon. Thanks for contributing to ObjectOS! 🚀 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 5e0ebcad..154a096c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,4 +1,4 @@ -name: "Lint" +name: 'Lint' on: push: diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index 32432749..e42ff857 100644 --- a/.github/workflows/pr-labeler.yml +++ b/.github/workflows/pr-labeler.yml @@ -1,4 +1,4 @@ -name: "PR Auto Label" +name: 'PR Auto Label' on: pull_request: diff --git a/.github/workflows/pr-size.yml b/.github/workflows/pr-size.yml index 541a61f7..ab449d10 100644 --- a/.github/workflows/pr-size.yml +++ b/.github/workflows/pr-size.yml @@ -1,4 +1,4 @@ -name: "PR Size Labeler" +name: 'PR Size Labeler' on: pull_request: @@ -22,25 +22,25 @@ jobs: repo: context.repo.repo, pull_number: context.issue.number }); - + const additions = pr.data.additions; const deletions = pr.data.deletions; const totalChanges = additions + deletions; - + let sizeLabel = ''; if (totalChanges <= 10) sizeLabel = 'size/xs'; else if (totalChanges <= 100) sizeLabel = 'size/s'; else if (totalChanges <= 500) sizeLabel = 'size/m'; else if (totalChanges <= 1000) sizeLabel = 'size/l'; else sizeLabel = 'size/xl'; - + // Remove existing size labels const labels = await github.rest.issues.listLabelsOnIssue({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number }); - + for (const label of labels.data) { if (label.name.startsWith('size/')) { await github.rest.issues.removeLabel({ @@ -51,7 +51,7 @@ jobs: }); } } - + // Add new size label await github.rest.issues.addLabels({ owner: context.repo.owner, @@ -59,7 +59,7 @@ jobs: issue_number: context.issue.number, labels: [sizeLabel] }); - + // Comment if XL if (sizeLabel === 'size/xl') { await github.rest.issues.createComment({ diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 4399dd3d..886a90fe 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -1,4 +1,4 @@ -name: "Stale Issues and PRs" +name: 'Stale Issues and PRs' on: schedule: @@ -17,7 +17,7 @@ jobs: - uses: actions/stale@v10 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - + # Issue settings stale-issue-message: > This issue has been automatically marked as stale because it has not had @@ -32,7 +32,7 @@ jobs: days-before-close: 7 stale-issue-label: 'stale' exempt-issue-labels: 'pinned,security,bug,enhancement,good first issue' - + # PR settings stale-pr-message: > This pull request has been automatically marked as stale because it has not had @@ -47,7 +47,7 @@ jobs: days-before-pr-close: 7 stale-pr-label: 'stale' exempt-pr-labels: 'pinned,security,in-progress,blocked' - + # Operation limits operations-per-run: 100 remove-stale-when-updated: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0bbe2a39..e85b8e2e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: test: name: Test (Node ${{ matrix.node-version }}) runs-on: ubuntu-latest - + strategy: matrix: node-version: [20.x, 22.x] diff --git a/.gitignore b/.gitignore index 1b4a6a6a..24d85f5c 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,9 @@ package-lock.json .vercel .next +# Coverage +coverage + # Playwright playwright-report test-results diff --git a/.turbo/cache/7675b7d95a0a010e-meta.json b/.turbo/cache/7675b7d95a0a010e-meta.json deleted file mode 100644 index 2075dd22..00000000 --- a/.turbo/cache/7675b7d95a0a010e-meta.json +++ /dev/null @@ -1 +0,0 @@ -{"hash":"7675b7d95a0a010e","duration":6061} \ No newline at end of file diff --git a/.turbo/cache/7675b7d95a0a010e.tar.zst b/.turbo/cache/7675b7d95a0a010e.tar.zst deleted file mode 100644 index 8205dd73..00000000 Binary files a/.turbo/cache/7675b7d95a0a010e.tar.zst and /dev/null differ diff --git a/.turbo/daemon/404b8a4f4d48b05a-turbo.log.2026-02-05 b/.turbo/daemon/404b8a4f4d48b05a-turbo.log.2026-02-05 deleted file mode 100644 index e69de29b..00000000 diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 55ca1427..03e2965b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -9,22 +9,25 @@ ObjectOS is a **metadata-driven runtime engine** that transforms declarative YAM ObjectOS is built on the **[@objectstack/spec](https://github.com/objectstack-ai/spec)** protocol, which defines the "DNA" of the ObjectStack ecosystem. The spec provides: ### 1. **Strict Type Definitions** + - **Zod Schemas**: Runtime validation for configuration and data - **TypeScript Interfaces**: Compile-time type safety via `z.infer<>` - **JSON Schemas**: VS Code IntelliSense and tooling support ### 2. **Five Protocol Namespaces** -| Namespace | Scope | Key Types | -|-----------|-------|-----------| -| **Data** | Business objects, fields, queries | `ServiceObject`, `Field`, `QueryAST`, `Hook` | +| Namespace | Scope | Key Types | +| ---------- | ------------------------------------ | -------------------------------------------------------------- | +| **Data** | Business objects, fields, queries | `ServiceObject`, `Field`, `QueryAST`, `Hook` | | **Kernel** | Plugin lifecycle, manifests, context | `PluginDefinition`, `ObjectStackManifest`, `PluginContextData` | -| **System** | Runtime infrastructure, security | `AuditEvent`, `Job`, `Event` | -| **UI** | Presentation layer | `App`, `View`, `Dashboard` | -| **API** | Connectivity contracts | `Endpoint`, `Contract` | +| **System** | Runtime infrastructure, security | `AuditEvent`, `Job`, `Event` | +| **UI** | Presentation layer | `App`, `View`, `Dashboard` | +| **API** | Connectivity contracts | `Endpoint`, `Contract` | ### 3. **Plugin Lifecycle Hooks** + The spec defines a standardized plugin lifecycle: + - `onInstall`: First-time setup - `onEnable`: Plugin activation - `onLoad`: Metadata registration @@ -36,6 +39,7 @@ All ObjectOS plugins must conform to this lifecycle for consistency and predicta ## The Three-Repository Model ### @objectstack/spec (Protocol Definition) + - **Location**: https://github.com/objectstack-ai/spec - **Purpose**: Defines the protocol and type contracts - **Key Exports**: @@ -46,6 +50,7 @@ All ObjectOS plugins must conform to this lifecycle for consistency and predicta - `API.*` - Endpoint contracts ### ObjectQL Repository (Data Layer Implementation) + - **Location**: https://github.com/objectstack-ai/objectql - **Purpose**: Defines the metadata standard and provides core implementations - **Key Packages**: @@ -55,6 +60,7 @@ All ObjectOS plugins must conform to this lifecycle for consistency and predicta - `@objectql/driver-mongo` - MongoDB driver ### ObjectOS Repository (Runtime Implementation) + - **Location**: This repository - **Purpose**: Implements the runtime engine and plugin ecosystem - **Key Packages**: @@ -70,6 +76,7 @@ All ObjectOS plugins must conform to this lifecycle for consistency and predicta > **"Runtime manages plugins, Plugins implement features, Drivers handle data."** This separation ensures: + 1. **Testability**: Each layer can be tested independently 2. **Flexibility**: Add/remove features via plugins without touching core 3. **Scalability**: Plugins can be distributed and loaded dynamically @@ -80,6 +87,7 @@ This separation ensures: ### What is ObjectQL? ObjectQL is a **declarative metadata format** for describing: + - Business objects (entities) - Fields and data types - Relationships (lookup, master-detail) @@ -98,17 +106,17 @@ fields: type: text label: First Name required: true - + last_name: type: text label: Last Name required: true - + email: type: email label: Email unique: true - + account: type: lookup reference_to: accounts @@ -152,16 +160,16 @@ export class ObjectOS { private hooks: HookManager; // Load metadata into registry - async load(config: ObjectConfig): Promise + async load(config: ObjectConfig): Promise; // CRUD operations - async find(objectName: string, options: FindOptions): Promise - async insert(objectName: string, data: any): Promise - async update(objectName: string, id: string, data: any): Promise - async delete(objectName: string, id: string): Promise + async find(objectName: string, options: FindOptions): Promise; + async insert(objectName: string, data: any): Promise; + async update(objectName: string, id: string, data: any): Promise; + async delete(objectName: string, id: string): Promise; // Driver management - useDriver(driver: ObjectQLDriver): void + useDriver(driver: ObjectQLDriver): void; } ``` @@ -177,8 +185,8 @@ kernel.on('beforeInsert', async (context) => { }); // Hook types -type HookType = - | 'beforeFind' +type HookType = + | 'beforeFind' | 'afterFind' | 'beforeInsert' | 'afterInsert' @@ -201,12 +209,15 @@ class ObjectOS { } // ✅ GOOD: Injected dependency -const driver = new PostgresDriver({ /* config */ }); +const driver = new PostgresDriver({ + /* config */ +}); const kernel = new ObjectOS(); kernel.useDriver(driver); ``` This allows: + - Unit testing with mock drivers - Swapping databases at runtime - Multi-tenant applications with different databases per tenant @@ -246,10 +257,10 @@ interface ObjectQLDriver { ### Supported Drivers -| Driver | Package | Databases | -|--------|---------|-----------| -| SQL Driver | `@objectql/driver-sql` | PostgreSQL, MySQL, SQLite | -| MongoDB Driver | `@objectql/driver-mongo` | MongoDB | +| Driver | Package | Databases | +| -------------- | ------------------------ | ------------------------- | +| SQL Driver | `@objectql/driver-sql` | PostgreSQL, MySQL, SQLite | +| MongoDB Driver | `@objectql/driver-mongo` | MongoDB | ## Layer 4: HTTP Layer (@objectos/server) @@ -274,7 +285,7 @@ export class ObjectDataController { async query( @Param('objectName') name: string, @Body() body: QueryDTO, - @CurrentUser() user: User + @CurrentUser() user: User, ) { // Controller only handles HTTP translation return this.kernel.find(name, { @@ -282,7 +293,7 @@ export class ObjectDataController { fields: body.fields, sort: body.sort, limit: body.limit, - user: user // For permission checks + user: user, // For permission checks }); } } @@ -297,18 +308,18 @@ export class ObjectDataController { ### REST API Endpoints -| Method | Path | Description | -|--------|------|-------------| -| POST | `/api/v1/data/:object/query` | Query records | -| POST | `/api/v1/data/:object` | Create record | -| PATCH | `/api/v1/data/:object/:id` | Update record | -| DELETE | `/api/v1/data/:object/:id` | Delete record | -| GET | `/api/v1/meta/:object` | Get object metadata | -| ALL | `/api/v1/auth/*` | Authentication (BetterAuth) | -| GET | `/api/v1/audit/events` | Audit log events | -| GET | `/api/v1/jobs` | Job queue status | -| GET | `/api/v1/metrics/prometheus` | Prometheus metrics | -| GET | `/api/v1/permissions/sets` | Permission sets | +| Method | Path | Description | +| ------ | ---------------------------- | --------------------------- | +| POST | `/api/v1/data/:object/query` | Query records | +| POST | `/api/v1/data/:object` | Create record | +| PATCH | `/api/v1/data/:object/:id` | Update record | +| DELETE | `/api/v1/data/:object/:id` | Delete record | +| GET | `/api/v1/meta/:object` | Get object metadata | +| ALL | `/api/v1/auth/*` | Authentication (BetterAuth) | +| GET | `/api/v1/audit/events` | Audit log events | +| GET | `/api/v1/jobs` | Job queue status | +| GET | `/api/v1/metrics/prometheus` | Prometheus metrics | +| GET | `/api/v1/permissions/sets` | Permission sets | ## Layer 5: UI Layer @@ -345,7 +356,7 @@ kernel.registerFieldType('gps_location', { }, format: (value) => { // Format for display - } + }, }); ``` @@ -463,9 +474,11 @@ describe('ObjectOS', () => { const kernel = new ObjectOS(); const mockDriver = createMockDriver(); kernel.useDriver(mockDriver); - + await expect( - kernel.insert('contacts', { /* missing required field */ }) + kernel.insert('contacts', { + /* missing required field */ + }), ).rejects.toThrow('first_name is required'); }); }); @@ -480,7 +493,7 @@ describe('POST /api/data/contacts', () => { .post('/api/data/contacts') .send({ first_name: 'John', last_name: 'Doe' }) .expect(201); - + expect(response.body).toHaveProperty('id'); }); }); @@ -559,6 +572,7 @@ describe('Contact Management', () => { ### 1. Microservices As the application grows, layers can be split: + - Metadata Service (reads object definitions) - CRUD Service (handles data operations) - Auth Service (handles authentication) @@ -566,6 +580,7 @@ As the application grows, layers can be split: ### 2. Event Sourcing Instead of updating records directly: + - Store events (ContactCreated, ContactUpdated) - Rebuild state from events - Enables audit trails and time travel @@ -573,6 +588,7 @@ Instead of updating records directly: ### 3. GraphQL Support Alternative to REST: + - Single endpoint - Client specifies fields - Reduces over-fetching diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 619d9fb6..dd5c5416 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,18 +48,18 @@ objectos/ ### Package Responsibilities -| Package | Role | Dependencies | -|---------|------|-------------| -| `@objectos/auth` | Identity — BetterAuth, SSO, 2FA, Sessions | `@objectstack/spec`, `@objectstack/runtime` | -| `@objectos/permissions` | Authorization — RBAC, Permission Sets | `@objectstack/spec`, `@objectstack/runtime` | -| `@objectos/audit` | Compliance — CRUD events, field history | `@objectstack/spec`, `@objectstack/runtime` | -| `@objectos/workflow` | Flow — FSM engine, approval processes | `@objectstack/spec`, `@objectstack/runtime` | -| `@objectos/automation` | Triggers — WorkflowRule, action types | `@objectstack/spec`, `@objectstack/runtime` | -| `@objectos/jobs` | Background — queues, cron, retry | `@objectstack/spec`, `@objectstack/runtime` | -| `@objectos/realtime` | Sync — WebSocket, presence | `@objectstack/spec`, `@objectstack/runtime` | -| `@objectos/graphql` | GraphQL API — schema generation, subscriptions | `@objectstack/spec`, `graphql` | -| `@objectos/agent` | AI — LLM agents, tools, orchestration | `@objectstack/spec`, `@objectstack/runtime` | -| `@objectos/analytics` | Analytics — aggregation, reports, dashboards | `@objectstack/spec`, `@objectstack/runtime` | +| Package | Role | Dependencies | +| ----------------------- | ---------------------------------------------- | ------------------------------------------- | +| `@objectos/auth` | Identity — BetterAuth, SSO, 2FA, Sessions | `@objectstack/spec`, `@objectstack/runtime` | +| `@objectos/permissions` | Authorization — RBAC, Permission Sets | `@objectstack/spec`, `@objectstack/runtime` | +| `@objectos/audit` | Compliance — CRUD events, field history | `@objectstack/spec`, `@objectstack/runtime` | +| `@objectos/workflow` | Flow — FSM engine, approval processes | `@objectstack/spec`, `@objectstack/runtime` | +| `@objectos/automation` | Triggers — WorkflowRule, action types | `@objectstack/spec`, `@objectstack/runtime` | +| `@objectos/jobs` | Background — queues, cron, retry | `@objectstack/spec`, `@objectstack/runtime` | +| `@objectos/realtime` | Sync — WebSocket, presence | `@objectstack/spec`, `@objectstack/runtime` | +| `@objectos/graphql` | GraphQL API — schema generation, subscriptions | `@objectstack/spec`, `graphql` | +| `@objectos/agent` | AI — LLM agents, tools, orchestration | `@objectstack/spec`, `@objectstack/runtime` | +| `@objectos/analytics` | Analytics — aggregation, reports, dashboards | `@objectstack/spec`, `@objectstack/runtime` | ## Development Standards @@ -68,6 +68,7 @@ objectos/ > **"Kernel handles logic, Drivers handle data, Server handles HTTP."** This must be maintained at all times: + - Kernel never touches HTTP or database connections directly - Server never touches database queries directly - Drivers are injected via dependency injection @@ -75,12 +76,14 @@ This must be maintained at all times: ### Code Style **TypeScript** + - Use strict mode (`strict: true` in tsconfig) - No `any` - use `unknown` with type guards if needed - Prefer interfaces over type aliases for public APIs - Use async/await for all I/O operations **Naming Conventions** + - Files: `kebab-case.ts` - Classes: `PascalCase` - Functions/variables: `camelCase` @@ -88,19 +91,21 @@ This must be maintained at all times: - Constants: `UPPER_SNAKE_CASE` **Comments** + - Use JSDoc for all public APIs -- Explain *why*, not just *what* +- Explain _why_, not just _what_ - Include examples for complex functions Example: + ```typescript /** * Loads an object definition into the registry. * Triggers a schema sync if the driver supports it. - * + * * @param config The object metadata from YAML * @throws {ValidationError} If the config is invalid - * + * * @example * await kernel.load({ * name: 'contacts', @@ -155,27 +160,28 @@ async find( - **E2E Tests**: For critical user flows Example test: + ```typescript describe('ObjectOS.insert', () => { let kernel: ObjectOS; let mockDriver: jest.Mocked; - + beforeEach(() => { kernel = new ObjectOS(); mockDriver = createMockDriver(); kernel.useDriver(mockDriver); }); - + it('should validate required fields', async () => { await kernel.load({ name: 'contacts', fields: { - email: { type: 'email', required: true } - } + email: { type: 'email', required: true }, + }, }); - + await expect( - kernel.insert('contacts', {}) // Missing email + kernel.insert('contacts', {}), // Missing email ).rejects.toThrow('email is required'); }); }); @@ -278,6 +284,7 @@ git checkout -b fix/issue-123 ### 3. Make Changes Follow the coding standards above and ensure: + - Code compiles without errors - Tests pass - Documentation is updated @@ -307,6 +314,7 @@ git commit -m "docs(guide): add architecture examples" ``` Types: + - `feat`: New feature - `fix`: Bug fix - `docs`: Documentation only @@ -330,6 +338,7 @@ git push origin feature/your-feature-name ### PR Checklist Before submitting, ensure: + - [ ] Code follows style guidelines - [ ] All tests pass - [ ] New tests added for new features @@ -375,7 +384,7 @@ describe('CachePlugin', () => { it('should return undefined for expired keys', async () => { await plugin.set('key', 'value', { ttl: 1 }); - await new Promise(r => setTimeout(r, 10)); + await new Promise((r) => setTimeout(r, 10)); const result = await plugin.get('key'); expect(result).toBeUndefined(); }); @@ -397,11 +406,11 @@ pnpm test:coverage **Coverage thresholds** are enforced per package: | Metric | Server Packages | Frontend (apps/web) | -|------------|:--------------:|:-------------------:| -| Branches | 70% | 60% | -| Functions | 70% | 60% | -| Lines | 80% | 70% | -| Statements | 80% | 70% | +| ---------- | :-------------: | :-----------------: | +| Branches | 70% | 60% | +| Functions | 70% | 60% | +| Lines | 80% | 70% | +| Statements | 80% | 70% | CI automatically collects coverage from all packages and uploads to Codecov. diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index d36b5190..1ca92457 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -4,20 +4,20 @@ ObjectOS can run in two modes: -| Mode | Command | Description | -|---|---|---| +| Mode | Command | Description | +| --------------- | ------------ | -------------------------------------------------- | | **Self-hosted** | `pnpm start` | Single Node.js process serving API + static assets | -| **Vercel** | Push to Git | Serverless function (API) + CDN (static assets) | +| **Vercel** | Push to Git | Serverless function (API) + CDN (static assets) | --- ## Build Outputs (dist folders) -| Output | Path | Contents | -|---|---|---| -| Admin Console (SPA) | `apps/web/dist` | Vite build — HTML/JS/CSS | -| Documentation (static) | `apps/site/out` | Next.js static export — HTML/CSS | -| Server Plugins | `packages/*/dist` | tsup bundles — ESM + CJS | +| Output | Path | Contents | +| ---------------------- | ----------------- | -------------------------------- | +| Admin Console (SPA) | `apps/web/dist` | Vite build — HTML/JS/CSS | +| Documentation (static) | `apps/site/out` | Next.js static export — HTML/CSS | +| Server Plugins | `packages/*/dist` | tsup bundles — ESM + CJS | Run `pnpm build` (Turborepo) to build everything. Build order is managed automatically via workspace dependency graph. @@ -44,11 +44,11 @@ pnpm start ### URL Routing -| URL Pattern | Served By | -|---|---| -| `/api/v1/*` | Hono API routes (plugins register handlers) | +| URL Pattern | Served By | +| ------------ | ------------------------------------------------------- | +| `/api/v1/*` | Hono API routes (plugins register handlers) | | `/console/*` | `apps/web/dist` — Vite SPA (static mount, SPA fallback) | -| `/docs/*` | `apps/site/out` — Next.js static HTML (static mount) | +| `/docs/*` | `apps/site/out` — Next.js static HTML (static mount) | ### Production Steps @@ -78,12 +78,12 @@ PostgreSQL + Redis services. ### Environment Variables -| Variable | Default | Description | -|---|---|---| -| `PORT` | `5320` | Server listen port | -| `LOG_LEVEL` | `info` | Pino log level | -| `CORS_ORIGINS` | `http://localhost:5321,http://localhost:5320` | Comma-separated allowed origins | -| `NODE_ENV` | — | Set to `production` for production | +| Variable | Default | Description | +| -------------- | --------------------------------------------- | ---------------------------------- | +| `PORT` | `5320` | Server listen port | +| `LOG_LEVEL` | `info` | Pino log level | +| `CORS_ORIGINS` | `http://localhost:5321,http://localhost:5320` | Comma-separated allowed origins | +| `NODE_ENV` | — | Set to `production` for production | --- @@ -112,11 +112,11 @@ Vercel CDN (Edge) Vercel Serverless (Node.js) ### Files -| File | Purpose | -|---|---| -| `vercel.json` | Build command, output directory, rewrites, function config | -| `api/index.ts` | Serverless function — kernel bootstrap + Hono handler | -| `apps/web/vite.config.ts` | Sets `base: '/'` when `VERCEL` env is detected | +| File | Purpose | +| ------------------------- | ---------------------------------------------------------- | +| `vercel.json` | Build command, output directory, rewrites, function config | +| `api/index.ts` | Serverless function — kernel bootstrap + Hono handler | +| `apps/web/vite.config.ts` | Sets `base: '/'` when `VERCEL` env is detected | ### Build Flow @@ -133,11 +133,11 @@ Output directory: **`apps/web/dist`** (includes `docs/` subfolder) ### URL Routing on Vercel -| URL Pattern | Destination | Type | -|---|---|---| -| `/api/v1/*` | `api/index.ts` serverless function | Rewrite | -| `/docs/*` | Static files from `apps/web/dist/docs/` | CDN | -| `/*` (other) | `index.html` (SPA fallback) | Rewrite | +| URL Pattern | Destination | Type | +| ------------ | --------------------------------------- | ------- | +| `/api/v1/*` | `api/index.ts` serverless function | Rewrite | +| `/docs/*` | Static files from `apps/web/dist/docs/` | CDN | +| `/*` (other) | `index.html` (SPA fallback) | Rewrite | ### Vercel Setup @@ -148,14 +148,14 @@ Output directory: **`apps/web/dist`** (includes `docs/` subfolder) ### Key Differences from Self-Hosted -| Aspect | Self-Hosted (`pnpm start`) | Vercel | -|---|---|---| -| API runtime | Long-running Node.js process | Serverless function (cold-start) | -| Static assets | Served by Hono static mounts | Served by Vercel CDN (Edge) | -| SPA base path | `/console/` | `/` (root) | -| Docs path | `/docs/` (static mount) | `/docs/` (CDN subfolder) | -| WebSocket | Supported (`@objectos/realtime`) | Not supported (Vercel limitation) | -| Background Jobs | In-process queues | Limited by function timeout (30s) | +| Aspect | Self-Hosted (`pnpm start`) | Vercel | +| --------------- | -------------------------------- | --------------------------------- | +| API runtime | Long-running Node.js process | Serverless function (cold-start) | +| Static assets | Served by Hono static mounts | Served by Vercel CDN (Edge) | +| SPA base path | `/console/` | `/` (root) | +| Docs path | `/docs/` (static mount) | `/docs/` (CDN subfolder) | +| WebSocket | Supported (`@objectos/realtime`) | Not supported (Vercel limitation) | +| Background Jobs | In-process queues | Limited by function timeout (30s) | ### Limitations on Vercel diff --git a/README.md b/README.md index a3573ed2..8c8bf008 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ State. Identity. Synchronization. Orchestration. Admin Console. -*Built on [ObjectQL](https://github.com/objectstack-ai/objectql) & [ObjectStack](https://objectstack.ai).* +_Built on [ObjectQL](https://github.com/objectstack-ai/objectql) & [ObjectStack](https://objectstack.ai)._ [![License](https://img.shields.io/badge/license-AGPL%20v3-red.svg)](LICENSE) [![Stack](https://img.shields.io/badge/stack-Hono%20%7C%20React%20%7C%20TypeScript-blue.svg)](#-tech-stack) @@ -15,11 +15,11 @@ State. Identity. Synchronization. Orchestration. Admin Console. **ObjectOS** is the system layer of the ObjectStack ecosystem. -| Layer | Repo | Responsibility | -|---|---|---| -| **ObjectQL** | [objectql/objectql](https://github.com/objectstack-ai/objectql) | Data — metadata, drivers, queries | -| **ObjectUI** | [objectql/objectui](https://github.com/objectstack-ai/objectui) | Views — amis-like control library | -| **ObjectOS** | this repo | **State, Identity, Sync, Orchestration, Admin Console** | +| Layer | Repo | Responsibility | +| ------------ | --------------------------------------------------------------- | ------------------------------------------------------- | +| **ObjectQL** | [objectql/objectql](https://github.com/objectstack-ai/objectql) | Data — metadata, drivers, queries | +| **ObjectUI** | [objectql/objectui](https://github.com/objectstack-ai/objectui) | Views — amis-like control library | +| **ObjectOS** | this repo | **State, Identity, Sync, Orchestration, Admin Console** | ObjectOS acts as the "Kernel" that boots up, loads drivers (ObjectQL) and applications (Plugins), then governs every request through Authentication, Authorization, and Audit. @@ -103,28 +103,28 @@ ObjectUI (Controls) → apps/web (App Shell) → ObjectStack Hono (API) ### Kernel Packages -| Package | Role | -|---|---| -| `@objectos/auth` | Identity — BetterAuth, SSO, 2FA, Sessions | -| `@objectos/permissions` | Authorization — RBAC, Permission Sets | -| `@objectos/audit` | Compliance — CRUD events, field history | -| `@objectos/workflow` | Flow — FSM engine, approval processes | -| `@objectos/automation` | Triggers — WorkflowRule, action types | -| `@objectos/jobs` | Background — queues, cron, retry | -| `@objectos/notification` | Outbound — Email/SMS/Push/Webhook | -| `@objectos/realtime` | Sync — WebSocket, presence | -| `@objectos/cache` | Performance — LRU + Redis | -| `@objectos/storage` | Persistence — KV (Memory/Redis/SQLite) | -| `@objectos/metrics` | Observability — Prometheus export | -| `@objectos/i18n` | Localization — multi-locale | -| `@objectos/browser` | Offline — SQLite WASM, OPFS | +| Package | Role | +| ------------------------ | ----------------------------------------- | +| `@objectos/auth` | Identity — BetterAuth, SSO, 2FA, Sessions | +| `@objectos/permissions` | Authorization — RBAC, Permission Sets | +| `@objectos/audit` | Compliance — CRUD events, field history | +| `@objectos/workflow` | Flow — FSM engine, approval processes | +| `@objectos/automation` | Triggers — WorkflowRule, action types | +| `@objectos/jobs` | Background — queues, cron, retry | +| `@objectos/notification` | Outbound — Email/SMS/Push/Webhook | +| `@objectos/realtime` | Sync — WebSocket, presence | +| `@objectos/cache` | Performance — LRU + Redis | +| `@objectos/storage` | Persistence — KV (Memory/Redis/SQLite) | +| `@objectos/metrics` | Observability — Prometheus export | +| `@objectos/i18n` | Localization — multi-locale | +| `@objectos/browser` | Offline — SQLite WASM, OPFS | ### Application Packages -| Package | Role | Stack | -|---|---|---| -| `apps/web` | Admin Console | Vite + React 19 + React Router 7 | -| `apps/site` | Documentation | Next.js 16 + Fumadocs | +| Package | Role | Stack | +| ----------- | ------------- | -------------------------------- | +| `apps/web` | Admin Console | Vite + React 19 + React Router 7 | +| `apps/site` | Documentation | Next.js 16 + Fumadocs | --- @@ -196,51 +196,51 @@ with API proxy to ObjectStack at `:5320`. #### Development Commands -| Command | Description | -|---|---| -| `pnpm dev` | API `:5320` + Web `:5321` (daily development) | -| `pnpm dev:all` | API + Web + Site (full stack) | -| `pnpm start` | Production — single process with static mounts | -| `pnpm build` | Build all packages (Turborepo) | -| `pnpm test` | Run all tests | -| `pnpm lint` | Lint all packages | -| `pnpm type-check` | TypeScript check all packages | +| Command | Description | +| ----------------- | ---------------------------------------------- | +| `pnpm dev` | API `:5320` + Web `:5321` (daily development) | +| `pnpm dev:all` | API + Web + Site (full stack) | +| `pnpm start` | Production — single process with static mounts | +| `pnpm build` | Build all packages (Turborepo) | +| `pnpm test` | Run all tests | +| `pnpm lint` | Lint all packages | +| `pnpm type-check` | TypeScript check all packages | #### ObjectStack CLI Commands -| Command | Description | -|---|---| -| `pnpm objectstack:serve` | Start ObjectStack server (port 5320) | -| `pnpm objectstack:dev` | Start dev mode with hot-reload | -| `pnpm objectstack:studio` | Launch Console UI with dev server | +| Command | Description | +| --------------------------- | --------------------------------------- | +| `pnpm objectstack:serve` | Start ObjectStack server (port 5320) | +| `pnpm objectstack:dev` | Start dev mode with hot-reload | +| `pnpm objectstack:studio` | Launch Console UI with dev server | | `pnpm objectstack:validate` | Validate configuration against protocol | -| `pnpm objectstack:compile` | Compile configuration to JSON artifact | -| `pnpm objectstack:info` | Display metadata summary | -| `pnpm objectstack:doctor` | Check development environment health | +| `pnpm objectstack:compile` | Compile configuration to JSON artifact | +| `pnpm objectstack:info` | Display metadata summary | +| `pnpm objectstack:doctor` | Check development environment health | #### Code Generation Commands -| Command | Description | -|---|---| -| `pnpm generate` | Generate metadata files (interactive) | -| `pnpm generate:object ` | Generate a new object schema | -| `pnpm generate:flow ` | Generate a new workflow/flow | -| `pnpm generate:view ` | Generate a new view definition | -| `pnpm generate:action ` | Generate a new action | -| `pnpm generate:agent ` | Generate a new AI agent | -| `pnpm generate:dashboard ` | Generate a new dashboard | -| `pnpm generate:app ` | Generate a new application | -| `pnpm create:plugin ` | Create a new plugin from template | -| `pnpm create:example ` | Create a new example from template | +| Command | Description | +| -------------------------------- | ------------------------------------- | +| `pnpm generate` | Generate metadata files (interactive) | +| `pnpm generate:object ` | Generate a new object schema | +| `pnpm generate:flow ` | Generate a new workflow/flow | +| `pnpm generate:view ` | Generate a new view definition | +| `pnpm generate:action ` | Generate a new action | +| `pnpm generate:agent ` | Generate a new AI agent | +| `pnpm generate:dashboard ` | Generate a new dashboard | +| `pnpm generate:app ` | Generate a new application | +| `pnpm create:plugin ` | Create a new plugin from template | +| `pnpm create:example ` | Create a new example from template | #### App-Specific Commands -| Command | Description | -|---|---| -| `pnpm web:dev` | Admin Console only (port 5321) | -| `pnpm web:build` | Build Admin Console | -| `pnpm site:dev` | Documentation site only | -| `pnpm site:build` | Build documentation | +| Command | Description | +| ----------------- | ------------------------------ | +| `pnpm web:dev` | Admin Console only (port 5321) | +| `pnpm web:build` | Build Admin Console | +| `pnpm site:dev` | Documentation site only | +| `pnpm site:build` | Build documentation | ### Production diff --git a/api/index.ts b/api/index.ts index 55b80298..e95911ac 100644 --- a/api/index.ts +++ b/api/index.ts @@ -83,16 +83,10 @@ async function bootstrapKernel(): Promise { honoApp.use('/api/v1/*', sanitize()); // ── Rate limiting — General API (100 req/min per IP) ───── - honoApp.use( - '/api/v1/*', - rateLimit({ windowMs: 60_000, maxRequests: 100 }), - ); + honoApp.use('/api/v1/*', rateLimit({ windowMs: 60_000, maxRequests: 100 })); // ── Rate limiting — Auth endpoints (10 req/min — brute-force protection) ── - honoApp.use( - '/api/v1/auth/*', - rateLimit({ windowMs: 60_000, maxRequests: 10 }), - ); + honoApp.use('/api/v1/auth/*', rateLimit({ windowMs: 60_000, maxRequests: 10 })); // Health-check (always available) honoApp.get('/api/v1/health', (c) => diff --git a/api/middleware/body-limit.ts b/api/middleware/body-limit.ts index f8f2d1f9..53ba8274 100644 --- a/api/middleware/body-limit.ts +++ b/api/middleware/body-limit.ts @@ -24,10 +24,7 @@ export function bodyLimit(config: BodyLimitConfig = {}): MiddlewareHandler { return async (c, next) => { const contentLength = c.req.header('content-length'); if (contentLength && parseInt(contentLength, 10) > maxSize) { - return c.json( - { error: 'Payload too large', maxSize }, - 413, - ); + return c.json({ error: 'Payload too large', maxSize }, 413); } await next(); }; diff --git a/api/middleware/content-type-guard.ts b/api/middleware/content-type-guard.ts index 76f655fc..f875dadb 100644 --- a/api/middleware/content-type-guard.ts +++ b/api/middleware/content-type-guard.ts @@ -21,9 +21,7 @@ export interface ContentTypeGuardConfig { * Creates a middleware that rejects mutation requests without an allowed * Content-Type header. */ -export function contentTypeGuard( - config: ContentTypeGuardConfig = {}, -): MiddlewareHandler { +export function contentTypeGuard(config: ContentTypeGuardConfig = {}): MiddlewareHandler { const allowedTypes = config.allowedTypes ?? ['application/json']; const excludePaths = config.excludePaths ?? []; diff --git a/api/middleware/sanitize.ts b/api/middleware/sanitize.ts index 20a9b98f..bdeef838 100644 --- a/api/middleware/sanitize.ts +++ b/api/middleware/sanitize.ts @@ -50,9 +50,7 @@ export function sanitizeValue(value: unknown): unknown { } /** Sanitize every value in an object (shallow copy). */ -export function sanitizeObject( - obj: Record, -): Record { +export function sanitizeObject(obj: Record): Record { const result: Record = {}; for (const [key, val] of Object.entries(obj)) { result[key] = sanitizeValue(val); diff --git a/apps/site/app/blog/[slug]/page.tsx b/apps/site/app/blog/[slug]/page.tsx index 7dd42ca8..8f164f5a 100644 --- a/apps/site/app/blog/[slug]/page.tsx +++ b/apps/site/app/blog/[slug]/page.tsx @@ -4,11 +4,7 @@ import { DocsBody } from 'fumadocs-ui/layouts/docs/page'; import Link from 'next/link'; import { ArrowLeft } from 'lucide-react'; -export default async function BlogPostPage({ - params, -}: { - params: Promise<{ slug: string }>; -}) { +export default async function BlogPostPage({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; const post = blog.find((post) => post.slug === slug); @@ -19,8 +15,8 @@ export default async function BlogPostPage({ return (
- @@ -32,28 +28,26 @@ export default async function BlogPostPage({

{(post as any).title}

{(post as any).date && ( -
- - - {MDX ? :

No content found

} -
+ + {MDX ? :

No content found

}
); } export function generateStaticParams() { - return blog.map((post) => ({ - slug: post.slug, - })); + return blog.map((post) => ({ + slug: post.slug, + })); } diff --git a/apps/site/app/blog/layout.tsx b/apps/site/app/blog/layout.tsx index a669dbfd..1804dc0b 100644 --- a/apps/site/app/blog/layout.tsx +++ b/apps/site/app/blog/layout.tsx @@ -3,9 +3,5 @@ import { baseOptions } from '../layout.config'; import type { ReactNode } from 'react'; export default function BlogLayout({ children }: { children: ReactNode }) { - return ( - - {children} - - ); + return {children}; } diff --git a/apps/site/app/blog/page.tsx b/apps/site/app/blog/page.tsx index 1eb6f874..c1c533f6 100644 --- a/apps/site/app/blog/page.tsx +++ b/apps/site/app/blog/page.tsx @@ -31,9 +31,7 @@ export default function BlogIndex() {

{(post as any).description}

)}
- {(post as any).date && ( - timeElement((post as any).date) - )} + {(post as any).date && timeElement((post as any).date)} {(post as any).author && By {(post as any).author}}
@@ -45,14 +43,14 @@ export default function BlogIndex() { } function timeElement(date: string | Date) { - const d = new Date(date); - return ( - - ) + const d = new Date(date); + return ( + + ); } diff --git a/apps/site/app/docs/[[...slug]]/page.tsx b/apps/site/app/docs/[[...slug]]/page.tsx index 93bade80..e6b06d39 100644 --- a/apps/site/app/docs/[[...slug]]/page.tsx +++ b/apps/site/app/docs/[[...slug]]/page.tsx @@ -3,21 +3,19 @@ import { DocsBody, DocsPage } from 'fumadocs-ui/layouts/docs/page'; import { notFound } from 'next/navigation'; import type { Metadata } from 'next'; -export default async function Page(props: { - params: Promise<{ slug?: string[] }>; -}) { +export default async function Page(props: { params: Promise<{ slug?: string[] }> }) { const params = await props.params; const page = source.getPage(params.slug); if (!page) notFound(); const MDX = (page.data as any)._exports?.default || (page.data as any).exports?.default; - const githubPath = params.slug + const githubPath = params.slug ? `apps/site/content/docs/${params.slug.join('/')}.mdx` : 'apps/site/content/docs/index.mdx'; return ( - - + Edit this page on GitHub diff --git a/apps/site/app/docs/layout.tsx b/apps/site/app/docs/layout.tsx index 95448821..e6cd7845 100644 --- a/apps/site/app/docs/layout.tsx +++ b/apps/site/app/docs/layout.tsx @@ -18,4 +18,3 @@ export default function Layout({ children }: { children: ReactNode }) { ); } - diff --git a/apps/site/app/global.css b/apps/site/app/global.css index 394052e6..59c14f9c 100644 --- a/apps/site/app/global.css +++ b/apps/site/app/global.css @@ -47,7 +47,7 @@ --input: 214.3 31.8% 91.4%; --ring: 222.2 84% 4.9%; } - + .dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; diff --git a/apps/site/app/layout.config.tsx b/apps/site/app/layout.config.tsx index 2ee09ced..e3c19def 100644 --- a/apps/site/app/layout.config.tsx +++ b/apps/site/app/layout.config.tsx @@ -5,12 +5,7 @@ export const baseOptions: BaseLayoutProps = { nav: { title: (
- ObjectOS Logo + ObjectOS Logo ObjectOS
), diff --git a/apps/site/app/layout.tsx b/apps/site/app/layout.tsx index b4962c9f..1c4a657b 100644 --- a/apps/site/app/layout.tsx +++ b/apps/site/app/layout.tsx @@ -31,20 +31,33 @@ export const metadata = { default: 'ObjectOS - The Enterprise Low-Code Runtime Engine', template: '%s | ObjectOS', }, - description: 'The Business Operating System. Instant Backend. Security Kernel. Workflow Automation. Turn YAML schemas into secure, scalable APIs built on ObjectQL & NestJS.', - keywords: ['ObjectOS', 'Low-Code', 'Enterprise', 'Runtime Engine', 'RBAC', 'Workflow', 'NestJS', 'ObjectQL', 'Metadata-Driven'], + description: + 'The Business Operating System. Instant Backend. Security Kernel. Workflow Automation. Turn YAML schemas into secure, scalable APIs built on ObjectQL & NestJS.', + keywords: [ + 'ObjectOS', + 'Low-Code', + 'Enterprise', + 'Runtime Engine', + 'RBAC', + 'Workflow', + 'NestJS', + 'ObjectQL', + 'Metadata-Driven', + ], authors: [{ name: 'ObjectOS Team' }], openGraph: { type: 'website', locale: 'en_US', url: 'https://objectos.dev', title: 'ObjectOS - The Enterprise Low-Code Runtime Engine', - description: 'The Business Operating System. Instant Backend. Security Kernel. Workflow Automation.', + description: + 'The Business Operating System. Instant Backend. Security Kernel. Workflow Automation.', siteName: 'ObjectOS', }, twitter: { card: 'summary_large_image', title: 'ObjectOS - The Enterprise Low-Code Runtime Engine', - description: 'The Business Operating System. Instant Backend. Security Kernel. Workflow Automation.', + description: + 'The Business Operating System. Instant Backend. Security Kernel. Workflow Automation.', }, }; diff --git a/apps/site/app/page.tsx b/apps/site/app/page.tsx index 658f92ad..ff57b243 100644 --- a/apps/site/app/page.tsx +++ b/apps/site/app/page.tsx @@ -6,258 +6,301 @@ import { Shield, Zap, Cog, Lock, Workflow, Database, ArrowRight } from 'lucide-r export default function HomePage() { return ( -
- {/* Hero Section */} -
-
-
-
-
- - Enterprise Low-Code Runtime Engine -
- - {/* Preview Release Notice */} -
- 🚀 - Preview Version Coming March 2026 -
- -

- ObjectOS -

-

- The Business Operating System -

-

- Instant Backend. Security Kernel. Workflow Automation. - Built on ObjectQL & NestJS. -

-
- - Get Started - - - - View on GitHub - +
+ {/* Hero Section */} +
+
+
+
+
+ + Enterprise Low-Code Runtime Engine +
+ + {/* Preview Release Notice */} +
+ 🚀 + Preview Version Coming March 2026 +
+ +

+ ObjectOS +

+

+ The Business Operating System +

+

+ Instant Backend. Security Kernel. Workflow Automation. Built on{' '} + + ObjectQL + {' '} + & NestJS. +

+
+ + Get Started + + + + View on GitHub + +
-
-
+ - {/* Introduction */} -
-
-
-

- The Operating System for Enterprise Data -

-

- ObjectOS is a production-ready, metadata-driven runtime platform. - While ObjectQL defines how data looks, ObjectOS defines how business runs. -

-

- Instantly turn static YAML schemas into secure, scalable, and compliant APIs. -

-
+ {/* Introduction */} +
+
+
+

+ The Operating System for Enterprise Data +

+

+ ObjectOS is a production-ready, metadata-driven runtime platform. While{' '} + ObjectQL defines how data looks, ObjectOS{' '} + defines how business runs. +

+

+ Instantly turn static YAML schemas into secure, scalable, and compliant APIs. +

+
-
-
-
-
- +
+
+
+
+ +
+

The Enforcer

-

The Enforcer

+

+ Intercepts every request to enforce RBAC (Role-Based Access Control) and + Record-Level Security (RLS). +

-

- Intercepts every request to enforce RBAC (Role-Based Access Control) and Record-Level Security (RLS). -

-
-
-
-
- +
+
+
+ +
+

The Server

-

The Server

+

+ Automatically serves REST, GraphQL, and JSON-RPC APIs for Object UI. +

-

- Automatically serves REST, GraphQL, and JSON-RPC APIs for Object UI. -

-
-
-
-
- +
+
+
+ +
+

The Automator

-

The Automator

+

+ Runs server-side triggers, workflows, and scheduled jobs. +

-

- Runs server-side triggers, workflows, and scheduled jobs. -

-
-
+
- {/* Key Features */} -
-
-
-

Key Features

-

- Everything you need to build enterprise applications. -

-
+ {/* Key Features */} +
+
+
+

Key Features

+

+ Everything you need to build enterprise applications. +

+
-
- {/* Enterprise Security Kernel */} -
-
-
- +
+ {/* Enterprise Security Kernel */} +
+
+
+ +
+

Enterprise Security Kernel

-

Enterprise Security Kernel

+

+ ObjectOS doesn't just read data; it protects it. +

+
    +
  • + + + Authentication: Integrated OIDC, SAML, and LDAP support + +
  • +
  • + + + Fine-Grained Permission: Field-level and record-level sharing + rules + +
  • +
  • + + + Audit Logging: Built-in tracking of who did what and when + +
  • +
-

- ObjectOS doesn't just read data; it protects it. -

-
    -
  • - - Authentication: Integrated OIDC, SAML, and LDAP support -
  • -
  • - - Fine-Grained Permission: Field-level and record-level sharing rules -
  • -
  • - - Audit Logging: Built-in tracking of who did what and when -
  • -
-
- {/* Instant API Gateway */} -
-
-
- + {/* Instant API Gateway */} +
+
+
+ +
+

Instant API Gateway

-

Instant API Gateway

+

Stop writing boilerplate controllers.

+
    +
  • + + + Auto-generated REST API: Works out-of-the-box + +
  • +
  • + + + Auto-generated GraphQL: Instant schema stitching + +
  • +
  • + + + Metadata API: Serves UI configuration to frontend clients + +
  • +
-

- Stop writing boilerplate controllers. -

-
    -
  • - - Auto-generated REST API: Works out-of-the-box -
  • -
  • - - Auto-generated GraphQL: Instant schema stitching -
  • -
  • - - Metadata API: Serves UI configuration to frontend clients -
  • -
-
- {/* Workflow & Automation */} -
-
-
- + {/* Workflow & Automation */} +
+
+
+ +
+

Workflow & Automation

-

Workflow & Automation

+

+ Business logic that adapts to your needs. +

+
    +
  • + + + Triggers: Run code beforeInsert, afterUpdate, beforeDelete + +
  • +
  • + + + Flow Engine: Visual workflow execution (BPMN-style) + +
  • +
  • + + + Job Queue: Background task processing based on Redis + +
  • +
-

- Business logic that adapts to your needs. -

-
    -
  • - - Triggers: Run code beforeInsert, afterUpdate, beforeDelete -
  • -
  • - - Flow Engine: Visual workflow execution (BPMN-style) -
  • -
  • - - Job Queue: Background task processing based on Redis -
  • -
-
-
+
- {/* Architecture */} -
-
-
-

Built as a Modular Monorepo

-

- ObjectOS is built with NestJS and organized into focused packages. -

-
+ {/* Architecture */} +
+
+
+

Built as a Modular Monorepo

+

+ ObjectOS is built with NestJS and organized into focused packages. +

+
-
- {[ - { name: '@objectos/kernel', role: 'The Brain', desc: 'Core logic engine. Wraps ObjectQL, manages plugins, and handles the event bus.' }, - { name: '@objectos/server', role: 'The Gateway', desc: 'NestJS application layer. Handles HTTP/WS traffic, Middlewares, and Guards.' }, - { name: '@objectos/plugin-auth', role: 'Auth', desc: 'Authentication strategies (Local, OAuth2, Enterprise SSO).' }, - { name: '@objectos/plugin-workflow', role: 'Logic', desc: 'Workflow engine and trigger runner.' }, - { name: '@objectos/presets', role: 'Config', desc: 'Standard system objects (_users, _roles, _audit_log).' } - ].map((pkg) => ( -
- {pkg.name} -

{pkg.role}

-

{pkg.desc}

-
- ))} +
+ {[ + { + name: '@objectos/kernel', + role: 'The Brain', + desc: 'Core logic engine. Wraps ObjectQL, manages plugins, and handles the event bus.', + }, + { + name: '@objectos/server', + role: 'The Gateway', + desc: 'NestJS application layer. Handles HTTP/WS traffic, Middlewares, and Guards.', + }, + { + name: '@objectos/plugin-auth', + role: 'Auth', + desc: 'Authentication strategies (Local, OAuth2, Enterprise SSO).', + }, + { + name: '@objectos/plugin-workflow', + role: 'Logic', + desc: 'Workflow engine and trigger runner.', + }, + { + name: '@objectos/presets', + role: 'Config', + desc: 'Standard system objects (_users, _roles, _audit_log).', + }, + ].map((pkg) => ( +
+ {pkg.name} +

{pkg.role}

+

{pkg.desc}

+
+ ))} +
-
-
+
- {/* CTA Section */} -
-
-

- Start Building with ObjectOS -

-

- Open source. AGPL v3 Licensed. Production ready. -

-
- - Read Documentation - - - - Star on GitHub - + {/* CTA Section */} +
+
+

Start Building with ObjectOS

+

+ Open source. AGPL v3 Licensed. Production ready. +

+
+ + Read Documentation + + + + Star on GitHub + +
-
-
-
+ +
); } diff --git a/apps/site/content/blog/deep-dive-kernel-architecture.mdx b/apps/site/content/blog/deep-dive-kernel-architecture.mdx index 9a260526..6951e38e 100644 --- a/apps/site/content/blog/deep-dive-kernel-architecture.mdx +++ b/apps/site/content/blog/deep-dive-kernel-architecture.mdx @@ -1,8 +1,8 @@ --- -title: "Architecture Internals: The Plugin Lifecycle & Dependency Graph" -description: "How the ObjectOS Kernel manages dependency injection, plugin isolation, and the boot sequence." +title: 'Architecture Internals: The Plugin Lifecycle & Dependency Graph' +description: 'How the ObjectOS Kernel manages dependency injection, plugin isolation, and the boot sequence.' date: 2024-04-20 -author: "ObjectOS Engineering" +author: 'ObjectOS Engineering' --- # Architecture Internals: The Plugin Lifecycle @@ -20,11 +20,8 @@ Every unit of functionality in ObjectOS is a plugin. A plugin is defined by its export const InventoryPlugin = definePlugin({ id: 'com.objectos.inventory', version: '1.2.0', - dependencies: [ - 'com.objectos.auth', - 'com.objectos.products' - ], - provides: ['inventory.service'] + dependencies: ['com.objectos.auth', 'com.objectos.products'], + provides: ['inventory.service'], }); ``` @@ -36,7 +33,7 @@ When `ObjectOS.boot()` is called, the kernel performs the following steps: 2. **Graph Construction:** Builds a Directed Acyclic Graph (DAG) based on the `dependencies` array. 3. **Cycle Detection:** If Plugin A depends on Plugin B, and Plugin B depends on Plugin A, the kernel throws a `CircularDependencyError` and halts boot. 4. **Ordering:** Performs a Topological Sort to determine the linear load order. - - *Result:* `Auth` -> `Products` -> `Inventory`. + - _Result:_ `Auth` -> `Products` -> `Inventory`. ### Code Snapshot: The Resolver @@ -55,7 +52,7 @@ function resolveLoadOrder(plugins: Map): Plugin[] { if (visited.has(pluginId)) return; tempStack.add(pluginId); - + const plugin = plugins.get(pluginId); for (const depId of plugin.manifest.dependencies) { visit(depId); @@ -67,7 +64,7 @@ function resolveLoadOrder(plugins: Map): Plugin[] { } for (const id of plugins.keys()) visit(id); - + return sorted; } ``` @@ -77,6 +74,7 @@ function resolveLoadOrder(plugins: Map): Plugin[] { Once loaded, plugins do not access global variables. They interact with the system via the `Context`. The Kernel creates a **Sandbox** for each plugin. + - **Service Registry:** A plugin can `provide()` services. - **Service Injection:** A plugin `inject()`s services from dependencies. @@ -85,7 +83,7 @@ The Kernel creates a **Sandbox** for each plugin. async function onLoad(ctx: PluginContext) { // Safe Injection: guaranteed to be available because of the DAG sort const authService = ctx.inject('auth.service'); - + // Registering own service ctx.provide('inventory.reserve', async (itemId, qty) => { // Implementation @@ -106,8 +104,9 @@ ObjectOS wraps every plugin hook in a `try/catch` block that acts as an **Error ## Hot Module Replacement (HMR) In development, the Kernel supports HMR. Because the dependency graph is known, when you edit `InventoryPlugin`, the kernel can: + 1. Dispose `InventoryPlugin`. -2. Dispose any plugins that *depend* on `InventoryPlugin` (reverse topological walk). +2. Dispose any plugins that _depend_ on `InventoryPlugin` (reverse topological walk). 3. Reload the code. 4. Re-initialize them in order. diff --git a/apps/site/content/blog/deep-dive-sync-engine.mdx b/apps/site/content/blog/deep-dive-sync-engine.mdx index 629c62ae..13c4107d 100644 --- a/apps/site/content/blog/deep-dive-sync-engine.mdx +++ b/apps/site/content/blog/deep-dive-sync-engine.mdx @@ -1,8 +1,8 @@ --- -title: "Deep Dive: Inside the ObjectOS Sync Engine (HLCs & Merkle Trees)" -description: "A technical walkthrough of how we implemented distributed consistency using Hybrid Logical Clocks, Merkle Trees, and Differential Synchronization." +title: 'Deep Dive: Inside the ObjectOS Sync Engine (HLCs & Merkle Trees)' +description: 'A technical walkthrough of how we implemented distributed consistency using Hybrid Logical Clocks, Merkle Trees, and Differential Synchronization.' date: 2024-04-10 -author: "ObjectOS Engineering" +author: 'ObjectOS Engineering' --- # Deep Dive: Inside the ObjectOS Sync Engine @@ -22,6 +22,7 @@ We solve this using **Hybrid Logical Clocks (HLC)**. ### HLC Structure An HLC timestamp is a 64-bit value composed of: + 1. **Physical Component (PT):** 48 bits representing the physical time (milliseconds). 2. **Logical Component (LC):** 16 bits serving as a counter for events effectively happening within the "same" millisecond. @@ -34,9 +35,9 @@ export class HLC { // to safeguard strong causality. const now = Date.now(); const millis = Math.max(local.millis, remote.millis, now); - + let counter = local.counter; - + if (millis === local.millis && millis === remote.millis) { // Tie-breaking via logical counter counter = Math.max(local.counter, remote.counter) + 1; @@ -47,7 +48,7 @@ export class HLC { } else { counter = 0; } - + return new Timestamp(millis, counter, local.nodeId); } } @@ -65,7 +66,7 @@ We use **Merkle Trees** (Hash Trees) to optimize synchronization. 1. **Partitioning:** We bucket records based on their ID hashes. 2. **Hashing:** Each bucket maintains a hash of its contents (XOR of the record hashes). -3. **Comparison:** +3. **Comparison:** - Client sends its Root Hash. - Server compares it with its Root Hash. - If they match -> No changes. @@ -79,7 +80,7 @@ Our synchronization protocol occurs over a WebSocket connection (for real-time) ### Phase 1: Push (Client -> Server) -The client sends a "Mutation Log". Crucially, we do not send the *state*; we send the *intent*. +The client sends a "Mutation Log". Crucially, we do not send the _state_; we send the _intent_. ```json { @@ -96,6 +97,7 @@ The client sends a "Mutation Log". Crucially, we do not send the *state*; we sen ``` The Server applies these mutations using a **Last-Write-Wins (LWW) Register** Map. + - If the incoming HLC > existing HLC for that field, update. - If incoming HLC < existing HLC, ignore (stale update). @@ -109,10 +111,10 @@ async function getChangesSince(cursor: HLC) { // Query the 'Mutations' table, which is an append-only log const changes = await db.mutationLog.find({ where: { - timestamp: { $gt: cursor.toString() } - } + timestamp: { $gt: cursor.toString() }, + }, }); - + return compactChanges(changes); } ``` @@ -124,7 +126,7 @@ While LWW is the default, it is lossy. For complex scenarios, ObjectOS supports ### The JSON-Merge-Patch Problem If User A updates `{"color": "red"}` and User B updates `{"size": "large"}` on the same JSON blob, a naive LWW overwrites one. -ObjectOS treats JSON fields as maps. We track timestamps *per key*. +ObjectOS treats JSON fields as maps. We track timestamps _per key_. ```typescript // Internal representation of a Resolve map diff --git a/apps/site/content/blog/local-first-architecture.mdx b/apps/site/content/blog/local-first-architecture.mdx index 34ef1a9a..dc2310ed 100644 --- a/apps/site/content/blog/local-first-architecture.mdx +++ b/apps/site/content/blog/local-first-architecture.mdx @@ -1,8 +1,8 @@ --- -title: "Client-Side Storage Engines: Optimizing SQLite WASM for ObjectOS" -description: "A deep dive into how ObjectOS bridges the gap between ObjectQL and local storage engines like SQLite WASM and IndexedDB." +title: 'Client-Side Storage Engines: Optimizing SQLite WASM for ObjectOS' +description: 'A deep dive into how ObjectOS bridges the gap between ObjectQL and local storage engines like SQLite WASM and IndexedDB.' date: 2024-02-15 -author: "ObjectOS Engineering" +author: 'ObjectOS Engineering' --- # Client-Side Storage Engines: Optimizing SQLite WASM for ObjectOS @@ -14,9 +14,10 @@ This article explores how we implemented the storage layer using `SQLite WASM` a ## 1. The Storage Abstraction Interface We do not bind directly to SQLite. Instead, we defined a generic `StorageAdapter` interface. This allows ObjectOS to run on: -* **Web:** SQLite WASM (OPFS) or RxDB (IndexedDB). -* **React Native:** `react-native-quick-sqlite` (JSI). -* **Electron:** Native `better-sqlite3`. + +- **Web:** SQLite WASM (OPFS) or RxDB (IndexedDB). +- **React Native:** `react-native-quick-sqlite` (JSI). +- **Electron:** Native `better-sqlite3`. ### OPFS (Origin Private File System) @@ -40,8 +41,11 @@ The core challenge: **ObjectQL acts as an ORM.** The client sends: ```typescript const leads = await objectos.find('lead', { - filters: [['status', '=', 'new'], ['owner.name', 'contains', 'John']], - fields: ['name', 'amount', 'owner.email'] + filters: [ + ['status', '=', 'new'], + ['owner.name', 'contains', 'John'], + ], + fields: ['name', 'amount', 'owner.email'], }); ``` @@ -52,14 +56,15 @@ On the client, we must compile this into a SQL query. 1. **AST Transformation:** The JSON filter is parsed into an Abstract Syntax Tree. 2. **Schema Resolution:** We check the local metadata cache to resolve `owner.name`. We see that `owner` is a `ManyToOne` to `users`. 3. **Join Strategy:** - * **Server-Side:** We might use huge JOINs. - * **Client-Side:** We prefer **Lazy Loading** or **Sub-Selects** because keeping huge indices in browser memory is expensive. + - **Server-Side:** We might use huge JOINs. + - **Client-Side:** We prefer **Lazy Loading** or **Sub-Selects** because keeping huge indices in browser memory is expensive. Eventually, it emits: + ```sql -SELECT t0.name, t0.amount, t1.email -FROM leads AS t0 -LEFT JOIN users AS t1 ON t0.owner = t1._id +SELECT t0.name, t0.amount, t1.email +FROM leads AS t0 +LEFT JOIN users AS t1 ON t0.owner = t1._id WHERE t0.status = 'new' AND t1.name LIKE '%John%' ``` @@ -73,6 +78,7 @@ indices: ``` When the specific plugin loads on the client, the Kernel runs a schema migration on the encapsulated SQLite instance: + ```sql CREATE INDEX IF NOT EXISTS idx_leads_status_created ON leads (status, created_at); ``` @@ -80,8 +86,9 @@ CREATE INDEX IF NOT EXISTS idx_leads_status_created ON leads (status, created_at ### Performance Metrics We benchmarked 10,000 records on a mid-range Android device: -* **Insert 1k records:** 120ms (WASM) vs 800ms (IndexedDB). -* **Complex Query (Join + Sort):** 15ms (WASM) vs 400ms (IndexedDB scan). + +- **Insert 1k records:** 120ms (WASM) vs 800ms (IndexedDB). +- **Complex Query (Join + Sort):** 15ms (WASM) vs 400ms (IndexedDB scan). This performance gap is why we mandate SQLite WASM for data-heavy enterprise plugins. diff --git a/apps/site/content/blog/orchestrating-business-logic.mdx b/apps/site/content/blog/orchestrating-business-logic.mdx index 3308ff15..ada7b769 100644 --- a/apps/site/content/blog/orchestrating-business-logic.mdx +++ b/apps/site/content/blog/orchestrating-business-logic.mdx @@ -1,8 +1,8 @@ --- -title: "Compiling Workflow YAML to Deterministic State Machines" -description: "Analyzing the ObjectOS Workflow Compiler: How we transform static YAML definitions into executable Hierarchical State Machines (HSM) with transactional guarantees." +title: 'Compiling Workflow YAML to Deterministic State Machines' +description: 'Analyzing the ObjectOS Workflow Compiler: How we transform static YAML definitions into executable Hierarchical State Machines (HSM) with transactional guarantees.' date: 2024-03-01 -author: "ObjectOS Engineering" +author: 'ObjectOS Engineering' --- # Compiling Workflow YAML to Deterministic State Machines @@ -18,14 +18,14 @@ Converting a YAML file into an executable state machine involves several passes. 1. **Parse & Validate:** We use `ajv` to validate the YAML against the Workflow JSON Schema. 2. **Graph Construction:** We build an adjacency list representing the states and transitions. 3. **Static Analysis (The Safety Check):** - * **Unreachable States:** We run a Breadth-First Search (BFS) starting from `initial`. Any node not visited is dead code. - * **Determinism Check:** We verify that no two transitions from the same state have overlapping triggers *and* overlapping guards (which would create race conditions). + - **Unreachable States:** We run a Breadth-First Search (BFS) starting from `initial`. Any node not visited is dead code. + - **Determinism Check:** We verify that no two transitions from the same state have overlapping triggers _and_ overlapping guards (which would create race conditions). ### Cycle Detection While cycles are allowed in Workflows (e.g., Request Changes -> Resubmit), **Infinite loops** in automated actions are dangerous. -We use **Tarjan's Algorithm** to identify Strongly Connected Components (SCCs) in the graph of *automatic* transitions (transitions without user input). If an automatic SCC exists, the compiler throws an error, preventing a Stack Overflow at runtime. +We use **Tarjan's Algorithm** to identify Strongly Connected Components (SCCs) in the graph of _automatic_ transitions (transitions without user input). If an automatic SCC exists, the compiler throws an error, preventing a Stack Overflow at runtime. ## 2. Hierarchical States (Sub-States) @@ -55,16 +55,16 @@ The `WorkflowRunner` executes a transition as a single **ACID Transaction**. await ctx.transaction(async (trx) => { // 1. Lock the record (SELECT FOR UPDATE) const record = await trx.find(id, { lock: true }); - + // 2. Evaluate Guards - if (!guards.every(g => g(record, ctx))) throw new Error('Guard failed'); - + if (!guards.every((g) => g(record, ctx))) throw new Error('Guard failed'); + // 3. Execute 'on_exit' actions of old state await executeHooks(oldState.onExit, trx); - + // 4. Update Status Field await trx.update(id, { status: newState }); - + // 5. Execute 'on_enter' actions of new state await executeHooks(newState.onEnter, trx); }); diff --git a/apps/site/content/blog/security-as-a-primitive.mdx b/apps/site/content/blog/security-as-a-primitive.mdx index 54601b1d..63a88709 100644 --- a/apps/site/content/blog/security-as-a-primitive.mdx +++ b/apps/site/content/blog/security-as-a-primitive.mdx @@ -1,15 +1,15 @@ --- -title: "The Policy Engine: Compiling ABAC Rules into Abstract Syntax Trees" -description: "How ObjectOS implements Zero Trust not by filtering arrays in memory, but by compiling Attribute-Based Access Control policies directly into SQL predicates." +title: 'The Policy Engine: Compiling ABAC Rules into Abstract Syntax Trees' +description: 'How ObjectOS implements Zero Trust not by filtering arrays in memory, but by compiling Attribute-Based Access Control policies directly into SQL predicates.' date: 2024-03-20 -author: "ObjectOS Engineering" +author: 'ObjectOS Engineering' --- # The Policy Engine: Compiling ABAC Rules into ASTs "Security as a Primitive" means security cannot be an afterthought. In ObjectOS, the access control system is an **Attribute-Based Access Control (ABAC)** engine that sits directly in front of the Data Access Layer. -Instead of fetching data and *then* filtering it (which is slow and insecure pagination-wise), ObjectOS **compiles security rules into the database query**. +Instead of fetching data and _then_ filtering it (which is slow and insecure pagination-wise), ObjectOS **compiles security rules into the database query**. ## 1. Defining Policies (The DSL) @@ -22,8 +22,8 @@ rule: - effect: allow action: [read, update] condition: - owner: "{user.id}" - amount: + owner: '{user.id}' + amount: $lt: 10000 ``` @@ -45,8 +45,8 @@ If a user has multiple roles (e.g., "Sales Rep" AND "Regional Manager"), the Com ```sql -- Compiled Query -SELECT * FROM sales_order -WHERE +SELECT * FROM sales_order +WHERE -- Role: Sales Rep (owner = 'u_123' AND amount < 10000) OR @@ -62,8 +62,8 @@ If a user cannot see the `margin` field, we don't just "hide" it in the UI. We s The Query Planner iterates through the requested fields: ```typescript -const allowedFields = requestedFields.filter(field => - policyEngine.can(user, 'read_field', 'sales_order', field) +const allowedFields = requestedFields.filter((field) => + policyEngine.can(user, 'read_field', 'sales_order', field), ); // Generates: SELECT id, date, amount FROM ... (omits 'margin') ``` diff --git a/apps/site/content/blog/why-your-business-needs-an-os.mdx b/apps/site/content/blog/why-your-business-needs-an-os.mdx index 1acb8c80..f753215b 100644 --- a/apps/site/content/blog/why-your-business-needs-an-os.mdx +++ b/apps/site/content/blog/why-your-business-needs-an-os.mdx @@ -1,8 +1,8 @@ --- -title: "Micro-Kernel Architecture: Implementing Inter-Process Communication in Node.js" +title: 'Micro-Kernel Architecture: Implementing Inter-Process Communication in Node.js' description: "A deep analysis of ObjectOS's module isolation, the Event Bus design, and how we achieve loose coupling without the latency of microservices." date: 2024-02-01 -author: "ObjectOS Engineering" +author: 'ObjectOS Engineering' --- # Micro-Kernel Architecture: Implementing Inter-Process Communication @@ -22,8 +22,9 @@ import { InventoryService } from '../inventory/service'; ``` In ObjectOS, we enforce a strict boundary. -* **Kernel Space:** The `PluginLoader`, `EventBroker`, and `Context` manager. -* **User Space:** The code inside your `plugins/` directory. + +- **Kernel Space:** The `PluginLoader`, `EventBroker`, and `Context` manager. +- **User Space:** The code inside your `plugins/` directory. ### The Service Registry (V-Table equivalent) @@ -42,6 +43,7 @@ const stock = await ctx.call('inventory.check_stock', { sku: 'A123' }); ``` **Why this indirection?** + 1. **Middleware Injection:** The Kernel intercepts every `call`. It automates Tracing (OpenTelemetry), Logging, and Argument Validation (Zod) before the target function ever runs. 2. **Swappability:** You can hot-swap the implementation of `inventory.check_stock` without restarting the callers. @@ -53,11 +55,12 @@ We implemented a custom **Broker** that supports: 1. **Transactional Outbox Pattern:** When a plugin emits an event alongside a DB mutation, the event is not fired until the DB transaction commits. + ```typescript await ctx.transaction(async (trx) => { await trx.update('orders', ...); // Buffered effectively until commit - ctx.emit('order.created', { id }); + ctx.emit('order.created', { id }); }); ``` @@ -82,6 +85,7 @@ Request(Context ID: 100) ``` This inheritance chain allows us to: + 1. **Trace Causality:** If Inventory Plugin throws an error, the stack trace links back to the Order Plugin's action. 2. **Sandboxing:** We can attach "Budgets" to contexts (e.g., Max SQL Queries = 50). If a plugin goes rogue, the Kernel kills that specific Context branch without crashing the server. diff --git a/apps/site/lib/page-tree.ts b/apps/site/lib/page-tree.ts index 8cc633d6..762c56a1 100644 --- a/apps/site/lib/page-tree.ts +++ b/apps/site/lib/page-tree.ts @@ -12,9 +12,7 @@ export const pageTree: Root = { name: 'Introduction', url: '/docs/getting-started', }, - children: [ - { type: 'page', name: 'Installation', url: '/docs/getting-started/installation' }, - ], + children: [{ type: 'page', name: 'Installation', url: '/docs/getting-started/installation' }], }, { type: 'folder', diff --git a/apps/site/lib/source.ts b/apps/site/lib/source.ts index ef03b2e2..b7d30290 100644 --- a/apps/site/lib/source.ts +++ b/apps/site/lib/source.ts @@ -8,8 +8,7 @@ export const source = loader({ baseUrl: '/docs', source: toFumadocsSource(docs, meta), icon(icon) { - if (icon && icon in icons) - return createElement(icons[icon as keyof typeof icons]); + if (icon && icon in icons) return createElement(icons[icon as keyof typeof icons]); }, }); diff --git a/apps/site/tailwind.config.ts b/apps/site/tailwind.config.ts index b7cc5579..7902a199 100644 --- a/apps/site/tailwind.config.ts +++ b/apps/site/tailwind.config.ts @@ -11,9 +11,7 @@ const config: Config = { './node_modules/fumadocs-ui/dist/**/*.js', './node_modules/fumadocs-twoslash/dist/**/*.js', ], - plugins: [ - require('@tailwindcss/typography'), - ], + plugins: [require('@tailwindcss/typography')], }; export default config; diff --git a/apps/site/tsconfig.json b/apps/site/tsconfig.json index e7ff3a26..705f5ce5 100644 --- a/apps/site/tsconfig.json +++ b/apps/site/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ES2017", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -23,9 +19,7 @@ } ], "paths": { - "@/*": [ - "./*" - ] + "@/*": ["./*"] } }, "include": [ @@ -35,7 +29,5 @@ ".next/types/**/*.ts", ".next/dev/types/**/*.ts" ], - "exclude": [ - "node_modules" - ] + "exclude": ["node_modules"] } diff --git a/apps/web/package.json b/apps/web/package.json index f5b85ad5..269e84cb 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -45,6 +45,7 @@ "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.5.2", + "@vitest/coverage-v8": "^4.0.18", "autoprefixer": "^10.4.20", "jsdom": "^28.0.0", "tailwindcss": "^4.1.0", diff --git a/apps/web/public/sw.js b/apps/web/public/sw.js index 45e7333f..42a234be 100644 --- a/apps/web/public/sw.js +++ b/apps/web/public/sw.js @@ -13,18 +13,18 @@ const STATIC_ASSETS = ['/console/', '/console/index.html']; // ── Install: pre-cache the app shell ──────────────────────────── self.addEventListener('install', (event) => { - event.waitUntil( - caches.open(CACHE_NAME).then((cache) => cache.addAll(STATIC_ASSETS)), - ); + event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(STATIC_ASSETS))); self.skipWaiting(); }); // ── Activate: clean old caches ────────────────────────────────── self.addEventListener('activate', (event) => { event.waitUntil( - caches.keys().then((keys) => - Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))), - ), + caches + .keys() + .then((keys) => + Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))), + ), ); self.clients.claim(); }); diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 4cf43a25..d2c944c1 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -63,7 +63,6 @@ export function App() { {/* Protected routes */} }> - {/* ── Create Org (accessible to any authenticated user) ── */} } /> @@ -97,7 +96,6 @@ export function App() { } /> } /> - {/* Fallback */} diff --git a/apps/web/src/__tests__/components/ApprovalActions.test.tsx b/apps/web/src/__tests__/components/ApprovalActions.test.tsx index 0f8422a1..9a0eed0f 100644 --- a/apps/web/src/__tests__/components/ApprovalActions.test.tsx +++ b/apps/web/src/__tests__/components/ApprovalActions.test.tsx @@ -52,11 +52,7 @@ describe('ApprovalActions', () => { { name: 'approve', label: 'Approve', from: 'pending', to: 'approved' }, ]; render( - {}} - isExecuting - />, + {}} isExecuting />, ); const button = screen.getByText('Approve').closest('button'); expect(button?.disabled).toBe(true); diff --git a/apps/web/src/__tests__/components/CsvExportButton.test.tsx b/apps/web/src/__tests__/components/CsvExportButton.test.tsx index d04d2b29..68df4fc1 100644 --- a/apps/web/src/__tests__/components/CsvExportButton.test.tsx +++ b/apps/web/src/__tests__/components/CsvExportButton.test.tsx @@ -69,6 +69,10 @@ describe('CsvExportButton', () => { expect(revokeObjectURL).toHaveBeenCalled(); vi.restoreAllMocks(); - Object.defineProperty(window, 'URL', { value: originalURL, writable: true, configurable: true }); + Object.defineProperty(window, 'URL', { + value: originalURL, + writable: true, + configurable: true, + }); }); }); diff --git a/apps/web/src/__tests__/components/FilterPanel.test.tsx b/apps/web/src/__tests__/components/FilterPanel.test.tsx index ab8aeedf..b5a428ed 100644 --- a/apps/web/src/__tests__/components/FilterPanel.test.tsx +++ b/apps/web/src/__tests__/components/FilterPanel.test.tsx @@ -51,13 +51,7 @@ describe('FilterPanel', () => { }); it('renders filter button', () => { - render( - , - ); + render(); expect(screen.getByText('Filters')).toBeDefined(); }); @@ -85,13 +79,7 @@ describe('FilterPanel', () => { }); it('shows filter builder when expanded', () => { - render( - , - ); + render(); fireEvent.click(screen.getByText('Filters')); expect(screen.getByLabelText('Filter field')).toBeDefined(); }); diff --git a/apps/web/src/__tests__/components/ViewSwitcher.test.tsx b/apps/web/src/__tests__/components/ViewSwitcher.test.tsx index a6af91e7..834627f7 100644 --- a/apps/web/src/__tests__/components/ViewSwitcher.test.tsx +++ b/apps/web/src/__tests__/components/ViewSwitcher.test.tsx @@ -49,12 +49,18 @@ describe('findKanbanField', () => { category: { type: 'select', label: 'Category', - options: [{ label: 'A', value: 'a' }, { label: 'B', value: 'b' }], + options: [ + { label: 'A', value: 'a' }, + { label: 'B', value: 'b' }, + ], }, priority: { type: 'select', label: 'Priority', - options: [{ label: 'Low', value: 'low' }, { label: 'High', value: 'high' }], + options: [ + { label: 'Low', value: 'low' }, + { label: 'High', value: 'high' }, + ], }, }, }; @@ -73,11 +79,7 @@ describe('ViewSwitcher', () => { it('calls onViewChange when a button is clicked', () => { const onViewChange = vi.fn(); render( - , + , ); // Click the kanban button (second button) const buttons = screen.getAllByRole('button'); @@ -87,11 +89,7 @@ describe('ViewSwitcher', () => { it('disables kanban when no select field available', () => { render( - {}} - objectDef={noSelectObjectDef} - />, + {}} objectDef={noSelectObjectDef} />, ); const buttons = screen.getAllByRole('button'); // Kanban button should be disabled @@ -99,13 +97,7 @@ describe('ViewSwitcher', () => { }); it('enables kanban when select field is available', () => { - render( - {}} - objectDef={taskObjectDef} - />, - ); + render( {}} objectDef={taskObjectDef} />); const buttons = screen.getAllByRole('button'); // Kanban button should NOT be disabled expect(buttons[1].hasAttribute('disabled')).toBe(false); diff --git a/apps/web/src/__tests__/components/WorkflowStatusBadge.test.tsx b/apps/web/src/__tests__/components/WorkflowStatusBadge.test.tsx index d94777c9..7eed3c10 100644 --- a/apps/web/src/__tests__/components/WorkflowStatusBadge.test.tsx +++ b/apps/web/src/__tests__/components/WorkflowStatusBadge.test.tsx @@ -31,10 +31,7 @@ describe('WorkflowStatusBadge', () => { it('shows workflow name when showWorkflowName is true', () => { render( - , + , ); expect(screen.getByText('leave_flow:')).toBeDefined(); }); diff --git a/apps/web/src/__tests__/components/phase-l.test.tsx b/apps/web/src/__tests__/components/phase-l.test.tsx index f9b6cb7b..4dca936d 100644 --- a/apps/web/src/__tests__/components/phase-l.test.tsx +++ b/apps/web/src/__tests__/components/phase-l.test.tsx @@ -10,7 +10,12 @@ import { usePrefetch } from '@/hooks/use-prefetch'; import { useVirtualScroll } from '@/hooks/use-virtual-scroll'; import { ErrorBoundaryPage } from '@/components/ui/error-boundary-page'; import { EmptyState } from '@/components/ui/empty-state'; -import { TableSkeleton, CardGridSkeleton, FormSkeleton, DetailSkeleton } from '@/components/ui/loading-skeleton'; +import { + TableSkeleton, + CardGridSkeleton, + FormSkeleton, + DetailSkeleton, +} from '@/components/ui/loading-skeleton'; describe('Phase L hook exports', () => { it('exports useDebounce (L.3)', () => { diff --git a/apps/web/src/__tests__/hooks/use-keyboard-shortcuts.test.ts b/apps/web/src/__tests__/hooks/use-keyboard-shortcuts.test.ts index c28922d4..7b4c2113 100644 --- a/apps/web/src/__tests__/hooks/use-keyboard-shortcuts.test.ts +++ b/apps/web/src/__tests__/hooks/use-keyboard-shortcuts.test.ts @@ -14,9 +14,27 @@ describe('useKeyboardShortcuts', () => { expect(SHORTCUT_PRESETS.search).toEqual({ key: 'k', ctrl: true, description: 'Open search' }); expect(SHORTCUT_PRESETS.save).toEqual({ key: 's', ctrl: true, description: 'Save' }); expect(SHORTCUT_PRESETS.escape).toEqual({ key: 'Escape', description: 'Close / Cancel' }); - expect(SHORTCUT_PRESETS.newRecord).toEqual({ key: 'n', ctrl: true, shift: true, description: 'New record' }); - expect(SHORTCUT_PRESETS.goHome).toEqual({ key: 'h', ctrl: true, shift: true, description: 'Go home' }); - expect(SHORTCUT_PRESETS.goSettings).toEqual({ key: ',', ctrl: true, description: 'Open settings' }); - expect(SHORTCUT_PRESETS.help).toEqual({ key: '?', shift: true, description: 'Show keyboard shortcuts' }); + expect(SHORTCUT_PRESETS.newRecord).toEqual({ + key: 'n', + ctrl: true, + shift: true, + description: 'New record', + }); + expect(SHORTCUT_PRESETS.goHome).toEqual({ + key: 'h', + ctrl: true, + shift: true, + description: 'Go home', + }); + expect(SHORTCUT_PRESETS.goSettings).toEqual({ + key: ',', + ctrl: true, + description: 'Open settings', + }); + expect(SHORTCUT_PRESETS.help).toEqual({ + key: '?', + shift: true, + description: 'Show keyboard shortcuts', + }); }); }); diff --git a/apps/web/src/__tests__/lib/i18n.test.ts b/apps/web/src/__tests__/lib/i18n.test.ts index 76e87ab6..868dd817 100644 --- a/apps/web/src/__tests__/lib/i18n.test.ts +++ b/apps/web/src/__tests__/lib/i18n.test.ts @@ -2,13 +2,7 @@ * Tests for i18n library functions. */ import { describe, it, expect } from 'vitest'; -import { - resolveKey, - interpolate, - translate, - createI18nState, - loadTranslations, -} from '@/lib/i18n'; +import { resolveKey, interpolate, translate, createI18nState, loadTranslations } from '@/lib/i18n'; describe('resolveKey', () => { const map = { diff --git a/apps/web/src/__tests__/lib/mock-data.test.ts b/apps/web/src/__tests__/lib/mock-data.test.ts index e96410c9..d3b6a65f 100644 --- a/apps/web/src/__tests__/lib/mock-data.test.ts +++ b/apps/web/src/__tests__/lib/mock-data.test.ts @@ -73,7 +73,7 @@ describe('mock-data', () => { describe('data consistency', () => { it('all app objects reference existing object definitions', () => { for (const app of mockAppDefinitions) { - for (const objName of (app.objects ?? [])) { + for (const objName of app.objects ?? []) { expect( mockObjectDefinitions[objName], `App "${app.name}" references undefined object "${objName}"`, diff --git a/apps/web/src/__tests__/lib/sync-engine.test.ts b/apps/web/src/__tests__/lib/sync-engine.test.ts index b253f2dd..ad552dc4 100644 --- a/apps/web/src/__tests__/lib/sync-engine.test.ts +++ b/apps/web/src/__tests__/lib/sync-engine.test.ts @@ -115,7 +115,15 @@ describe('SyncEngine', () => { const deltas = await engine.pullFromServer(async (cursor) => { expect(cursor).toBeNull(); return { - deltas: [{ objectName: 'accounts', recordId: 'a-1', type: 'update' as const, data: { name: 'Updated' }, serverTimestamp: Date.now() }], + deltas: [ + { + objectName: 'accounts', + recordId: 'a-1', + type: 'update' as const, + data: { name: 'Updated' }, + serverTimestamp: Date.now(), + }, + ], cursor: 'cursor-1', }; }); diff --git a/apps/web/src/__tests__/pages/sign-in.test.tsx b/apps/web/src/__tests__/pages/sign-in.test.tsx index 4ac4d317..9a22bc63 100644 --- a/apps/web/src/__tests__/pages/sign-in.test.tsx +++ b/apps/web/src/__tests__/pages/sign-in.test.tsx @@ -107,10 +107,12 @@ describe('SignInPage', () => { }); it('should display error message on sign-in failure', async () => { - mockSignInEmail.mockImplementation((_data: unknown, callbacks: { onError?: (ctx: { error: { message: string } }) => void }) => { - callbacks.onError?.({ error: { message: 'Invalid credentials' } }); - return Promise.resolve(); - }); + mockSignInEmail.mockImplementation( + (_data: unknown, callbacks: { onError?: (ctx: { error: { message: string } }) => void }) => { + callbacks.onError?.({ error: { message: 'Invalid credentials' } }); + return Promise.resolve(); + }, + ); renderSignIn(); diff --git a/apps/web/src/__tests__/pages/sign-up.test.tsx b/apps/web/src/__tests__/pages/sign-up.test.tsx index 1ff20032..058dd24f 100644 --- a/apps/web/src/__tests__/pages/sign-up.test.tsx +++ b/apps/web/src/__tests__/pages/sign-up.test.tsx @@ -99,10 +99,12 @@ describe('SignUpPage', () => { }); it('should display error message on sign-up failure', async () => { - mockSignUpEmail.mockImplementation((_data: unknown, callbacks: { onError?: (ctx: { error: { message: string } }) => void }) => { - callbacks.onError?.({ error: { message: 'Email already exists' } }); - return Promise.resolve(); - }); + mockSignUpEmail.mockImplementation( + (_data: unknown, callbacks: { onError?: (ctx: { error: { message: string } }) => void }) => { + callbacks.onError?.({ error: { message: 'Email already exists' } }); + return Promise.resolve(); + }, + ); renderSignUp(); diff --git a/apps/web/src/__tests__/types/workflow.test.ts b/apps/web/src/__tests__/types/workflow.test.ts index a63ce134..16609252 100644 --- a/apps/web/src/__tests__/types/workflow.test.ts +++ b/apps/web/src/__tests__/types/workflow.test.ts @@ -18,9 +18,7 @@ describe('workflow types', () => { { name: 'draft', label: 'Draft', initial: true, color: 'default' }, { name: 'done', label: 'Done', final: true, color: 'green' }, ], - transitions: [ - { name: 'complete', label: 'Complete', from: 'draft', to: 'done' }, - ], + transitions: [{ name: 'complete', label: 'Complete', from: 'draft', to: 'done' }], }; expect(def.states).toHaveLength(2); expect(def.transitions).toHaveLength(1); @@ -34,9 +32,7 @@ describe('workflow types', () => { currentState: 'draft', currentStateLabel: 'Draft', color: 'default', - availableTransitions: [ - { name: 'complete', label: 'Complete', from: 'draft', to: 'done' }, - ], + availableTransitions: [{ name: 'complete', label: 'Complete', from: 'draft', to: 'done' }], canApprove: true, }; expect(status.currentState).toBe('draft'); diff --git a/apps/web/src/components/auth/AuthLayout.tsx b/apps/web/src/components/auth/AuthLayout.tsx index b6780372..a87f132f 100644 --- a/apps/web/src/components/auth/AuthLayout.tsx +++ b/apps/web/src/components/auth/AuthLayout.tsx @@ -27,12 +27,8 @@ export function AuthLayout({ children, title, subtitle, alternativeLink }: AuthL ObjectOS -

- {title} -

- {subtitle && ( -

{subtitle}

- )} +

{title}

+ {subtitle &&

{subtitle}

}
{children}
diff --git a/apps/web/src/components/auth/RequireOrgAdmin.tsx b/apps/web/src/components/auth/RequireOrgAdmin.tsx index c798f459..b2f6cbe1 100644 --- a/apps/web/src/components/auth/RequireOrgAdmin.tsx +++ b/apps/web/src/components/auth/RequireOrgAdmin.tsx @@ -32,9 +32,7 @@ export function RequireOrgAdmin() { // ── Resolve current user's role in the active org ────────── const userId = session?.user?.id; - const currentMember = activeOrg.members?.find( - (m: { userId: string }) => m.userId === userId, - ); + const currentMember = activeOrg.members?.find((m: { userId: string }) => m.userId === userId); const role = currentMember?.role; const isAdmin = role === 'owner' || role === 'admin'; @@ -50,8 +48,8 @@ export function RequireOrgAdmin() {

Access Denied

- The Admin Console is restricted to organization owners and administrators. - Contact your organization admin if you need access. + The Admin Console is restricted to organization owners and administrators. Contact your + organization admin if you need access.

); diff --git a/apps/web/src/components/dashboard/AppSwitcher.tsx b/apps/web/src/components/dashboard/AppSwitcher.tsx index 2a00f07e..ea537a14 100644 --- a/apps/web/src/components/dashboard/AppSwitcher.tsx +++ b/apps/web/src/components/dashboard/AppSwitcher.tsx @@ -64,15 +64,9 @@ function useAppSwitcherState() { }, [normalizedQuery, apps]); const pinnedApps = filteredApps.filter((app) => app.pinned); - const systemApps = filteredApps.filter( - (app) => app.category === 'system' && !app.pinned, - ); - const businessApps = filteredApps.filter( - (app) => app.category === 'business' && !app.pinned, - ); - const customApps = filteredApps.filter( - (app) => app.category === 'custom' && !app.pinned, - ); + const systemApps = filteredApps.filter((app) => app.category === 'system' && !app.pinned); + const businessApps = filteredApps.filter((app) => app.category === 'business' && !app.pinned); + const customApps = filteredApps.filter((app) => app.category === 'custom' && !app.pinned); return { navigate, @@ -122,13 +116,8 @@ function AppDropdownBody({ return ( <> - - Search - - event.preventDefault()} - > + Search + event.preventDefault()}> setQuery(event.target.value)} @@ -140,18 +129,14 @@ function AppDropdownBody({
{pinnedApps.length > 0 && ( <> - - Pinned - + Pinned {pinnedApps.map(renderAppItem)} )} {systemApps.length > 0 && ( <> - - System - + System {systemApps.map(renderAppItem)} )} @@ -167,17 +152,13 @@ function AppDropdownBody({ {customApps.length > 0 && ( <> - - Custom - + Custom {customApps.map(renderAppItem)} )} {filteredApps.length === 0 && ( -
- No apps match your search. -
+
No apps match your search.
)}
@@ -234,9 +215,7 @@ export function AppSwitcher({ variant = 'sidebar' }: AppSwitcherProps) {
{state.displayName} - - Business Apps - + Business Apps
diff --git a/apps/web/src/components/dashboard/DashboardLayout.tsx b/apps/web/src/components/dashboard/DashboardLayout.tsx index 9b660914..b165a0ca 100644 --- a/apps/web/src/components/dashboard/DashboardLayout.tsx +++ b/apps/web/src/components/dashboard/DashboardLayout.tsx @@ -32,9 +32,7 @@ import { Separator } from '@/components/ui/separator'; import { NavUser } from '@/components/dashboard/NavUser'; import { AppSwitcher } from '@/components/dashboard/AppSwitcher'; -const navMain = [ - { title: 'Dashboard', href: '/dashboard', icon: LayoutDashboard }, -]; +const navMain = [{ title: 'Dashboard', href: '/dashboard', icon: LayoutDashboard }]; const navOrganization = [ { title: 'Members', href: '/organization/members', icon: Users }, diff --git a/apps/web/src/components/dashboard/NavUser.tsx b/apps/web/src/components/dashboard/NavUser.tsx index 8ef42498..1efdef43 100644 --- a/apps/web/src/components/dashboard/NavUser.tsx +++ b/apps/web/src/components/dashboard/NavUser.tsx @@ -6,15 +6,7 @@ import { useActiveOrganization, useListOrganizations, } from '@/lib/auth-client'; -import { - BadgeCheck, - Building2, - Check, - ChevronsUpDown, - LogOut, - Plus, - Shield, -} from 'lucide-react'; +import { BadgeCheck, Building2, Check, ChevronsUpDown, LogOut, Plus, Shield } from 'lucide-react'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { DropdownMenu, @@ -55,7 +47,6 @@ export function NavUser() { window.location.reload(); }; - return ( @@ -100,10 +91,7 @@ export function NavUser() { {organizations.map((org) => ( - handleSwitchOrg(org.id)} - > + handleSwitchOrg(org.id)}> {org.name} {activeOrg?.id === org.id && ( diff --git a/apps/web/src/components/dashboard/OrgSwitcher.tsx b/apps/web/src/components/dashboard/OrgSwitcher.tsx index 1cd738e1..a2f1646a 100644 --- a/apps/web/src/components/dashboard/OrgSwitcher.tsx +++ b/apps/web/src/components/dashboard/OrgSwitcher.tsx @@ -1,9 +1,5 @@ import { useNavigate } from 'react-router-dom'; -import { - organization, - useActiveOrganization, - useListOrganizations, -} from '@/lib/auth-client'; +import { organization, useActiveOrganization, useListOrganizations } from '@/lib/auth-client'; import { ChevronsUpDown, Plus, Check } from 'lucide-react'; import { DropdownMenu, @@ -52,9 +48,7 @@ export function OrgSwitcher() {
{orgName} {orgSlug && ( - - {orgSlug} - + {orgSlug} )}
@@ -79,9 +73,7 @@ export function OrgSwitcher() { {org.name.charAt(0).toUpperCase()} {org.name} - {activeOrg?.id === org.id && ( - - )} + {activeOrg?.id === org.id && }
))} diff --git a/apps/web/src/components/dashboard/TeamSwitcher.tsx b/apps/web/src/components/dashboard/TeamSwitcher.tsx index 60bba31e..2e83dac8 100644 --- a/apps/web/src/components/dashboard/TeamSwitcher.tsx +++ b/apps/web/src/components/dashboard/TeamSwitcher.tsx @@ -1,9 +1,5 @@ import { useNavigate } from 'react-router-dom'; -import { - useListOrganizations, - organization, - useActiveOrganization, -} from '@/lib/auth-client'; +import { useListOrganizations, organization, useActiveOrganization } from '@/lib/auth-client'; import { ChevronsUpDown, Plus, Blocks } from 'lucide-react'; import { DropdownMenu, @@ -26,8 +22,7 @@ export function TeamSwitcher() { const { isMobile } = useSidebar(); const navigate = useNavigate(); - const currentOrg = - organizations?.find((o) => o.id === activeOrg?.id) || organizations?.[0]; + const currentOrg = organizations?.find((o) => o.id === activeOrg?.id) || organizations?.[0]; const displayName = currentOrg?.name || 'Personal'; const initial = displayName.charAt(0).toUpperCase(); @@ -87,9 +82,7 @@ export function TeamSwitcher() { className="gap-2 p-2" >
- - {org.name.charAt(0).toUpperCase()} - + {org.name.charAt(0).toUpperCase()}
{org.name}
@@ -102,9 +95,7 @@ export function TeamSwitcher() {
- - Add organization - + Add organization
diff --git a/apps/web/src/components/layouts/AppLayout.tsx b/apps/web/src/components/layouts/AppLayout.tsx index 24030bf0..c83868d5 100644 --- a/apps/web/src/components/layouts/AppLayout.tsx +++ b/apps/web/src/components/layouts/AppLayout.tsx @@ -40,9 +40,7 @@ function Breadcrumbs({ objectName?: string; recordTitle?: string; }) { - const items: { label: string; href?: string }[] = [ - { label: appName, href: `/apps/${appId}` }, - ]; + const items: { label: string; href?: string }[] = [{ label: appName, href: `/apps/${appId}` }]; if (objectName) { items.push({ label: objectName, href: `/apps/${appId}/${objectName}` }); @@ -141,7 +139,11 @@ export function AppLayout() { {appRecentItems.map((item) => ( - + {item.title} diff --git a/apps/web/src/components/layouts/SettingsLayout.tsx b/apps/web/src/components/layouts/SettingsLayout.tsx index d28c4293..ab4ba6ca 100644 --- a/apps/web/src/components/layouts/SettingsLayout.tsx +++ b/apps/web/src/components/layouts/SettingsLayout.tsx @@ -37,9 +37,7 @@ import { Separator } from '@/components/ui/separator'; import { NavUser } from '@/components/dashboard/NavUser'; import { AppSwitcher } from '@/components/dashboard/AppSwitcher'; -const navOverview = [ - { title: 'Overview', href: '/settings', icon: LayoutDashboard }, -]; +const navOverview = [{ title: 'Overview', href: '/settings', icon: LayoutDashboard }]; const navOrganization = [ { title: 'General', href: '/settings/organization', icon: Building2 }, @@ -81,11 +79,7 @@ export function SettingsLayout() { {items.map((item) => ( - + {item.title} diff --git a/apps/web/src/components/objectui/BulkActionBar.tsx b/apps/web/src/components/objectui/BulkActionBar.tsx index 9e9855d8..5774e23e 100644 --- a/apps/web/src/components/objectui/BulkActionBar.tsx +++ b/apps/web/src/components/objectui/BulkActionBar.tsx @@ -45,9 +45,7 @@ export function BulkActionBar({ const [updateField, setUpdateField] = useState(''); const [updateValue, setUpdateValue] = useState(''); - const editableFields = resolveFields(objectDef.fields, ['id']).filter( - (f) => !f.readonly, - ); + const editableFields = resolveFields(objectDef.fields, ['id']).filter((f) => !f.readonly); const handleDelete = () => { onBulkDelete(selectedIds); @@ -113,12 +111,7 @@ export function BulkActionBar({ - @@ -128,11 +121,13 @@ export function BulkActionBar({ - Delete {selectedIds.length} record{selectedIds.length !== 1 ? 's' : ''}? + + Delete {selectedIds.length} record{selectedIds.length !== 1 ? 's' : ''}? + This action cannot be undone. The selected{' '} - {(objectDef.pluralLabel ?? objectDef.label ?? 'records').toLowerCase()}{' '} - will be permanently deleted. + {(objectDef.pluralLabel ?? objectDef.label ?? 'records').toLowerCase()} will be + permanently deleted. @@ -150,7 +145,9 @@ export function BulkActionBar({ - Update field for {selectedIds.length} record{selectedIds.length !== 1 ? 's' : ''} + + Update field for {selectedIds.length} record{selectedIds.length !== 1 ? 's' : ''} + Choose a field and value to apply to all selected records. diff --git a/apps/web/src/components/objectui/ChartWidget.tsx b/apps/web/src/components/objectui/ChartWidget.tsx index 2af03f8a..01ed4e7b 100644 --- a/apps/web/src/components/objectui/ChartWidget.tsx +++ b/apps/web/src/components/objectui/ChartWidget.tsx @@ -13,8 +13,16 @@ interface ChartWidgetProps { } const DEFAULT_COLORS = [ - '#3b82f6', '#8b5cf6', '#22c55e', '#f59e0b', '#ef4444', - '#06b6d4', '#ec4899', '#14b8a6', '#f97316', '#6366f1', + '#3b82f6', + '#8b5cf6', + '#22c55e', + '#f59e0b', + '#ef4444', + '#06b6d4', + '#ec4899', + '#14b8a6', + '#f97316', + '#6366f1', ]; function getColor(index: number, explicit?: string): string { @@ -52,9 +60,7 @@ function BarChart({ config }: { config: ChartConfig }) { }} /> - - {point.value} - + {point.value} ))} @@ -107,7 +113,13 @@ function PieChart({ config, donut = false }: { config: ChartConfig; donut?: bool return (
- + {slices}
diff --git a/apps/web/src/components/objectui/CloneRecordDialog.tsx b/apps/web/src/components/objectui/CloneRecordDialog.tsx index ea3ea03b..67832eb9 100644 --- a/apps/web/src/components/objectui/CloneRecordDialog.tsx +++ b/apps/web/src/components/objectui/CloneRecordDialog.tsx @@ -72,9 +72,7 @@ export function CloneRecordDialog({ const handleOpenChange = (isOpen: boolean) => { setOpen(isOpen); if (isOpen) { - setSelectedFields( - new Set(cloneableFields.filter((f) => !f.readonly).map((f) => f.name)), - ); + setSelectedFields(new Set(cloneableFields.filter((f) => !f.readonly).map((f) => f.name))); } }; @@ -89,9 +87,7 @@ export function CloneRecordDialog({ Clone {objectDef.label ?? objectDef.name} - - Select which fields to copy to the new record. - + Select which fields to copy to the new record.
@@ -104,7 +100,9 @@ export function CloneRecordDialog({