The front and rear ends are separated!

When I first knew this, I was confused.

The front end all go out to do SPA, SEO people agree?

Then SSR came.

He said, “The SEOs agreed!”

Anyone’s objections are of no use. Times have changed.

All kinds of spas are coming, and all kinds of little programs are wearing the same clothes as spas.

Do something for them? RxModels was born as a back end that didn’t want to be abandoned and wanted to serve the front end in a more convenient way.

By the way, share how to design and make it, maybe there will be some reference significance. Even if there is something unreasonable, someone will kindly point it out.

Stay open, giving and receiving will happen at the same time, and it’s a two-way process.

What is rxModels?

An open source, generic, low code backend.

With rxModels, you can customize an out-of-the-box back end by simply drawing ER diagrams. Provides field-granular permission management and expression support for instance level permission management.

The main modules include: graphical entity, relationship management interface (RX-Models Client), general JSON format data manipulation interface service (RX-Models), front-end call assistant Hooks library (RXModels-SWR), etc.

RxModels is implemented based on TypeScript, NestJS, TypeORM, and Antv X6.

TypeScript’s strong typing support resolves errors at compile time, and IDE’s strong typing support automatically introduces dependencies, improving development efficiency and saving time.

TypeScript compiled target-runtime JS, a runtime interpretation language, gives rxModels the ability to dynamically publish entities and hot-load instructions. Users can use instructions to implement business logic and extend the generic JSON data interface. Added more usage scenarios to rxModels.

NestJS helps organize your code so that it has a good architecture.

TypeORM is a lightweight ORM library that maps an object model to a relational database. It has the ability to “separate entity definitions,” build databases by passing in JSON descriptions, and provide object-oriented query support for databases. Thanks to this feature, rxModels can be converted from graphical business models to database database models with minimal code.

AntV X6 has a relatively comprehensive function, it supports the node (node) embedded React component, use this personality, use it to draw ER diagram, the effect is very good. If you have time later, you can write another article on how to draw ER diagrams using AntV X6.

In order to follow through with this article, it is best to study the technology stack mentioned in this section in advance.

RxModels target location

It mainly serves small and medium-sized projects.

Why are you afraid to serve big projects?

I don’t dare, the author is an amateur programmer without any experience in big projects.

Comb through data and data mapping

Take a look at the demo to get a visual idea of what the project looks like: the rxModels demo.

Metadata definition

Meta, data that describes the business entity model. A portion of the metadata is converted into TypeORM entity definitions, which generate a database. Another part of the metadata business model is graphical information, such as size and position of entities, position and shape of relationships, etc.

Metadata that needs to be converted into TypeORM entity definitions are:

import { ColumnMeta } from "./column-meta";

/** * Enumeration of entity types, currently only support ordinary entities and enumerated entities, * enumeration entities are similar to syntax sugar, do not map to the database, * enumeration type fields map to the database is string type */
export enum EntityType{
  NORMAL = "Normal",
  ENUM = "Enum",}/** * Entity metadata */
export interface EntityMeta{
  /** Unique identifier */
  uuid: string;

  /** Entity name */
  name: string;

  /** tableName, if tableName is not set, the entity name is converted to a snake-like nomenclature and used as the tableName */tableName? :string;

  /** Entity type */entityType? : EntityType|"";

  /** Field metadata list */
  columns: ColumnMeta[];

  /** Enumeration value JSON, used by enumeration type entities, does not participate in database mapping */enumValues? :any;
}
Copy the code

/** * field type, enumeration, the current version only supports these types, can be extended later */
export enum ColumnType{

  /** Number type */
  Number = 'Number'./** Boolean type */
  Boolean = 'Boolean'./** String type */  
  String = 'String'./** Date type */  
  Date = 'Date'./** JSON */
  SimpleJson = 'simple-json'./** Array type */
  SimpleArray = 'simple-array'./** Enumeration type */
  Enum = 'Enum'
}

/**
* 字段元数据,基本跟 TypeORM Column 对应
*/
export interface ColumnMeta{

  /** Unique identifier */
  uuid: string;

  /** Field name */
  name: string;

  /** Field type */
  type: ColumnType;

  /** Is the primary key */primary? :boolean;

  /** Whether to automatically generate */generated? :boolean;

  /** Can be empty */nullable? :boolean;

  /** Field default */
  default? :any;

  /** is unique */unique? :boolean;

  /** Is the creation date */createDate? :boolean;

