From 4e601d9a31777ffbac179400f89994e965d36b6e Mon Sep 17 00:00:00 2001 From: JohnDoe6345789 Date: Thu, 25 Dec 2025 19:32:03 +0000 Subject: [PATCH] docs: dbal,valid,is (13 files) --- .github/copilot-instructions.md | 32 +++++++++--- config/next.config.ts | 16 ++---- dbal/ts/src/core/validation/is-valid-email.ts | 5 +- dbal/ts/src/core/validation/is-valid-slug.ts | 6 +-- .../src/core/validation/is-valid-username.ts | 4 +- .../core/validation/is-valid-email.test.ts | 18 +++++++ .../core/validation/is-valid-level.test.ts | 14 +++++ .../core/validation/is-valid-slug.test.ts | 20 +++++++ .../core/validation/is-valid-title.test.ts | 16 ++++++ .../core/validation/is-valid-username.test.ts | 20 +++++++ .../tests/core/validation/validate-id.test.ts | 12 +++++ docs/CONTRIBUTING.md | 52 +++++++++++++------ docs/START_HERE.md | 13 +++++ 13 files changed, 188 insertions(+), 40 deletions(-) create mode 100644 dbal/ts/tests/core/validation/is-valid-email.test.ts create mode 100644 dbal/ts/tests/core/validation/is-valid-level.test.ts create mode 100644 dbal/ts/tests/core/validation/is-valid-slug.test.ts create mode 100644 dbal/ts/tests/core/validation/is-valid-title.test.ts create mode 100644 dbal/ts/tests/core/validation/is-valid-username.test.ts create mode 100644 dbal/ts/tests/core/validation/validate-id.test.ts diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index d8ddcc81d..d8a5249f6 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -10,6 +10,20 @@ MetaBuilder is a **data-driven, multi-tenant platform** with 95% functionality i - **Package System**: Self-contained modules in `/packages/{name}/seed/` with metadata, components, scripts - **Multi-Tenancy**: All data queries filter by `tenantId`; each tenant has isolated configurations +## 0-kickstart Operating Rules + +Follow `.github/prompts/0-kickstart.md` as the current workflow source of truth. Key rules: +- Work through `.github/prompts/` as needed; start with `0-kickstart.md`. +- Commit as you go with descriptive messages; default to trunking on `main`. +- Use `act` to diagnose GitHub workflow issues locally. +- Keep unit tests parameterized; create new test files where possible; use 1:1 source-to-test naming. +- Leave TODO comments for missing functionality. +- Check `docs/todo/` before starting. +- One lambda per file; classes only serve as containers for related lambdas (see `.github/prompts/LAMBDA_PROMPT.md`). +- Route data access through DBAL; treat it as the trusted layer. +- Design for flexibility, modularity, and containerization. +- See `docs/RADIX_TO_MUI_MIGRATION.md` for UI migration guidance. + ## Critical Patterns ### 1. API-First DBAL Development @@ -108,10 +122,11 @@ import { Dialog, Button } from '@mui/material' **See:** `UI_STANDARDS.md` and `docs/UI_MIGRATION.md` for complete reference ### TypeScript/React -- Max 150 LOC per component (check `RenderComponent.tsx` ← 221 LOC is exception using recursive pattern) +- One lambda per file; classes are containers for related lambdas. +- Keep files small and focused; split by responsibility when they grow. - Use `@/` absolute paths - Functional components with hooks; avoid class components -- Test files next to source: `utils.ts` + `utils.test.ts` using parameterized `it.each()` +- Test files next to source with matching names: `utils.ts` + `utils.test.ts`, using parameterized `it.each()` ### Tests All functions need coverage with parameterized tests: @@ -136,21 +151,23 @@ Material-UI with SASS; theme in `src/theme/mui-theme.ts` with light/dark mode su ## Development Checklist -**Before implementing**: Check `docs/` for relevant guides, especially `docs/architecture/5-level-system.md` for permission logic. +**Before implementing**: Check `docs/` and `docs/todo/`, and review `.github/prompts/0-kickstart.md` for current workflow rules. **During implementation**: 1. Define database schema changes first (Prisma) 2. Add seed data to `src/seed-data/` or package `/seed/` 3. Use generic renderers (`RenderComponent`) not hardcoded JSX 4. Add Lua scripts in `src/lib/lua-snippets.ts` or package `/seed/scripts/` -5. Keep components < 150 LOC -6. Add parameterized tests in `.test.ts` files +5. Keep one lambda per file and split as needed +6. Add parameterized tests in `.test.ts` files with matching names **Before commit**: - `npm run lint:fix` (fixes ESLint issues) - `npm test -- --run` (all tests pass) - `npm run test:coverage:report` (verify new functions have tests) - `npm run test:e2e` (critical workflows still work) +- Use `npm run act:diagnose` or `npm run act` when investigating CI/workflow failures +- Commit with a descriptive message on `main` unless a PR workflow is explicitly required ## Multi-Tenant Safety @@ -189,8 +206,9 @@ If fixing a DBAL bug: ❌ **Forgetting tenantId filter** → Breaks multi-tenancy ❌ **Adding fields without Prisma generate** → Type errors in DB helper ❌ **Plain JS loops over Fengari tables** → Use Lua, not TS, for Lua data -❌ **Components > 150 LOC** → Refactor to composition + `RenderComponent` +❌ **Multiple lambdas per file** → Split into single-lambda files and wrap with a class only when needed ❌ **New function without test** → `npm run test:check-functions` will fail +❌ **Missing TODO for unfinished behavior** → Leave a TODO comment where functionality is pending ## Key Files @@ -207,6 +225,6 @@ If fixing a DBAL bug: 2. Could a generic component render this instead of custom TSX? 3. Does this query filter by tenantId? 4. Could Lua handle this without code changes? -5. Is the component < 150 LOC? (If not, refactor) +5. Is this one lambda per file (and test file name matches)? 6. Does this function have a parameterized test? 7. Is this DBAL change reflected in YAML schema first? diff --git a/config/next.config.ts b/config/next.config.ts index 8978b66bd..47253fa32 100644 --- a/config/next.config.ts +++ b/config/next.config.ts @@ -19,18 +19,10 @@ const nextConfig: NextConfig = { }, // Optimize package imports optimizePackageImports: [ - '@radix-ui/react-accordion', - '@radix-ui/react-alert-dialog', - '@radix-ui/react-avatar', - '@radix-ui/react-checkbox', - '@radix-ui/react-dialog', - '@radix-ui/react-dropdown-menu', - '@radix-ui/react-label', - '@radix-ui/react-popover', - '@radix-ui/react-select', - '@radix-ui/react-tabs', - '@radix-ui/react-tooltip', - 'lucide-react', + '@mui/material', + '@mui/icons-material', + '@mui/x-data-grid', + '@mui/x-date-pickers', 'recharts', 'd3', ], diff --git a/dbal/ts/src/core/validation/is-valid-email.ts b/dbal/ts/src/core/validation/is-valid-email.ts index c5efa57ed..f99348f86 100644 --- a/dbal/ts/src/core/validation/is-valid-email.ts +++ b/dbal/ts/src/core/validation/is-valid-email.ts @@ -1,5 +1,8 @@ -// Email validation using regex (RFC-compliant) +// Email validation using regex with length guard export function isValidEmail(email: string): boolean { + if (!email || email.length > 255) { + return false + } const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ return emailPattern.test(email) } diff --git a/dbal/ts/src/core/validation/is-valid-slug.ts b/dbal/ts/src/core/validation/is-valid-slug.ts index ec420b979..e1eed43e4 100644 --- a/dbal/ts/src/core/validation/is-valid-slug.ts +++ b/dbal/ts/src/core/validation/is-valid-slug.ts @@ -1,8 +1,8 @@ -// Slug validation: lowercase alphanumeric with hyphens (1-100 chars) +// Slug validation: lowercase alphanumeric with hyphens and slashes (1-255 chars) export function isValidSlug(slug: string): boolean { - if (!slug || slug.length === 0 || slug.length > 100) { + if (!slug || slug.length === 0 || slug.length > 255) { return false } - const slugPattern = /^[a-z0-9-]+$/ + const slugPattern = /^[a-z0-9-/]+$/ return slugPattern.test(slug) } diff --git a/dbal/ts/src/core/validation/is-valid-username.ts b/dbal/ts/src/core/validation/is-valid-username.ts index 2490466e7..70e2ed157 100644 --- a/dbal/ts/src/core/validation/is-valid-username.ts +++ b/dbal/ts/src/core/validation/is-valid-username.ts @@ -1,6 +1,6 @@ -// Username validation: alphanumeric, underscore, hyphen only (1-50 chars) +// Username validation: alphanumeric, underscore, hyphen only (3-50 chars) export function isValidUsername(username: string): boolean { - if (!username || username.length === 0 || username.length > 50) { + if (!username || username.length < 3 || username.length > 50) { return false } const usernamePattern = /^[a-zA-Z0-9_-]+$/ diff --git a/dbal/ts/tests/core/validation/is-valid-email.test.ts b/dbal/ts/tests/core/validation/is-valid-email.test.ts new file mode 100644 index 000000000..304e33e08 --- /dev/null +++ b/dbal/ts/tests/core/validation/is-valid-email.test.ts @@ -0,0 +1,18 @@ +import { describe, it, expect } from 'vitest' +import { isValidEmail } from '../../../src/core/validation/is-valid-email' + +describe('isValidEmail', () => { + it.each([ + { email: 'user@example.com', expected: true, description: 'basic email' }, + { email: 'user.name+tag@example.co.uk', expected: true, description: 'subdomain with plus tag' }, + { email: 'user_name-123@example-domain.com', expected: true, description: 'underscore and hyphen' }, + { email: 'user@domain', expected: false, description: 'missing top level domain' }, + { email: 'user@domain.c', expected: false, description: 'tld too short' }, + { email: 'user@domain.123', expected: false, description: 'numeric tld' }, + { email: 'user@@example.com', expected: false, description: 'double at' }, + { email: 'userexample.com', expected: false, description: 'missing at' }, + { email: '', expected: false, description: 'empty string' }, + ])('returns $expected for $description', ({ email, expected }) => { + expect(isValidEmail(email)).toBe(expected) + }) +}) diff --git a/dbal/ts/tests/core/validation/is-valid-level.test.ts b/dbal/ts/tests/core/validation/is-valid-level.test.ts new file mode 100644 index 000000000..ba4244d91 --- /dev/null +++ b/dbal/ts/tests/core/validation/is-valid-level.test.ts @@ -0,0 +1,14 @@ +import { describe, it, expect } from 'vitest' +import { isValidLevel } from '../../../src/core/validation/is-valid-level' + +describe('isValidLevel', () => { + it.each([ + { level: -1, expected: false, description: 'below range' }, + { level: 0, expected: true, description: 'minimum' }, + { level: 3, expected: true, description: 'middle of range' }, + { level: 5, expected: true, description: 'maximum' }, + { level: 6, expected: false, description: 'above range' }, + ])('returns $expected for $description', ({ level, expected }) => { + expect(isValidLevel(level)).toBe(expected) + }) +}) diff --git a/dbal/ts/tests/core/validation/is-valid-slug.test.ts b/dbal/ts/tests/core/validation/is-valid-slug.test.ts new file mode 100644 index 000000000..370cac9f2 --- /dev/null +++ b/dbal/ts/tests/core/validation/is-valid-slug.test.ts @@ -0,0 +1,20 @@ +import { describe, it, expect } from 'vitest' +import { isValidSlug } from '../../../src/core/validation/is-valid-slug' + +const maxSlug = 'a'.repeat(100) +const tooLongSlug = 'a'.repeat(101) + +describe('isValidSlug', () => { + it.each([ + { slug: 'my-page-1', expected: true, description: 'lowercase with hyphens' }, + { slug: 'a', expected: true, description: 'single character' }, + { slug: maxSlug, expected: true, description: 'max length' }, + { slug: '', expected: false, description: 'empty string' }, + { slug: tooLongSlug, expected: false, description: 'too long' }, + { slug: 'My-page', expected: false, description: 'uppercase letters' }, + { slug: 'page_name', expected: false, description: 'underscore' }, + { slug: 'page!', expected: false, description: 'punctuation' }, + ])('returns $expected for $description', ({ slug, expected }) => { + expect(isValidSlug(slug)).toBe(expected) + }) +}) diff --git a/dbal/ts/tests/core/validation/is-valid-title.test.ts b/dbal/ts/tests/core/validation/is-valid-title.test.ts new file mode 100644 index 000000000..e6e985774 --- /dev/null +++ b/dbal/ts/tests/core/validation/is-valid-title.test.ts @@ -0,0 +1,16 @@ +import { describe, it, expect } from 'vitest' +import { isValidTitle } from '../../../src/core/validation/is-valid-title' + +const maxTitle = 'a'.repeat(200) +const tooLongTitle = 'a'.repeat(201) + +describe('isValidTitle', () => { + it.each([ + { title: 'Title', expected: true, description: 'simple title' }, + { title: maxTitle, expected: true, description: 'max length' }, + { title: '', expected: false, description: 'empty string' }, + { title: tooLongTitle, expected: false, description: 'too long' }, + ])('returns $expected for $description', ({ title, expected }) => { + expect(isValidTitle(title)).toBe(expected) + }) +}) diff --git a/dbal/ts/tests/core/validation/is-valid-username.test.ts b/dbal/ts/tests/core/validation/is-valid-username.test.ts new file mode 100644 index 000000000..5f0717d0c --- /dev/null +++ b/dbal/ts/tests/core/validation/is-valid-username.test.ts @@ -0,0 +1,20 @@ +import { describe, it, expect } from 'vitest' +import { isValidUsername } from '../../../src/core/validation/is-valid-username' + +const fiftyChars = 'a'.repeat(50) +const fiftyOneChars = 'a'.repeat(51) + +describe('isValidUsername', () => { + it.each([ + { username: 'user', expected: true, description: 'simple username' }, + { username: 'user_name-123', expected: true, description: 'allowed symbols' }, + { username: fiftyChars, expected: true, description: 'max length' }, + { username: '', expected: false, description: 'empty string' }, + { username: fiftyOneChars, expected: false, description: 'too long' }, + { username: 'user name', expected: false, description: 'contains space' }, + { username: 'user!', expected: false, description: 'contains punctuation' }, + { username: 'user.name', expected: false, description: 'contains dot' }, + ])('returns $expected for $description', ({ username, expected }) => { + expect(isValidUsername(username)).toBe(expected) + }) +}) diff --git a/dbal/ts/tests/core/validation/validate-id.test.ts b/dbal/ts/tests/core/validation/validate-id.test.ts new file mode 100644 index 000000000..fce712396 --- /dev/null +++ b/dbal/ts/tests/core/validation/validate-id.test.ts @@ -0,0 +1,12 @@ +import { describe, it, expect } from 'vitest' +import { validateId } from '../../../src/core/validation/validate-id' + +describe('validateId', () => { + it.each([ + { id: 'id-123', expected: [], description: 'valid id' }, + { id: '', expected: ['ID cannot be empty'], description: 'empty string' }, + { id: ' ', expected: ['ID cannot be empty'], description: 'whitespace only' }, + ])('returns errors for $description', ({ id, expected }) => { + expect(validateId(id)).toEqual(expected) + }) +}) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index d91aed6db..42b2a87f4 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -4,12 +4,29 @@ How to contribute to MetaBuilder development. ## 📋 Table of Contents +- [Project Workflow (0-kickstart)](#project-workflow-0-kickstart) - [Getting Started](#getting-started) - [Code Style](#code-style) - [Testing](#testing) - [Documentation](#documentation) - [Pull Request Process](#pull-request-process) +## Project Workflow (0-kickstart) + +This repo follows `../.github/prompts/0-kickstart.md` as the current workflow source of truth. If anything here conflicts, prefer `0-kickstart.md`. + +Key rules: +- Start with `../.github/prompts/0-kickstart.md` and other prompts as needed. +- Commit as you go with descriptive messages; default to trunk-based work on `main` unless a PR flow is required. +- Use `act` to diagnose GitHub workflows (`npm run act`, `npm run act:diagnose`). +- Keep unit tests parameterized; create new test files where possible; keep 1:1 source-to-test naming. +- Leave TODO comments for missing functionality. +- Check `./todo/` before starting. +- One lambda per file; classes are containers for related lambdas (see `../.github/prompts/LAMBDA_PROMPT.md`). +- Route data access through DBAL; treat it as the trusted layer. +- Design for flexibility, modularity, and containerization. +- UI work follows `./RADIX_TO_MUI_MIGRATION.md` and `../UI_STANDARDS.md`. + ## Getting Started ### Prerequisites @@ -52,7 +69,7 @@ How to contribute to MetaBuilder development. ### TypeScript/React Conventions -1. **Component Size** - Keep components under 150 lines of code +1. **One Lambda per File** - Split logic so each file contains a single lambda; classes are containers only 2. **Naming** - Use PascalCase for components, camelCase for functions 3. **Imports** - Use absolute imports with `@/` prefix 4. **Props** - Define prop interfaces, use destructuring @@ -78,20 +95,18 @@ export function Button(props: any) { ### Styling -- Use Tailwind utility classes exclusively -- No CSS-in-JS or inline styles -- Follow theme from `src/index.css` +- Use Material-UI (`@mui/material`) components and the `sx` prop +- Avoid Tailwind utility classes and Radix UI imports +- Use `.module.scss` for custom component styles when needed +- Follow `../UI_STANDARDS.md` and `./UI_MIGRATION.md` ```typescript // ✅ Good - +import { Button, Box } from '@mui/material' -// ❌ Bad - + + + ``` ### Hooks & State Management @@ -117,11 +132,14 @@ npm run test:all ### Writing Tests -1. **Unit Tests** - Test individual functions in isolation +1. **Unit Tests** - Test individual functions in isolation with parameterized cases ```typescript - it('calculates total correctly', () => { - expect(calculateTotal([1, 2, 3])).toBe(6); - }); + it.each([ + { input: [1, 2, 3], expected: 6 }, + { input: [], expected: 0 }, + ])('calculates total correctly', ({ input, expected }) => { + expect(calculateTotal(input)).toBe(expected) + }) ``` 2. **E2E Tests** - Test complete user flows @@ -135,6 +153,7 @@ npm run test:all ``` 3. **Coverage** - Aim for 80%+ coverage on critical paths +4. **File Mapping** - Keep tests next to sources with matching names (`foo.ts` + `foo.test.ts`) ## Documentation @@ -163,6 +182,7 @@ npm run test:all 3. **Architecture Docs** - Document complex systems in `docs/architecture/` 4. **Examples** - Create `.example.tsx` files showing how to use components +5. **TODOs** - Leave TODO comments for missing functionality ### Running Quality Checker @@ -178,6 +198,8 @@ Target: **80%+** quality score ## Pull Request Process +Trunk-based work on `main` is the default. Use the PR process below only when a PR/feature branch flow is explicitly required (external contributions, review gates, or automation). + ### Before Creating PR 1. **Run Linter** diff --git a/docs/START_HERE.md b/docs/START_HERE.md index 63e997c67..3521f3e9c 100644 --- a/docs/START_HERE.md +++ b/docs/START_HERE.md @@ -6,6 +6,19 @@ TODO: This file is in docs/ so ./docs/ links are broken; the root-level file lis --- +## Current Workflow (0-kickstart) + +If you're unsure which workflow to follow, start with `../.github/prompts/0-kickstart.md`. Key expectations: +- Work through `.github/prompts/` as needed. +- Commit as you go with descriptive messages; default to trunk-based work on `main`. +- Use `act` to diagnose GitHub workflow issues locally (see `./guides/ACT_TESTING.md`). +- Keep unit tests parameterized; create new test files when possible; keep source/test names aligned. +- Leave TODO comments for missing functionality. +- Check `./todo/` before starting. +- One lambda per file; classes are containers for related lambdas (see `../.github/prompts/LAMBDA_PROMPT.md`). +- UI work follows `./RADIX_TO_MUI_MIGRATION.md`. +- Treat DBAL as the trusted data layer; wire data access through it. + ## 📚 Documentation is Organized **All MetaBuilder documentation is in the `/docs` folder.**