Notes Design

Design Document: Note-Taking Application

Overview

The note-taking application is a React-based single-page application (SPA) that provides authenticated users with the ability to create, view, and edit notes organized in a flat folder structure. The application leverages AWS Amplify for authentication and direct DynamoDB access without a backend server.

The architecture follows a serverless, client-side approach where:

This design focuses on Milestone h: establishing a working foundation with basic CRUD operations, authentication, and a simple UI. The implementation prioritizes incremental development with each step producing a runnable system.

Architecture

High-Level Architecture

┌─────────────────────────────────────────────────────────────┐
│                        Browser                               │
│  ┌───────────────────────────────────────────────────────┐  │
│  │              React Application (Vite)                  │  │
│  │                                                         │  │
│  │  ┌──────────────┐  ┌──────────────┐  ┌─────────────┐ │  │
│  │  │ Authenticator│  │  Forest View │  │  Note Editor│ │  │
│  │  │  Component   │  │  Component   │  │  Component  │ │  │
│  │  └──────────────┘  └──────────────┘  └─────────────┘ │  │
│  │                                                         │  │
│  │  ┌──────────────────────────────────────────────────┐ │  │
│  │  │         AWS Amplify Client Library               │ │  │
│  │  └──────────────────────────────────────────────────┘ │  │
│  └───────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘
                            │
                            │ HTTPS
                            ▼
┌─────────────────────────────────────────────────────────────┐
│                         AWS Cloud                            │
│                                                               │
│  ┌──────────────────┐         ┌──────────────────┐          │
│  │  Cognito User    │         │  Cognito Identity│          │
│  │  Pool            │────────▶│  Pool            │          │
│  │  (Auth)          │         │  (Credentials)   │          │
│  └──────────────────┘         └──────────────────┘          │
│                                        │                     │
│                                        │ IAM Role            │
│                                        ▼                     │
│                            ┌──────────────────┐             │
│                            │   DynamoDB       │             │
│                            │   Table          │             │
│                            │   (User Data)    │             │
│                            └──────────────────┘             │
└─────────────────────────────────────────────────────────────┘

Component Responsibilities

React Application Layer:

AWS Amplify Layer:

AWS Cognito:

DynamoDB:

Components and Interfaces

Data Access Layer

DynamoDB Client Interface:

interface DynamoDBClient {
  // Read operations
  getItem(params: GetItemParams): Promise<Item | null>
  batchGetItems(keys: ItemKey[]): Promise<Item[]>

  // Write operations
  putItem(params: PutItemParams): Promise<void>
  transactWrite(items: TransactWriteItem[]): Promise<void>
}

interface GetItemParams {
  pk: string  // Cognito identity
  sk: string  // Sort key (FOREST, NOTE#id, FOLDER#id)
}

interface PutItemParams {
  pk: string
  sk: string
  attributes: Record<string, any>
}

interface TransactWriteItem {
  operation: 'Put' | 'Update' | 'Delete'
  pk: string
  sk: string
  attributes?: Record<string, any>
}

Forest Management

Forest Structure:

interface Forest {
  [nodeId: string]: ForestNode
}

interface ForestNode {
  type: 'folder' | 'note'
  children?: string[]  // Only for folders
}

// Example forest:
const exampleForest: Forest = {
  "root": { type: "folder", children: ["xyz", "abc"] },
  "bin": { type: "folder", children: [] },
  "xyz": { type: "folder", children: ["def"] },
  "abc": { type: "note" },
  "def": { type: "note" }
}

Forest Operations:

interface ForestManager {
  // Read forest from DynamoDB
  loadForest(): Promise<Forest>

  // Initialize empty forest if none exists
  initializeForest(): Promise<Forest>

  // Add a note to a folder in the forest
  addNoteToFolder(forest: Forest, folderId: string, noteId: string): Forest

  // Get all note IDs from the forest
  getAllNoteIds(forest: Forest): string[]

  // Save forest to DynamoDB
  saveForest(forest: Forest): Promise<void>
}

Note Management

Note Data Model:

interface Note {
  id: string          // nanoid
  title: string
  content: string
  createdAt: string   // ISO 8601 timestamp
  updatedAt: string   // ISO 8601 timestamp
}

interface NoteRecord {
  PK: string          // Cognito identity
  SK: string          // "NOTE#{id}"
  title: string
  content: string
  createdAt: string
  updatedAt: string
}

Note Operations:

interface NoteManager {
  // Create a new note and update forest atomically
  createNote(folderId: string, title: string, content: string): Promise<Note>

  // Read a single note by ID
  getNote(noteId: string): Promise<Note | null>

  // Batch read note titles for forest display
  getNoteMetadata(noteIds: string[]): Promise<Map<string, string>>