  /** is the update date */updateDate? :boolean;

  /** Whether to delete the date, the soft delete function uses */deleteDate? :boolean;

  /** * whether this can be selected at query time, if this is false, then hidden at query time. * The password field will use it */select? :boolean;

  / * * * / lengthlength? :string | number;

  /** ** is used when the entity is an enumeration typeenumEnityUuid? :string;

  /** * ============ The following attributes correspond to TypeORM, but */ is not enabledwidth? :number; version? :boolean;
  readonly? :boolean; comment? :string; precision? :number; scale? :number;
}
Copy the code
/** * Relationship type */
export enum RelationType {
  ONE_TO_ONE = 'one-to-one',
  ONE_TO_MANY = 'one-to-many',
  MANY_TO_ONE = 'many-to-one',
  MANY_TO_MANY = 'many-to-many',}/** ** Relational metadata */
export interface RelationMeta {
  /** Unique identifier */
  uuid: string;

  /** Relationship type */  
  relationType: RelationType;

  /** Identifies the source entity of the relationship */  
  sourceId: string;

  /** Relationship target entity identifier */  
  targetId: string;

  /** Relationship attributes on the source entity */  
  roleOnSource: string;

  /** The relationship property on the target entity */    
  roleOnTarget: string;

  /** ID of the entity that owns the relationship, corresponding to TypeORM JoinTable or JoinColumn */ownerId? :string;
}
Copy the code

Metadata that does not need to be converted to TypeORM entity definitions are:

/** * Package metadata */
export interface PackageMeta{
  /** ID, primary key */id? :number;

  /** Unique identifier */
  uuid: string;

  Package name / * * * /
  name: string;

  /** Entity list */entities? : EntityMeta[];/**ER graph list */diagrams? : DiagramMeta[];/** Relationship list */relations? : RelationMeta[]; }Copy the code
import { X6EdgeMeta } from "./x6-edge-meta";
import { X6NodeMeta } from "./x6-node-meta";

/** * ER graph metadata */
export interface DiagramMeta {
  /** Unique identifier */
  uuid: string;

  /** ER graph name */
  name: string;

  / * * * / node
  nodes: X6NodeMeta[];

  /** connection */
  edges: X6EdgeMeta[];
}

Copy the code
export interface X6NodeMeta{
  /** Corresponding entity identifier UUID */
  id: string;
  /** Node x coordinate */x? :number;
  /** Node y coordinate */y? :number;
  /** Node width */width? :number;
  /** Node height */height? :number;
}
Copy the code
import { Point } from "@antv/x6";

export type RolePosition = {
  distance: number.offset: number.angle: number,}export interface X6EdgeMeta{
  /** Uuid */
  id: string;

  /** fold point data */vertices? : Point.PointLike[];/** Source relationship attribute position tag position */roleOnSourcePosition? : RolePosition;/** Target relationship attribute position tag position */roleOnTargetPosition? : RolePosition; }Copy the code

RxModels has a back-end service that builds databases based on this data.

RxModels has a front end management interface that manages and produces this data.

The service side rx – models

The core of the whole project is built on NestJS. TypeORM installation is required, only normal TypeORM core project is installed, NestJS package is not required.

nest new rx-models

cd rx-models

npm install npm install typeorm
Copy the code

This is just the key installation, other libraries, not to mention.

Specific project has been completed, code address: github.com/rxdrag/rx-m… .

The first version took on the task of technical exploration and only supported MySQL.

Generic JSON interface

Design a set of interfaces and specify the semantics of the interface, just like GraphQL did. This has the advantage of eliminating the need for interface documentation and defining interface versions.

An interface that takes JSON as an argument and returns JSON data can be called a JSON interface.

Query interface

Interface Description:

url: /get/jsonstring... Method: get Return value :{data:any, pagination? :{ pageSize: number, pageIndex: number, totalCount: number } }Copy the code

The URL length is 2048 bytes, which is enough to pass a query string. In the query interface, you can put JSON query parameters in the URL and use the GET method to look up the data.

Putting JSON query parameters in urls has the obvious advantage of allowing clients to cache query results based on urls, such as using SWR libraries.

One of the most important things to watch out for is URL transcoding, otherwise using % like will cause backend errors. Therefore, it is necessary to write a query SDK for the client to encapsulate these transcoding operations.

Query Interface Example

If you pass in the entity name, you can query the instance of the entity. For example, to query all articles (Post), you can write:

{
  "entity": "Post"
}
Copy the code

Select * from articles where id = 1;

{
  "entity": "Post"."id": 1
}
Copy the code

Sort the articles by title and date. Write like this:

{
  "entity": "Post"."@orderBy": {
    "title": "ASC"."updatedAt": "DESC"}}Copy the code

Simply query the title field of the article and write:

{
  "entity": "Post"."@select": ["title"]}Copy the code

You can also write this:

{
  "entity @select(title)": "Post"
}
Copy the code

Take only one record:

{
  "entity": "Post"."@getOne": true
}
Copy the code

Or:

{
  "entity @getOne": "Post"
}
Copy the code

Only look up articles with the word “water” in the title:

{
  "entity": "Post"."title @like": Water "% %"
}
Copy the code

You need a more complex query with an sqL-like expression embedded in it:

{
  "entity": "Post"."@where": "Name %like '% wind %' and..."
}
Copy the code

Too much data, paging, 25 records per page take the first page:

{
  "entity": "Post"."@paginate": [25.0]}Copy the code

Or:

{
  "entity @paginate(25, 0)": "Post"
}
Copy the code

Relational query, image relational medias with articles:

{
  "entity": "Post"."medias": {}}Copy the code

Relational nesting:

{
  "entity": "Post"."medias": {
    "owner": {}}}Copy the code

Add a condition to the relationship:

{
  "entity": "Post"."medias": {
    "name @like": "% %" scenery}}Copy the code

Just take the first five relationships

{
  "entity": "Post"."medias @count(5)": {}}Copy the code

You are smart enough to make further design changes to the interface in this direction.

The thing after the @ sign is called an instruction.

By putting business logic in instructions, interfaces can be extended very flexibly. For example, to add a copyright notice to the bottom of the content, you could define an @addCopyright directive:

{
  "entity": "Post"."@addCopyRight": "content"
}
Copy the code

Or:

{
  "entity @addCopyRight(content)": "Post"
}
Copy the code

Does the directive look like a plug-in?

Since it’s a plugin, give it hot loading capability!

Through the management interface, upload the third party instruction code, you can insert the instruction system.

The first version did not support command uploads, but the architecture was designed to do so, but the interface was not.

Post interface

Interface Description:

Url: / POST method: POST Parameter: JSON Returned value: The object on which the operation succeedsCopy the code

JSON data is passed in through the POST method.

The POST interface is expected to have the ability to pass in a set of object combinations (or tree of objects with relational constraints) and directly synchronize the set of objects to the database.

If an ID field is provided for an object, the existing object is updated; if no ID field is provided, a new object is created.

Post Interface Example

To upload an article with a picture, say:

{
  "Post": {
    "title": "Quietly, I go."."content": "...".// Author association ID
    "author": 1.// Image associated ID
    "medias": [3.5.6. ] }}Copy the code

You can also pass in more than one article at a time

{
  "Post": [{"id": 1."title": "Quietly, I go."."content": "The content has changed..."."author": 1."medias": [3.5.6. ] }, {"title": "As quietly as I come."."content": "..."."author": 1."medias": [6.7.8. ] }}]Copy the code

The first article has an ID field, which is the operation to update the database. The second article does not have an ID field, which is the creation of a new one.

It is also possible to pass in instances of multiple entities, like Post and Media:

{
  "Post": [{...}, {...}],"Media": [{...}]}Copy the code

If an article is associated with a SeoMeta object, create a SeoMeta at the same time:

{
  "Post": {
    "title": "Quietly, I go."."content": "..."."author": 1."medias": [3.5.6. ] ."seoMeta": {"title": "Psalm reading: gently, I walked | poem reading network"."descript": "..."."keywords": "Psalms, interpretation, interpretation of psalms."}}}Copy the code

Passing this parameter creates two objects at the same time and establishes an association between them.

To normally delete this association, write:

{
  "Post": {
    "title": "Quietly, I go."."content": "..."."author": 1."medias": [3.5.6. ] ."seoMeta":null}}Copy the code

This way of saving the article will delete the association with the SeoMeta, but the SeoMeta object is not deleted. Other articles do not need this SeoMeta. If you do not delete it actively, a garbage data will be generated in the database.

To solve this problem, add a @cascade directive to save the article:

{
  "Post @cascade(medias)": {
    "title": "Quietly, I go."."content": "..."."author": 1."medias": [3.5.6. ] ."seoMeta":null}}Copy the code

