Building your own structure
Building your own structure starts with types and validators.
Getting started
The entrypoint for building your own structure is the TypeCreator
. It defines the group or flow that the type belongs to and all the methods for building your own structure which we will explore later.
import { Generator, TypeCreator } from "@compas/code-gen";
const generator = new Generator();
const T = new TypeCreator("myFlowName"); // like 'user' or 'userOnboarding'
generator.add(
T.bool("myType"),
T.bool("otherType"),
);
generator.generate({
targetLanguage: "ts",
outputDirectory: "./src/generated",
generators: {
validators: { includeBaseTypes: true },
},
});
Calling this script should give you src/generated/common/types.d.ts
with two types defined;
// Concatenation of 'MyFlowName' and 'MyType'
export type MyFlowNameMyType = boolean;
export type MyFlowNameOtherType = boolean;
Primitives
The following primitives are available
import { TypeCreator } from "@compas/code-gen";
const T = new TypeCreator();
T.any();
T.bool();
T.number();
T.string();
T.uuid();
T.date();
Top level types provided to generator.add
need to have a name:
import { Generator, TypeCreator } from "@compas/code-gen";
const generator = new Generator();
const T = new TypeCreator(); // defaults to `App`
generator.add(T.number("integer"));
// generator.generate
After executing the generators the following is be runnable:
import type { AppInteger } from "generated/common/types";
import { validateAppInteger } from "generated/app/validators";
import { AppItem } from ".cache/debug/common/types";
// group 'App', name 'integer' becomes 'AppInteger'
// It's definition is something like `type AppInteger = number;`
const x: AppInteger = 5;
validateAppInteger(5); // => { value: 5 }
validateAppInteger({}); // => { error: { "$": { key: "validator.type" } }
Optionality
Values are by default required and null
is coerced to undefined
for JavaScript and TypeScript targets.
const T = new TypeCreator();
generator.add(
T.bool("requiredBoolean"),
T.bool("optionalBoolean").optional(),
T.bool("nullableBoolean").allowNull(),
);
// And usage
import {
validateAppRequiredBoolean,
validateAppOptionalBoolean,
validateAppNullableBoolean,
} from "generated/app/validators";
// Input type: boolean
// Output type: boolean
validateAppRequiredBoolean(true); // => { value: true }
validateAppRequiredBoolean(undefined); // => { error: { "$": { key: "validator.undefined" } }
// Input type: boolean|undefined|null
// Output type: boolean|undefined
validateAppOptionalBoolean(undefined); // => { value: undefined }
validateAppOptionalBoolean(null); // => { value: undefined }
// Input type: boolean|undefined|null
// Output type: boolean|undefined|null
validateAppNullableBoolean(undefined); // => { value: undefined }
validateAppNullableBoolean(null); // => { value: null }
Objects and arrays
Less primitive, but as important are objects and arrays. We will quickly glance over the basics
const T = new TypeCreator();
generator.add(
T.object("user").keys({
id: T.uuid(), // using primitive types without names
name: T.string(),
}),
T.array("objectList").values(
T.object().keys({
// Object without a name. You can still define a name here, which will
// result in a named type for your target language.
velocity: T.number(),
}),
),
);
// Generated types
export type AppUser = { id: string; name: string };
export type AppObjectList = { velocity: number }[];
// Usage
validateAppUser({ id: generateUuid(), name: "Compas admin" }); // => { value: { id: "..", name: "Compas admin" } }
validateAppObjectList([]); // => { value: [] }
validateAppObjectList([{ velocity: 5 }]); // => { value: [{ velocity: 5 }] }
Inferred builders
Compas supports inferring unnamed builders for objects, arrays and literals
T.object("named").keys({
// inline objects are the same as `T.object().keys({ ... })`
inferredObject: {
key: T.string(),
},
// Arrays are inferred by providing a single length array instead of `T.array().values(...)`
inferredArray: [T.string()],
// boolean, number and string literals are supported as well. This uses `.oneOf()`, which
// is further detailed below.
inferredBoolean: true,
inferredNumber: 5,
inferredString: "north",
});
Boolean
Boolean types and validators can be customized
import { TypeCreator } from "@compas/code-gen";
const T = new TypeCreator();
T.bool();
// -> Typescript type: boolean
// -> Valid validator inputs: 0, 1, true, false
// -> Validator outputs: true, false
// Only allows `true`. Since the validators will auto coerce booleans, `1` is a valid input as well.
T.bool().oneOf(true);
Number
Integers and floats are represented by T.number()
T.number();
// -> Typescript type: number
// -> Valid validator inputs: 1, 15, 3, "5"
// -> Validator outputs: 1, 15, 3, 5
// Number validation defaults to integers only.
T.number().float();
// Only allow specific values
T.number().oneOf(1, 2, 3);
T.number().float().oneOf(1.1, 2.2, 3.3);
T.number().min(4); // >= 4
T.number().max(4); // <= 4
String
import { TypeCreator } from "@compas/code-gen";
const T = new TypeCreator();
T.string();
// -> Typescript type: number
// -> Valid validator inputs: "f", "foo"
// -> Validator outputs: "f", "foo"
// Allow empty strings.
T.string().min(0);
// Specific string values
T.string().oneOf("NORTH", "SOUTH");
T.string().min(3); // str.length >= 3
T.string().max(3); // str.length <= 3
T.string().trim(); // Remove leading and trailing whitepsace
T.string().lowerCase(); // Convert the input to lower case
T.string().upperCase(); // Convert the input to upper case
T.string().disallowCharacters(["\n"]); // Error when specific characters are in the input
T.string().pattern(/^\d{4}$/g); // Enforce a specific regex
Uuid
The uuid type does not have any options. It accepts any string in an uuid v4 like format.
T.uuid();
// -> Typescript type: number
// -> Valid validator inputs: "70f20a8b-0372-44aa-8135-137981083d9b"
// -> Validator outputs: "70f20a8b-0372-44aa-8135-137981083d9b"
Any
Accept any value. Can be used as an escape hatch to validate values that are not natively supported by Compas.
T.any();
// -> Typescript type: any
// -> Valid validator inputs: Buffer.from("Hello world");
// -> Validator outputs: <Buffer ...>
Date
The Date input accepts full datetime strings with timezone offsets or a number representing milliseconds since Unix epoch.
T.date();
T.date().dateOnly(); // Accepts yyyy-MM-dd only
T.date().timeOnly(); // Accepts HH:mm(:ss(.SSS))
T.date().min(new Date(2023, 0, 1)); // Only accept dates after 2023-01-01
T.date().max(new Date(2023, 0, 1)); // Only accept dates before 2023-01-01
T.date().inTheFuture(); // Accept dates that are in the future
T.date().inThePast(); // Accept dates that represent a datetime in the past.
Array
T.array().values(T.bool());
// -> Typescript type: boolean[]
// -> Valid validator inputs: [true], [false]
// -> Validator outputs: [true], [false]
T.array().min(1); // Enforce a minimum number of items
T.array().max(5); // Enforce a maximum number of items
T.array().values(T.bool()).convert(); // Convert non-array values to an array
// -> Typescript input type: boolean|boolean[]
// -> Typescript output type: boolean[]
// -> Valid validator inputs: true, [false]
// -> Validator outputs: [true], [false]
// Note the auto conversion to array for the first input.
Object
T.object().keys({
foo: T.bool(),
});
// -> Typescript type: { foo: boolean };
// -> Valid validator inputs: { foo: true }
// -> Validator outputs: { foo: true }
// By default objects don't allow extra keys to be provided. Applying `.loose()`
// will ignore the extra keys.
T.object().loose();
Generic
Represent an object with dynamic validated keys and values
T.generic().keys(T.string()).values(T.bool());
// -> Typescript type: { [k: string]: boolean };
// -> Valid validator inputs: { foo: true }, { bar: true },
// -> Validator outputs: { foo: true }, { bar: true}
Any of
Represent a type that could be any of the defined types
T.anyOf().values(T.bool(), T.string());
// -> Typescript type: boolean|string;
// -> Valid validator inputs: true, "foo"
// -> Validator outputs: true, "foo"
// A discriminated union with named types is the most common usage
T.anyOf("state")
.values(
T.object("startState").keys({
type: "start",
// ... extra keys
}),
T.object("inProgressState").keys({
type: "inProgress",
// ... extra keys
}),
)
// Adding a discriminant ensures faster validators and cleaner validation errors
// This can only be used when all possible values are objects and have a literal string value on the 'discriminant' property.
.discriminant("type");
File
Only usable in route / api client definitions. This is typed specifically to the router or api client target that is used.
T.file();
// Static mime type validator if the router supports that.
T.file().mimeTypes("image/png", "image/jpeg");
Reference
Named types can be reused as well.
generator.add(
T.object("item").keys({
id: T.uuid(),
name: T.string(),
}),
T.object("itemList").keys({
total: T.number(),
items: [T.reference("app", "item")],
}),
);
// Generates the following;
// Note; `App` is the default group if no value is explicitly provided to `new TypeCreator()`;
type AppItem = { id: string; name: string };
type AppItemList = { total: number; items: AppItem[] };
Object extensions
Omit
Copy all keys from a named object type and omit some of them
T.object("bigObject").keys({
key1: T.string(),
key2: T.string(),
key3: T.string(),
});
// { key1: string, key2: string, key3: string }
T.omit("ommitBigObject").object(T.reference("app", "bigObject")).keys("key3");
// { key1: string, key2: string }
Pick
Copy some fields from a named object type
T.object("bigObject").keys({
key1: T.string(),
key2: T.string(),
key3: T.string(),
});
// { key1: string, key2: string, key3: string }
T.pick("pickBigObject").object(T.reference("app", "bigObject")).keys("key1", "key2");
// { key1: string, key2: string }
Extend
It could happen that you want to add extra properties on an T.object()
that is provided by a library. A use case is when using Compas' store package to save files. It provides a typed fileMeta
type which you can extend to add extra properties.
T.extendNamedObject(T.reference("store", "fileMeta")).keys({
hashCode: T.string(),
});
// type StoreFileMeta = { ...existingKeys; hashCode: string, };