  // Update an existing note
  updateNote(noteId: string, updates: Partial<Note>): Promise<void>
}

Folder Management

Folder Data Model:

interface Folder {
  id: string          // nanoid
  name: string
}

interface FolderRecord {
  PK: string          // Cognito identity
  SK: string          // "FOLDER#{id}"
  name: string
}

Folder Operations:

interface FolderManager {
  // Create a new folder
  createFolder(name: string): Promise<Folder>

  // Get folder details
  getFolder(folderId: string): Promise<Folder | null>

  // Batch read folder names for forest display
  getFolderMetadata(folderIds: string[]): Promise<Map<string, string>>
}

UI Components

App Component:

function App() {
  // Wraps entire app with Amplify Authenticator
  // Manages authenticated user state
  // Renders ForestView when authenticated
}

ForestView Component:

interface ForestViewProps {
  userId: string  // Cognito identity
}

function ForestView({ userId }: ForestViewProps) {
  // Loads forest on mount
  // Displays forest as nested list
  // Handles note selection
  // Renders NoteEditor when note selected
}

NoteEditor Component:

interface NoteEditorProps {
  noteId: string
  onClose: () => void
}

function NoteEditor({ noteId, onClose }: NoteEditorProps) {
  // Loads note content
  // Provides editable title input
  // Provides editable content textarea
  // Handles save operation
}

Data Models

DynamoDB Table Schema

Table Configuration:

Record Types:

1. Forest Record:

PK: "us-east-1:12345678-1234-1234-1234-123456789abc"
SK: "FOREST"
forest: {
  "root": { "type": "folder", "children": ["note1", "folder1"] },
  "bin": { "type": "folder", "children": [] },
  "note1": { "type": "note" },
  "folder1": { "type": "folder", "children": [] }
}

2. Note Record:

PK: "us-east-1:12345678-1234-1234-1234-123456789abc"
SK: "NOTE#abc123xyz"
title: "My First Note"
content: "This is the content of my note."
createdAt: "2024-01-15T10:30:00.000Z"
updatedAt: "2024-01-15T14:45:00.000Z"

3. Folder Record:

PK: "us-east-1:12345678-1234-1234-1234-123456789abc"
SK: "FOLDER#def456uvw"
name: "Work Notes"

Access Patterns

1. Load Forest:

2. Load Note Metadata (Batch):

3. Load Single Note:

4. Create Note:

5. Update Note:

IAM Policy for User Isolation

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:PutItem",
        "dynamodb:UpdateItem",
        "dynamodb:BatchGetItem",
        "dynamodb:Query"
      ],
      "Resource": "arn:aws:dynamodb:*:*:table/NotesTable",
      "Condition": {
        "ForAllValues:StringEquals": {
          "dynamodb:LeadingKeys": ["${cognito-identity.amazonaws.com:sub}"]
        }
      }
    }
  ]
}

This policy ensures users can only access records where PK matches their Cognito identity.

Correctness Properties

A property is a characteristic or behavior that should hold true across all valid executions of a system—essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.

Property 1: All database operations use user’s Cognito identity as PK

For any database operation (read or write), the partition key (PK) must equal the authenticated user’s Cognito identity.

Validates: Requirements 4.3

Property 2: All database operations include sort key