The @cascade instruction will cascade and delete the associated SeoMeta object.

Can this directive be placed on an association property, written like this?

{
  "Post": {
    "title": "Quietly, I go."."content": "..."."author": 1."medias @cascade": [3.5.6. ] ."seoMeta":null}}Copy the code

It’s better not to do that, because it’s not very easy to use on the client.

Custom directives extend the POST interface. For example, to add a mail service, you can develop a @sendemail directive:

{
  "Post @sendEmail(title, content, [email protected])": {
    "title": "Quietly, I go."."content": "..."."author": 1."medias @cascade": [3.5.6. ] ,}}Copy the code

Assume that the sendEmail directive sends the title and content to the specified mailbox each time the post is saved successfully.

The update interface

Interface Description:

Url: /update method: POST Parameter: JSON Returned value: The object on which the operation succeedsCopy the code

Why create an update interface when the POST interface already has the update function?

Sometimes, you need the ability to batch modify one or more fields, such as marking a specific message as read.

To deal with this scenario, the UPDATE interface is designed. If you want to update the status of all articles to “published” :

{
  "Post": {
    "status": "published"."@ids": [3.5.6. ] ,}}Copy the code

For security reasons, the interface does not provide conditional directives, only @IDS directives (for legacy reasons, the demo does not need the @ symbol, just write IDS directly, which will be changed later).

Delete the interface

Interface Description:

Url: /delete Method: POST Parameter: JSON Returned value: the object to be deletedCopy the code

The DELETE interface, like the UPDATE interface, does not provide conditional instructions and accepts only ids or arrays of ids.

To delete an article, simply write:

{
  "Post": [3.5. ] }Copy the code

Such a delete, like an update, does not delete objects associated with the article. Cascading deletes require the @cascade command.

Cascade to delete SeoMeta, write:

{
  "Post @cascade(seoMeta)": [3.5. ] }Copy the code

The upload interface

Url: /upload method: post FormData headers: {" content-type ": "multipart/form-data; boundary=..." } Return value: RxMedia object generated after successful uploadCopy the code

RxModels better provide online file management service functions, with third-party object management services, such as Tencent Cloud, Ali Cloud, qiniu, etc., combined.

The first version does not implement integration with third-party object management. Files are stored locally and only images are supported.

To manage these uploaded files with the entity RxMedia, the client creates FormData and sets the following parameters:

{
   "entity": "RxMedia"."file":... ."name": "File name"
   }
Copy the code

Now that you’ve covered all the JSON interfaces, it’s time to implement and use them.

Before moving on, let me explain why I chose JSON over any other method.

Why not use oData

I didn’t know much about oData when I started this project.

It is only necessary to design RESTful apis according to OData protocol if Open Data is needed.

The introduction of oData adds to the clutter, if not to opening up the data to other organizations. You need to develop a parsing engine for oData parameters.

OData has been around for a long time, but it’s not as popular as GraphQL.

Why not use GraphQL?

I tried. It didn’t work.

One person, doing open source projects, can only access the existing open source ecosystem. It is impossible to do everything by yourself.

To use GraphQL, you can only use existing open source libraries. Most of the major GraphQL open source libraries are based on code generation. As mentioned in the previous article, I don’t want to do a low-code project based on code generation.

Another reason is that the target is small and medium-sized projects. GraphQL has two problems for these small and medium-sized projects: 1. 2. High learning cost for users.

Some small projects are just three or five pages, pull a small, lightweight back end, and put it together in a very short time, no need to use GraphQL.

The learning cost of GraphQL is not low, and some users of small and medium projects are not willing to pay these learning costs.

Combining these factors, the first version of the interface did not use GraphQL.

What do I need to do with GraphQL?

When I talked to some of my friends, some of them really liked GraphQL. And after several years of development, GraphQL is slowly gaining popularity.

What would you do if you made a similar project using GraphQL?

We need to develop a GraphQL server by ourselves. This server is similar to Hasura and can not use code generation mechanism, but use dynamic running mechanism. Hasura compiles GQL to SQL. You may or may not choose to do so, as long as you can pull the object as required by the GQL query without the compilation process.

In the framework of GraphQL, permissions management, business logic extension and hot loading should be fully considered. This requires a deeper understanding of GraphQL.

If you want to do a low code front-end, then you need to do a special front-end framework, such as Apollo GraphQL front-end library, is not suitable for low code front-end. Because low-code front ends require dynamic type binding, this requirement does not fit particularly well with these front end libraries.

