{"id":27690447,"url":"https://github.com/fluent-cms/fluent-cms","last_synced_at":"2025-04-25T11:02:04.013Z","repository":{"id":245122106,"uuid":"816308933","full_name":"FormCms/FormCms","owner":"FormCms","description":"Open-source headless CMS built with ASP.NET Core (C#) and React, featuring REST APIs, GraphQL, and a GrapesJS page designer.","archived":false,"fork":false,"pushed_at":"2025-04-16T20:46:05.000Z","size":143350,"stargazers_count":185,"open_issues_count":0,"forks_count":15,"subscribers_count":5,"default_branch":"main","last_synced_at":"2025-04-19T10:15:52.932Z","etag":null,"topics":["asp-net-core","asp-net-core-mvc","asp-net-core-web-api","cms","csharp","drag-and-drop","entity-framework-core","grapesjs","graphql","graphql-server","handlebars","headless","headless-cms","hooks","open-source","react","restfull-api","wysiwyg-html-editor"],"latest_commit_sha":null,"homepage":"https://fluent-cms-admin.azurewebsites.net/","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/FormCms.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-06-17T13:30:37.000Z","updated_at":"2025-04-17T23:44:15.000Z","dependencies_parsed_at":"2024-10-26T03:38:47.358Z","dependency_job_id":"57af583e-dd52-488a-81e4-6966f3f088a6","html_url":"https://github.com/FormCms/FormCms","commit_stats":null,"previous_names":["jaikechen/fluent-cms","formcms/formcms","fluent-cms/fluent-cms"],"tags_count":27,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/FormCms%2FFormCms","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/FormCms%2FFormCms/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/FormCms%2FFormCms/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/FormCms%2FFormCms/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/FormCms","download_url":"https://codeload.github.com/FormCms/FormCms/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250805548,"owners_count":21490183,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["asp-net-core","asp-net-core-mvc","asp-net-core-web-api","cms","csharp","drag-and-drop","entity-framework-core","grapesjs","graphql","graphql-server","handlebars","headless","headless-cms","hooks","open-source","react","restfull-api","wysiwyg-html-editor"],"created_at":"2025-04-25T11:00:54.394Z","updated_at":"2025-04-25T11:02:03.983Z","avatar_url":"https://github.com/FormCms.png","language":"C#","readme":"# FormCMS, powered by Asp.net Core(c#) and React, featuring Rest APIs, GraphQL and Grapes.js Page designer.\n\nWelcome to [FormCMS](https://github.com/FormCMS/FormCMS)! 🚀  \n[![GitHub stars](https://img.shields.io/github/stars/FormCMS/FormCMS.svg?style=social\u0026label=Star)](https://github.com/FormCMS/FormCMS/stargazers)\n\nOur mission is to make **data modeling**, **backend development**, and **frontend development** as simple and intuitive as filling out a **form** 📋  \nWe’d love for you to contribute to FormCMS! Check out our [CONTRIBUTING guide](https://github.com/formcms/formcms/blob/main/CONTRIBUTING.md) to get started.  \nLove FormCMS? Show your support by giving us a ⭐ on GitHub and help us grow! 🌟  \nHave suggestions? Connect with us on Reddit! https://www.reddit.com/r/FormCMS/\n\n\n---\n## What is FormCMS?\n\n**FormCMS** is an open-source Content Management System designed to streamline and accelerate web development workflows. Ideal for CMS projects and general web applications, it minimizes repetitive REST/GraphQL API development while offering powerful tools for data management, page design, and user interaction.\n\n### Key Features\n\n1. **Data Modeling and CRUD with RESTful APIs**  \n   FormCMS provides robust data modeling capabilities and built-in RESTful APIs for Create, Read, Update, and Delete (CRUD) operations. These are complemented by a React-based admin panel, enabling efficient and intuitive data management for developers and content creators.\n\n2. **GraphQL Queries and Grapes.js Page Designer**  \n   Leverage GraphQL to fetch multiple related entities in a single query, boosting client-side performance, security, and flexibility. Additionally, the integrated [Grapes.js](https://grapesjs.com/) page designer, powered by [Handlebars](https://handlebarsjs.com/), allows for effortless creation of dynamic, data-bound pages, streamlining the design process.\n\n3. **User Social Activity**  \n   FormCMS now includes user engagement features, allowing users to like, share, and save content, with the system tracking view counts. Users can access their interaction history, liked posts, and saved posts through a dedicated user portal, enhancing interactivity and personalization.\n\n\n\n---\n## New CMS? — Data Modeling\n\n### Data Modeling in Current CMS Solutions\n\nMost CMS solutions support entity customization and adding custom properties, but they implement these changes in three distinct ways:\n\n1. **Denormalized Key-Value Storage**: Custom properties are stored in a table with columns like ContentItemId, Key, and Value.\n2. **JSON Data Storage**: Some CMS platforms store custom properties as JSON data in a document database, while others use relational databases.\n3. **Manually Created C# Classes**: Writing code adds custom properties to create classes that the system uses with Entity Framework.\n\n#### The Pros and Cons:\n- **Key-Value Storage**: This approach offers flexibility but suffers from performance inefficiencies and lacks relational integrity.\n- **Document Database**: Storing data as documents lacks a structured format and makes data integrity harder to enforce.\n- **C# Classes**: While my preferred method, it lacks flexibility. Any minor changes require rebuilding and redeploying the system.\n\n### Data Modeling with FormCMS\n\nIn contrast, FormCMS adopts a normalized, structured data approach, where each property is mapped to a corresponding table field:\n\n1. **Maximized Relational Database Functionality**: By leveraging indexing and constraints, FormCMS enhances performance and ensures data integrity.\n2. **Data Accessibility**: This model allows for easy data integration with other applications, Entity Framework, or even non-C# languages.\n3. **Support for Relationships**: FormCMS enables complex relationships (many-to-one, one-to-many, many-to-many), making it easy to provide GraphQL Query out of the box and provide more advanced querying capabilities.\n\n---\n\n## New CMS? — GraphQL Issues\n\n### Key Challenges\n\n1. **Security \u0026 Over-Fetching** – Complex or poorly optimized queries can overload the backend, exposing vulnerabilities and impacting performance.\n2. **Caching Limitations** – GraphQL lacks built-in CDN caching, making performance optimization harder.\n3. **N+1 Query Problem** – Individual resolver calls can lead to inefficient database queries.\n\n---\n\n### Solution: Persisted Queries with GET Requests\n\nMany GraphQL frameworks support persisted queries with GET requests, enabling caching and improved performance.\n\n---\n\n### How FormCMS Solves These Issues\n\nFormCMS automatically saves GraphQL queries and converts them into RESTful GET requests. For example:\n\n```graphql\nquery TeacherQuery($id: Int) {\n  teacherList(idSet: [$id]) {\n    id firstname lastname\n    skills { id name }\n  }\n}\n```\n\nbecomes `GET /api/queries/TeacherQuery`.\n\n- **Security \u0026 Efficiency** – Only Admins can define GraphQL queries, preventing abuse. Backend and frontend teams optimize queries to avoid excessive data requests.\n- **Caching** – GET requests enable efficient CDN caching, while ASP.NET Core’s hybrid cache further boosts performance.\n- **Performance** – Related entities are retrieved in a single optimized query, avoiding the N+1 problem.\n\nBy transforming GraphQL into optimized REST-like queries, FormCMS ensures a secure, efficient, and scalable API experience.\n\n---\n## Online Course System Demo\n\n### Live Demo\n- **Public Site:** [fluent-cms-admin.azurewebsites.net](https://fluent-cms-admin.azurewebsites.net/)\n- **Admin Panel:** [fluent-cms-admin.azurewebsites.net/admin](https://fluent-cms-admin.azurewebsites.net/admin)\n  - **Email:** `admin@cms.com`\n  - **Password:** `Admin1!`\n\n### Additional Resources\n- **GraphQL Playground:** [fluent-cms-admin.azurewebsites.net/graph](https://fluent-cms-admin.azurewebsites.net/graph)\n- **Documentation:** [fluent-cms-admin.azurewebsites.net/doc/index.html](https://fluent-cms-admin.azurewebsites.net/doc/index.html)  \n\n### Examples Source Code\nexample code can be found at /formCMS/examples\n\n- for Sqlite: run the SqliteDemo project\n- for SqlServer: run the SqlServerDemo/SqlServerAppHost project\n- for PostgreSQL : run the PostgresDemo/PostgresAppHost project\n\nDefult login:  \n  - Eamil : `samdmin@cms.com`  \n  - Password: `Admin1!`  \n\nAfter login to `Admin Panel`, you can go to `Tasks`, click `Import Demo Data`, to import demo data.\n\n---\n## Online Course System Backend\n\n\u003cdetails\u003e \n\u003csummary\u003e \nThis section provides detailed guidance on developing a foundational online course system, encompassing key entities: `teacher`, `course`, `lesson`,`skill`, and `material`.\n\u003c/summary\u003e\n\n### Database Schema\n\n#### 1. **Teachers Table**\nThe `Teachers` table maintains information about instructors, including their personal and professional details.\n\n| **Field**        | **Header**       | **Data Type** |\n|-------------------|------------------|---------------|\n| `id`             | ID               | Int           |\n| `firstname`      | First Name       | String        |\n| `lastname`       | Last Name        | String        |\n| `email`          | Email            | String        |\n| `phone_number`   | Phone Number     | String        |\n| `image`          | Image            | String        |\n| `bio`            | Bio              | Text          |\n\n#### 2. **Courses Table**\nThe `Courses` table captures the details of educational offerings, including their scope, duration, and prerequisites.\n\n| **Field**        | **Header**       | **Data Type** |\n|-------------------|------------------|---------------|\n| `id`             | ID               | Int           |\n| `name`           | Course Name      | String        |\n| `status`         | Status           | String        |\n| `level`          | Level            | String        |\n| `summary`        | Summary          | String        |\n| `image`          | Image            | String        |\n| `desc`           | Description      | Text          |\n| `duration`       | Duration         | String        |\n| `start_date`     | Start Date       | Datetime      |\n\n#### 3. **Lessons Table**\nThe `Lessons` table contains detailed information about the lessons within a course, including their title, content, and associated teacher.\n\n| **Field**        | **Header**        | **Data Type** |\n|-------------------|-------------------|---------------|\n| `id`             | ID                | Int           |\n| `name`           | Lesson Name       | String        |\n| `description`    | Description       | Text          |\n| `teacher`     | Teacher           | Int (Foreign Key) |\n| `course`      | Course            | Int (Foreign Key) |\n| `created_at`     | Created At        | Datetime      |\n| `updated_at`     | Updated At        | Datetime      | \n\n\n#### 4. **Skills Table**\nThe `Skills` table records competencies attributed to teachers.\n\n| **Field**        | **Header**       | **Data Type** |\n|-------------------|------------------|---------------|\n| `id`             | ID               | Int           |\n| `name`           | Skill Name       | String        |\n| `years`          | Years of Experience | Int      |\n| `created_by`     | Created By       | String        |\n| `created_at`     | Created At       | Datetime      |\n| `updated_at`     | Updated At       | Datetime      |\n\n#### 5. **Materials Table**\nThe `Materials` table inventories resources linked to courses.\n\n| **Field**        | **Header**  | **Data Type** |\n|-------------------|-------------|---------------|\n| `id`             | ID          | Int           |\n| `name`           | Name        | String        |\n| `type`           | Type        | String        |\n| `image`          | Image       | String        |\n| `link`           | Link        | String        |\n| `file`           | File        | String        |\n\n---\n\n### Relationships\n- **Courses to Teachers**: Man-to-One(Each teacher can teach multiple courses; each course is assigned to one teacher. A teacher can exist independently of a course).\n- **Teachers to Skills**: Many-to-Many (Multiple teachers can share skills, and one teacher may have multiple skills).\n- **Courses to Materials**: Many-to-Many (A course may include multiple materials, and the same material can be used in different courses).\n- **Courses to Lessons**: One-to-Many (Each course can have multiple lessons, but each lesson belongs to one course. A lesson cannot exist without a course, as it has no meaning on its own).\n\n---\n\n### Schema Creation via FormCMS Schema Builder\n\n#### Accessing Schema Builder\nAfter launching the web application, locate the **Schema Builder** menu on the homepage to start defining your schema.\n\n#### Adding Entities\n[Example Configuration](https://fluent-cms-admin.azurewebsites.net/_content/FormCMS/schema-ui/list.html?schema=entity)  \n1. Navigate to the **Entities** section of the Schema Builder.\n2. Create entities such as \"Teacher\" and \"Course.\"\n3. For the `Course` entity, add attributes such as `name`, `status`, `level`, and `description`.\n---\n### Defining Relationships\n[Example Configuration](https://fluent-cms-admin.azurewebsites.net/_content/FormCMS/schema-ui/edit.html?schema=entity\u0026id=27)  \n\n#### 1. **Course and Teacher (Many-to-One Relationship)**\nTo establish a many-to-one relationship between the `Course` and `Teacher` entities, you can include a `Lookup` attribute in the `Course` entity. This allows selecting a single `Teacher` record when adding or updating a `Course`.\n\n| **Attribute**   | **Value**    |\n|-----------------|--------------|\n| **Field**       | `teacher`    |\n| **DataType**    | Lookup       |\n| **DisplayType** | Lookup       |\n| **Options**     | Teacher      |\n\n**Description:** When a course is created or modified, a teacher record can be looked up and linked to the course.\n\n#### 2 **Course and Lesson(One-to-Many Relationship)**\nTo establish a one-to-many relationship between the `Course` and `Lesson` entities, use a `Collection` attribute in the `Course` entity. This enables associating multiple lessons with a single course.\n\n| **Attribute**   | **Value**  |\n|-----------------|------------|\n| **Field**       | `lessons`  |\n| **DataType**    | Collection |\n| **DisplayType** | EditTable  |\n| **Options**     | Lesson     |\n\n**Description:** When managing a course , you can manage lessons of this course.\n\n#### 3. **Course and Materials (Many-to-Many Relationship)**\nTo establish a many-to-many relationship between the `Course` and `Material` entities, use a `Junction` attribute in the `Course` entity. This enables associating multiple materials with a single course.\n\n| **Attribute** | **Value**   |\n|---------------|-------------|\n| **Field**     | `materials` |\n| **DataType**  | Junction    |\n| **DisplayType** | Picklist    |\n| **Options**   | Material    |\n\n**Description:** When managing a course, you can select multiple material records from the `Material` table to associate with the course.\n\n\n\u003c/details\u003e\n\n\n\n---\n\n## **Admin Panel**\n\u003cdetails\u003e  \n\u003csummary\u003e  \nThe last chapter introduced how to model entities, this chapter introduction how to use Admin-Panel to manage data of those entities.\n\u003c/summary\u003e  \n\n### **Display Types**\nThe Admin Panel supports various UI controls to display attributes:\n\n- `\"text\"`: Single-line text input.\n- `\"textarea\"`: Multi-line text input.\n- `\"editor\"`: Rich text input.\n- `\"dictionary\"`: Key-Value pairs\n\n- `\"number\"`: Single-line text input for numeric values only.\n- `\"localDatetime\"`: Datetime picker for date and time inputs, displayed as the browser's timezone.\n- `\"datetime\"`: Datetime picker for date and time inputs.\n- `\"date\"`: Date picker for date-only inputs.\n\n- `\"image\"`: Upload a single image, storing the image URL.\n- `\"gallery\"`: Upload multiple images, storing their URLs.\n- `\"file\"`: Upload a file, storing the file URL.\n\n- `\"dropdown\"`: Select an item from a predefined list.\n- `\"multiselect\"`: Select multiple items from a predefined list.\n\n- `\"lookup\"`: Select an item from another entity with a many-to-one relationship (requires `Lookup` data type).\n- `\"treeSelect\"`: Select an item from another entity with a many-to-one relationship (requires `Lookup` data type), items are organized as tree.\n\n- `\"picklist\"`: Select multiple items from another entity with a many-to-many relationship (requires `Junction` data type).\n- `\"tree\"`: Select multiple items from another entity with a many-to-many relationship (requires `Junction` data type), items are organized as a tree.\n- `\"edittable\"`: Manage items of a one-to-many sub-entity (requires `Collection` data type).  \n\n\n---\n[See this example how to configure entity `category`, so it's item can be organized as tree.](https://fluent-cms-admin.azurewebsites.net/_content/FormCMS/schema-ui/edit.html?schema=entity\u0026id=103)\n### **DataType to DisplayType Mapping Table**\nBelow is a mapping of valid `DataType` and `DisplayType` combinations:\n\n| **DataType**  | **DisplayType** | **Description**                               |\n|---------------|-----------------|-----------------------------------------------|\n| Int           | Number          | Input for integers.                           |\n| Datetime      | LocalDatetime   | Datetime picker for local datetime.           |\n| Datetime      | Datetime        | Datetime picker for date and time inputs.     |\n| Datetime      | Date            | Date picker for date-only inputs.             |\n| String        | Number          | Input for numeric values.                     |\n| String        | Datetime        | Datetime picker for date and time inputs.     |\n| String        | Date            | Date picker for date-only inputs.             |\n| String        | Text            | Single-line text input.                       |\n| String        | Textarea        | Multi-line text input.                        |\n| String        | Image           | Single image upload.                          |\n| String        | Gallery         | Multiple image uploads.                       |\n| String        | File            | File upload.                                  |\n| String        | Dropdown        | Select an item from a predefined list.        |\n| String        | Multiselect     | Select multiple items from a predefined list. |\n| Text          | Multiselect     | Select multiple items from a predefined list. |\n| Text          | Gallery         | Multiple image uploads.                       |\n| Text          | Textarea        | Multi-line text input.                        |\n| Text          | Editor          | Rich text editor.                             |\n| Text          | Dictionary      | Key-Value Pair                                |\n| Lookup        | Lookup          | Select an item from another entity.           |\n| Lookup        | TreeSelect      | Select an item from another entity.           |\n| Junction      | Picklist        | Select multiple items from another entity.    |\n| Lookup        | Tree            | Select multiple items from another entity.    |\n| Collection    | EditTable       | Manage items of a sub-entity.                 |  \n\n---\n\n### **List Page**\n[Example Course List Page](https://fluent-cms-admin.azurewebsites.net/_content/FormCMS/admin/entities/course?offset=0\u0026limit=20)\n\nThe **List Page** displays entities in a tabular format, supporting sorting, searching, and pagination for efficient browsing or locating of specific records.\n\n\n#### **Sorting**\nSort records by clicking the `↑` or `↓` icon in the table header.\n- [Order by Created At Example](https://fluent-cms-admin.azurewebsites.net/_content/FormCMS/admin/entities/course?offset=0\u0026limit=20\u0026sort[created_at]=-1)\n- [Order by Name Example](https://fluent-cms-admin.azurewebsites.net/_content/FormCMS/admin/entities/course?offset=0\u0026limit=20\u0026sort[name]=1)\n\n#### **Filtering**\nApply filters by clicking the Funnel icon in the table header.\n\n- [Filter by Created At (2024-09-07)](https://fluent-cms-admin.azurewebsites.net/_content/FormCMS/admin/entities/course?offset=0\u0026limit=20\u0026created_at[dateIs]=2024-09-07\u0026sort[created_at]=1)\n- [Filter by Course Name (Starts with A or C)](https://fluent-cms-admin.azurewebsites.net/_content/FormCMS/admin/entities/course?offset=0\u0026limit=20\u0026name[operator]=or\u0026name[startsWith]=A\u0026name[startsWith]=C\u0026sort[created_at]=1)\n\n#### **Duplicate**\nClicking the duplicate button opens the \"Add New Data\" page with prefilled values from the selected record for quick entry.\n \n---\n\n### **Detail Page**  \nDetail page provides an interface to manage single record.  \n\n#### Example of display types `date`,`image`, `gallery`, `muliselect`, `dropdown`,\n[Lesson Detail Page](https://fluent-cms-admin.azurewebsites.net/_content/FormCMS/admin/entities/lesson/6?ref=https%3A%2F%2Ffluent-cms-admin.azurewebsites.net%2F_content%FormCMS%2Fadmin%2Fentities%2Fcourse%2F27%3Fref%3Dhttps%253A%252F%252Ffluent-cms-admin.azurewebsites.net%252F_content%FormCMS%252Fadmin%252Fentities%252Fcourse%253Foffset%253D0%2526limit%253D20).\n\n#### Example of `lookup`,`picklist`,`edittable`\n[Course Detail Page](https://fluent-cms-admin.azurewebsites.net/_content/FormCMS/admin/entities/course/22)\n\n\u003c/details\u003e  \n\n\n\n\n\n---\n\n## Publish / Preview Content\n\u003cdetails\u003e\n\u003csummary\u003e\nThis feature allows content creators to plan and organize their work, saving drafts for later completion.\n\u003c/summary\u003e\n\n### Content Publication Status\nContent can have one of the following publication statuses:\n- **`draft`**\n- **`scheduled`**\n- **`published`**\n- **`unpublished`**\n\nOnly content with the status **`published`** can be retrieved through GraphQL queries.\n\n---\n\n### Setting Default Publication Status in the Schema Builder\nWhen defining an entity in the Schema Builder, you can configure its default publication status as either **`draft`** or **`published`**.\n\n---\n\n### Managing Publication Status in the Admin Panel\nOn the content edit page, you can:\n- **Publish**: Make content immediately available.\n- **Unpublish**: Remove content from public view.\n- **Schedule**: Set a specific date and time for the content to be published.\n\n---\n### Preview Draft/Scheduled/Unpublished Content\n\nBy default, only published content appears in query results. \nIf you want to preview how the content looks on a page before publishing, you can add the query parameter `preview=1` to the page URL.\n\nFor a more convenient approach, you can set the **Preview URL** in the **Entity Settings** page. \n[Example Entity Settings Page](https://fluent-cms-admin.azurewebsites.net/_content/FormCMS/schema-ui/edit.html?schema=entity\u0026id=27)\n\nOnce set, you can navigate to the **Entity Management** page and simply click the **Preview** button to view the content in preview mode.\n[Example Content Manage Page](https://fluent-cms-admin.azurewebsites.net/_content/FormCMS/admin/entities/course/3?ref=https%3A%2F%2Ffluent-cms-admin.azurewebsites.net%2F_content%2FFormCMS%2Fadmin%2Fentities%2Fcourse%3Foffset%3D0%26limit%3D20#)\n---\n\n### Publication Worker\nThe **Publication Worker** automates the process of updating scheduled items in batches, transitioning them to the **`published`** status at the appropriate time.\n\n\u003c/details\u003e  \n\n\n\n\n---\n## Concurrent Update Protection\n\u003cdetails\u003e\n\u003csummary\u003e\nProtect user from dirty write(concurrent update)\n\u003c/summary\u003e\n\n###  How does it work?\n\nReturn the updated_at timestamp when fetching the item. When updating, compare the stored updated_at with the one in the request. If they differ, reject the update\n\n### When Is updatedAt Checked\n- During updates\n- During Deletions\n\nIf a concurrent modification is detected, the system will throw the following exception:  \n\"Error: Concurrent Write Detected. Someone else has modified this item since you last accessed it. Please refresh the data and try again.\"\n\n\u003c/details\u003e  \n\n\n\n\n---\n## Audit Logging\n\u003cdetails\u003e\n\u003csummary\u003e\nAudit logging in FormCMS helps maintain accountability and provides a historical record of modifications made to entities within the system. \n\u003c/summary\u003e\n\n###  Audit Log Entity\nAn audit log entry captures essential information about each modification. The entity structure includes the following fields:\n\n- **UserId** (*string*): The unique identifier of the user performing the action.\n- **UserName** (*string*): The name of the user.\n- **Action** (*ActionType*): The type of action (Create, update, Delete) performed. \n- **EntityName** (*string*): The name of the entity affected.\n- **RecordId** (*string*): The unique identifier of the record modified.\n- **RecordLabel** (*string*): A human-readable label for the record.\n- **Payload** (*Record*): The data associated with the action.\n- **CreatedAt** (*DateTime*): The timestamp when the action occurred.\n\n### When Is Audit Log Added\nAn audit log entry is created when a user performs any of the following actions:\n\n- **Creating** a new record.\n- **Updating** an existing record.\n- **Deleting** a record.\n### How to view Audit Log\nAudit logs can be accessed and searched by users with appropriate permissions. The following roles have access:\n\n- **Admin**\n- **Super Admin**\n\nThese users can:\n- View a list of audit logs.\n- Search logs by user, entity, or action type.\n- Filter logs by date range.\n\n### Benefits of Audit Logging\n- Ensures transparency and accountability.\n- Helps with troubleshooting and debugging.\n- Provides insights into system usage and modifications.\n- Supports compliance with regulatory requirements.\n\nBy maintaining a detailed audit trail, the System enhances security and operational efficiency, ensuring that all modifications are tracked and can be reviewed when necessary.\n\u003c/details\u003e  \n\n\n\n---\n\n## Asset Library\n\u003cdetails\u003e\n\u003csummary\u003e\nThe Asset Library centralizes the management of uploaded assets (e.g., images, files), supporting both local and cloud storage. It enables reuse, optimizes storage, and provides robust permissions and extensibility for various cloud providers.\n\u003c/summary\u003e\n### Overview\n\nAssets are stored in a repository, each identified by a unique `Path` (e.g., `2025-03/abc123`, where `2025-03` is a folder based on `yyyy-MM` and `abc123` is a ULID) and a fixed `Url` (e.g., `/files/2025-03/abc123` or a cloud-specific URL). Relationships to data entities are tracked via `AssetLink` records. The system supports local storage by default and integrates with cloud storage providers (e.g., Azure Cloud Storage) via the `IFileStore` interface. For images, uploads are resized to a default maximum width of 1200 pixels and a compression quality of 75, configurable in `SystemSettings`.\n\n---\n\n### Core Features\n\n#### Adding Assets to Data\nIn forms with `image`, `file`, or `gallery` fields, users can:   \n- **Upload**: Upload a new file via `IFileStore.Upload`. The system generates a unique `Path` (e.g., `2025-03/abc123`), sets a `Url` (local or cloud-based), and records metadata (`Name`, `Size`, `Type`, `CreatedBy`, `CreatedAt`). Images are resized (max width: 1200px, quality: 75) before saving to the chosen storage provider in the `2025-03` folder. A default `Title` is derived from `Name`.  \n  - **Choose**: Select an existing asset from a dialog with:      \n    - `Gallery View`: Thumbnails for images.    \n    - `List View`: Table with `Name`, `Title`, `Size`, `CreatedAt`, and `Type`. Filterable by keyword, size range, or date range; sortable in ascending/descending order.    \n    - Selection links the asset, incrementing `LinkCount` and adding an `AssetLink`.    \n- **Delete**: Remove the asset reference from the entity, reducing `LinkCount`.    \n- **Edit**: Update `Title` or metadata.    \n\n#### Managing Orphan Assets   \nThe **Asset List Page** lists assets with `Name`, `Title`, `Size`, `Type`, `CreatedAt`, and `LinkCount`. Assets with `LinkCount` of 0 (orphans) can be deleted via `IFileStore.Del`, removing them from storage (e.g., the `2025-03` folder) and the system.   \n\n#### Replacing Files   \nOn the **Asset Detail Page**, users can replace content:   \n- Upload a new file via `IFileStore.Upload` to the same `Path` (e.g., `2025-03/abc123`), updating `Size`, `Type`, and `UpdatedAt`.   \n- Images are resized per settings (max width: 1200px, quality: 75).   \n- `Path` and `Url` remain unchanged, ensuring continuity for linked entities.   \n\n#### Updating Metadata   \nOn the **Asset Detail Page**, users can modify:   \n- **Title**: Change the display name (defaults to `Name`).   \n- **Metadata**: Update key-value pairs (e.g., `{\"AltText\": \"Description\"}`), updating `UpdatedAt`.   \n\n---\n\n### Cloud Storage Integration   \n\nThe Asset Library supports cloud storage through the `IFileStore` interface, with Azure Cloud Storage as an example. Other providers (e.g., Google Cloud Storage, AWS S3) can be integrated by implementing this interface and registering it in the dependency injection container.   \n\n#### `IFileStore` Interface   \n```csharp   \nnamespace FormCMS.Infrastructure.FileStore;   \n\npublic record FileMetadata(long Size, string ContentType);\n\npublic interface IFileStore\n{\n    Task Upload(IEnumerable\u003c(string, IFormFile)\u003e files, CancellationToken ct);\n    Task Upload(string localPath, string path, CancellationToken ct);\n    Task\u003cFileMetadata?\u003e GetMetadata(string filePath, CancellationToken ct);\n    string GetUrl(string file);\n    Task Download(string path, string localPath, CancellationToken ct);\n    Task Del(string file, CancellationToken ct);\n}\n```\n\n#### Functionality   \n- **Upload**: Stores files in the provider, using the folder structure (e.g., `2025-03/abc123`).   \n- **GetMetadata**: Retrieves `Size` and `ContentType`.   \n- **GetUrl**: Returns the asset’s URL (e.g., `https://\u003caccount\u003e.blob.core.windows.net/\u003ccontainer\u003e/2025-03/abc123` for Azure).   \n- **Download**: Retrieves the file to a local path.   \n- **Del**: Deletes the file from its folder.   \n\n#### Extending to Other Providers   \nTo use Google Cloud Storage, AWS S3, or others:   \n1. Implement `IFileStore` with provider-specific logic (e.g., S3’s `PutObject` for uploads to `2025-03/abc123`).   \n2. Register it in dependency injection (e.g., `services.AddScoped\u003cIFileStore, AwsS3FileStore\u003e()`).   \n\n#### Example: Azure Cloud Storage   \n- Uploads files to Azure Blob Storage under the `2025-03` folder (e.g., `2025-03/abc123`).   \n- Generates URLs like `https://\u003caccount\u003e.blob.core.windows.net/\u003ccontainer\u003e/2025-03/abc123`.   \n- Supports metadata retrieval and deletion via Azure APIs.   \n\n---\n\n### Permissions   \n\nA role-based system controls asset access:   \n- **Super Admin**: Full control over all assets, including those in cloud folders (e.g., `2025-03`).   \n- **No Permissions**: No asset interaction.   \n- **Restricted Read**: Choose only self-uploaded assets.   \n- **Full Read**: Choose any asset.   \n- **Restricted Write**: Manage only self-uploaded assets.   \n- **Full Write**: Manage all assets (except assigning).   \n\nPermissions filter the library dialog and validate actions against ownership and storage location.   \n\n---\n\n### Configuration   \n\n- **Image Compression** (`ImageCompressionOptions`):   \n  - `MaxWidth`: Default 1200px.   \n  - `Quality`: Default 75 (0-100).   \n- **Asset URL**: Local prefix defaults to `/files` (e.g., `/files/2025-03/abc123`); cloud URLs depend on the provider (via `IFileStore.GetUrl`).   \n- **Storage Provider**: Set via dependency injection (e.g., Azure, local).   \n\n---\n\n### Benefits   \n\n- **Reuse**: Assets are shared across entities, reducing redundancy.   \n- **Storage**: Orphan deletion, image resizing, and folder-based organization (e.g., `2025-03`) optimize usage; cloud storage scales capacity.   \n- **Consistency**: Fixed `Url` ensures seamless updates.   \n- **Flexibility**: Metadata, replacements, and cloud extensibility adapt to needs.   \n- **Tracking**: `LinkCount` and `AssetLink` monitor usage.   \n- **SEO**: `Title` as alt text enhances image discoverability.   \n- **Scalability**: Cloud integration (e.g., Azure) and `IFileStore` support growing storage demands.   \n\u003c/details\u003e\n\n\n\n---\n## Date and Time\n\u003cdetails\u003e\n\u003csummary\u003e\nThe Date and Time system in FormCMS manages how dates and times are displayed and stored, supporting three distinct formats: `localDatetime`, `datetime`, and `date`. It ensures accurate representation across time zones and consistent storage in the database.\n\u003c/summary\u003e\n### Overview   \n\nFormCMS provides three display formats for handling date and time data, each serving a specific purpose:    \n    `localDatetime`: Displays dates and times adjusted to the user's browser time zone (e.g., a start time that varies by location).    \n    `datetime`: Zone-agnostic, showing the same date and time universally (e.g., a fixed event time).    \n    `date`: Zone-agnostic, displaying only the date without time (e.g., a birthday).    \n\n---\n\n### Display Formats\n\n#### `localDatetime`\n- **Purpose**: Represents a date and time that adjusts to the user's local time zone, ideal for events like start times where the local context matters.\n- **Behavior**: The system converts the stored UTC `datetime` to the browser's time zone for display. For example, an event starting at `2025-03-19 14:00 UTC` would appear as `2025-03-19 09:00 EST` for a user in New York (UTC-5) or `2025-03-19 23:00 JST` for a user in Tokyo (UTC+9).\n- **Storage**: When entered, the system converts the user’s local input to UTC before saving. For instance, `2025-03-19 09:00 EST` is stored as `2025-03-19 14:00 UTC`.\n- **Use Case**: Event schedules, deadlines, or anything requiring local time awareness.\n\n#### `datetime`\n- **Purpose**: Displays a fixed date and time that remains consistent regardless of the user’s time zone, suitable for universal reference points.\n- **Behavior**: No conversion occurs; the stored value is shown as-is. For example, `2025-03-19 14:00` is displayed as `2025-03-19 14:00` everywhere.\n- **Storage**: Saved exactly as input, without time zone adjustments, assuming it’s a universal time.\n- **Use Case**: Logs, publication timestamps, or system events where a single point in time applies globally.\n\n#### `date`\n- **Purpose**: Represents only a date without time, zone-agnostic, and consistent across all users.\n- **Behavior**: Displayed as a date only (e.g., `2025-03-19`), with no time component or zone consideration.\n- **Storage**: Stored as a `datetime` with the time set to `00:00:00` (midnight), typically in UTC for consistency, but the time portion is ignored in display.\n- **Use Case**: Birthdays, anniversaries, or any date-specific data where time is irrelevant.\n\n---\n\n### Storage in Database\n\n- **System-Generated Timestamps**: All automatically generated times (e.g., `CreatedAt`, `UpdatedAt`) are stored as UTC `datetime` values (e.g., `2025-03-19 14:00:00Z`). This ensures a universal reference point for auditing and synchronization.\n- **`localDatetime` Handling**:     \n  Input: Converted from the user’s local time (based on browser settings) to UTC before storage.    \n  Output: Converted from UTC back to the user’s local time zone for display.\n- **`datetime` Handling**: Stored and retrieved as entered, with no conversion, assuming it’s a fixed point in time.\n- **`date` Handling**: Stored as a `datetime` with the time component set to `00:00:00` (e.g., `2025-03-19 00:00:00Z`), though only the date part is used in display.\n\n---\n\n### Examples\n\n1. **Event Start (`localDatetime`)**:\n    - User in New York enters: `2025-03-19 09:00 EST`.\n    - Stored as: `2025-03-19 14:00:00Z` (UTC).\n    - Displayed in Tokyo: `2025-03-19 23:00 JST`.\n\n2. **Log Entry (`datetime`)**:\n    - Entered and stored as: `2025-03-19 14:00`.\n    - Displayed everywhere as: `2025-03-19 14:00`.\n\n3. **Birthday (`date`)**:\n    - Entered as: `2025-03-19`.\n    - Stored as: `2025-03-19 00:00:00Z`.\n    - Displayed as: `2025-03-19`.\n\n---\n\n### Benefits\n\n- **Consistency**: UTC storage for system times ensures reliable auditing and cross-time-zone integrity.\n- **Flexibility**: `localDatetime` adapts to user locations, while `datetime` and `date` provide universal clarity.\n- **Simplicity**: Clear separation of use cases reduces confusion in data entry and display.\n- **Scalability**: Standardized UTC storage supports global applications without time zone conflicts.\n\n\u003c/details\u003e\n\n\n\n---\n\n## Export and Import Data\n\u003cdetails\u003e\n\u003csummary\u003e\nThis feature allows you to export or import data\n\u003c/summary\u003e\n\nThis feature is helpful for the following scenarios:  \n1. Migrating data from one server to another, or even between different types of databases.   \n2. Backing up your data.   \n3. Cleaning data by exporting only the latest schema, excluding audit log data.   \n\n### Exporting Data\n1. Log in to the 'Admin Panel' and navigate to `Tasks`.\n2. Click `Add Export Task`.\n3. Wait a few minutes, then refresh the page. Once the task is complete, you can download the exported zip file.\n\n### Importing Data\n1. Log in to the 'Admin Panel' and go to `Tasks`.\n2. Click `Add Import Task`, then select the zip file you wish to import.\n3. Wait a few minutes, then refresh the page to check if the task was successful.\n\n\u003c/details\u003e  \n\n\n\n\n\n---\n\n## Customizing the Admin Panel\n\u003cdetails\u003e  \n\u003csummary\u003e  \nFormCms' modular component structure makes it easy to modify UI text, replace components, or swap entire pages.  \n\u003c/summary\u003e  \n\n### FormCmsAdminSdk and FormCmsAdminApp\n\nThe FormCms Admin Panel is built with React and split into two projects:\n\n- **[FormCmsAdminSdk](https://github.com/FormCms/FormCmsAdminSdk)**  \n  This SDK handles backend interactions and complex state management. It is intended to be a submodule of your own React App. It follows a minimalist approach, relying only on:\n  - `\"react\"`, `\"react-dom\"`, `\"react-router-dom\"`: Essential React and routing dependencies.\n  - `\"axios\"` and `\"swr\"`: For API access and state management.\n  - `\"qs\"`: Converts query objects to strings.\n  - `\"react-hook-form\"`: Manages form inputs.\n\n- **[FormCmsAdminApp](https://github.com/FormCms/FormCmsAdminApp)**  \n  A demo implementation showing how to build a React app with the FormCmsAdminSdk. Fork this project to customize the layout, UI text, or add features.\n\n### Why is FormCmsAdminSdk a Submodule Instead of an NPM package?\n\nA **Git submodule** embeds an external repository (e.g., [FormCmsAdminSdk](https://github.com/FormCms/FormCmsAdminSdk)) as a subdirectory in your project. Unlike NPM packages, which deliver bundled code, submodules provide the full, readable source, pinned to a specific commit. This offers flexibility for customization, debugging, or upgrading the SDK directly in your repository.\n\nTo update a submodule:\n```\ngit submodule update --remote\n```  \nThen commit the updated reference in your parent repository.\n\n### Setting Up Your Custom AdminPanelApp\n\nTo create a custom AdminPanelApp with submodules, start with the example repo [FormCmsAdminApp](https://github.com/FormCms/FormCmsAdminApp):\n```\ngit clone --recurse-submodules https://github.com/FormCms/FormCmsAdminApp.git\n```  \nThe `--recurse-submodules` flag ensures the SDK submodule is cloned alongside the main repo.\n```\ncd FormCmsAdminApp\npnpm install\n```  \nStart the formCms backend, you might need to modify .env.development, change the Api url to your backend.\n```\nVITE_REACT_APP_API_URL='http://127.0.0.1:5000/api'\n```  \nStart the React App\n```\npnpm dev\n```\n\n### Deploying Your Customized Admin Panel\n\nAfter customizing, build your app:\n```\npnpm build\n```  \nCopy the contents of the `dist` folder to `\u003cyour backend project\u003e\\wwwroot\\admin` to replace the default Admin Panel.\n\n### Customizing Layout and Logo\n\nThe SDK ([FormCmsAdminSdk](https://github.com/FormCms/FormCmsAdminSdk)) includes an integrated router and provides three hooks for menu items:\n- `useEntityMenuItems`\n- `useAssetMenuItems`\n- `useSystemMenuItems`\n\nUse these to design your app’s layout and update the logo within this structure.\n\n### Modifying Page Text\n\nEach page (a root-level component tied to the router) can use a corresponding `use***Page` hook from the SDK. These hooks handle state and API calls, returning components for your UI.\n\nTo customize text:\n- Pass specific prompts and labels via the `pageConfig` argument in the hook.\n- For text shared across pages, use the `componentConfig` argument.\n\n### Swapping UI Components\n\nReplace table columns, input fields, or other UI components with your custom versions as needed.\n\u003c/details\u003e  \n\n---\n## **GraphQL Query**\n\n\u003cdetails\u003e\n\u003csummary\u003e\nFormCMS simplifies frontend development by offering robust GraphQL support.\n\u003c/summary\u003e\n\n### Getting Started\n#### Accessing the GraphQL IDE\nTo get started, launch the web application and navigate to `/graph`. You can also try our [online demo](https://fluent-cms-admin.azurewebsites.net/graph).\n\n---\n#### Singular vs. List Response\nFor each entity in FormCMS, two GraphQL fields are automatically generated:  \n- `\u003centityName\u003e`: Returns a record.\n- `\u003centityNameList\u003e`: Returns a list of records.  \n\n**Single Course **\n```graphql\n{\n  course {\n    id\n    name\n  }\n}\n```\n[Try it here](https://fluent-cms-admin.azurewebsites.net/graph?query=%7B%0A%20%20course%7B%0A%20%20%20%20id%2C%0A%20%20%20%20name%0A%20%20%7D%0A%7D%0A)\n\n**List of Courses **\n```graphql\n{\n  courseList {\n    id\n    name\n  }\n}\n```\n[Try it here](https://fluent-cms-admin.azurewebsites.net/graph?query=%7B%0A%20%20courseList%7B%0A%20%20%20%20id%2C%0A%20%20%20%20name%0A%20%20%7D%0A%7D%0A)\n\n---\n#### Field Selection\nYou can query specific fields for both the current entity and its related entities.\n**Example Query:**\n```graphql\n{\n  courseList{\n    id\n    name\n    teacher{\n      id\n      firstname\n      lastname\n      skills{\n        id\n        name\n      }\n    }\n    materials{\n      id,\n      name\n    }\n  }\n}\n```\n[Try it here](https://fluent-cms-admin.azurewebsites.net/graph?query=%7B%0A%20%20courseList%7B%0A%20%20%20%20id%0A%20%20%20%20name%0A%20%20%20%20teacher%7B%0A%20%20%20%20%20%20id%0A%20%20%20%20%20%20firstname%0A%20%20%20%20%20%20lastname%0A%20%20%20%20%20%20skills%7B%0A%20%20%20%20%20%20%20%20id%0A%20%20%20%20%20%20%20%20name%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%20%20materials%7B%0A%20%20%20%20%20%20id%2C%0A%20%20%20%20%20%20name%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A)\n\n---\n#### Filtering with `Value Match` in FormCMS\n\nFormCMS provides flexible filtering capabilities using the `idSet` field (or any other field), enabling precise data queries by matching either a single value or a list of values.\n\n**Filter by a Single Value Example:**\n```graphql\n{\n  courseList(idSet: 5) {\n    id\n    name\n  }\n}\n```\n[Try it here](https://fluent-cms-admin.azurewebsites.net/graph?query=%7B%0A%20%20courseList(idSet%3A5)%7B%0A%20%20%20%20id%2C%0A%20%20%20%20name%0A%20%20%7D%0A%7D%0A)\n\n**Filter by Multiple Values Example:**\n```graphql\n{\n  courseList(idSet: [5, 7]) {\n    id\n    name\n  }\n}\n```\n[Try it here](https://fluent-cms-admin.azurewebsites.net/graph?query=%7B%0A%20%20courseList(idSet%3A%5B5%2C7%5D)%7B%0A%20%20%20%20id%2C%0A%20%20%20%20name%0A%20%20%7D%0A%7D%0A)\n\n---\n#### Advanced Filtering with `Operator Match` in FormCMS\n\nFormCMS supports advanced filtering options with `Operator Match`, allowing users to combine various conditions for precise queries.\n\n##### `matchAll` Example:\nFilters where all specified conditions must be true.  \nIn this example: `id \u003e 5 and id \u003c 15`.\n\n```graphql\n{\n  courseList(id: {matchType: matchAll, gt: 5, lt: 15}) {\n    id\n    name\n  }\n}\n```\n[Try it here](https://fluent-cms-admin.azurewebsites.net/graph?query=%7B%0A%20%20courseList(id%3A%7BmatchType%3AmatchAll%2Cgt%3A5%2Clt%3A15%7D)%7B%0A%20%20%20%20id%2C%0A%20%20%20%20name%0A%20%20%7D%0A%7D%0A)\n\n##### `matchAny` Example:\nFilters where at least one of the conditions must be true.  \nIn this example: `name starts with \"A\"` or `name starts with \"I\"`.\n\n```graphql\n{\n  courseList(name: [{matchType: matchAny}, {startsWith: \"A\"}, {startsWith: \"I\"}]) {\n    id\n    name\n  }\n}\n```\n[Try it here](https://fluent-cms-admin.azurewebsites.net/graph?query=%7B%0A%20%20courseList(name%3A%5B%7BmatchType%3AmatchAny%7D%2C%20%7BstartsWith%3A%22A%22%7D%2C%7BstartsWith%3A%22I%22%7D%5D)%7B%0A%20%20%20%20id%2C%0A%20%20%20%20name%0A%20%20%7D%0A%7D%0A)\n\n\n---\n\n#### `Filter Expressions` in FormCMS\n\nFilter Expressions allow precise filtering by specifying a field, including nested fields using JSON path syntax. This enables filtering on subfields for complex data structures.\n\n***Example: Filter by Teacher's Last Name***\nThis query returns courses taught by a teacher whose last name is \"Circuit.\"\n\n```graphql\n{\n  courseList(filterExpr: {field: \"teacher.lastname\", clause: {equals: \"Circuit\"}}) {\n    id\n    name\n    teacher {\n      id\n      lastname\n    }\n  }\n}\n```\n[Try it here](https://fluent-cms-admin.azurewebsites.net/graph?query=%7B%0A%20%20courseList(filterExpr%3A%20%7Bfield%3A%20%22teacher.lastname%22%2C%20clause%3A%20%7Bequals%3A%20%22Circuit%22%7D%7D)%20%7B%0A%20%20%20%20id%0A%20%20%20%20name%0A%20%20%20%20teacher%20%7B%0A%20%20%20%20%20%20id%0A%20%20%20%20%20%20lastname%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D)\n\n---\n\n#### Sorting  \nSorting by a single field\n```graphql\n{\n  courseList(sort:nameDesc){\n    id,\n    name\n  }\n}\n```\n[Try it here](https://fluent-cms-admin.azurewebsites.net/graph?query=%7B%0A%20%20courseList(sort%3AnameDesc)%7B%0A%20%20%20%20id%2C%0A%20%20%20%20name%0A%20%20%7D%0A%7D%0A)\n\nSorting by multiple fields\n```graphql\n{\n  courseList(sort:[level,id]){\n    id,\n    level\n    name\n  }\n}\n```\n[Try it here](https://fluent-cms-admin.azurewebsites.net/graph?query=%7B%0A%20%20courseList(sort%3A%5Blevel%2Cid%5D)%7B%0A%20%20%20%20id%2C%0A%20%20%20%20level%0A%20%20%20%20name%0A%20%20%7D%0A%7D%0A)\n\n---\n\n#### Sort Expressions in FormCMS\n\n\nSort Expressions allow sorting by nested fields using JSON path syntax. \n\n***Example: Sort by Teacher's Last Name***\n\n```graphql\n{\n  courseList(sortExpr:{field:\"teacher.lastname\", order:Desc}) {\n    id\n    name\n    teacher {\n      id\n      lastname\n    }\n  }\n}\n```\n[Try it here](https://fluent-cms-admin.azurewebsites.net/graph?query=%7B%0A%20%20courseList(sortExpr%3A%7Bfield%3A%22teacher.lastname%22%2C%20order%3ADesc%7D)%20%7B%0A%20%20%20%20id%0A%20%20%20%20name%0A%20%20%20%20teacher%20%7B%0A%20%20%20%20%20%20id%0A%20%20%20%20%20%20lastname%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D)\n\n---\n\n#### Pagination\nPagination on root field\n```graphql\n{\n  courseList(offset:2, limit:3){\n    id,\n    name\n  }\n}\n```\n[Try it here](https://fluent-cms-admin.azurewebsites.net/graph?query=%20%20%7B%0A%20%20%20%20courseList(offset%3A2%2C%20limit%3A3)%7B%0A%20%20%20%20%20%20id%2C%0A%20%20%20%20%20%20name%0A%20%20%20%20%7D%0A%20%20%7D%0A)   \nPagination on sub field\n```graphql\n{\n  courseList{\n    id,\n    name\n    materials(limit:2){\n      id,\n      name\n    }\n  }\n}\n```\n[Try it here](https://fluent-cms-admin.azurewebsites.net/graph?query=%20%20%7B%0A%20%20%20%20courseList%7B%0A%20%20%20%20%20%20id%2C%0A%20%20%20%20%20%20name%0A%20%20%20%20%20%20materials(limit%3A2)%7B%0A%20%20%20%20%20%20%20%20id%2C%0A%20%20%20%20%20%20%20%20name%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A)\n\n---\n\n\n\n#### Variable\n\nVariables are used to make queries more dynamic, reusable, and secure.\n##### Variable in `Value filter`\n```\nquery ($id: Int!) {\n  teacher(idSet: [$id]) {\n    id\n    firstname\n    lastname\n  }\n}\n```\n[Try it here](https://fluent-cms-admin.azurewebsites.net/graph?query=query%20(%24id%3A%20Int!)%20%7B%0A%20%20teacher(idSet%3A%20%5B%24id%5D)%20%7B%0A%20%20%20%20id%0A%20%20%20%20firstname%0A%20%20%20%20lastname%0A%20%20%7D%0A%7D\u0026variables=%7B%0A%20%20%22id%22%3A3%0A%7D)\n\n##### Variable in `Operator Match` filter\n```\nquery ($id: Int!) {\n  teacherList(id:{equals:$id}){\n    id\n    firstname\n    lastname\n  }\n}\n```\n[Try it here](https://fluent-cms-admin.azurewebsites.net/graph?query=query%20(%24id%3A%20Int!)%20%7B%0A%20%20teacherList(id%3A%7Bequals%3A%24id%7D)%7B%0A%20%20%20%20id%0A%20%20%20%20firstname%0A%20%20%20%20lastname%0A%20%20%7D%0A%7D\u0026variables=%7B%0A%20%20%22id%22%3A%203%0A%7D)\n\n##### Variable in `Filter Expression`\n```\nquery ($years: String) {\n  teacherList(filterExpr:{field:\"skills.years\",clause:{gt:$years}}){\n    id\n    firstname\n    lastname\n    skills{\n      id\n      name\n      years\n    }\n  }\n}\n```\n[Try it here](https://fluent-cms-admin.azurewebsites.net/graph?query=query%20(%24years%3A%20String)%20%7B%0A%20%20teacherList(filterExpr%3A%7Bfield%3A%22skills.years%22%2Cclause%3A%7Bgt%3A%24years%7D%7D)%7B%0A%20%20%20%20id%0A%20%20%20%20firstname%0A%20%20%20%20lastname%0A%20%20%20%20skills%7B%0A%20%20%20%20%20%20id%0A%20%20%20%20%20%20name%0A%20%20%20%20%20%20years%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D\u0026variables=%7B%0A%20%20%22years%22%3A%20%229%22%0A%7D)\n\n##### Variable in Sort \n```\nquery ($sort_field:TeacherSortEnum) {\n  teacherList(sort:[$sort_field]) {\n    id\n    firstname\n    lastname\n  }\n}\n```\n[Try it here](https://fluent-cms-admin.azurewebsites.net/graph?query=query%20(%24sort_field%3ATeacherSortEnum)%20%7B%0A%20%20teacherList(sort%3A%5B%24sort_field%5D)%20%7B%0A%20%20%20%20id%0A%20%20%20%20firstname%0A%20%20%20%20lastname%0A%20%20%7D%0A%7D\u0026variables=%7B%22sort_field%22%3A%20%22idDesc%22%7D)\n##### Variable in Sort Expression\n```\nquery ($sort_order:  SortOrderEnum) {\n  courseList(sortExpr:{field:\"teacher.id\", order:$sort_order}){\n    id,\n    name,\n    teacher{\n      id,\n      firstname\n    }\n  }\n}\n```\n[Try it here](https://fluent-cms-admin.azurewebsites.net/graph?query=query%20(%24sort_order%3A%20%20SortOrderEnum)%20%7B%0A%20%20courseList(sortExpr%3A%7Bfield%3A%22teacher.id%22%2C%20order%3A%24sort_order%7D)%7B%0A%20%20%20%20id%2C%0A%20%20%20%20name%2C%0A%20%20%20%20teacher%7B%0A%20%20%20%20%20%20id%2C%0A%20%20%20%20%20%20firstname%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D\u0026variables=%7B%0A%20%20%22sort_order%22%3A%20%22Asc%22%0A%7D)\n\n##### Variable in Pagination\n```\nquery ($offset:Int) {\n  teacherList(limit:2, offset:$offset) {\n    id\n    firstname\n    lastname\n  }\n}\n```\n[Try it here](https://fluent-cms-admin.azurewebsites.net/graph?query=query%20(%24offset%3AInt)%20%7B%0A%20%20teacherList(limit%3A2%2C%20offset%3A%24offset)%20%7B%0A%20%20%20%20id%0A%20%20%20%20firstname%0A%20%20%20%20lastname%0A%20%20%7D%0A%7D\u0026variables=%7B%0A%09%22offset%22%3A%202%0A%7D)\n\n---\n\n#### Required vs Optional\nIf you want a variable to be mandatory, you can add a  `!` to the end of the type\n```\nquery ($id: Int!) {\n  teacherList(id:{equals:$id}){\n    id\n    firstname\n    lastname\n  }\n}\n```\n[Try it here](https://fluent-cms-admin.azurewebsites.net/graph?query=query%20(%24id%3A%20Int!)%20%7B%0A%20%20teacherList(id%3A%7Bequals%3A%24id%7D)%7B%0A%20%20%20%20id%0A%20%20%20%20firstname%0A%20%20%20%20lastname%0A%20%20%7D%0A%7D)\n\nExplore the power of FormCMS GraphQL and streamline your development workflow!\n***\n***\n### Saved Query\n\n**Realtime queries** may expose excessive technical details, potentially leading to security vulnerabilities.\n\n**Saved Queries** address this issue by abstracting the GraphQL query details. They allow clients to provide only variables, enhancing security while retaining full functionality.\n\n---\n\n#### Transitioning from **Real-Time Queries** to **Saved Queries**\n\n##### Using `OperationName` as the Saved Query Identifier\nIn FormCMS, the **Operation Name** in a GraphQL query serves as a unique identifier for saved queries. For instance, executing the following query automatically saves it as `TeacherQuery`:\n\n```graphql\nquery TeacherQuery($id: Int) {\n  teacherList(idSet: [$id]) {\n    id\n    firstname\n    lastname\n    skills {\n      id\n      name\n    }\n  }\n}\n```\n\n[Try it here](https://fluent-cms-admin.azurewebsites.net/graph?query=query%20TeacherQuery(%24id%3A%20Int)%20%7B%0A%20%20teacherList(idSet%3A%5B%24id%5D)%7B%0A%20%20%20%20id%0A%20%20%20%20firstname%0A%20%20%20%20lastname%0A%20%20%20%20skills%7B%0A%20%20%20%20%20%20id%2C%0A%20%20%20%20%20%20name%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D\u0026operationName=TeacherQuery)\n\n---\n\n##### Saved Query Endpoints\nFormCMS generates two API endpoints for each saved query:\n\n1. **List Records:**  \n   [https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery](https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery)\n\n2. **Single Record:**  \n   [https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery/single/](https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery/single)\n\n---\n\n##### Using REST API Query Strings as Variables\nThe Saved Query API allows passing variables via query strings:\n\n- **Single Value:**  \n  [https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery/?id=3](https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery/?id=3)\n\n- **Array of Values:**  \n  [https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery?id=3\u0026id=4](https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery?id=3\u0026id=4)  \n  This passes `[3, 4]` to the `idSet` argument.\n\n---\n\n#### Additional Features of `Saved Query`\n\nBeyond performance and security improvements, `Saved Query` introduces enhanced functionalities to simplify development workflows.\n\n---\n\n##### Pagination by `offset`\nBuilt-in variables `offset` and `limit` enable efficient pagination. For example:  \n[https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery?limit=2\u0026offset=2](https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery?limit=2\u0026offset=2)\n\n---\n\n##### `offset` Pagination for Subfields\nTo display a limited number of subfield items (e.g., the first two skills of a teacher), use the JSON path variable, such as `skills.limit`:  \n[https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery?skills.limit=2](https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery?skills.limit=2)\n\n---\n\n##### Pagination by `cursor`\nFor large datasets, `offset` pagination can strain the database. For example, querying with `offset=1000\u0026limit=10` forces the database to retrieve 1010 records and discard the first 1000.\n\nTo address this, `Saved Query` supports **cursor-based pagination**, which reduces database overhead.  \nExample response for [https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery?limit=3](https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery?limit=3):\n\n```json\n[\n  {\n    \"hasPreviousPage\": false,\n    \"cursor\": \"eyJpZCI6M30\"\n  },\n  {\n  },\n  {\n    \"hasNextPage\": true,\n    \"cursor\": \"eyJpZCI6NX0\"\n  }\n]\n```\n\n- If `hasNextPage` of the last record is `true`, use the cursor to retrieve the next page:  \n  [https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery?limit=3\u0026last=eyJpZCI6NX0](https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery?limit=3\u0026last=eyJpZCI6NX0)\n\n- Similarly, if `hasPreviousPage` of the first record is `true`, use the cursor to retrieve the previous page:  \n  [https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery?limit=3\u0026first=eyJpZCI6Nn0](https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery?limit=3\u0026first=eyJpZCI6Nn0)\n\n---\n\n##### Cursor-Based Pagination for Subfields\nSubfields also support cursor-based pagination. For instance, querying [https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery?skills.limit=2](https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery?skills.limit=2) returns a response like this:\n\n```json\n[\n  {\n    \"id\": 3,\n    \"firstname\": \"Jane\",\n    \"lastname\": \"Debuggins\",\n    \"hasPreviousPage\": false,\n    \"skills\": [\n      {\n        \"hasPreviousPage\": false,\n        \"cursor\": \"eyJpZCI6MSwic291cmNlSWQiOjN9\"\n      },\n      {\n        \"hasNextPage\": true,\n        \"cursor\": \"eyJpZCI6Miwic291cmNlSWQiOjN9\"\n      }\n    ],\n    \"cursor\": \"eyJpZCI6M30\"\n  }\n]\n```\n\nTo fetch the next two skills, use the cursor:  \n[https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery/part/skills?limit=2\u0026last=eyJpZCI6Miwic291cmNlSWQiOjN9](https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery/part/skills?limit=2\u0026last=eyJpZCI6Miwic291cmNlSWQiOjN9)\n\nBelow is a rewritten version of the **Asset Type** and **Distinct** chapters from your GraphQL Query documentation. The rewrite aims to improve clarity, structure, and readability while preserving the technical details.\n\n---\n\n### Asset Type\n\nIn FormCMS, attributes with display types such as `image`, `file`, or `gallery` are represented as **Asset Objects** in GraphQL query results. These objects correspond to assets stored in the system's centralized Asset Library (see the **Asset Library** section for details). When querying entities with these attributes, the response includes structured asset data, such as the asset’s `Path`, `Url`, `Name`, `Title`, and other metadata.\n\n#### Example Query\nConsider a `course` entity with an `image` field:\n```graphql\n{\n  courseList {\n    id\n    name\n    image {\n      id\n      path\n      url\n      name\n      title\n      size\n      type\n    }\n  }\n}\n```\n[Try it here](https://fluent-cms-admin.azurewebsites.net/graph?query=%7B%0A%20%20courseList%20%7B%0A%20%20%20%20id%0A%20%20%20%20name%0A%20%20%20%20image%20%7B%0A%20%20%20%20%20%20id%0A%20%20%20%20%20%20path%0A%20%20%20%20%20%20url%0A%20%20%20%20%20%20name%0A%20%20%20%20%20%20title%0A%20%20%20%20%20%20size%0A%20%20%20%20%20%20type%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D)\n\n#### Response Example\n```json\n{\n  \"data\": {\n    \"courseList\": [\n      {\n        \"id\": 1,\n        \"name\": \"Introduction to GraphQL\",\n        \"image\": {\n          \"id\": \"abc123\",\n          \"path\": \"2025-03-abc123\",\n          \"url\": \"/files/2025-03-abc123\",\n          \"name\": \"graphql_intro.jpg\",\n          \"title\": \"GraphQL Course Banner\",\n          \"size\": 102400,\n          \"type\": \"image/jpeg\"\n        }\n      }\n    ]\n  }\n}\n```\n\n#### Benefits\n- **Consistency**: The `url` field provides a fixed access point to the asset, ensuring reliable retrieval across the application.\n- **Metadata**: Fields like `title` can be used as captions or `alt` text for images, enhancing accessibility and SEO.\n- **Flexibility**: The Asset Object structure supports various file types (`image`, `file`, `gallery`) with a uniform response format.\n\n---\n\n### Distinct\n\nWhen querying related entities in FormCMS, joining tables can result in duplicate records due to one-to-many relationships. The `DISTINCT` keyword helps eliminate these duplicates, but it has limitations that require careful query design.\n\n#### Why Use `DISTINCT`?\nConsider the following data structure:\n- **Posts**: `[{id: 1, title: \"p1\"}]`\n- **Tags**: `[{id: 1, name: \"t1\"}, {id: 2, name: \"t2\"}]`\n- **Post_Tag**: `[{post_id: 1, tag_id: 1}, {post_id: 1, tag_id: 2}]`\n\nA query joining these tables might look like this in SQL:\n```sql\nSELECT posts.id, posts.title\nFROM posts\nLEFT JOIN post_tag ON posts.id = post_tag.post_id\nLEFT JOIN tags ON post_tag.tag_id = tags.id\nWHERE tags.id \u003e 0;\n```\n\nWithout `DISTINCT`, the result duplicates the post for each tag:\n```json\n[\n  {\"id\": 1, \"title\": \"p1\"},  // For tag_id: 1\n  {\"id\": 1, \"title\": \"p1\"}   // For tag_id: 2\n]\n```\n\nUsing `DISTINCT` ensures each post appears only once:\n```sql\nSELECT DISTINCT posts.id, posts.title\nFROM posts\nLEFT JOIN post_tag ON posts.id = post_tag.post_id\nLEFT JOIN tags ON post_tag.tag_id = tags.id\nWHERE tags.id \u003e 0;\n```\nResult:\n```json\n[\n  {\"id\": 1, \"title\": \"p1\"}\n]\n```\n\n#### Limitation of `DISTINCT`\nIn some databases, such as SQL Server, `DISTINCT` cannot be applied to fields of type `TEXT` (or large data types like `NTEXT` or `VARCHAR(MAX)`). Including such fields in a query with `DISTINCT` causes errors.\n\n#### Solution/Workaround\nTo address this limitation, split the entity’s queries into two parts: \n\n1. **List Query**: Retrieves a lightweight list of records without `TEXT` fields, using `DISTINCT` to avoid duplicates. \n```\n{\n    postList {\n       id\n       title\n   }\n}\n```\n2. **Detail Query**: Retrieves full details, including `TEXT` fields, by querying a single record using its ID. \n```\n{\n    post(idSet: 1) {\n       id\n       title\n       description  # TEXT field\n   }\n}\n```\n\n\n#### Example with GraphQL\nList query to avoid duplicates:\n```\n{\n  postList {\n    id\n    title\n    tags {\n      id\n      name\n    }\n  }\n}\n```\nDetail query for a specific post:\n```graphql\n{\n  post(idSet: 1) {\n    id\n    title\n    description  # TEXT field, only queried here\n    tags {\n      id\n      name\n    }\n  }\n}\n```\n\n#### Benefits\n- **Efficiency**: The list query remains lightweight and deduplicated.\n- **Compatibility**: Avoids database-specific limitations on `DISTINCT`.\n- **Flexibility**: Developers can fetch detailed data only when needed.\n\n---\n\u003c/details\u003e\n\n---\n## Drag and Drop Page Designer\n\u003cdetails\u003e \n\u003csummary\u003e \nThe page designer utilizes the open-source GrapesJS and Handlebars, enabling seamless binding of `GrapesJS Components` with `FormCMS Queries` for dynamic content rendering. \n\u003c/summary\u003e\n\n---\n### Page Types: Landing Page, Detail Page, and Home Page\n\n#### **Landing Page**\nA landing page is typically the first page a visitor sees.  \n- **URL format**: `/page/\u003cpagename\u003e`  \n- **Structure**: Comprised of multiple sections, each section retrieves data via a `query`.  \n\n**Example**:    \n[Landing Page](https://fluent-cms-admin.azurewebsites.net/)    \nThis page fetches data from:  \n- [https://fluent-cms-admin.azurewebsites.net/api/queries/course/?status=featured](https://fluent-cms-admin.azurewebsites.net/api/queries/course/?status=featured)  \n- [https://fluent-cms-admin.azurewebsites.net/api/queries/course/?level=Advanced](https://fluent-cms-admin.azurewebsites.net/api/queries/course/?level=Advanced)  \n\n---\n\n#### **Detail Page**\nA detail page provides specific information about an item.  \n- **URL format**: `/page/\u003cpagename\u003e/\u003crouter parameter\u003e`  \n- **Data Retrieval**: FormCMS fetches data by passing the router parameter to a `query`.  \n\n**Example**:  \n[Course Detail Page](https://fluent-cms-admin.azurewebsites.net/pages/course/22)  \nThis page fetches data from:  \n[https://fluent-cms-admin.azurewebsites.net/api/queries/course/one?course_id=22](https://fluent-cms-admin.azurewebsites.net/api/queries/course/one?course_id=22)\n\n---\n\n#### **Home Page**\nThe homepage is a special type of landing page named `home`.  \n- **URL format**: `/pages/home`   \n- **Special Behavior**: If no other route matches the path `/`, FormCMS renders `/pages/home` by default.  \n\n**Example**:    \nThe URL `/` will be resolved to `/pages/home` unless explicitly overridden.  \n\n---\n### Introduction to GrapesJS Panels\n\nUnderstanding the panels in GrapesJS is crucial for leveraging FormCMS's customization capabilities in the Page Designer UI. This section explains the purpose of each panel and highlights how FormCMS enhances specific areas to streamline content management and page design. \n\n![GrapesJS Toolbox](https://raw.githubusercontent.com/formcms/formcms/doc/doc/screenshots/grapes-toolbox.png)\n\n1. **Style Manager**:\n    - Used to customize CSS properties of elements selected on the canvas.\n    - *FormCMS Integration*: This panel is left unchanged by FormCMS, as it already provides powerful styling options.\n\n2. **Traits Panel**:\n    - Allows modification of attributes for selected elements.\n    - *FormCMS Integration*: Custom traits are added to this panel, enabling users to bind data to components dynamically.\n\n3. **Layers Panel**:\n    - Displays a hierarchical view of elements on the page, resembling a DOM tree.\n    - *FormCMS Integration*: While FormCMS does not alter this panel, it’s helpful for locating and managing FormCMS blocks within complex page designs.\n\n4. **Blocks Panel**:\n    - Contains pre-made components that can be dragged and dropped onto the page.\n    - *FormCMS Integration*: FormCMS enhances this panel by adding custom-designed blocks tailored for its CMS functionality.\n\nBy familiarizing users with these panels and their integration points, this chapter ensures a smoother workflow and better utilization of FormCMS's advanced page-building tools.\n\n---\n### Data Binding: Singleton or List\n\nFormCMS leverages [Handlebars expressions](https://github.com/Handlebars-Net/Handlebars.Net) for dynamic data binding in pages and components.\n\n---\n\n#### **Singleton**\n\nSingleton fields are enclosed within `{{ }}` to dynamically bind individual values.\n\n- **Example Page Settings:** [Page Schema Settings](https://fluent-cms-admin.azurewebsites.net/_content/FormCMS/schema-ui/page.html?schema=page\u0026id=33)\n- **Example Query:** [Retrieve Course Data](https://fluent-cms-admin.azurewebsites.net/api/queries/course/?course_id=22)\n- **Example Rendered Page:** [Rendered Course Page](https://fluent-cms-admin.azurewebsites.net/pages/course/22)\n\n---\n\n#### **List**\n\n`Handlebars` supports iterating over arrays using the `{{#each}}` block for repeating data structures.\n\n```handlebars\n{{#each course}}\n    \u003cli\u003e{{title}}\u003c/li\u003e\n{{/each}}\n```\n\nIn FormCMS, you won’t explicitly see the `{{#each}}` statement in the Page Designer. If a block's data source is set to `data-list`, the system automatically generates the loop.\n\n- **Example Page Settings:** [Page Schema Settings](https://fluent-cms-admin.azurewebsites.net/_content/FormCMS/schema-ui/page.html?schema=page\u0026id=32)\n- **Example Rendered Page:** [Rendered List Page](https://fluent-cms-admin.azurewebsites.net/)\n- **Example Queries:**\n   - [Featured Courses](https://fluent-cms-admin.azurewebsites.net/api/queries/course/?status=featured)\n   - [Advanced Level Courses](https://fluent-cms-admin.azurewebsites.net/api/queries/course/?level=Advanced)\n\n---\n\n#### **Steps to Bind a Data Source**\n\nTo bind a `Data List` to a component, follow these steps:\n\n1. Drag a block from the **Data List** category in the Page Designer.\n2. Open the **Layers Panel** and select the `Data List` component.\n3. In the **Traits Panel**, configure the following fields:\n\n| **Field**     | **Description**                                                                                                                                                        |\n|---------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| **Query**     | The query to retrieve data.                                                                                                                                           |\n| **Qs**        | Query string parameters to pass (e.g., `?status=featured`, `?level=Advanced`).                                                                                        |\n| **Offset**    | Number of records to skip.                                                                                                                                            |\n| **Limit**     | Number of records to retrieve.                                                                                                                                        |\n| **Pagination**| Options for displaying content:                                                                                                                                       |\n|               | - **Button**: Divides content into multiple pages with navigation buttons (e.g., \"Next,\" \"Previous,\" or numbered buttons).                                            |\n|               | - **Infinite Scroll**: Automatically loads more content as users scroll. Ideal for a single component at the bottom of the page.                                      |\n|               | - **None**: Displays all available content at once without requiring additional user actions.                                                                          |\n\n\u003c/details\u003e\n\n---\n## Online Course System Frontend\n\u003cdetails\u003e \n\u003csummary\u003e \nHaving established our understanding of FormCMS essentials like Entity, Query, and Page, we're ready to build a frontend for an online course website.\n\u003c/summary\u003e\n\n---\n### Key Pages\n\n- **Home Page (`home`)**: The main entry point, featuring sections like *Featured Courses* and *Advanced Courses*. Each course links to its respective **Course Details** page.\n- **Course Details (`course/{course_id}`)**: Offers detailed information about a specific course and includes links to the **Teacher Details** page.\n- **Teacher Details (`teacher/{teacher_id}`)**: Highlights the instructor’s profile and includes a section displaying their latest courses, which link back to the **Course Details** page.\n\n\n```plaintext\n             Home Page\n                 |\n                 |\n       +-------------------+\n       |                   |\n       v                   v\n Latest Courses       Course Details \n       |                   |        \n       |                   |       \n       v                   v            \nCourse Details \u003c-------\u003e Teacher Details \n```\n\n---\n\n### Designing the Home Page\n\n1. **Drag and Drop Components**: Use the  FormCMS page designer to drag a `Content-B` component.\n2. **Set Data Source**: Assign the component's data source to the `course` query.\n3. **Link Course Items**: Configure the link for each course to `/pages/course/{{id}}`. The Handlebars expression `{{id}}` is dynamically replaced with the actual course ID during rendering.\n\n![Link Example](https://raw.githubusercontent.com/formcms/formcms/doc/doc/screenshots/designer-link.png)\n\n---\n\n### Creating the Course Details Page\n\n1. **Page Setup**: Name the page `course/{course_id}` to capture the `course_id` parameter from the URL (e.g., `/pages/course/20`).\n2. **Query Configuration**: The variable `{course_id:20}` is passed to the `course` query, generating a `WHERE id IN (20)` clause to fetch the relevant course data.\n3. **Linking to Teacher Details**: Configure the link for each teacher item on this page to `/pages/teacher/{{teacher.id}}`. Handlebars dynamically replaces `{{teacher.id}}` with the teacher’s ID. For example, if a teacher object has an ID of 3, the link renders as `/pages/teacher/3`.\n\n---\n\n### Creating the Teacher Details Page\n\n1. **Page Setup**: Define the page as `teacher/{teacher_id}` to capture the `teacher_id` parameter from the URL.\n2. **Set Data Source**: Assign the `teacher` query as the page’s data source.\n\n#### Adding a Teacher’s Courses Section\n\n- Drag a `ECommerce A` component onto the page.\n- Set its data source to the `course` query, filtered by the teacher’s ID (`WHERE teacher IN (3)`).\n\n![Teacher Page Designer](https://raw.githubusercontent.com/formcms/formcms/doc/doc/screenshots/designer-teacher.png)\n\nWhen rendering the page, the `PageService` automatically passes the `teacher_id` (e.g., `{teacher_id: 3}`) to the query.\n\u003c/details\u003e  \n\n\n\n---\n\n## Navigation by Category  \n\u003cdetails\u003e  \n\u003csummary\u003eCategory trees and breadcrumbs provide structure, context, and clarity, enabling users to find and navigate data more efficiently.\u003c/summary\u003e  \n\n### **Demo of Category Tree and Breadcrumb Navigation**  \n- **Category Tree Navigation**:  \n  [View Demo](https://fluent-cms-admin.azurewebsites.net/course)  \n- **Breadcrumb Navigation**:  \n  [View Demo](https://fluent-cms-admin.azurewebsites.net/course/27)  \n\n---\n\n### **Creating a Category Entity**\nTo create a category entity in the Schema Builder, include `parent` and `children` attributes.\n- **Example Configuration**:  \n  [Edit Example](https://fluent-cms-admin.azurewebsites.net/_content/FormCMS/schema-ui/edit.html?schema=entity\u0026id=103)\n\n---\n\n### **Configuration Options for Navigation**\n- **DataType: `lookup`** \u0026 **DisplayType: `TreeSelect`**  \n  Use this configuration to display a category as a property.  \n  [Edit Example](https://fluent-cms-admin.azurewebsites.net/_content/FormCMS/schema-ui/edit.html?schema=entity\u0026id=96)\n\n- **DataType: `junction`** \u0026 **DisplayType: `Tree`**  \n  Use this configuration to enable category-based navigation.  \n  [Edit Example](https://fluent-cms-admin.azurewebsites.net/_content/FormCMS/schema-ui/edit.html?schema=entity\u0026id=27)\n\n---\n\n### **Using Navigation Components in Page Designer**\n- **Tree Layer Menu**:  \n  Use the `Tree Layer Menu` component for hierarchical navigation.  \n  [Edit Example](https://fluent-cms-admin.azurewebsites.net/_content/FormCMS/schema-ui/page.html?schema=page\u0026id=42)\n\n- **Breadcrumbs**:  \n  Use the `Breadcrumbs` component to display navigation paths.  \n  [Edit Example](https://fluent-cms-admin.azurewebsites.net/_content/FormCMS/schema-ui/page.html?schema=page\u0026id=33)\n\n\u003c/details\u003e  \n\n\n\n---\n\n## Schema Version Control\n\u003cdetails\u003e  \n\u003csummary\u003e  \nFormCMS saves each version of schemas, allowing users to roll back to earlier versions. Admins can freely test draft versions, while only published versions take effect.  \n\u003c/summary\u003e  \n\n### Requirements\nTo illustrate this feature, let's take a `Page` as an example. Once a page is published, it becomes publicly accessible. You may need version control for two main reasons:\n\n- You want to make changes but ensure they do not take effect until thoroughly tested.\n- If issues arise in the latest version, you need the ability to roll back to a previous version.\n\n### Choosing a Version\nAfter making changes, the latest version's status changes to `draft` in the [Page List Page](https://fluent-cms-admin.azurewebsites.net/_content/FormCMS/schema-ui/list.html?type=page).  \nTo manage versions, click the `View History` button to navigate to the [History Version List Page](https://fluent-cms-admin.azurewebsites.net/_content/FormCMS/schema-ui/history.html?schemaId=01JKKB85KWA651945N5W0T6PJR).  \nHere, you can select any version and set it to `published` status.\n\n### Testing a `Draft` Version\nTo preview a draft version, append `sandbox=1` as a query parameter in the URL: [Preview Draft Version Page](https://fluent-cms-admin.azurewebsites.net/story/?sandbox=1).  \nAlternatively, click the `View Page` button on the `Page Design` page.\n\n### Compare schema Changes\nYou can compare the difference between different versions, use the [Schema Diff Tool](https://fluent-cms-admin.azurewebsites.net/_content/FormCMS/schema-ui/diff.html?schemaId=01JKKA93AJG2HNY648H9PC16PN\u0026type=query\u0026oldId=126\u0026newId=138).\n\n### Duplicate\nYou can duplicate any schema version and save it as a new schema.\n\n\u003c/details\u003e  \n\n\n\n---\n\n## Social Activity\n\u003cdetails\u003e  \n\u003csummary\u003e  \nThe Social Activity feature enhances user engagement by enabling views, likes, saves, and shares. It also provides detailed analytics to help understand content performance.\n\u003c/summary\u003e  \n\n### Endpoints\n- `GET /api/activities/{entityName}/{recordId:long}`  \n  Increments the view count by 1. Returns the active status and count for: like, view, share, and save.\n\n- `GET /api/activities/record/{entityName}/{recordId}?type={view|share}`  \n  Retrieves activity info of type `view` or `share` for a given entity record.\n\n- `POST /api/activities/toggle/{entityName}/{recordId}?type={like|save}\u0026active={true|false}`  \n  Toggles the activity (like or save) on or off based on the `active` flag.\n\n### Challenges\nThe system cannot leverage traditional output caching due to dynamic nature of the content, which may lead to high database load under heavy traffic.\n\nTo address this, buffered writes are introduced. Activity events are first stored in a buffer (in-memory or Redis), and then periodically flushed to the database, balancing performance and accuracy.\n\n---\n\n### Load Testing\n\nBelow is a test script using [k6](https://k6.io/) to simulate traffic and measure performance:\n\n```javascript\nimport http from 'k6/http';\nimport { check } from 'k6';\nimport { Trend } from 'k6/metrics';\n\nconst ResponseTime = new Trend('response_time', true);\n\nexport const options = {\n    stages: [\n        { duration: '30s', target: 300 },\n        { duration: '30s', target: 300 },\n        { duration: '30s', target: 0 },\n    ],\n    thresholds: {\n        'http_req_failed': ['rate\u003c0.01'],\n        'http_req_duration': ['p(95)\u003c500'],\n        'response_time': ['p(95)\u003c500'],\n    },\n};\n\nexport default function () {\n    const id = Math.floor(Math.random() * 100) + 1;\n    const res = http.get(`http://localhost:5000/api/activities/post/${id}`);\n    ResponseTime.add(res.timings.duration);\n    check(res, { 'status is 200': (r) =\u003e r.status === 200 });\n}\n```\n\n---\n\n### Performance Comparison\n\n#### No Buffer\n\n- ✅ Simple to deploy and debug\n- ❌ High database load under heavy traffic\n- ⏱ Avg. response time: **35.28ms**\n- 🧪 Total requests: **509,762**\n- 📉 Throughput: ~**5,664 req/s**\n\n#### Redis Buffer\n\n- ✅ High performance\n- ✅ Scalable across instances\n- ❌ More complex infrastructure (requires Redis setup)\n- ⏱ Avg. response time: **11.78ms**\n- 🧪 Total requests: **1,520,072**\n- 📉 Throughput: ~**16,889 req/s**\n\n#### Memory Buffer\n\n- ✅ Highest performance\n- ✅ Easy to deploy\n- ❌ Not horizontally scalable (buffer is local to instance)\n- ⏱ Avg. response time: **4ms**\n- 🧪 Total requests: **4,132,111**\n- 📉 Throughput: ~**45,912 req/s**\n\n---\n\n### Summary\n\nEach buffering strategy has its tradeoffs:\n\n| Strategy       | Performance | Scalability | Complexity | Avg Response Time |\n|----------------|-------------|-------------|------------|-------------------|\n| No Buffer      | Medium      | High        | Low        | ~35ms             |\n| Memory Buffer  | High        | Low         | Low        | ~4ms              |\n| Redis Buffer   | High        | High        | Medium     | ~12ms             |\n\nChoose the approach based on your system’s scalability requirements and infrastructure constraints.\n\n\u003c/details\u003e\n\n\n\n---\n\n## User Portal\n\n\u003cdetails\u003e  \n\u003csummary\u003e  \nUsers can access their view history, liked posts, and bookmarked posts in a personalized portal.  \n\u003c/summary\u003e \n\nThe **User Portal** in FormCMS provides a centralized interface for users to manage their social activity, including viewing their interaction history, liked posts, and bookmarked content. This enhances user engagement by offering a tailored experience to revisit and organize content.\n\n### History\nUsers can view a list of all items they have previously accessed, such as pages, posts, or other content. Each item in the history is displayed with a clickable link, allowing users to easily revisit the content.\n\n### Liked Items\nThe Liked Items section displays all posts or content that the user has liked. Users can browse their liked items, with options to unlike content or click through to view the full item, fostering seamless interaction with preferred content.\n\n### Bookmarked Items\nUsers can organize and view their saved content in the Bookmarked Items section. Bookmarks can be grouped into custom folders for easy categorization, enabling users to efficiently manage and access their saved items by folder or as a complete list.\n\n### Configuration\nThe User Portal displays items with the following metadata:\n- **Image**: A thumbnail or visual representation of the item.\n- **Title**: The primary name or heading of the item.\n- **Subtitle**: A brief description or secondary text for the item.\n- **URL**: The link to access the full item.\n- **PublishedAt**: The publication date and time of the item.\n\nMetadata mappings are configured on the **Entity Settings** page, where administrators can define how data fields map to the portal's display. The following settings are available:\n\n- **PageUrl**: Specifies the base URL for item links (e.g., \"/content/\").\n- **BookmarkQuery**: Defines the query used to fetch bookmarked items.\n- **BookmarkQueryParamName**: Sets the parameter name for the query (e.g., \"id\").\n- **BookmarkTitleField**: Maps the field containing the item's title.\n- **BookmarkSubtitleField**: Maps the field for the item's subtitle.\n- **BookmarkImageField**: Maps the field for the item's image URL.\n- **BookmarkPublishTimeField**: Maps the field for the item's publication timestamp.\n\nThese settings allow for flexible customization, ensuring the User Portal displays content accurately and consistently across history, liked items, and bookmarked items.\n\n\u003c/details\u003e\n\n---\n## Optimizing Caching\n\n\u003cdetails\u003e\n\u003csummary\u003e\nFormCMS employs advanced caching strategies to boost performance.  \n\u003c/summary\u003e\n\nFor detailed information on ASP.NET Core caching, visit the official documentation: [ASP.NET Core Caching Overview](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/overview?view=aspnetcore-9.0).\n\n### Cache Schema\n\nFormCMS automatically invalidates schema caches whenever schema changes are made. The schema cache consists of two types:\n\n1. **Entity Schema Cache**  \n   Caches all entity definitions required to dynamically generate GraphQL types.\n\n2. **Query Schema Cache**  \n   Caches query definitions, including dependencies on multiple related entities, to compose efficient SQL queries.\n\nBy default, schema caching is implemented using `IMemoryCache`. However, you can override this by providing a `HybridCache`. Below is a comparison of the two options:\n\n#### **IMemoryCache**\n- **Advantages**:\n    - Simple to debug and deploy.\n    - Ideal for single-node web applications.\n- **Disadvantages**:\n    - Not suitable for distributed environments. Cache invalidation on one node (e.g., Node A) does not propagate to other nodes (e.g., Node B).\n\n#### **HybridCache**\n- **Key Features**:\n    - **Scalability**: Combines the speed of local memory caching with the consistency of distributed caching.\n    - **Stampede Resolution**: Effectively handles cache stampede scenarios, as verified by its developers.\n- **Limitations**:  \n  The current implementation lacks \"Backend-Assisted Local Cache Invalidation,\" meaning invalidation on one node does not instantly propagate to others.\n- ** FormCMS Strategy**:  \n  FormCMS mitigates this limitation by setting the local cache expiration to 20 seconds (one-third of the distributed cache expiration, which is set to 60 seconds). This ensures cache consistency across nodes within 20 seconds, significantly improving upon the typical 60-second delay in memory caching.\n\nTo implement a `HybridCache`, use the following code:\n\n```csharp\nbuilder.AddRedisDistributedCache(connectionName: CmsConstants.Redis);\nbuilder.Services.AddHybridCache();\n```\n\n### Cache Data\n\nFormCMS does not automatically invalidate data caches. Instead, it leverages ASP.NET Core's output caching for a straightforward implementation. Data caching consists of two types:\n\n1. **Query Data Cache**  \n   Caches the results of queries for faster access.\n\n2. **Page Cache**  \n   Caches the output of rendered pages for quick delivery.\n\nBy default, output caching is disabled in FormCMS. To enable it, configure and inject the output cache as shown below:\n\n```csharp\nbuilder.Services.AddOutputCache(cacheOption =\u003e\n{\n    cacheOption.AddBasePolicy(policyBuilder =\u003e policyBuilder.Expire(TimeSpan.FromMinutes(1)));\n    cacheOption.AddPolicy(CmsOptions.DefaultPageCachePolicyName,\n        b =\u003e b.Expire(TimeSpan.FromMinutes(2)));\n    cacheOption.AddPolicy(CmsOptions.DefaultQueryCachePolicyName,\n        b =\u003e b.Expire(TimeSpan.FromSeconds(1)));\n});\n\n// After builder.Build();\napp.UseOutputCache();\n```\n\n\u003c/details\u003e\n\n\n---\n## Aspire Integration\n\u003cdetails\u003e \n\u003csummary\u003e \nFormCMS leverages Aspire to simplify deployment.\n\u003c/summary\u003e\n\n\n### Architecture Overview\n\nA scalable deployment of  FormCMS involves multiple web application nodes, a Redis server for distributed caching, and a database server, all behind a load balancer.\n\n\n```\n                 +------------------+\n                 |  Load Balancer   |\n                 +------------------+\n                          |\n        +-----------------+-----------------+\n        |                                   |\n+------------------+              +------------------+\n|    Web App 1     |              |    Web App 2     |\n|   +-----------+  |              |   +-----------+  |\n|   | Local Cache| |              |   | Local Cache| |\n+------------------+              +------------------+\n        |                                   |\n        |                                   |\n        +-----------------+-----------------+\n                 |                       |\n       +------------------+    +------------------+\n       | Database Server  |    |   Redis Server   |\n       +------------------+    +------------------+\n```\n\n---\n\n### Local Emulation with Aspire and Service Discovery\n\n[Example Web project on GitHub](https://github.com/formcms/formcms/tree/main/server/FormCMS.Course)  \n[Example Aspire project on GitHub](https://github.com/formcms/formcms/tree/main/server/FormCMS.Course.AppHost)  \n\nTo emulate the production environment locally,  FormCMS leverages Aspire. Here's an example setup:\n\n```csharp\nvar builder = DistributedApplication.CreateBuilder(args);\n\n// Adding Redis and PostgreSQL services\nvar redis = builder.AddRedis(name: CmsConstants.Redis);\nvar db = builder.AddPostgres(CmsConstants.Postgres);\n\n// Configuring the web project with replicas and references\nbuilder.AddProject\u003cProjects.FormCMS_Course\u003e(name: \"web\")\n    .WithEnvironment(CmsConstants.DatabaseProvider, CmsConstants.Postgres)\n    .WithReference(redis)\n    .WithReference(db)\n    .WithReplicas(2);\n\nbuilder.Build().Run();\n```\n\n### Benefits:\n1. **Simplified Configuration**:  \n   No need to manually specify endpoints for the database or Redis servers. Configuration values can be retrieved using:\n   ```csharp\n   builder.Configuration.GetValue\u003cstring\u003e();\n   builder.Configuration.GetConnectionString();\n   ```\n2. **Realistic Testing**:  \n   The local environment mirrors the production architecture, ensuring seamless transitions during deployment.\n\nBy adopting these caching and deployment strategies,  FormCMS ensures improved performance, scalability, and ease of configuration.\n\u003c/details\u003e\n\n---\n## Query with Document DB\n\u003cdetails\u003e\n\u003csummary\u003e\nOptimizing query performance by syncing relational data to a document database, such as MongoDB, significantly improves speed and scalability for high-demand applications.\n\u003c/summary\u003e\n\n### Limitations of ASP.NET Core Output Caching\nASP.NET Core's output caching reduces database access when repeated queries are performed. However, its effectiveness is limited when dealing with numerous distinct queries:\n\n1. The application server consumes excessive memory to cache data. The same list might be cached multiple times in different orders.\n2. The database server experiences high load when processing numerous distinct queries simultaneously.\n\n### Using Document Databases to Improve Query Performance\n\nFor the query below, FormCMS joins the `post`, `tag`, `category`, and `author` tables:\n```graphql\nquery post_sync($id: Int) {\n  postList(idSet: [$id], sort: id) {\n    id, title, body, abstract\n    tag {\n      id, name\n    }\n    category {\n      id, name\n    }\n    author {\n      id, name\n    }\n  }\n}\n```\nBy saving each post along with its related data as a single document in a document database, such as MongoDB, several improvements are achieved:\n- Reduced database server load since data retrieval from multiple tables is eliminated.\n- Reduced application server processing, as merging data is no longer necessary.\n\n### Performance Testing\nUsing K6 scripts with 1,000 virtual users concurrently accessing the query API, the performance difference between PostgreSQL and MongoDB was tested, showing MongoDB to be significantly faster:\n```javascript\nexport default function () {\n    const id = Math.floor(Math.random() * 1000000) + 1; // Random id between 1 and 1,000,000\n    /* PostgreSQL */\n    // const url = `http://localhost:5091/api/queries/post_sync/?id=${id}`;\n\n    /* MongoDB */\n    const url = `http://localhost:5091/api/queries/post/?id=${id}`;\n\n    const res = http.get(url);\n\n    check(res, {\n        'is status 200': (r) =\u003e r.status === 200,\n        'response time is \u003c 200ms': (r) =\u003e r.timings.duration \u003c 200,\n    });\n}\n/*\nMongoDB:\n     http_req_waiting...............: avg=50.8ms   min=774µs    med=24.01ms max=3.23s    p(90)=125.65ms p(95)=211.32ms\nPostgreSQL:\n     http_req_waiting...............: avg=5.54s   min=11.61ms med=4.08s max=44.67s  p(90)=13.45s  p(95)=16.53s\n*/\n```\n\n### Synchronizing Query Data to Document DB\n\n#### Architecture Overview\n![Architecture Overview](https://raw.githubusercontent.com/formcms/formcms/doc/doc/diagrams/mongo-sync.png)\n\n#### Enabling Message Publishing in WebApp\nTo enable publishing messages to the Message Broker, use Aspire to add a NATS resource. Detailed documentation is available in [Microsoft Docs](https://learn.microsoft.com/en-us/dotnet/aspire/messaging/nats-integration?tabs=dotnet-cli).\n\nAdd the following line to the Aspire HostApp project:\n```csharp\nbuilder.AddNatsClient(AppConstants.Nats);\n```\nAdd the following lines to the WebApp project:\n```csharp\nbuilder.AddNatsClient(AppConstants.Nats);\nvar entities = builder.Configuration.GetRequiredSection(\"TrackingEntities\").Get\u003cstring[]\u003e()!;\nbuilder.Services.AddNatsMessageProducer(entities);\n```\nFormCMS publishes events for changes made to entities listed in `appsettings.json`:\n```json\n{\n  \"TrackingEntities\": [\n    \"post\"\n  ]\n}\n```\n\n#### Enabling Message Consumption in Worker App\n\nAdd the following to the Worker App:\n```csharp\nvar builder = Host.CreateApplicationBuilder(args);\n\nbuilder.AddNatsClient(AppConstants.Nats);\nbuilder.AddMongoDBClient(AppConstants.MongoCms);\n\nvar apiLinksArray = builder.Configuration.GetRequiredSection(\"ApiLinksArray\").Get\u003cApiLinks[]\u003e()!;\nbuilder.Services.AddNatsMongoLink(apiLinksArray);\n```\nDefine the `ApiLinksArray` in `appsettings.json` to specify entity changes and the corresponding query API:\n```json\n{\n  \"ApiLinksArray\": [\n    {\n      \"Entity\": \"post\",\n      \"Api\": \"http://localhost:5001/api/queries/post_sync\",\n      \"Collection\": \"post\",\n      \"PrimaryKey\": \"id\"\n    }\n  ]\n}\n```\nWhen changes occur to the `post` entity, the Worker Service calls the query API to retrieve aggregated data and saves it as a document.\n\n#### Migrating Query Data to Document DB\nAfter adding a new entry to `ApiLinksArray`, the Worker App will perform a migration from the start to populate the Document DB.\n\n### Replacing Queries with Document DB\n\n#### Architecture Overview\n![Architecture Overview](https://raw.githubusercontent.com/formcms/formcms/doc/doc/diagrams/mongo-query.png)   \n\nTo enable MongoDB queries in your WebApp, use the Aspire MongoDB integration. Details are available in [Microsoft Docs](https://learn.microsoft.com/en-us/dotnet/aspire/database/mongodb-integration?tabs=dotnet-cli).\n\nAdd the following code to your WebApp:\n```csharp\nbuilder.AddMongoDBClient(connectionName: AppConstants.MongoCms);\nvar queryLinksArray = builder.Configuration.GetRequiredSection(\"QueryLinksArray\").Get\u003cQueryLinks[]\u003e()!;\nbuilder.Services.AddMongoDbQuery(queryLinksArray);\n```\n\nDefine `QueryLinksArray` in `appsettings.json` to specify MongoDB queries:\n```json\n{\n  \"QueryLinksArray\": [\n    { \"Query\": \"post\", \"Collection\": \"post\" },\n    { \"Query\": \"post_test_mongo\", \"Collection\": \"post\" }\n  ]\n}\n```\nThe WebApp will now query MongoDB directly for the specified collections.\n\n\u003c/details\u003e\n\n\n\n---\n## Integrating it into Your Project\n\n\u003cdetails\u003e\n\u003csummary\u003e\nFollow these steps to integrate FormCMS into your project using a NuGet package.\n\u003c/summary\u003e\n\nYou can reference the code from https://github.com/FormCMS/FormCMS/tree/main/examples\n\n1. **Create a New ASP.NET Core Web Application**.\n\n2. **Add the NuGet Package**:\n   To add FormCMS, run the following command:  \n   ```\n   dotnet add package FormCMS\n   ```\n\n3. **Modify `Program.cs`**:\n   Add the following line before `builder.Build()` to configure the database connection (use your actual connection string):  \n   ```\n   builder.AddSqliteCms(\"Data Source=cms.db\");\n   var app = builder.Build();\n   ```\n   Currently,  FormCMS supports `AddSqliteCms`, `AddSqlServerCms`, and `AddPostgresCms`.\n\n4. **Initialize FormCMS**:\n   Add this line after `builder.Build()` to initialize the CMS:  \n   ```\n   await app.UseCmsAsync();\n   ```  \n   This will bootstrap the router and initialize the FormCMS schema table.\n\n\u003c/details\u003e\n\n---\n## Adding Business Logic\n\n\u003cdetails\u003e\n\u003csummary\u003e\nLearn how to customize your application by adding validation logic, hook functions, and producing events to Kafka.\n\u003c/summary\u003e\n\n### Adding Validation Logic with Simple C# Expressions\n\n#### Simple C# Validation\nYou can define simple C# expressions in the `Validation Rule` of attributes using [Dynamic Expresso](https://github.com/dynamicexpresso/DynamicExpresso). For example, a rule like `name != null` ensures the `name` attribute is not null.\n\nAdditionally, you can specify a `Validation Error Message` to provide users with feedback when validation fails.\n\n#### Using Regular Expressions\n`Dynamic Expresso` supports regular expressions, allowing you to write rules like `Regex.IsMatch(email, \"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\\\.[a-zA-Z0-9-.]+$\")`.\n\n\u003e Note: Since `Dynamic Expresso` doesn't support [verbatim strings](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/tokens/verbatim), you must escape backslashes (`\\`).\n\n---\n\n### Extending Functionality with Hook Functions\n\nTo implement custom business logic, such as verifying that a `teacher` entity has valid email and phone details, you can register hook functions to run before adding or updating records:\n\n```csharp\nvar registry = app.GetHookRegistry();\n\n// Hook function for pre-add validation\nregistry.EntityPreAdd.Register(\"teacher\", args =\u003e\n{\n    VerifyTeacher(args.RefRecord);\n    return args;\n});\n\n// Hook function for pre-update validation\nregistry.EntityPreUpdate.Register(\"teacher\", args =\u003e\n{\n    VerifyTeacher(args.RefRecord);\n    return args;\n});\n```\n\n---\n\n### Producing Events to an Event Broker (e.g., Kafka)\n\nTo enable asynchronous business logic through an event broker like Kafka, you can produce events using hook functions. This feature requires just a few additional setup steps:\n\n1. Add the Kafka producer configuration:\n   ```csharp\n   builder.AddKafkaMessageProducer(\"localhost:9092\");\n   ```\n\n2. Register the message producer hook:\n   ```csharp\n   app.RegisterMessageProducerHook();\n   ```\n\nHere’s a complete example:\n\n```csharp\nbuilder.AddSqliteCms(\"Data Source=cmsapp.db\").PrintVersion();\nbuilder.AddKafkaMessageProducer(\"localhost:9092\");\nvar app = builder.Build();\nawait app.UseCmsAsync(false);\napp.RegisterMessageProducerHook();\n```\n\nWith this setup, events are produced to Kafka, allowing consumers to process business logic asynchronously.\n\n\u003c/details\u003e\n\n\n\n\n---\n\n## Development Guide\n\n\u003cdetails\u003e\n\u003csummary\u003e\nThe backend is written in ASP.NET Core, the Admin Panel uses React, and the Schema Builder is developed with jQuery.\n\u003c/summary\u003e\n\n### Overview  \nThe system comprises three main components:  \n1. **Backend** - Developed in ASP.NET Core.  \n2. **Admin Panel** - Built using React.  \n3. **Schema Builder** - Created with jQuery.  \n\n#### System Diagram  \n![System Overview](https://raw.githubusercontent.com/formcms/formcms/doc/doc/diagrams/overview.png)\n\n### Repository Links  \n- [**Backend Server**](https://github.com/formcms/formcms/tree/main/server/FormCMS)  \n- [**Admin Panel Sdk**](https://github.com/FormCms/FormCmsAdminSdk)\n- [**Admin Panel App**](https://github.com/FormCms/FormCmsAdminApp)\n- [**Schema Builder**](https://github.com/formcms/formcms/tree/main/server/FormCMS/wwwroot/schema-ui)  \n\n---\n\n### Backend Server\n#### Tools\n- **ASP.NET Core**\n- **SqlKata** ([SqlKata Documentation](https://sqlkata.com/))\n\n#### Architecture\nThe backend is influenced by Domain-Driven Design (DDD).  \n![DDD Architecture](https://raw.githubusercontent.com/formcms/formcms/doc/doc/diagrams/ddd-architecture.png)\n\nCode organization follows this diagram:  \n![Backend Code Structure](https://raw.githubusercontent.com/formcms/formcms/doc/doc/diagrams/C4_Elements-Backend.png)\n\n##### Core (Domain Layer)\nThe **Core layer** encapsulates:\n- **Descriptors**: Includes `Entity`, `Filter`, `Sort`, and similar components for building queries.\n- **HookFactory**: Maintains a global `Hook Registry`, enabling developers to integrate custom plugins.\n\n\u003e **Note**: The Core layer is independent of both the Application and Infrastructure layers.\n\n##### Application Layer\nThe **Application layer** provides the following functionalities:\n1. **CMS**: Entity CRUD, GraphQL Queries, and Page Designer.\n2. **Auth**: Manages permissions and roles.\n3. **DataLink**: Integrates DocumentDB and Event Streams for scalability.\n\n\u003e Includes `Builders` to configure Dependency Injection and manage Infrastructure components.\n\n##### Infrastructure Layer\nThe **Infrastructure layer** defines reusable system infrastructural components.\n- Application services depend on interfaces instead of implementations.\n- Components are designed for portability and can be reused across other projects.\n\n##### Util Layer\nA separate **Util component** contains static classes with pure functions.\n- Accessible across all layers.\n\n---\n\n### Admin Panel UI\n#### Tools\n- **React**\n- **PrimeReact** ([PrimeReact UI Library](https://primereact.org/))\n- **SWR** ([Data Fetching/State Management](https://swr.vercel.app/))\n\n#### Admin Panel Sequence\n![Admin Panel Sequence](https://raw.githubusercontent.com/formcms/formcms/doc/doc/diagrams/admin-panel-sequence.png)\n\n---\n\n### Schema Builder UI\n#### Tools\n- **jsoneditor** ([JSON Editor Documentation](https://github.com/json-editor/json-editor))\n\n\u003c/details\u003e  \n\n---\n## Testing Strategy\n\u003cdetails\u003e\n\u003csummary\u003e\nThis chapter describes the systems' automated testing strategy\n\u003c/summary\u003e\n\nFavors integration testing over unit testing because integration tests can catch more real-world issues.\nFor example, when inserting a record into the database, multiple modules are involved:\n- `EntitiesHandler`\n- `EntitiesService`\n- `Entity` (in core)\n- `Query executors` (e.g., `SqlLite`, `Postgres`, `SqlServer`)\n\nWriting unit tests for each function and mocking its upstream and downstream services can be tedious.\nInstead, FormCMS focuses on checking the input and output of RESTful API endpoints in its integration tests.\n\n### Integration Testing for FormCMS.Course `/formcms/server/FormCMS.Course.Tests`\nThis project focuses on verifying the functionalities of the FormCMS.Course example project.\n\n### New Feature Testing `/formcms/server/FormCMS.App.Tests`\nThis project is dedicated to testing experimental functionalities, like MongoDB and Kafka plugins.\n\n\u003c/details\u003e","funding_links":[],"categories":["CMS","🗒️ Cheatsheets"],"sub_categories":["📦 Libraries"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffluent-cms%2Ffluent-cms","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffluent-cms%2Ffluent-cms","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffluent-cms%2Ffluent-cms/lists"}