# Guide

# About Aca.ts

ACA.ts is a fullstack development toolkit, which can automatically synchronize database schema and generate database query APIs according to a typescript ORM document; Automatically generate the frontend access backend APIs according to the backend functions and database query APIs, so that the backend code can be developed on the frontend.

# Features

  • Support database transactions
  • Support mapping of database tables and columns (map)
  • Support multiple databases
  • Supports table and function namespaces
  • Automatic generation of frontend and backend data interaction interfaces (like RPC, no need to define routes)
  • Framework independent, applicable to any existing frontend and backend JS frameworks
  • The backend code is developed and tested on the frontend, no communication with the backend
  • The TS code prompt is perfect and can be developed without looking at the document

# Quick started

Prerequisites: Please install (if not installed) node.js(v18.0.0^) and ts-node.
Run the following commands in sequence in the terminal:

$ npm install -g aca.ts
$ aca create acaProject && cd acaProject
$ aca server
$ aca client
$ aca up

Then, start server and client in two seperate terminals.
Run in the server directory:

$ npm run dev

Run in the client directory:

$ npm start

The example has a built-in Better-Sqlite3 database. Clicking the button on the opened page will create a database user table record and return its id.
For specific code, please visit App.tsx page, which shows how to use database API and RPC functions on the frontend.

# Configuration

config.json is located in the .aca directory, and the specific fields are described as follows:

  • ORM: The name of the typescript ORM file you wrote yourself, and the ORM file must be stored in the .aca directory

  • databases: Database related configuration (supports multiple databases), the field name is the variable name specified when the database model is defined in the ORM file. The default is: default, and the database configurations that can be specified are:

    • tableNameWithNamespace:The default is true. When defining the ORM, it supports the use of namespaces to classify each table definition. When creating a database, aca should also add the namespace prefix. If it is false, the name of the database table is just the name of the class you defined.
    • onlyApi: The default is false. When you need to use a database provided by a third party in your project, you cannot modify such a database schema, but only generate an API without synchronizing the database schema
    • idDefaultType:The default is "cuid". When the id type is not specified, this type is used by default
    • foreignKeyConstraint: Defaults to true, foreign key tables use foreign key constraints
    • onUpdate onDelete:The default is "cascade". When the primary key is modified or deleted, the action of the foreign key
    • connectOption:Database connection options, refer to Knex.js connection options
  • serverApps;It is a dictionary object. All backend apps under the project are registered here. You can also fill in the relevant information of the backend apps you created here (the same effect as adding name by aca add)

    • Field name: the name of the app,
    • apiDir:The path name of the generated API (relative to the App), the default is: "src/aca.server"
  • clientApps:It is a dictionary object. All frontend apps under the project are registered here. You can also fill in the relevant information of the frontend apps you created here (the same effect as adding name by aca add)

    • Field name: the name of the app,
    • apiDir:The path name of the generated API (relative to the App), the default is "src/aca.client"
    • allowRPCs:It is an array, indicating that the app can only access the list of RPC servers in the backend. If there is no such field, the RPC of all backend apps under the project can be accessed by default.

# CLI

Aca.ts has the following commands:

  1. Create a new project:
   $ aca create [projectName]

projectName: Your own project name
Running this command will create a projectName folder in the current directory, which contains a folder named .aca, and the configuration file config.json is located under the .aca folder
The following commands need to be run in the projectName directory or the created app directory.

  1. Create a backend app:
   $ aca server <appName>

appName default:server

Create a simple backend service based on the koa framework

  1. Create a frontend app:
   $ aca client <appName>

appName default:client
Call create-react-app to create a react app

  1. Add the app you created to the project (make sure the app is in the aca project directory):
   $ aca add <appName> --server --client --apiDir <path>

appName: The name of the app you created for yourself
--server(-s):indicates that it is a backend App
--client(-c):indicates that it is a frontend App
--apiDir(-a):The path where the generated API is stored,
path:Relative to the App path, the default is src/aca.server or src/aca.client,If -s and -c are specified at the same time, -a uses the default path

  1. Generate API:
   $ aca up

Generate frontend and backend API and automatically synchronize database schemas (If there is any change). The API is stored in the directory specified by each App in the project

  1. Database schema rollback:
   $ aca rollback

Restore the database schema to the last change. Note: the data of the deleted column cannot be restored

# ORM