Each task requires a lot of time and energy. It is not a job that can be done by one person, but by a team.

Or one day, there is an opportunity, the author would like to try this aspect.

However, it is unlikely to succeed. GraphQL itself does not stand for much, but if it can bring tangible benefits to users, it is the reason to choose it.

Login authentication interface

Implement two logon-related interfaces using THE JWT authentication mechanism.

Url: /auth/login method: POST Parameter: {username: string, password: string} Returned value: JWT tokenCopy the code
Url: /auth/me method: get Returned value: the current login user. Type: RxUserCopy the code

These two interface implementation, what is not difficult, follow the NestJs documentation to do a line.

Metadata storage

The client produces metadata in the form of an ER graph, stored in the database, and a single entity RxPackage is sufficient:

export interface RxPackage {
  /* id database primary key */
  id: number;

  /** uniquely identifies the UUID, which is useful when metadata is shared between different projects */
  uuid: string;

  Package name / * * * /
  name: string;

  /** All entity metadata of the package is stored in the database as JSON */
  entities: any;

  /** All ER diagrams of the package are stored in the database as JSON */diagrams? :any;

  /** All relationships of the package are stored in the database as JSON */relations? :any;
}
Copy the code

After the data mapping is complete, all the contents of a package seen in the interface correspond to a data record in the RX_Package table.

How is this data used?

If the package is published, make a JSON file according to the database record and put it in the schemas directory. The file name is ${uuid}. JSON.

When the server creates a TypeORM connection, these JSON files are hot loaded and parsed into TypeORM entity definition data.

Application installation interface

The ultimate goal of rxModels is to release a code package that users can install through a graphical interface without touching the code.

Two page wizard, you can complete the installation, need interface:

url: install
method: post parameter: {/** Database type */
  type: string;

  /** Database host */
  host: string;

  /** Database port */
  port: string;

  /** Database schema name */
  database: string;

  /** Data login user */
  username: string;

  /** Database login password */
  password: string;

  /** Super administrator login name */
  admin: string;

  /** Super administrator password */
  adminPassword: string;

  /** Whether to create demo account */
  withDemo: boolean;
}
Copy the code

You also need an interface to check if it is installed:

Url: / IS-installed method: get Return value: {installed: Boolean}Copy the code

Once these interfaces are complete, the functionality of the back end is complete, come on!

Architecture design

Thanks to the elegant Framework of NestJs, the entire back-end service can be divided into the following modules:

  • Auth, common NestJS module, implement login authentication interface. This module is very simple and will not be covered separately later.

  • Package-manage, metadata management publishing module.

  • Install, common NestJS module, to achieve the installation function.

  • Schema, common NestJS Module, manages system metadata and converts the previously defined metadata into entity definitions accepted by TypeORM. The core code is SchemaService.

  • Typeorm, the encapsulation of TypeOrm, provides a Connection with metadata definition, the core code is TypeOrmService, this module has no Controller.

  • Magic, the most core module of the project, universal JSON interface implementation module.

  • Directive, the directive definition module, defines the basic classes used by the directive function, hot load the directive, and provides the directive retrieval service.

  • Cache, all instruction implementation class, the system hot-loads all directives from this directory.

  • Magic-meta, the data format used to parse JSON parameters, mainly uses module magic. Since the directive module also uses such data, to avoid circular dependency between modules, this part of data is extracted as a separate module, and the two modules depend on this module at the same time.

  • Entity-interface is a system sub-data type interface used for TypeScript compiler type recognition. Client code export function exported files, directly copied over. The client also makes a copy of the same code to use.

Package management package – the manage

Provide an interface publishPackages. Publish the metadata of the parameter to the system and synchronize it to the database schema:

  • The schemas are stored in the directory of the root directory. The file name is the uuid +. Json suffix of the package.

  • Notify the TypeORM module to re-create the database connection while synchronizing the database.

Install module

The module contains a seed file, install.sed. json, which contains some preset entities in the metadata format defined above. These data are organized in the System package.

When the client is not completed, a handwritten TS file is used for debugging. After the client is completed, a JSON file is directly exported using the package export function to replace the handwritten TS file. Equivalent to the basic data section, can be self-lift.

The core code of this module is in InstallService, and it is done step by step:

  • Write the database configuration information from the client to the dbconfig.json file in the root directory.

  • Publish the predefined packages in the install.seed.json file. Implement the publishing functionality by calling publishPackages directly.

