API Maker

The framework for AI era

Database APIs

Schema & Validation

Describe a table once. Every write through the schema APIs is converted and checked for you.

The schema of a table is a TypeScript object: the type of each field, how to clean it, its default, its rules and its relations. API Maker detects a first version from your data. From then on, the schema APIs trim, convert, validate, encrypt and number your data before it reaches the database.

How it works

  1. A schema is detected for every table.

    From the columns of a SQL table, or from the first 100 documents of a MongoDB collection. Edit it to add your rules.

  2. Conversions clean the data.

    Values are converted to the field type, trimmed or cased, filled with defaults, and passed through your own conversion function.

  3. Validations check it.

    required, min, max, minLength, maxLength, email, enum and your own validator function run on every save and update.

  4. Keys and numbers are filled in.

    Ids are generated or incremented by API Maker when the request has none.

  5. Bad data is refused.

    The API answers 400 with one error per problem, with the field and the rule it broke.

What you get

Conversions

trim, trimStart, trimEnd, toLowerCase and toUpperCase, default values or default functions, and conversionFun to compute a value, like a slug from a name.

Validations

required, min, max, minLength, maxLength, email and enum, plus validatorFun for rules that need the whole object.

Unique combinations

A SUPER_KEY validation keeps a group of fields unique together, like a slug inside a category, with your own error message.

Numbers and ids

isAutoIncrementByAM counts from your start value with your step. isAutoGenerateByAM creates a UUID, ObjectID, ULID or short UUID.

Encryption and hashing

encryption stores a field encrypted with the key of your secret (AES, RC4 or TripleDES); decrypt it with g.sys.system.decrypt when you need it. hashing stores a one-way HMAC SHA-256 hash.

Safe concurrent updates

Mark a version field with isConcurrencyControlField: an update carrying an old version fails instead of overwriting someone else's change.

An example

One set of rules for every client

A web app, a mobile app and a partner integration all save products. The rules live in the schema, so each of them gets the same checks, the same defaults and the same error messages, and none of them can store a product without a category.

Schema of a products tableproducts.schema.ts
import { EType, ISchemaType, ISchemaProperty } from 'types';import * as T from 'types';const schema: ISchemaType = {    _id: <ISchemaProperty>{ __type: EType.objectId, isPrimaryKey: true,        isAutoGenerateByAM: { valueGeneratorType: 'ObjectID' } },    product_no: <ISchemaProperty>{ __type: EType.number, isAutoIncrementByAM: { start: 1000, step: 1 } },    name: <ISchemaProperty>{ __type: EType.string,        validations: { required: true, minLength: 2, maxLength: 120 }, conversions: { trim: true } },    status: <ISchemaProperty>{ __type: EType.string,        validations: { enum: ['DRAFT', 'ACTIVE', 'ARCHIVED'] },        conversions: { toUpperCase: true, defaults: { defaultValue: 'DRAFT' } } },    category: <ISchemaProperty>{ __type: EType.objectId, validations: { required: true },        database: 'shop', collection: 'categories', column: '_id' },    slug: <ISchemaProperty>{ __type: EType.string },    supplier_tax_id: <ISchemaProperty>{ __type: EType.string, conversions: { encryption: true } },    version: <ISchemaProperty>{ __type: EType.number, isConcurrencyControlField: true },};const validations: T.IDataValidation[] = [{    name: 'unique_slug_per_category',    paths: ['category', 'slug'],    type: T.EDataValidationType.SUPER_KEY,    errorMessage: 'That slug already exists in this category.',}];module.exports = { schema, validations };
What a bad save gets backresponse.json
{    "success": false,    "statusCode": 400,    "errors": [        { "type": "required", "field": "category", "code": 400,          "message": "Please provide valid 'category' field with type 'objectId'." },        { "type": "enumValidation", "field": "status", "code": 400,          "message": "Property 'status' should have any value from [DRAFT, ACTIVE, ARCHIVED]." }    ]}

Messages come from the language pack of the caller, in the language it asks for.

Good to know

  • The rules are applied by the schema APIs. Generated APIs (/api/gen) write what they get.
  • An encrypted field can not be searched by its value. Store a hashed copy next to it when you need to find rows by it.
  • Optimistic concurrency control does not apply to update many, which updates rows directly in the database.

Questions

Can I check data without saving it?

Yes. The system API is-valid-data-for-table (g.sys.system.isValidDataForTable) runs the same conversions and validations and returns the errors.

What happens to a field that is not in the schema?

The schema APIs refuse it with a schemaKeyNotFound error, "Property not found in schema for key …", instead of storing it.

Does the schema also drive relations?

Yes. instance, database, collection or table, and column on a field point to another table. Deep populate, find and join and master save follow them, even into another database.

Is optimistic concurrency control on for my own code?

Not by default: calls from custom code skip it. Pass skipConcurrencyControl: false to use it there too.