The definition of ORM can be written by referring to example-blog.ts, the details are as follows:
Supported scalar data types are:id、boolean、string、int、float、object(json object)、Date、enum(enumeration type)

  • id:Each table has only one id and supports 5 types:cuid uuid autoincrement int string, written as:id['autoincrement'],if only id is written, it is specified according to .aca/config.json/idDefaultType. If not exists, the default is:cuid

  • boolean:boolean type(true false)

  • string:string type

  • int:integer type

  • float:floating point type

  • object:json object type

  • Date:datetime type

  • enum:custom enum type

  • scalar arrays:currently only support pg

  • Define a database: use a top-level namespace to represent a database, the name of the namespace points to one of your databases, contains an optional constant definition and multiple class definitions, class definitions can also be organized using namespaces

  • Optional constant definition: the type is string whose value is a field name defined in .aca/config.json/database. If there is no such definition, the default is: default

  • To define a table: use the class definition syntax:

    • Database table name: class name
    • Column name of database table: field name
    • Data type of column: Field Type
    • Relationship between tables: field name, type is the name of the table
  • Decorator:

    • class decorator:integrated in $,with map、id、unique、index、deprecated
      • @$.map(name);the name of the database table is:name
      • @$.id(fields):specify id, fields: an array of column names
      • @$.unique(fields):specified as a unique constraint, fields: an array of column names
      • @$.index(fields):Specify the index to build the table, fields: an array of column names
      • @$.deprecated;Indicates that the table has been obsolete
    • Class member decorator:integrated in the $_ decorator,including:unique、index、dbType、createdAt、updatedAt、foreign、deprecated
      • @$_.unique:indicates that the field is a unique field
      • @$_.index:indicates that the field is an index field
      • @$_.dbType(name):defines the database type of the field, name must be the name of the type supported by the database, such as varchar(23)
      • @$_.createdAt、@$_.updatedAt:timestamp of data record creation and modification, automatically populated by the system
      • @$_.foreign(obj):see the relationship between tables
      • @$_.deprecated;indicates that the column has been deprecated
  • Relationships between tables: Both relational tables have a class field that points to the opposite table, and the type of the field is the table name of the opposite table. If it contains a namespace, the namespace prefix is ​​also required.
    There are one-to-one, one-to-many, and many-to-many relationships between tables
    One-to-one and one-to-many relationships need to specify @$_.foreign(obj) in the corresponding foreign key field, and the foreign key field will also be established in this table
    How to write obj :

    • If obj is a string or string array, then obj is the foreign key name, which refers to a field of type id in the primary key table by default
    • If obj is an object, it has the following fields:
      • keys: is a string or array of strings whose members are foreign key names (aca.ts are automatically added to the database table
      • references:is a string or array of strings, referring to the id or unique field name of the primary key table
      • onUpdate、onDelete:When the primary key is modified or deleted, the action of the foreign key table, if default, adopts the corresponding setting under the corresponding field of .aca/config.json/databases
    • One-to-one relationship: at least one of the primary key field or foreign key field is optional (add a question mark (?) after the field name)
    • One-to-many relationship: add the array type after the primary key table type (add square brackets ([]) after the type), and the foreign key field is automatically optional.
    • Many-to-many relationship: The types of the two tables are both array types. At this time, aca.ts will automatically create a many-to-many relationship mapping table, which is not visible to the outside world. If it needs to be visible, you need to explicitly define the many-to-many relationship mapping table
      Note: When there are multiple relationships between two tables, there is at most one set of relationship without specifying the object field name in the type, and the other relationships must specify the field name of the opposite table in the type

# Database API

  • ts types:$Enum $TB $TbOper $ApiBridge

    • $Enum:All custom enumeration types are concentrated here, referenced in the code as follows::$Enum['enum name']

    • $TB:The type definitions of all table fields are concentrated here. If it is a relation type, it is: $Relation<'relation table name', 'relation table field name', 'relation type'> Relationship types: toOne (one-to-one relationship), toMany (to-many relationship), M2M (many-to-many relationship) The type of reference to a table in the code is as follows: $TB['top-level namespace name_table name']

    • $TbOper:The operation type definitions for the table,including:unique where insert update updateMany select selectScalar,use as folllows:
      $TbOper<'top-level namespace name_table name'>['where']

    • $ApiBridge:Data format definition for connecting frontend and backend:

      export type $ApiBridge =
        | {
            kind: 'rpc'
            method: string[]
            args: any
          }
        | {
            kind: 'orm'
            query: string
            dbVar: string
            method: string[]
            args: any
          }
        | {
            kind: 'raw'
            dbVar: string
            args: any
          }
      
  • Database API: For all defined ORM, generate an instantiated class object each, the object name is the top-level namespace name, and generate processing methods for each table, including:

    • findOne

    • findFirst

    • findMany

    • insert

    • upsert

    • update

    • updateMany

    • delete

    • deleteMany

    • aggregate function:count countDistinct sum sumDistinct avg avgDistinct max mix,centralized in the $namespace of each table

    • raw:Generate a raw function for each database, represented by $.raw access method:<top-level namespace name>.<table name>.<method>,the parameters of each method are as follows:

      // These methods and types are the same on the frontend and backend
      type findOne<Tb extends keyof $TB> = {
        where: $TbOper<Tb>['unique']
        select?: $TbOper<Tb>['select']
        sql?: true
      }
      findFirst<Tb extends keyof $TB> = {
        where?: $TbOper<Tb>['where']
        select?: $TbOper<Tb>['select']
        sql?: true
        orderBy?: $Enumerable<{ [K in $ScalarColumns<Tb>]?: $Order }>
      }
      findMany<Tb extends keyof $TB> ={
        where?: $TbOper<Tb>['where']
        select?: $TbOper<Tb>['select']
        distinct?: '*' | $Enumerable<$ScalarColumns<Tb>>
        limit?: number
        offset?: number
        sql?: boolean
        orderBy?: $Enumerable<{ [K in $ScalarColumns<Tb>]?: $Order }>
      }
      insert<Tb extends keyof $TB> = {
        data: $TbOper<Tb>['insert'] | $TbOper<Tb>['insert'][]
        select?: $TbOper<Tb>['select']
        sql?: true
      }
      upsert<Tb extends keyof $TB> = {
        where: $UN[Tb]
        insert: $TbOper<Tb>['insert']
        update?: $TbOper<Tb>['update']
        select?: $TbOper<Tb>['select']
        sql?: true
      }
      update<Tb extends keyof $TB> = {
        where: $UN[Tb]
        data: $TbOper<Tb>['update']
        select?: $TbOper<Tb>['select']
        sql?: true
      }
      updateMany<Tb extends keyof $TB> = {
        where?: $TbOper<Tb>['where']
        data: $TbOper<Tb>['updateMany']
        select?: $TbOper<Tb>['selectScalar']
        sql?: true
      }
      delete<Tb extends keyof $TB> ={
        where: $UN[Tb]
        select?: $TbOper<Tb>['selectScalar']
        sql?: true
      }
      deleteMany<Tb extends keyof $TB> = {
        where: $TbOper<Tb>['where']
        select?: $TbOper<Tb>['selectScalar']
        sql?: true
      }
      // Aggregate function
      aggregate<Tb extends keyof $TB> = {
        select: {
          [K in keyof Required<$TB[Tb]>]?: number extends $TB[Tb][K] ? K : never
        }[keyof $TB[Tb]]
        where?: $TbOper<Tb>['where']
        sql?: true
      }
      
      
  • Transactions: Database transactions are used as follows:

  const trx = <top-level namespace name>.transaction()
  // Note: be sure to use commit() and rollback()
  try {
    await trx.<table name>.<method>
    await trx.commit()
  } catch(e){
    await trx.rollback()
  }

Note:Frontend also has transactions, but it cannot implement real transactions. It is just to write code in a transactional way so that code changes can be reduced when the code is migrated to the backend in the future!

  • $ApiBridge:This method is to bridge the data transmitted from frontend to backend. It will be parsed into the same method name and parameters of the frontend, then call the same method of backend end, and return the result

# RPC function

RPC function is defined in the backend and needs to be stored in the RPC folder in the API directory (eg:src/aca.server/RPC),which is written as:

export async function fn(ctx: any, param: string): stirng {
  return param
}

Special note:

  1. The first parameter of the function is the context, which will be omitted when the frontend function of the same name is generated
  2. If there are multiple files in the RPC directory, aca.ts will automatically rewrite the index.ts file in the RPC directory to export the reference of other pages. At this time, the RPC function cannot be written in index.ts
  3. Support namespace

# Safety

  • The frontend database API is only recommended for use in the development phase and is prohibited in the production environment
  • Access to backend data in the production environment must undergo strict data verification
  • Production web access uses https
  • Access backend with token
  • Raw function is prohibited in production environment

# Development advice

  • When doing web development, try to concentrate the methods for accessing the backend in one folder, and do not directly embed calling database methods in the frontend logic
  • If a logic needs to access the backend database multiple times, try to write it as a transaction. After the code is stable, move the code to the backend and write it as an RPC function for the frontend to call
  • It is recommended to write the function of the frontend calling the backend as follows so that the code changes are minimal when migrating to the backend:
// ctx is the context of the backend, and the frontend can define a variable to simulate the context
async function fn(param) {
  // When the fn is migrated to the back end and becomes an RPC function
  // Uncomment the following line and call the backend function
  // return $RPC.fn(param)
  let ctx
  const trx = <database>.transaction()
  try {
    trx.<tableName>.<methodName>()
    await trx.commit()
  } catch (e) {
    await trx.rollback()
  }
}
// The above code is migrated to the backend and made into an RPC function with the following changes (using ctx as the input parameter of the function):
async function fn(ctx, param) {
  const trx = <database>.transaction()
  try {
    trx.<tableName>.<methodName>()
    await trx.commit()
  } catch (e) {
    await trx.rollback()
  }
}

The code and documentation are still in the internal testing stage, and valuable comments are welcome