Metadata management module Schema

This module provides aController named SchemaController. Provide a GET interface /published-schema to get published metadata information.

This published metadata information can be used by the client’s permission setting module, because it only makes sense to set permissions for a published module. A low-code visual editing front end can also use this information for pull-down and selective data binding.

The core SchemaService class provides additional functionality:

  • From the/Schemas directory, the published metadata is loaded.

  • This metadata is organized into a list + tree structure to provide query services by name, by UUID, and so on.

  • Parse the metadata into TypeORM accepted entity definition JSON.

Encapsulation TypeORM

Writing your own ORM library is a lot of work, so you have to use an existing one. TypeORM is a good choice, for one thing, she looks like a young girl, beautiful and energetic. Second, she’s not as puffy as Prisma.

To accommodate the existing TyeORM, some compromises had to be made. This kind of low code project back-end, the ideal way to achieve their own ORM library, completely according to their own needs to implement functions, that may have the feeling of childhood friends, but need a team, not a person can complete.

Since it is a person, then feel at ease to do what a person can do.

TypeORM has only one entry that can pass in an entity definition, createConnection. The metadata needs to be parsed and entity definitions separated before this function is called. The module’s TypeOrmService manages these connections, relying on the Schema module’s SchemaService.

With TypeOrmService, the current connection can be restarted (closed and recreated) to update the database definition. When creating the connection, use the dbconfig.json file created by the install module to get the database configuration. Note that the ormconfig.json file for TypeORM is not used.

Magic module

In magic module, no matter query or update, each interface implementation of the operation, are in a complete transaction.

Should the query interface also be included in a transaction?

Yes, because sometimes the query may contain some simple instructions to manipulate the database, such as query an article and add the number of times it has been read +1.

The operations of the magic module, such as adding, deleting, checking and changing, are restricted by permissions. The core module MagicInstanceService is passed to the instruction. The instruction code can safely use its interface to operate the database without concern about permissions.

MagicInstanceService

MagicInstanceService is an implementation of the interface MagicService. Interface definition:

import { QueryResult } from 'src/magic-meta/query/query-result';
import { RxUser } from 'src/entity-interface/RxUser';

export interface MagicService {
  me: RxUser;

  query(json: any) :Promise<QueryResult>;

  post(json: any) :Promise<any>;

  delete(json: any) :Promise<any>;

  update(json: any) :Promise<any>;
}

Copy the code

The Controller of the Magic module calls this class directly to implement the interface defined above.

AbilityService

Permission management: queries the permission configuration of the entity and field of the current login user.

query

/magic/query /get/json… Interface code.

MagicQuery is the core code that implements the query business logic. It uses MagicQueryParser to parse incoming JSON parameters into a data tree and separate related instructions. Data structures are defined in the /magic-meta/query directory. There is too much code to parse. Check it out by yourself and contact the author if you have any questions.

Of particular note is the parseWhereSql function. This function parses statements similar to SQL Where, using the open source library SQL-WHat-parser.

It is placed in this directory because it is needed by the magic module and also by the Directive module. To avoid circular dependency of the module, it is drawn to this directory independently.

The /magic/query/traverser directory houses traversers for parsing tree data.

MagicQuery builds queries using TypeORM’s QueryBuilder. Key points:

  • Use the QueryDirectiveService of the Directive module to get the instruction handling classes. Instruction processing classes can: 1, build QueryBuilder used conditional statements, 2, filter query results.

  • Get the permission configuration from AbilityService, modify QueryBuilder based on the permission configuration, and filter the fields in the query results based on the permission configuration.

  • Query statements used in QueryBuilder are divided into two parts: 1, statements that affect the number of query results, such as take instructions, paginate instructions. These instructions are simply the result of intercepting the number of instructions; 2. Other query statements that do not have this effect. Because when paging, need to return a total number of records, use the second type of query statement first search database, obtain the total number of records, and then add the first type of query statement to obtain the query results.

post

/magic/ POST directory, the implementation of/POST interface code.

The MagicPost class is the core code that implements the business logic. It uses MagicPostParser to parse incoming JSON parameters into a data tree and separate related instructions. The data structure is defined in the /magic-meta/post directory. It can:

  • Recursive preservation of associated objects, theoretically infinitely nested.

  • Perform permission checks against AbilityService.

  • Use the PostDirectiveService of the Directive module to get the directive handler class. It calls the directive handler before and after saving the instance. See the code for details.

