
Research
PyPI Package Disguised as Instagram Growth Tool Harvests User Credentials
A deceptive PyPI package posing as an Instagram growth tool collects user credentials and sends them to third-party bot services.
Welcome to FluentCMS! š
FluentCMS makes content management seamless with its powerful GraphQL API and intuitive drag-and-drop page design features.
If you'd like to contribute, please check out our CONTRIBUTING guide.
Enjoying FluentCMS? Donāt forget to give us a ā and help us grow!
Fluent CMS is an open-source Content Management System designed to simplify and accelerate web development workflows. While it's particularly suited for CMS projects, it is also highly beneficial for general web applications, reducing the need for repetitive REST/GraphQL API development.
Effortless CRUD Operations: Fluent CMS includes built-in RESTful APIs for Create, Read, Update, and Delete (CRUD) operations, complemented by a React-based admin panel for efficient data management.
Powerful GraphQL Queries: Access multiple related entities in a single query, enhancing client-side performance, security, and flexibility.
Drag-and-Drop Page Designer: Build dynamic pages effortlessly using the integrated page designer powered by Grapes.js and Handlebars. Easily bind data sources for an interactive and streamlined design experience.
admin@cms.com
Admin1!
This section provides detailed guidance on developing a foundational online course system, encompassing key entities: teacher
, course
, lesson
,skill
, and material
.
The Teachers
table maintains information about instructors, including their personal and professional details.
Field | Header | Data Type |
---|---|---|
id | ID | Int |
firstname | First Name | String |
lastname | Last Name | String |
email | String | |
phone_number | Phone Number | String |
image | Image | String |
bio | Bio | Text |
The Courses
table captures the details of educational offerings, including their scope, duration, and prerequisites.
Field | Header | Data Type |
---|---|---|
id | ID | Int |
name | Course Name | String |
status | Status | String |
level | Level | String |
summary | Summary | String |
image | Image | String |
desc | Description | Text |
duration | Duration | String |
start_date | Start Date | Datetime |
The Lessons
table contains detailed information about the lessons within a course, including their title, content, and associated teacher.
Field | Header | Data Type |
---|---|---|
id | ID | Int |
name | Lesson Name | String |
description | Description | Text |
teacher | Teacher | Int (Foreign Key) |
course | Course | Int (Foreign Key) |
created_at | Created At | Datetime |
updated_at | Updated At | Datetime |
The Skills
table records competencies attributed to teachers.
Field | Header | Data Type |
---|---|---|
id | ID | Int |
name | Skill Name | String |
years | Years of Experience | Int |
created_by | Created By | String |
created_at | Created At | Datetime |
updated_at | Updated At | Datetime |
The Materials
table inventories resources linked to courses.
Field | Header | Data Type |
---|---|---|
id | ID | Int |
name | Name | String |
type | Type | String |
image | Image | String |
link | Link | String |
file | File | String |
After launching the web application, locate the Schema Builder menu on the homepage to start defining your schema.
Course
entity, add attributes such as name
, status
, level
, and description
.To 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
.
Attribute | Value |
---|---|
Field | teacher |
DataType | Lookup |
DisplayType | Lookup |
Options | Teacher |
Description: When a course is created or modified, a teacher record can be looked up and linked to the course.
To 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.
Attribute | Value |
---|---|
Field | lessons |
DataType | Collection |
DisplayType | EditTable |
Options | Lesson |
Description: When managing a course , you can manage lessons of this course.
To 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.
Attribute | Value |
---|---|
Field | materials |
DataType | Junction |
DisplayType | Picklist |
Options | Material |
Description: When managing a course, you can select multiple material records from the Material
table to associate with the course.
The last chapter introduced how to model entities, this chapter introduction how to use Admin-Panel to manage data of those entities.
The Admin Panel supports various UI controls to display attributes:
"text"
: Single-line text input.
"textarea"
: Multi-line text input.
"editor"
: Rich text input.
"number"
: Single-line text input for numeric values only.
"datetime"
: Datetime picker for date and time inputs.
"date"
: Date picker for date-only inputs.
"image"
: Upload a single image, storing the image URL.
"gallery"
: Upload multiple images, storing their URLs.
"file"
: Upload a file, storing the file URL.
"dropdown"
: Select an item from a predefined list.
"multiselect"
: Select multiple items from a predefined list.
"lookup"
: Select an item from another entity with a many-to-one relationship (requires Lookup
data type).
"picklist"
: Select multiple items from another entity with a many-to-many relationship (requires Junction
data type).
"edittable"
: Manage items of a one-to-many sub-entity (requires Collection
data type).
Below is a mapping of valid DataType
and DisplayType
combinations:
DataType | DisplayType | Description |
---|---|---|
Int | Number | Input for integers. |
Datetime | Datetime | Datetime picker for date and time inputs. |
Datetime | Date | Date picker for date-only inputs. |
String | Number | Input for numeric values. |
String | Datetime | Datetime picker for date and time inputs. |
String | Date | Date picker for date-only inputs. |
String | Text | Single-line text input. |
String | Textarea | Multi-line text input. |
String | Image | Single image upload. |
String | Gallery | Multiple image uploads. |
String | File | File upload. |
String | Dropdown | Select an item from a predefined list. |
String | Multiselect | Select multiple items from a predefined list. |
Text | Multiselect | Select multiple items from a predefined list. |
Text | Gallery | Multiple image uploads. |
Text | Textarea | Multi-line text input. |
Text | Editor | Rich text editor. |
Lookup | Lookup | Select an item from another entity. |
Junction | Picklist | Select multiple items from another entity. |
Collection | EditTable | Manage items of a sub-entity. |
The List Page displays entities in a tabular format, supporting sorting, searching, and pagination for efficient browsing or locating of specific records.
Sort records by clicking the ā
or ā
icon in the table header.
Apply filters by clicking the Funnel icon in the table header.
Detail page provides an interface to manage single record.
date
,image
, gallery
, muliselect
, dropdown
,lookup
,picklist
,edittable
FluentCMS simplifies frontend development by offering robust GraphQL support.
To get started, launch the web application and navigate to /graph
. You can also try our online demo.
For each entity in FluentCMS, two GraphQL fields are automatically generated:
<entityName>
: Returns a record.<entityNameList>
: Returns a list of records.**Single Course **
{
course {
id
name
}
}
**List of Courses **
{
courseList {
id
name
}
}
You can query specific fields for both the current entity and its related entities. Example Query:
{
courseList{
id
name
teacher{
id
firstname
lastname
skills{
id
name
}
}
materials{
id,
name
}
}
}
Value Match
in FluentCMSFluentCMS 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.
Filter by a Single Value Example:
{
courseList(idSet: 5) {
id
name
}
}
Filter by Multiple Values Example:
{
courseList(idSet: [5, 7]) {
id
name
}
}
Operator Match
in FluentCMSFluentCMS supports advanced filtering options with Operator Match
, allowing users to combine various conditions for precise queries.
matchAll
Example:Filters where all specified conditions must be true.
In this example: id > 5 and id < 15
.
{
courseList(id: {matchType: matchAll, gt: 5, lt: 15}) {
id
name
}
}
matchAny
Example:Filters where at least one of the conditions must be true.
In this example: name starts with "A"
or name starts with "I"
.
{
courseList(name: [{matchType: matchAny}, {startsWith: "A"}, {startsWith: "I"}]) {
id
name
}
}
Filter Expressions
in FluentCMSFilter Expressions allow precise filtering by specifying a field, including nested fields using JSON path syntax. This enables filtering on subfields for complex data structures.
Example: Filter by Teacher's Last Name This query returns courses taught by a teacher whose last name is "Circuit."
{
courseList(filterExpr: {field: "teacher.lastname", clause: {equals: "Circuit"}}) {
id
name
teacher {
id
lastname
}
}
}
Sorting by a single field
{
courseList(sort:nameDesc){
id,
name
}
}
Sorting by multiple fields
{
courseList(sort:[level,id]){
id,
level
name
}
}
Sort Expressions allow sorting by nested fields using JSON path syntax.
Example: Sort by Teacher's Last Name
{
courseList(sortExpr:{field:"teacher.lastname", order:Desc}) {
id
name
teacher {
id
lastname
}
}
}
Pagination on root field
{
courseList(offset:2, limit:3){
id,
name
}
}
Try it here
Pagination on sub field
{
courseList{
id,
name
materials(limit:2){
id,
name
}
}
}
Variables are used to make queries more dynamic, reusable, and secure.
Value filter
query ($id: Int!) {
teacher(idSet: [$id]) {
id
firstname
lastname
}
}
Operator Match
filterquery ($id: Int!) {
teacherList(id:{equals:$id}){
id
firstname
lastname
}
}
Filter Expression
query ($years: String) {
teacherList(filterExpr:{field:"skills.years",clause:{gt:$years}}){
id
firstname
lastname
skills{
id
name
years
}
}
}
query ($sort_field:TeacherSortEnum) {
teacherList(sort:[$sort_field]) {
id
firstname
lastname
}
}
query ($sort_order: SortOrderEnum) {
courseList(sortExpr:{field:"teacher.id", order:$sort_order}){
id,
name,
teacher{
id,
firstname
}
}
}
query ($offset:Int) {
teacherList(limit:2, offset:$offset) {
id
firstname
lastname
}
}
If you want a variable to be mandatory, you can add a !
to the end of the type
query ($id: Int!) {
teacherList(id:{equals:$id}){
id
firstname
lastname
}
}
Explore the power of FluentCMS GraphQL and streamline your development workflow!
Realtime queries may expose excessive technical details, potentially leading to security vulnerabilities.
Saved Queries address this issue by abstracting the GraphQL query details. They allow clients to provide only variables, enhancing security while retaining full functionality.
OperationName
as the Saved Query IdentifierIn FluentCMS, 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
:
query TeacherQuery($id: Int) {
teacherList(idSet: [$id]) {
id
firstname
lastname
skills {
id
name
}
}
}
FluentCMS generates two API endpoints for each saved query:
List Records:
https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery
Single Record:
https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery/single/
The Saved Query API allows passing variables via query strings:
Single Value:
https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery/?id=3
Array of Values:
https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery?id=3&id=4
This passes [3, 4]
to the idSet
argument.
Saved Query
Beyond performance and security improvements, Saved Query
introduces enhanced functionalities to simplify development workflows.
offset
Built-in variables offset
and limit
enable efficient pagination. For example:
https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery?limit=2&offset=2
offset
Pagination for SubfieldsTo 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
:
https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery?skills.limit=2
cursor
For large datasets, offset
pagination can strain the database. For example, querying with offset=1000&limit=10
forces the database to retrieve 1010 records and discard the first 1000.
To address this, Saved Query
supports cursor-based pagination, which reduces database overhead.
Example response for https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery?limit=3:
[
{
"hasPreviousPage": false,
"cursor": "eyJpZCI6M30"
},
{
},
{
"hasNextPage": true,
"cursor": "eyJpZCI6NX0"
}
]
If hasNextPage
of the last record is true
, use the cursor to retrieve the next page:
https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery?limit=3&last=eyJpZCI6NX0
Similarly, if hasPreviousPage
of the first record is true
, use the cursor to retrieve the previous page:
https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery?limit=3&first=eyJpZCI6Nn0
Subfields also support cursor-based pagination. For instance, querying https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery?skills.limit=2 returns a response like this:
[
{
"id": 3,
"firstname": "Jane",
"lastname": "Debuggins",
"hasPreviousPage": false,
"skills": [
{
"hasPreviousPage": false,
"cursor": "eyJpZCI6MSwic291cmNlSWQiOjN9"
},
{
"hasNextPage": true,
"cursor": "eyJpZCI6Miwic291cmNlSWQiOjN9"
}
],
"cursor": "eyJpZCI6M30"
}
]
To fetch the next two skills, use the cursor:
https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery/part/skills?limit=2&last=eyJpZCI6Miwic291cmNlSWQiOjN9
The page designer utilizes the open-source GrapesJS and Handlebars, enabling seamless binding of GrapesJS Components
with FluentCMS Queries
for dynamic content rendering.
A landing page is typically the first page a visitor sees.
/page/<pagename>
query
.Example:
Landing Page
This page fetches data from:
A detail page provides specific information about an item.
/page/<pagename>/<router parameter>
query
.Example:
Course Detail Page
This page fetches data from:
https://fluent-cms-admin.azurewebsites.net/api/queries/course/one?course_id=22
The homepage is a special type of landing page named home
.
/pages/home
/
, FluentCMS renders /pages/home
by default.Example:
The URL /
will be resolved to /pages/home
unless explicitly overridden.
Understanding the panels in GrapesJS is crucial for leveraging FluentCMS's customization capabilities in the Page Designer UI. This section explains the purpose of each panel and highlights how FluentCMS enhances specific areas to streamline content management and page design.
Style Manager:
Traits Panel:
Layers Panel:
Blocks Panel:
By familiarizing users with these panels and their integration points, this chapter ensures a smoother workflow and better utilization of FluentCMS's advanced page-building tools.
FluentCMS leverages Handlebars expressions for dynamic data binding in pages and components.
Singleton fields are enclosed within {{ }}
to dynamically bind individual values.
Handlebars
supports iterating over arrays using the {{#each}}
block for repeating data structures.
{{#each course}}
<li>{{title}}</li>
{{/each}}
In FluentCMS, you wonāt explicitly see the {{#each}}
statement in the Page Designer. If a block's data source is set to data-list
, FluentCMS automatically generates the loop.
To bind a Data List
to a component, follow these steps:
Data List
component.Field | Description |
---|---|
Query | The query to retrieve data. |
Qs | Query string parameters to pass (e.g., ?status=featured , ?level=Advanced ). |
Offset | Number of records to skip. |
Limit | Number of records to retrieve. |
Pagination | Options for displaying content: |
- Button: Divides content into multiple pages with navigation buttons (e.g., "Next," "Previous," or numbered buttons). | |
- Infinite Scroll: Automatically loads more content as users scroll. Ideal for a single component at the bottom of the page. | |
- None: Displays all available content at once without requiring additional user actions. |
Having established our understanding of Fluent CMS essentials like Entity, Query, and Page, we're ready to build a frontend for an online course website.
home
): The main entry point, featuring sections like Featured Courses and Advanced Courses. Each course links to its respective Course Details page.course/{course_id}
): Offers detailed information about a specific course and includes links to the Teacher Details page.teacher/{teacher_id}
): Highlights the instructorās profile and includes a section displaying their latest courses, which link back to the Course Details page. Home Page
|
|
+-------------------+
| |
v v
Latest Courses Course Details
| |
| |
v v
Course Details <-------> Teacher Details
Content-B
component.course
query./pages/course/{{id}}
. The Handlebars expression {{id}}
is dynamically replaced with the actual course ID during rendering.course/{course_id}
to capture the course_id
parameter from the URL (e.g., /pages/course/20
).{course_id:20}
is passed to the course
query, generating a WHERE id IN (20)
clause to fetch the relevant course data./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
.teacher/{teacher_id}
to capture the teacher_id
parameter from the URL.teacher
query as the pageās data source.ECommerce A
component onto the page.course
query, filtered by the teacherās ID (WHERE teacher IN (3)
).When rendering the page, the PageService
automatically passes the teacher_id
(e.g., {teacher_id: 3}
) to the query.
Fluent CMS employs advanced caching strategies to boost performance.
For detailed information on ASP.NET Core caching, visit the official documentation: ASP.NET Core Caching Overview.
Fluent CMS automatically invalidates schema caches whenever schema changes are made. The schema cache consists of two types:
Entity Schema Cache
Caches all entity definitions required to dynamically generate GraphQL types.
Query Schema Cache
Caches query definitions, including dependencies on multiple related entities, to compose efficient SQL queries.
By default, schema caching is implemented using IMemoryCache
. However, you can override this by providing a HybridCache
. Below is a comparison of the two options:
To implement a HybridCache
, use the following code:
builder.AddRedisDistributedCache(connectionName: CmsConstants.Redis);
builder.Services.AddHybridCache();
Fluent CMS 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:
Query Data Cache
Caches the results of queries for faster access.
Page Cache
Caches the output of rendered pages for quick delivery.
By default, output caching is disabled in Fluent CMS. To enable it, configure and inject the output cache as shown below:
builder.Services.AddOutputCache(cacheOption =>
{
cacheOption.AddBasePolicy(policyBuilder => policyBuilder.Expire(TimeSpan.FromMinutes(1)));
cacheOption.AddPolicy(CmsOptions.DefaultPageCachePolicyName,
b => b.Expire(TimeSpan.FromMinutes(2)));
cacheOption.AddPolicy(CmsOptions.DefaultQueryCachePolicyName,
b => b.Expire(TimeSpan.FromSeconds(1)));
});
// After builder.Build();
app.UseOutputCache();
Fluent CMS leverages Aspire to simplify deployment.
A scalable deployment of Fluent CMS involves multiple web application nodes, a Redis server for distributed caching, and a database server, all behind a load balancer.
+------------------+
| Load Balancer |
+------------------+
|
+-----------------+-----------------+
| |
+------------------+ +------------------+
| Web App 1 | | Web App 2 |
| +-----------+ | | +-----------+ |
| | Local Cache| | | | Local Cache| |
+------------------+ +------------------+
| |
| |
+-----------------+-----------------+
| |
+------------------+ +------------------+
| Database Server | | Redis Server |
+------------------+ +------------------+
Example Web project on GitHub
Example Aspire project on GitHub
To emulate the production environment locally, Fluent CMS leverages Aspire. Here's an example setup:
var builder = DistributedApplication.CreateBuilder(args);
// Adding Redis and PostgreSQL services
var redis = builder.AddRedis(name: CmsConstants.Redis);
var db = builder.AddPostgres(CmsConstants.Postgres);
// Configuring the web project with replicas and references
builder.AddProject<Projects.FluentCMS_Blog>(name: "web")
.WithEnvironment(CmsConstants.DatabaseProvider, CmsConstants.Postgres)
.WithReference(redis)
.WithReference(db)
.WithReplicas(2);
builder.Build().Run();
builder.Configuration.GetValue<string>();
builder.Configuration.GetConnectionString();
By adopting these caching and deployment strategies, Fluent CMS ensures improved performance, scalability, and ease of configuration.
Optimizing query performance by syncing relational data to a document database, such as MongoDB, significantly improves speed and scalability for high-demand applications.
ASP.NET Core's output caching reduces database access when repeated queries are performed. However, its effectiveness is limited when dealing with numerous distinct queries:
For the query below, FluentCMS joins the post
, tag
, category
, and author
tables:
query post_sync($id: Int) {
postList(idSet: [$id], sort: id) {
id, title, body, abstract
tag {
id, name
}
category {
id, name
}
author {
id, name
}
}
}
By saving each post along with its related data as a single document in a document database, such as MongoDB, several improvements are achieved:
Using 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:
export default function () {
const id = Math.floor(Math.random() * 1000000) + 1; // Random id between 1 and 1,000,000
/* PostgreSQL */
// const url = `http://localhost:5091/api/queries/post_sync/?id=${id}`;
/* MongoDB */
const url = `http://localhost:5091/api/queries/post/?id=${id}`;
const res = http.get(url);
check(res, {
'is status 200': (r) => r.status === 200,
'response time is < 200ms': (r) => r.timings.duration < 200,
});
}
/*
MongoDB:
http_req_waiting...............: avg=50.8ms min=774µs med=24.01ms max=3.23s p(90)=125.65ms p(95)=211.32ms
PostgreSQL:
http_req_waiting...............: avg=5.54s min=11.61ms med=4.08s max=44.67s p(90)=13.45s p(95)=16.53s
*/
To enable publishing messages to the Message Broker, use Aspire to add a NATS resource. Detailed documentation is available in Microsoft Docs.
Add the following line to the Aspire HostApp project:
builder.AddNatsClient(AppConstants.Nats);
Add the following lines to the WebApp project:
builder.AddNatsClient(AppConstants.Nats);
var entities = builder.Configuration.GetRequiredSection("TrackingEntities").Get<string[]>()!;
builder.Services.AddNatsMessageProducer(entities);
FluentCMS publishes events for changes made to entities listed in appsettings.json
:
{
"TrackingEntities": [
"post"
]
}
Add the following to the Worker App:
var builder = Host.CreateApplicationBuilder(args);
builder.AddNatsClient(AppConstants.Nats);
builder.AddMongoDBClient(AppConstants.MongoCms);
var apiLinksArray = builder.Configuration.GetRequiredSection("ApiLinksArray").Get<ApiLinks[]>()!;
builder.Services.AddNatsMongoLink(apiLinksArray);
Define the ApiLinksArray
in appsettings.json
to specify entity changes and the corresponding query API:
{
"ApiLinksArray": [
{
"Entity": "post",
"Api": "http://localhost:5001/api/queries/post_sync",
"Collection": "post",
"PrimaryKey": "id"
}
]
}
When changes occur to the post
entity, the Worker Service calls the query API to retrieve aggregated data and saves it as a document.
After adding a new entry to ApiLinksArray
, the Worker App will perform a migration from the start to populate the Document DB.
To enable MongoDB queries in your WebApp, use the Aspire MongoDB integration. Details are available in Microsoft Docs.
Add the following code to your WebApp:
builder.AddMongoDBClient(connectionName: AppConstants.MongoCms);
var queryLinksArray = builder.Configuration.GetRequiredSection("QueryLinksArray").Get<QueryLinks[]>()!;
builder.Services.AddMongoDbQuery(queryLinksArray);
Define QueryLinksArray
in appsettings.json
to specify MongoDB queries:
{
"QueryLinksArray": [
{ "Query": "post", "Collection": "post" },
{ "Query": "post_test_mongo", "Collection": "post" }
]
}
The WebApp will now query MongoDB directly for the specified collections.
Follow these steps to integrate Fluent CMS into your project using a NuGet package.
Create a New ASP.NET Core Web Application.
Add the FluentCMS NuGet Package: To add Fluent CMS, run the following command:
dotnet add package FluentCMS
Modify Program.cs
:
Add the following line before builder.Build()
to configure the database connection (use your actual connection string):
builder.AddSqliteCms("Data Source=cms.db");
var app = builder.Build();
Currently, Fluent CMS supports AddSqliteCms
, AddSqlServerCms
, and AddPostgresCms
.
Initialize Fluent CMS:
Add this line after builder.Build()
to initialize the CMS:
await app.UseCmsAsync();
This will bootstrap the router and initialize the Fluent CMS schema table.
Optional: Set Up User Authorization: If you wish to manage user authorization, you can add the following code. If you're handling authorization yourself or donāt need it, you can skip this step.
builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlite(connectionString));
builder.AddCmsAuth<IdentityUser, IdentityRole, AppDbContext>();
If you'd like to create a default user, add this after app.Build()
:
InvalidParamExceptionFactory.CheckResult(await app.EnsureCmsUser("sadmin@cms.com", "Admin1!", [Roles.Sa]));
Once your web server is running, you can access the Admin Panel at /admin
and the Schema Builder at /schema
.
You can find an example project here.
Learn how to customize your application by adding validation logic, hook functions, and producing events to Kafka.
You can define simple C# expressions in the Validation Rule
of attributes using Dynamic Expresso. For example, a rule like name != null
ensures the name
attribute is not null.
Additionally, you can specify a Validation Error Message
to provide users with feedback when validation fails.
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-.]+$")
.
Note: Since
Dynamic Expresso
doesn't support verbatim strings, you must escape backslashes (\
).
To 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:
var registry = app.GetHookRegistry();
// Hook function for pre-add validation
registry.EntityPreAdd.Register("teacher", args =>
{
VerifyTeacher(args.RefRecord);
return args;
});
// Hook function for pre-update validation
registry.EntityPreUpdate.Register("teacher", args =>
{
VerifyTeacher(args.RefRecord);
return args;
});
To 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:
Add the Kafka producer configuration:
builder.AddKafkaMessageProducer("localhost:9092");
Register the message producer hook:
app.RegisterMessageProducerHook();
Hereās a complete example:
builder.AddSqliteCms("Data Source=cmsapp.db").PrintVersion();
builder.AddKafkaMessageProducer("localhost:9092");
var app = builder.Build();
await app.UseCmsAsync(false);
app.RegisterMessageProducerHook();
With this setup, events are produced to Kafka, allowing consumers to process business logic asynchronously.
The backend is written in ASP.NET Core, the Admin Panel uses React, and the Schema Builder is developed with jQuery.
This chapter describes Fluent CMS's automated testing strategy
Fluent CMS favors integration testing over unit testing because integration tests can catch more real-world issues. For example, when inserting a record into the database, multiple modules are involved:
EntitiesController
EntitiesService
Entity
(in the query builder)SqlLite
, Postgres
, SqlServer
)Writing unit tests for each individual function and mocking its upstream and downstream services can be tedious. Instead, Fluent CMS focuses on checking the input and output of RESTful API endpoints in its integration tests.
However, certain cases, such as the Hook Registry or application bootstrap, are simpler to cover with unit tests.
/fluent-cms/server/FluentCMS.Test
This project focuses on testing specific modules, such as:
/fluent-cms/server/FluentCMS.Blog.Tests
This project focuses on verifying the functionalities of the FluentCMS.Blog example project.
/fluent-cms/server/FluentCMS.App.Tests
This project is dedicated to testing experimental functionalities, like MongoDB and Kafka plugins.
FAQs
A headless CMS with GraphQL API and drag-and-drop page designer.
We found that fluentcms demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago.Ā It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
A deceptive PyPI package posing as an Instagram growth tool collects user credentials and sends them to third-party bot services.
Product
Socket now supports pylock.toml, enabling secure, reproducible Python builds with advanced scanning and full alignment with PEP 751's new standard.
Security News
Research
Socket uncovered two npm packages that register hidden HTTP endpoints to delete all files on command.