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.

See it live

Step through it, slow it down, or open the full canvas.

api-maker/features/schema-validationLive
  • Request
  • Your function
  • Passed
  • Refused
Saved
0rows
Refused
0writes
Errors
0in one reply
Your functions
2in the chain

Your table, described in TypeScript. API Maker detects a first schema from your table. You add the types, conversions, rules and relations, and every write through the schema APIs follows them.

How it works

  1. Your table, described in TypeScript.

    API Maker detects a first schema from your table. You add the types, conversions, rules and relations, and every write through the schema APIs follows them.

  2. The data is cleaned first.

    Keys that are not in the schema are refused. Values get their type, "25" becomes 25, and strings are trimmed or cased.

  3. Your own conversion function.

    conversionFun(value, all) gets the value and the whole object, and its return is stored: "Wireless Mouse" becomes the slug wireless-mouse.

  4. Ids, numbers and secrets are filled in.

    Fields are encrypted or hashed with your secret, ids are generated, numbers incremented, defaults set. Then the rules pass and the row is saved.

  5. Your own rules, and every error at once.

    validatorFun(value, all) refuses a value by returning false or throwing: the message you throw goes to the caller, next to the errors of the other rules.

  6. Safe concurrent updates.

    The field marked isConcurrencyControlField is a version. An update sends the version it read: when the row changed meanwhile, API Maker refuses it instead of overwriting the other change.

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.

Relations

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

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';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,        conversions: { conversionFun: () => new Date().getTime() } },};module.exports = { schema };
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.