update

/magic/update directory, implementing /update interface code.

Simple functionality, simple code.

delete

/magic/delete directory, implementing /delete interface code.

Simple functionality, simple code.

upload

/magic/upload directory to implement /upload interface code.

Upload has a relatively simple function at present, and some tailoring instructions and other functions can be added later.

Directive module

Command service module. Hot load instructions and provide query services for these instructions.

This module is also relatively simple, with hot loading using the require statement.

There’s not much to say about the other modules on the back end, just look at the code.

The client rx – models – the client

You need a client to manage production and metadata, test common data query interfaces, set entity permissions, install, and so on. Create a normal React project that supports TypeScript.

npx create-react-app rx-models-client--template typescript
Copy the code

The project is complete and is available on GitHub at github.com/rxdrag/rx-m… .

The amount of code is a little bit too much, all explained here, a little bit too much. Just pick out the key points. If you have any questions, please contact the author.

ER diagram – Graphical business model

This module is the core of the client, which looks bluffing, but is not difficult at all. SRC /components/entity-board is the entire code of the module.

Thanks to Antv X6, this module was much easier to build than expected.

X6 acts as a view layer. It is only responsible for rendering solid graphics and relationship lines, and returning some user interaction events. Its operation history function for undo and redo is not used in this project, it has to write all by itself.

Mobx also plays a very important role in this module, managing all the state and taking on some of the business logic. Low code and drag-and-drop projects, Mobx is really good to use and should be recommended.

Define Mobx Observable data

Each of the metadata defined above corresponds to a Mobx Observable class and a root index class. These data contain each other and form a tree structure under the SRC /components/entity-board/store directory.

  • EntityBoardStore, the root node in the tree structure is also the overall state data of the module, which records the following information:
export class EntityBoardStore{
  /** * Whether there is a change, for the unsaved prompt */
  changed = false;

  /** * all packages */
  packages: PackageStore[];

  /** * The current open ER graph */openedDiagram? : DiagramStore;/** * The X6 Graph object currently in use */graph? : Graph;/** * The relationship on the toolbar is pressed to record the specific type */pressedLineType? : RelationType;/** * drag the cursor to draw the line */
  drawingLine: LineAction | undefined;

  /** * The selected node */
  selectedElement: SelectedNode;

  /** * Command mode, undo list */
  undoList: Array<Command> = [];

  /** * Command mode, redo list */
  redoList: Array<Command> = [];

  /** * The constructor passes in the package metadata, which is automatically parsed into a Mobx Observable tree */
  constructor(packageMetas:PackageMeta[]) {
    this.packages = packageMetas.map(
      packageMeta= > new PackageStore(packageMeta,this)); makeAutoObservable(this);
  }
  
  / * * * behind a lot of set method, there is no need for the launched * /. }Copy the code
  • PackageStorePackageMeta = PackageMeta = PackageMeta = PackageMeta = PackageMeta = PackageMeta
export class PackageStore{ id? :number;
  uuid: string;
  name: string;
  entities: EntityStore[] = [];
  diagrams: DiagramStore[] = [];
  relations: RelationStore[] = [];
  status: PackageStatus;
  