For any database operation, the sort key (SK) must be present and follow the correct format (FOREST, NOTE#{id}, or FOLDER#{id}).

Validates: Requirements 4.2

Property 3: Forest contains unique node IDs

For any forest structure, all node IDs (keys in the forest object) must be unique.

Validates: Requirements 5.2

Property 4: Forest always contains root and bin

For any forest structure, it must contain both “root” and “bin” as top-level keys.

Validates: Requirements 5.3

Property 5: Folder nodes have children array

For any forest node with type=“folder”, the node must have a “children” property that is an array.

Validates: Requirements 5.4

Property 6: Forest nodes have correct type field

For any forest node, if it represents a note, its type must be “note”; if it represents a folder, its type must be “folder”.

Validates: Requirements 5.5, 5.6

Property 7: Node IDs follow nanoid format

For any generated node ID (for notes or folders), the ID must match the nanoid format (URL-safe characters, appropriate length).

Validates: Requirements 5.7, 9.1

Property 8: Note records use correct SK format

For any note record in DynamoDB, the sort key must match the pattern “NOTE#{id}” where {id} is the note’s nanoid.

Validates: Requirements 6.1, 9.2

Property 9: Created notes have all required fields

For any newly created note, the note record must contain all required fields: id, title, content, createdAt, and updatedAt.

Validates: Requirements 6.2, 6.3, 6.4, 6.5

Property 10: Note updates increment timestamp

For any note update operation, the updatedAt timestamp in the saved record must be greater than or equal to the previous updatedAt value.

Validates: Requirements 6.6, 13.2

Property 11: Folder records use correct SK format

For any folder record in DynamoDB, the sort key must match the pattern “FOLDER#{id}” where {id} is the folder’s nanoid.

Validates: Requirements 7.1

Property 12: Created folders have name field

For any newly created folder, the folder record must contain a name field.

Validates: Requirements 7.2

Property 13: Note creation is atomic

For any note creation operation, either both the note record and forest update succeed, or both fail (no partial state).

Validates: Requirements 9.4, 9.5

Property 14: Note creation updates forest

For any successfully created note, the note’s ID must appear in the forest structure.

Validates: Requirements 9.3

Property 15: Empty note titles display placeholder

For any note with an empty or whitespace-only title, the displayed title must be “[no title]”.

Validates: Requirements 10.3

Property 16: Displayed notes are clickable

For any note displayed in the forest view, the note title must have an associated click handler.

Validates: Requirements 10.5

Property 17: Loaded notes display all content

For any loaded note, both the title and content must be visible in the rendered output.

Validates: Requirements 11.2, 11.3

Property 18: Note save round-trip preserves changes

For any note with modified title or content, after saving and reloading the note, the title and content must match the saved values.

Validates: Requirements 13.3, 13.4

Property 19: Database errors display user-friendly messages

For any failed DynamoDB operation, the application must display a user-friendly error message to the user.

Validates: Requirements 14.1

Property 20: Errors are logged to console

For any error that occurs in the application, the error must be logged to the browser console using console.error.

Validates: Requirements 14.4

Error Handling

Authentication Errors

Unauthenticated Access:

Session Expiration:

Network Errors:

Database Errors

Read Failures:

Write Failures:

Transaction Conflicts:

Data Validation Errors

Invalid Forest Structure:

Missing Note Data:

Malformed Data:

UI Error States

Loading States:

Error Display:

Graceful Degradation:

Testing Strategy

Overview

The testing strategy employs a dual approach combining unit tests for specific examples and edge cases with property-based tests for universal correctness properties. This ensures both concrete behavior validation and comprehensive input coverage.

Property-Based Testing

Framework: fast-check (JavaScript/TypeScript property-based testing library)

Configuration:

Property Test Coverage:

Each correctness property listed in the Correctness Properties section must be implemented as a property-based test. Property tests will:

Example Property Test Structure:

import fc from 'fast-check';

// Feature: note-taking-app, Property 3: Forest contains unique node IDs
test('forest node IDs are unique', () => {
  fc.assert(
    fc.property(
      forestGenerator(), // Custom generator for valid forests
      (forest) => {
        const nodeIds = Object.keys(forest);
        const uniqueIds = new Set(nodeIds);
        expect(uniqueIds.size).toBe(nodeIds.length);
      }
    ),
    { numRuns: 100 }
  );
});

Custom Generators:

The test suite will include custom generators for:

Unit Testing

Framework: Vitest (fast unit test framework for Vite projects)

Unit Test Focus:

Unit tests complement property tests by covering:

Test Categories:

1. Component Tests:

2. Data Layer Tests:

3. Error Handling Tests:

4. Edge Case Tests:

Example Unit Test:

import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import { NoteEditor } from './NoteEditor';

describe('NoteEditor', () => {
  it('displays [no title] for empty title', () => {
    const note = { id: '123', title: '', content: 'test', createdAt: '...', updatedAt: '...' };
    render(<NoteEditor note={note} />);
    expect(screen.getByText('[no title]')).toBeInTheDocument();
  });
});

Integration Testing

Scope: Integration tests verify end-to-end flows without mocking internal components.

Key Integration Tests:

Mocking Strategy:

Test Organization

src/
  components/
    App.test.tsx
    ForestView.test.tsx
    NoteEditor.test.tsx
  services/
    forestManager.test.tsx
    noteManager.test.tsx
    folderManager.test.tsx
  properties/
    forest.properties.test.tsx
    note.properties.test.tsx
    database.properties.test.tsx
  integration/
    noteCreation.integration.test.tsx
    noteEditing.integration.test.tsx

Test Execution

Development:

CI/CD:

Testing Priorities

High Priority (Must Test):

  1. Data integrity properties (atomicity, user isolation)
  2. Forest structure correctness
  3. Note CRUD operations
  4. Authentication and authorization
  5. Error handling and recovery

Medium Priority (Should Test):

  1. UI component rendering
  2. User interactions (clicks, input changes)
  3. Loading states
  4. Edge cases

Low Priority (Nice to Test):

  1. Performance characteristics
  2. Accessibility features
  3. Browser compatibility

Continuous Testing

Edit Page Create Sub Page Delete Page