A schema describes one table or collection : the type of every field, how a value is cleaned before it is saved, what makes it invalid, which fields point to other tables. It is a TypeScript file kept with the table in Git, whatever language your code is written in, and it is what the schema APIs enforce. The generated APIs never read it.
Where
APIs & Logic → Instance API (DB API) : pick the instance, the database and the table, then the schema editor. Generate writes a first schema from the columns or from the documents of the table.
The /api/schema/… APIs : save, master save, update, replace, get with deep, the is valid data for table check.
Gives
Validation, conversions, defaults, relations, the TypeScript interfacedb.<instance>.<database>.I<Table>, the sample payloads of the API testing page and of Swagger.
This is the orders table of the sample shop every new account gets : open it in the admin panel to see the whole thing with the products, customers and orderItems schemas.
A field is a type alone (name: EType.string) or an object with __type and the rules below. A key the schema does not have is refused on save : the error names it.
The examples of the sections below are single fields : paste them inside const schema: ISchemaType = { … }.
MongoDB documents nest : address: { city: EType.string } is an embedded object, tags: [EType.string] an array of strings, lines: [{ sku: EType.string, qty: EType.number }] an array of objects. SQL tables are flat.
Columns of SQL which hold more than one value : a vector is [EType.number], an array of PostgreSQL [EType.string], and a JSON column EType.string which takes objects and lists. Data types has every type of every database.
Conversions run before the validations, on save and on update.
Key
Does
trim, trimStart, trimEnd
Removes the spaces around a string.
toLowerCase, toUpperCase
Changes the case.
conversionFun: (value, row) => …
Your function ; its return value is stored. It runs even when the field is absent from the payload, which is how a updated_at: () => new Date() field is stamped on every save and update.
defaults
{ defaultValue } or { defaultFun: () => … } for a field absent from a save or a replace. defaultValue wins over defaultFun. shouldReplaceNullWithDefault and shouldReplaceEmptyStringWithDefault also replace null and "".
encryption: true
Stored encrypted with encryptionAlgorithm and secret of the secret. The same text encrypts differently every time. The APIs answer the value as it is stored : your code reads it back on the server with g.sys.system.decrypt and the same keys (see below). An object { encryptionAlgorithm, secret } names other keys of the secret.
hashing: true
Stored as an HMAC SHA-256 hash with the nonce (or secret) of the secret : passwords, tax ids. Look a hashed value up with the hash data API. It is fast, with one key for every row and no salt per row : not a password hash like Argon2id or bcrypt.
city_name:<ISchemaProperty>{__type:EType.string,conversions:{conversionFun:(city_name,fullObj)=>city_name?city_name+'_IND':city_name},},updated_at:<ISchemaProperty>{__type:EType.date,conversions:{conversionFun:()=>newDate()},// runs even when the field is absent from the payload},
active:<ISchemaProperty>{__type:EType.boolean,conversions:{defaults:{defaultValue:true,shouldReplaceEmptyStringWithDefault:true,shouldReplaceNullWithDefault:true}},},created_at:<ISchemaProperty>{__type:EType.date,conversions:{defaults:{defaultFun:()=>newDate()}},// defaultValue wins over defaultFun when both are given},
contact_email:<ISchemaProperty>{__type:EType.string,conversions:{encryption:true}},// answered as stored : cipher textpassword:<ISchemaProperty>{__type:EType.string,conversions:{hashing:true}},// compare with the hash data API
Find by a hashed field, read an encrypted one back (custom API)
// The same text always gives the same hash : hash what you look forconstphone=awaitg.sys.system.hash(g.req.body.phone);constreviews=awaitg.sys.db.query({instance:'mongodb',database:'shop',collection:'reviews',find:{contact_phone:phone},select:{review_no:1,title:1,contact_email:1},});// The keys the schema encrypted with : the plain text exists on the server onlyconst[algorithm,pass]=awaitg.sys.system.getSecret(['common.encryptionAlgorithm','common.secret']);for(constreviewofreviews){review.contact_email=awaitg.sys.system.decrypt(review.contact_email,algorithm,pass);}
A missing, null or empty value on save. On update, only a null or empty value sent.
required
min, max
A number or a date outside the range.
min, max
minLength, maxLength
A string or an array outside the length range.
minLength, maxLength
email: true
A string which is not an email address.
emailNotValid
enum: [ … ]
A value outside the list. An empty list checks nothing. The sample data generator picks from the list.
enumValidation
validatorFun: (value, row) => …
Your function, run last : return true to accept ; return false or a message, or throw, to refuse.
invalidValue
unique: true
A value another row already has, or which appears twice in the same batch. Checked with a query before the write, so prefer a unique index for busy tables (create indexes).
city_name:<ISchemaProperty>{__type:EType.string,validations:{required:true,validatorFun:(city_name,fullObj)=>{if(city_name&&['AHMEDABAD','SURAT'].includes(city_name))thrownewError(`You can not save city_name as '${city_name}'`);returntrue;},},},
mobile_no:<ISchemaProperty>{__type:EType.number,validations:{unique:true}},// prefer a unique index for busy tables
The answer of a refused save is 400 with one entry per problem in errors : { type, field, message, code, dataIndex }, dataIndex being the position of the row in an array body. See Error codes.
conversionFun, defaultFun and validatorFun are JavaScript, whatever language the code of the account is written in. They run for whoever answers the save : the server for a request, or, for a save your code makes through g.sys, the sandbox of that code.
The schema itself is settings : API Maker runs its file once, when you save it, and keeps every function as its text. A function therefore runs on its own later, without the file around it : it sees its arguments, require and what every script sees, not a variable or an import of the top of the schema. Inside a function write require('dayjs'), where it runs with the packages of its sandbox ; the top of the schema imports types and your utility classes written in TypeScript, nothing of npm.
The request is answered by
Its functions run in
The server, or code in TypeScript : a schema API called directly, a custom API or a hook in TypeScript
the sandbox of TypeScript of the account (Node.js or Bun), with the npm packages of its settings
Code in another language : a custom API or a hook in Python, PHP, C#, Go, Rust, Java or Kotlin which saves through a schema
the sandbox of that language, in the JavaScript engine API Maker has there (QuickJS)
A function answers the same in both : the values, the row it changes, the dates, the ids and what it throws.
The engine of the other languages has JavaScript itself (promises, async, Date, JSON, Math, regular expressions, BigInt), require('types'), crypto (randomUUID, randomBytes, randomInt, getRandomValues, createHash and createHmac with md5, sha1, sha256, sha384 and sha512), Buffer, console, the timers and process.env.
It has no packages of npm and no Intl : require('dayjs') throws there (Cannot find module 'dayjs' : … not the packages of npm.), and localeCompare compares letter by letter, without the rules of a language. A function which needs one of them belongs to a table which is saved by code in TypeScript or through the schema APIs ; otherwise do that part in the code which saves.
An error JavaScript itself throws (calling what is no function, BigInt(1.5)) is worded by the engine which ran it, so its message can differ between the two. A message your function throws is the same.
// orderItems.order_id points to orders._idorder_id:<ISchemaProperty>{__type:EType.objectId,validations:{required:true},database:'shop',collection:'orders',column:'_id'},// orders.items : the line items of an order, read from orderItems through their order_iditems:<ISchemaProperty>{__type:EType.objectId,isVirtualField:true,database:'shop',collection:'orderItems',s_columnVirtualLinker:'_id',t_columnVirtualLinker:'order_id'},// a relation to a table of another instance and databaseowner_id:<ISchemaProperty>{__type:EType.number,instance:'mysql8',database:'crm',table:'owners',column:'id'},
Key
Meaning
collection or table, column
The table and the column this field points to. database and instance when they differ from the ones of the schema.
isVirtualField: true
A field which is not in the table : it is filled from the other table, and can be saved through master save. s_columnVirtualLinker is the column of this table, t_columnVirtualLinker the column of the other table which holds it.
A schema file can also export validations, a list of SUPER_KEY rules which refuse two rows with the same combination of fields. It is deprecated : create a unique index on the fields instead, it is faster and it holds for writes made outside API Maker too.