  constructor(meta:PackageMeta, public rootStore: EntityBoardStore){
    this.id = meta.id;
    this.uuid = meta? .uuid;this.name = meta?.name;
    this.entities = meta? .entities? .map(meta= >new EntityStore(meta, this.rootStore, this) | | [];this.diagrams = meta? .diagrams? .map(meta= >new DiagramStore(meta, this.rootStore, this) | | [];this.relations = meta? .relations? .map(meta= >new RelationStore(meta, this) | | [];this.status = meta.status;
    makeAutoObservable(this)}/** * omit the set method */./** * Finally provides a way to reverse Store into metadata for sending data to the back end */
  toMeta(): PackageMeta {
    return {
      id: this.id,
      uuid: this.uuid,
      name: this.name,
      entities: this.entities.map(entity= >entity.toMeta()),
      diagrams: this.diagrams.map(diagram= >diagram.toMeta()),
      relations: this.relations.map(relation= >relation.toMeta()),
      status: this.status,
    }
  }
}
Copy the code

By analogy, you can make EntityStore, ColumnStore, RelationStore, and DiagramStore.

The previously defined X6NodeMeta and X6EdgeMeta do not need to create the corresponding Store class, because there is no way to update the X6 view through Mobx’s mechanism.

The DiagramStore mainly provides data for displaying ER diagrams. Add two methods to it:

export type NodeConfig = X6NodeMeta & {data: EntityNodeData};
export type EdgeConfig = X6EdgeMeta & RelationMeta;

export class DiagramStore {.../** * get all nodes of the current ER graph, using mobx update mechanism, * whenever the data changes, the view that called this method will be automatically updated, * parameter is only used to indicate the node that is currently selected, or whether it needs to be connected, * these states affect the view, can be passed directly to each node here */
  getNodes(
    selectedId:string|undefined.isPressedRelation:boolean|undefined
  ): NodeConfig[]

  /** * Get all the lines of the current ER graph, using mobx update mechanism, * whenever the data changes, the view calling this method will be automatically updated */
  getAndMakeEdges(): EdgeConfig[]

}

Copy the code

How do I use Mobx Observable data

Use the React Context to pass the store data defined above to the child components.

Defines the Context:

export const EnityContext = createContext<EntityBoardStore>({} as EntityBoardStore);
export const EntityStoreProvider = EnityContext.Provider;
export const useEntityBoardStore = (): EntityBoardStore= > useContext(EnityContext);
Copy the code

To create the Context:

.const [modelStore, setModelStore] = useState(newEntityBoardStore([])); .return (
    <EntityStoreProvider value = {modelStore}>.</EntityStoreProvider>
  )
Copy the code

To retrieve data, call const rootStore = useEntityBoardStore() directly from the child component.

Tree editor

Use Mui tree control + Mobx object, the code is not complex, interested in words, look over, have a question message or contact the author.

How to use AntV X6

X6 supports installing React components in nodes. Define an EntityView component and embed it in it. X6 code is in this directory:

src/componets/entity-board/grahp-canvas
Copy the code

The business logic is split into many React Hooks:

  • UseEdgeChange, handle the relationship line being dragged

  • UseEdgeLineDraw, handle drawing lines moved

  • UseEdgeSelect, handling the relationship line is selected

  • UseEdgesShow, render relationship lines, including updates

  • UseGraphCreate creates a Grpah object for X6

  • UseNodeAdd, which handles the action of dragging in a node

  • UseNodeChange handles when a physical node is dragged or resized

  • UseNodeSelect, the processing node is selected

  • UseNodesShow, render solid nodes, including updates

Undo, redo

Undo and redo are related not only to the ER graph, but also to the entire Store tree. That said, X6’s undo and redo mechanism doesn’t work, so you have to redo it yourself.

Fortunately, the Command pattern in design mode is relatively simple. It is easy to define some commands and define positive and negative operations. The implementation code is in:

src/componets/entity-board/command
Copy the code

Global status AppStore

According to the method mentioned above, Mobx is used to make a global AppStore for state management, which is used to manage the state of the entire application, such as pop-up success message, pop-up error message, etc.

The code is in the SRC /store directory.

The interface test

The code is in SRC /components/api-board.

Very simple a module, the code should be easy to understand. You use the RXModels-SWR library, so just refer to its documentation.

Monaco’s react- Monaco-Editor package is simple to use. Install the React -app-rewired control.

Monaco is not yet familiar with it, but if you are familiar with it, you can add the following functions, such as input prompts and code verification.

Rights management

The code is in the SRC /components/auth-board directory.

This module is mainly about the back-end data organization and interface definition, with little front-end code, which is completed based on rXModels – SWR library.

Permission definitions support expressions that are similar to SQL statements and have a built-in variable $me to refer to the current logged-in user.

SQL expressions need to be verified when input from the front end, so the open source library SQL-WHat-parser is introduced.

Installation and Login

The installation code is in the SRC /components/install directory.

The login page is SRC /components/login.tsx.

Code can be read at a glance.

Afterword.

This article is quite long, but I am not sure if it has clarified what needs to be said. If you have any questions, please leave a comment or contact the author.

After the demonstration can run up, has taken the risk of being kicked, in a few QQ group hair. I received a lot of feedback. Thank you very much to my warm-hearted friends.

RxModels, finally go out the first step…

First contact with the front end

RxModels is here, and it’s moving to the front end with enthusiasm.

The front men frowned and said, “Stay away, you’re not what we want.”

RxModels said: ‘I will change, I will grow, and one day we will be the best team.’

Next article

“Build a visual low code front end from 0”, it will take a while to complete the front end refactoring.