Nancy's Preferred Machete

    tt-model
    TypeScript icon, indicating that this package has built-in type declarations

    0.1.2 • Public • Published

    tt-Model

    DOCS NEEDS UPGRADE. BREAKING CHANGES ADDED.

    Make better model schemas for GraphQL, REST APIs or any other purpose using only your already well known typescript classes and interfaces.

    Extract models from your already created typescript interfaces and classes.

    Separate logic, security and validation.

    Easy to use validations.

    Why to use tt-Model

    • Fast and easy to use Model framework for Typescript, GraphQL, REST, JSON...
    • Converts Typescript interfaces and classes into GraphQL and REST Schema.
    • Adds support for Generics and inheritance into GraphQL.
    • Write more complex graphQL easily.
    • Makes schema simple to maintain.
    • Write significantly less and lisible code (100x less code than GraphQL SDL)
    • Support's "promises" without decreasing performance on all levels.

    tt-Model vs Graphql SDL

    GraphQL SDL tt-Model
    Learning curve Totally new language Typescript that you already use
    Usable for programing no yes, it's your typescript code
    Inheritance no yes
    Generics no yes
    Namespaces no yes
    Macros no yes
    Omit fields no yes
    Ignore fields no yes
    Virtual fields no yes
    Add fields no yes
    Merge schemas no yes
    Input/output entities no yes
    Input validation no yes
    Asserts no yes
    Define custom scalers complex easy
    Convert schemas no yes
    Typed entities (Example: of type "User") no yes
    Comments hard codded strings /** jsDoc comments */
    Split into multiple files no yes
    Usable for other purposes no yes
    Do operations before sending to DataBase no yes
    Do operations before loading from DataBase no yes
    Generating graphQL At runtime At compile time

    Generating GraphQL at compile time optimizes running time, used memory and CPU performance.
    Eliminate the need for one time used libraries for compilation:

    • less memory and disk space for the app
    • Documentation is not generated for production mode
    • Eliminate security issues due to additional compile libraries and documentation.

    Define custom scalers

    Just export an object of type: Scalar<yourType>

    import type {ModelScaler} from 'tt-model';
    
    /**
     * This comment will be added as documentation to the API
     * in development mode.
     * Nice, it's what your IDE and any body already understand ;)
     */
    export const myScaler: Scalar<myScalersType> = {
    	/** @optional parser from JSON */
    	parse?: (value: string | boolean | number) => myType;
    
    	/** @optional serializer to JSON */
        serialize?: (value: myType) => string | boolean | number | undefined | null;
    
    	/** @optional converter from Database */
        fromDB?: (value: any) => myType;
    
    	/** @optional converter to Database */
        toDB?: (value: myType) => any;
    };

    Tip: You can do basic validation inside parse/serialize methods
    Throw error if validation fails.

    Example:

    import {ModelScaler} from 'tt-model';
    import {ObjectId} from 'mongodb';
    
    /** My Optional documentation */
    export const myScaler: Scalar<ObjectId> = {
    	parse(value: string){
    		if(typeof value!== 'string')
    			throw new Error(`Illegal id: ${value}`);
    		return ObjectId.fromHexString(value);
    	}
    	serialize(id: ObjectId){
    		return id.toHexString();
    	}
    };

    Example 2:

    import {ModelScaler, Clone} from 'tt-model';
    
    /** My custom type (just a clone of type string) */
    export CellPhone= Clone<string>;
    
    /** Define my serializer / parser of Cellphone */
    export const myScaler: Scalar<CellPhone> = {
    	parse(value: string){
    		// TODO Add your validation logic
    		return value as CellPhone;
    	}
    	serialize(value: Cellphone){
    		// No need to define this serializer since it's already a string ;)
    		return value;
    	}
    };

    Define Enumerations

    Just export your already created enumeration

    /**
     * An Optional documentation used by tt-Model
     * and your IDE
     */
    export enum MyEnumeration{
    	/** Optional field documentation */
    	MY_ENUMERATION_ITEM
    }

    Example

    /**
     * User roles
     */
    export enum UserRoles{
    	/** Admin role */
    	ADMIN,
    	/** Manager role */
    	MANAGER,
    	/** Client role */
    	CLIENT
    }

    Tip: Read the typescript documentation for more useful features about enumerations.

    Define Entities

    Entities with the same name even in separate files are merged into a single entity.

    Define Plain Entities using interfaces:

    Interfaces enables you to define Plain objects entities. Those entities do not need to be instantiated using classes.

    Example

    /**
     * My optional documentation used by tt-Model and your IDE
     */
    export interface User {
    	/** field documentation */
    	id: ID
    	/** Optional user's name, "?" means optional */
    	name?: string
    	age: number
    	/** Using custom type */
    	cellPhone: CellPhone
    	/** using list  */
    	emails: Email[]
    	/** An other entity "Job" as a type */
    	jobs: Job
    	/** Nested types (not recommended anyway) */
    	nestedDoc: {
    		field1: type1,
    		field2: type2
    	}
    }

    Visit typescript documentation for more interface features.

    Define Typed entity using class:

    Useful if you need additional methods for the entity

    (MUST HAVE DEFAULT CONSTRUCTOR OR NO CONSTRUCTOR AT ALL)

    /**
     * My optional documentation used by tt-Model and your IDE
     */
    export class User {
    	/** field documentation */
    	id: ID
    	/** Optional user's name, "?" means optional */
    	name?: string
    	age: number
    	/** Your method to use in code (Ignore by the API) */
    	myMethod( /* Args */ ){
    		// Your logic
    	}
    }

    Force order fields

    By default, tt-model keeps the same order as define in the code. To order fields by name, add @ordered in the jsDoc comment as follow

    /**
     * @ordered
     */
    export interface MyEntity{
    	// Fields
    }

    Ignore a field or class or interface

    Use jsDoc annotation @ignore

    export interface MyEntity{
    	/**
    	 * This field will be ignored by the API
    	 * @ignore
    	 */
    	ignoredField: fieldType
    	// Other fields ...
    }

    Do not send a field to the Database

    Use jsDoc annotation @virtual

    export interface MyEntity{
    	/**
    	 * This field with not be persisted into Database
    	 * @virtual
    	 */
    	virtualField: fieldType
    	// Other fields ...
    }

    Set an INPUT / OUTPUT only field

    Use jsDoc annotations @output and @input

    export interface MyEntity{
    	/**
    	 * @input
    	 */
    	myInputOnlyField: fieldType
    	/**
    	 * @output
    	 */
    	myOutputOnlyField: fieldType
    
    	// Other fields ...
    }

    Field alias

    Some times you need to rename a field outside your code (as input, output or both). For example to change MongoDB's main document id field _id to id. To do this use @alias in the jsDoc comment as follow:

    export interface MyEntity{
    	/**
    	 * In the INPUT and OUTPUT API, this field will be
    	 * renamed to "id"
    	 * @alias id
    	 */
    	_id: ObjectId
    }

    Set a field as optional

    Use the typescript syntax for this

    export interface MyEntity{
    	// use "?" to make a field optional
    	optionalField?: string
    }

    Set all fields as optional or required

    use typescript Partial generic to generate a new entity with all fields optional.

    The same use Required to generate a new entity with all fields required.

    export interface MyEntity{
    	field1: type1
    	field2?: type2
    }
    
    //* Entity with all fields optional
    export type MyEntityAllOptional= Partial<MyEntity>;
    
    //* Entity with all fields required
    export type MyEntityAllRequired= Required<MyEntity>;

    Remove fields from entity

    use typescript Omit to generate a new entity excluding removed fields

    export interface MyEntity{
    	field1: type1
    	field2: type2
    	field3: type3
    	field4: type4
    }
    
    //* Entity with one field removed
    export type MyEntity2= Omit<MyEntity, 'field2'>;
    
    //* Entity with multiple fields removed
    export type MyEntity3= Omit<MyEntity, 'field2' | 'field3'>;

    Remove fields of an other entity from entity

    use typescript Omit and keyof to generate a new entity excluding removed fields.

    //* base entity
    export interface MyEntity{
    	field1: type1
    	field2: type2
    	field3: type3
    	field4: type4
    }
    
    //* entity to remove fields
    export interface ExcludeEntity{
    	field2: type2
    	field3: type3
    	otherField: otherType
    }
    
    //* Entity with all fields from "ExcludeEntity" removed
    export type MyTargetEntity= Omit<MyEntity, keyof ExcludeEntity>;

    Add fields

    Better to use inheritance, but this is an other solution for this:

    //* base entity
    export interface MyEntity{
    	field1: type1
    	field2: type2
    	field3: type3
    	field4: type4
    }
    
    //* Add fields using "&"
    export type targetEntity= MyEntity & {additionalField: fieldType};
    
    //* Or merging two entities
    export interface MySecondEntity{
    	//...fields
    }
    export type MergedEntities= MyEntity & MySecondEntity;

    Merge fields from multiple entities

    It's better to organize your code in an inheritance format. But if you need merge anyway, use "&" to do that as follow:

    //* base entity
    export interface MyEntity1{
    	// Fields
    }
    export interface MyEntity2{
    	// Fields
    }
    export interface MyEntity3{
    	// Fields
    }
    
    //* Merging entities
    export type TwoMergedEntities= MyEntity1 & MyEntity2;
    export type MoreMergedEntities= MyEntity1 & MyEntity2 & MyEntity3;

    Your own typescript generating logic

    Typescript enables you to generate entities using your own logic. tt-Model just supports anything typescript supports.

    Example

    /** My custom generator that convert all fields type into string */
    export type ConvertAllFieldsToString<T> = {
    	[k in keyof T]: string
    };
    
    /** My entity */
    export interface MyEntity{
    	field1: number;
    	field2: boolean;
    	field3: 'hello' | 'world'
    }
    
    //* Use my generator to generate an entity with all fields from "MyEntity" set to 'string'
    export type MyStringEntity= ConvertAllFieldsToString<MyEntity>;

    Inheritance

    Just use inheritance as typescript enables you to do

    export interface MyBaseInterface{
    	// Fields
    }
    
    export interface MyInheritedEntity extends MyBaseInterface{
    	// Additional fields
    }

    Generics

    Generic are useful to reuse your code

    Example

    /** My Generic pagination */
    export interface MyGenericPageRequest<T>{
    	filter: T;
    	skip: number;
    	limit: number;
    }
    
    /** My filter 1 */
    export interface FilterUsers{
    	name?: string;
    	age?: string
    }
    
    /** My filter 1 */
    export interface FilterBooks{
    	title?: string
    	author?: ID
    }
    
    /**
     * My User pagination
     * (use it directly in your code or export it as a type)
     */
    MyGenericPageRequest<FilterUsers>
    
    /**
     * My Book pagination
     * (use it directly in your code or export it as a type)
     */
    MyGenericPageRequest<FilterBooks>

    Visit typescript documentation for more Generics features.

    Union

    For tt-Model, you need to define a logic on how to select appropriate type in case of field with union type

    /** Type 1 */
    export interface Client {
    	// Client fields
    }
    
    /** Type 2 */
    export interface Staff{
    	// Staff fields
    }
    
    // Union supports infinity of types
    
    /** My union type */
    export type Agent= Client | Staff;
    
    /**
     * To make "Agent" union usable for the API
     * Define a logic to choose between "Client" and "Staff"
     * 
     * @Warning
     * When used as a Graphql generator,
     * unions are only supported for output data
     * (Graphql limitation)
     */
    import {UNION} from 'tt-model';
    export const agentUnion: UNION<Agent> = {
    	resolveType(value:Agent, ctx: any, info: any){
    		// Return the aproppriate index of the type in the UNION definition
    		return value.type === 'staff' ? 1 : 0 
    	}
    };

    Converters

    Sometimes, you need to define separate complex schemas and validations for INPUT, OUTPUT and INSIDE code schema. To make this easy, you can use converters that convert data.

    Maybe you just need @input, @output, @ignore and @virtual jsDoc annotations or resolvers to achieve 98% of cases. this is reserve for more complex cases.

    /** My internal entity */
    export interface MyInternalEntity { /* Fields */}
    /** How entity appears in API output */
    export interface MyOutputEntity{ /* Fields and resolvers logic */}
    /** How entity appears as API input */
    export interface MyInputEntity{ /* Fields and validation logic */}
    
    /**
     * Define your converter
     * Could be defined only for INPUT, OUTPUT or both
     */
    import { Converter } from 'tt-model';
    export anyNameAsConverterName: Converter<MyInternalEntity> = {
    	/**
    	 * Optional: Define how entity is converted from INPUT
    	 * MUST define the Input entity schema
    	 * validation defined in the "Input entity" will be executed
    	 * No need for more validation in the "input" converter
    	 */
    	input(parent: any, value: MyInputEntity, ctx: Context, info: any){
    		// Return your converted data
    	}
    
    	/**
    	 * Optional: Define how entity is converted before OUTPUT
    	 * MUST define the Output entity schema
    	 * Resolvers in the "output entity" schema will be executed
    	 */
    	output(parent: any, value: MyInternalEntity, ctx: Context, info: any): MyOutputEntity{
    		// Return your converted data
    	}
    };

    Example:

    In this example, we store positions in GeoJSON format, but for input, we use more easy scheme:

    /** My GeoJSON types */
    export enum GeoTypes{
    	FEATURE= 'Feature',
    	POINT= 'Point'
    }
    /** My Cursor in GeoJSON format */
    export interface MapCursor{
    	type: GeoTypes.FEATURE,
    	geometry: GeoPosition,
    	properties: MyGeoProperties
    }
    /** My Geo position as point */
    export interface GeoPosition{
    	type: GeoTypes.POINT;
    	coordinates: number[];
    }
    /** My Custom properties */
    export interface MyGeoProperties{
    	// My custom fields
    	myOtherProperty: string
    }
    
    /**
     * Used schema to receive cursor from the client
     * Asserts and validations will be executed before convert.
     */
    export interface MapCursorInput{
    	/**
    	 * @assert min: -180, max: 180
    	 */
    	longitude: number
    	/**
    	 * @assert min: -90, max: 90
    	 */
    	latitude: number
    	/**
    	 * Any defined validator will be executed
    	 * before convert
    	 */
    	myOtherProperty: string
    }
    
    /**
     * Define our converter logic
     */
    import {Converter} from 'tt-model';
    export const anyNameAsConvertersName: Converter<MapCursor> = {
    	/** Convert input data */
    	input(parent: any, input: GeoLocationInputFormat): MapCursor {
    		if( input == null ) return undefined;
    		return {
    			type: GeoTypes.FEATURE,
    			geometry: {
    				type: GeoTypes.POINT,
    				coordinates: [input.longitude, input.latitude]	
    			},
    			properties: {
    				myOtherProperty: input.myOtherProperty
    			}
    		}
    	}
    };

    Output Entity Resolvers

    Resolvers enable you to define how a field is resolved. It enables you to change or resolve a field using your own logic.

    Resolver's output type will override original field type.

    Define Resolvers using classes

    Classes are used to define typed entities. To use theme as resolvers instead, use @resolvers jsDoc annotation.

    The limitation of this approach is that you need to define interfaces and classes of the same name in separated files.

    This approach is recommended only to define graphql Query, Mutation and Subscription

    Example

    /**
     * This class will be used as resolver instead of entity
     * because of the "resolvers" annotation
     * @resolvers
     * @output
     */
    export class Query {
    	/**
    	 * Add your resolver documentation as jsDoc comment
    	 * You must define the returned type
    	 */
    	myField(parent: any, args: any, ctx: Context, info: GraphQLInfos) : RequiredReturnedType{
    		// Your logic
    	}
    
    	/**
    	 * This is an example of Users resolver
    	 * here the input entity is defined as "UserFilter"
    	 * 
    	 * You can use typescript annotations to do more
    	 * like common validation or anything else
    	 */
    	@customTypescriptAnnotation(/* Args */)
    	users(parent: any, filter: UserFilter): User{
    		return DB.findUsers(filter);
    	}
    }
    
    /** User's interface entity */
    export interface User {
    	/** User's id */
    	id: ID
    	// User's fields
    	name: string
    	role: ID
    	/**
    	 * This field is virtual and output only
    	 * a resolver is defined bellow
    	 * @virtual
    	 * @output
    	 */
    	orders: Order[]
    }
    
    /**
     * Define User's field resolvers
     * This class needs to be defined in separate file.
     * @resolvers
     */
    export class User{
    	/**
    	 * Resolve role using user.id
    	 */
    	role(parent: User): Role {
    		return DB.resolveRole(parent.role);
    	}
    
    	/** resolve */
    	orders(parent: user, args: any): Order[]{
    		return DB.resolveOrdersByUid(parent.id);
    	}
    }

    Define Resolvers using helper

    This is the recommended approach to define entity resolvers.

    For consistency reason, you must explicitly define return type of resolvers

    /** Your entity using "interface" or "class" */
    export interface MyEntity{
    	// Fields
    	role: ID
    }
    
    /**
     * Define entity resolvers
     * use any name you wish as the class name
     */
    export class MyEntityResolvers implements ResolversOf<MyEntity>{
    	/** Resolve role */
    	role(parent: MyEntity, args: any): Role{
    		return DB.resolveRoleById(parent.role);
    	}
    
    	/**
    	 * Use typescript annotations for common validation
    	 * (like to check if has an access right)
    	 */
    	@annotationExample
    	otherField(parent): ReturnedType{
    		//* You can access any other method
    		//* from this class using "this" keyword
    		return this.resolver2(parent);
    	}
    
    	/**
    	 * Use "ignore" jsDoc annotation to ignore a method
    	 * @ignore
    	 */
    	ignoreMethod(/* Any arguments */){}
    
    	/**
    	 * Use "private" keyword to ignore methods
    	 */
    	private method(/* Any arguments */){}
    }

    Method 2 to define resolvers

    You can use ResolverConfig. but prefer using previous method ( enables you to use annotations and call member methods)

    export const anyVarName: ResolverConfig<MyEntity> = {
    	outputFields: {
    		// Define your resolvers here
    		// Maybe you'd better to use previous approach using classes for more flexibility
    		field(parent: any, args: any, ctx: Context, info: GQLInfo): ReturnedValue {
    			// Logic
    		}
    	}
    };

    Define a resolver on the whole entity

    Some times you need this

    /** My entity */
    export interface MyEntity {
    	field: fieldType
    	// Other fields
    }
    
    /** Validation pipeline */
    import type { ResolverConfig } from 'tt-model';
    export const anyVarName: ResolverConfig<MyEntity> = {
    	/**
    	 * Execute a resolver before all entity field's resolvers
    	 * it should be used as a pipeline only
    	 */
    	beforeOutput(parent: any, value: MyEntity, ctx:any, info: any){
    		// NB: You can't change the schema here, use "Converter"
    		// instead if you need schema transformation
    		return value;
    	}
    
    	/**
    	 * Execute a resolver after all entity field's resolvers are executed
    	 * it should be used as a pipeline only
    	 */
    	afterOutput(parent: any, value: MyEntity, ctx:any, info: any){
    		// NB: You can't change the schema here, use "Converter"
    		// instead if you need schema transformation
    		return value;
    	}
    };

    Entity Validation and Pipeline

    tt-model enables you to validate and convert your data before being processed by resolvers. This enables you to write validation only once for any type of data. This eliminates a huge amount of errors and security vulnerabilities.

    Validation is performed from deeper values to up values. In your data tree, child node is validated before it's parent node. You can use '::beforeInput' to execute operations on node before it's sub-nodes

    Asserts

    Asserts are a lightweight, lisible and performant way to validate input fields (data from client). Use jsDoc annotation @assert as follow.

    Optimal code will be generated at compile time, so it's better and faster than any possible third library.

    export interface MyEntity{
    	/**
    	 * age must be 18 <= age <= 120
    	 * @assert min: 18, max: 120
    	 */
    	age: number;
    	/**
    	 * password's length must be 8 <= password.length <= 100
    	 * @assert min: 8, max: 100
    	 */
    	password: string
    	/**
    	 * Array's length must be greater than 17: arr.length > 17
    	 * @assert gt: 17
    	 */
    	arr: number[]
    	/**
    	 * Check equals
    	 * @assert eq: "khalid"
    	 */
    	name: string
    	/**
    	 * Check using regex
    	 * <!> This only an example regex, not safe to check emails
    	 * @assert regex: /^[a-z0-9.]+@\w+\.\w{2,5}$/
    	 */
    	email: string
    	/**
    	 * Static math expression are allowed
    	 * and will be evaluated at compile time
    	 * @assert lte: 5 * 2**20
    	 */
    	bytes: number
    }

    possible options are:

    min: Minimum value or length
    max: Maximum value or length
    gte: Greater than or equals
    gt: Greater than value or length
    lte: Less than or equals value or length
    lt: Less than value or length
    eq: Equals value
    ne: Not equals value
    length: Has length equals to
    regex: Apply regular expression
    

    Validation using custom functions

    Validate/Pipeline entity fields

    Just like resolvers, define a class that implement the entity using ValidatorsOf<YourEntity>

    Received type as second argument will override field's INPUT type

    /** My entity */
    export interface MyEntity {
    	field: fieldType
    	// Other fields
    }
    
    /** Define validator for fields */
    export class anyClassName implements ValidatorsOf<MyEntity> {
    	/**
    	 * Define field validator just like resolvers
    	 */
    	field(parent: MyEntity, args: InputFieldType, ctx: Context, info: GQLInfo): fieldType{
    		// Do your validation and transformations logic
    		if(hasError)
    			throw new CustomError('Error message');
    		// Return appropriate data
    		return value;
    	}
    	
    	/**
    	 * You can use any JS/Typescript annotation for more control
    	 * "args" type here will override field's type for INPUT
    	 */
    	@aCustomAnnotation()
    	anOtherField(parent: MyEntity, args: InputFieldType, ctx: Context, info: GQLInfo): fieldType{
    		// Do your validation and return appropriate data
    	}
    }

    Method 2 to define field pipeLine / validator

    You can use ResolverConfig. but prefer using previous method ( enables you to use annotations and call member methods)

    export const anyVarName: ResolverConfig<MyEntity> = {
    	inputFields: {
    		// Define your validators/pipelines here
    		// Maybe you'd better to use previous approach using classes for more flexibility
    		field(parent: any, args: InputType, ctx: Context, info: GQLInfo): ReturnedValue {
    			// Logic
    			return args; // return appropriate data
    		}
    	}
    };

    Validate the whole entity

    You can do operations BEFORE and AFTER a validation process is executed on any entity as follow (NB: Child nodes are validated before their parent nodes)

    /** My entity */
    export interface MyEntity {
    	field: fieldType
    	// Other fields
    }
    
    /** Validation pipeline */
    import type { ResolverConfig } from 'tt-model';
    export const anyVarName: ResolverConfig<MyEntity> = {
    	/**
    	 * Process RAW entity
    	 * before any field validation / pipeline
    	 */
    	beforeInput(parent: any, value: MyEntity, ctx:any, info: any){
    		// Do your logic before "MyEntity" fields and sub-entities are validated
    		// NB: You can't change the schema here, use "Converter"
    		// instead if you need schema transformation
    		return value;
    	}
    
    	/**
    	 * Process validated / transformed data
    	 * (After all fields and sub-entities validation/modification)
    	 */
    	afterInput(parent: any, value: MyEntity, ctx:any, info: any){
    		// Do more transformation / validation on the entity
    		// NB: You can't change the schema here, use "Converter"
    		// instead if you need schema transformation
    		return value;
    	}
    };

    Add root Helpers

    You could need to do some operations before or after executing data processing maybe loading user's information from DB

    import { RootConfig } from 'tt-model';
    export const anyVarName: RootConfig = {
    	/**
    	 * Do operations before any data process, just after JSON parsing
    	 * @optional
    	 */
    	before(parent: any, args: any, ctx: any, info: any){
    		// Your logic
    		// Maybe load user's access rights for input args
    		return args;
    	}
    	/**
    	 * Do operations just before any data process, just after JSON parsing
    	 * @optional
    	 */
    	after(parent: any, args: any, ctx: any, info: any){
    		// Your logic
    		// Maybe load user's access rights for input args
    		return args;
    	}
    };

    Macro Decorators / Annotation

    Macro decorator or annotation helps you to redefine classes and methods. Generally to add extra code without scarifying performance.

    You may need knowledge of typescript compiler api to use this.

    Using the Decorator / Annotation

    Use macro annotation just like you do with JS or Typescript Decorator

    @MyMacroAnnotationOnClass(...args)
    export classMyClass{
    
    	@MyMarcoAnnotationOnAttribute(...args)
    	myAttribute: any
    
    	@MyMacroAnnotationOnMethod(...args)
    	myMethod(){ /* ... */}
    }

    Define the Macro Decorator / Annotation

    import { AnnotationMacro, MacroAnnotationNode, MacroUtils } from 'tt-model';
    
    export const yourMacroName= AnnotationMacro(function(
    	node: MacroAnnotationNode,
    	utils: MacroUtils,
    	...OtherSpecificArgs
    ){
    	// Your Macro logic
    	return node;
    });

    MacroAnnotationNode is a typescript compiler node. You can use typescript compiler api to do advanced transformations if needed.

    type MacroAnnotationNode= ts.ClassDeclaration | ts.MethodDeclaration | ts.PropertyDeclaration;

    MacroUtils contains easy to use helpers to transform the code. For advanced and full control use typescript compiler api.

    class MacroUtils{
    	/** Target node to be transformed: class or property */
    	node: ts.ClassDeclaration | ts.MethodDeclaration | ts.PropertyDeclaration;
    
    	/** Typescript Compiler Program */
    	program: ts.Program;
    
    	/** Typescript Compiler API */
    	ts: typeof ts;
    
    	/** Node printer */
    	printer: ts.Printer;
    
    	/** Decorator arguments: as wrote in the code */
    	args: string[]
    
    	/**
    	 * Decorator arguments as interpreted by the compiler
    	 * most of time: undefined
    	 */
    	argv: any[]
    
    	/** Check if a node is a method */
    	isMethod(node: ts.Node): node is ts.MethodDeclaration
    
    	/** Check a node is a property */
    	isProperty(node: ts.Node): node is ts.PropertyDeclaration
    
    	/** Check a node is a class declaration */
    	isClass(node: ts.Node): node is ts.ClassLikeDeclaration
    
    	/** Check a node has "static" keyword modifier */
    	isStatic(node: ts.Node): boolean
    
    	/** Get class or property name */
    	getName(node: ts.Node): string
    
    	/** Create unique name in the code */
    	uniqueName(hint: string): ts.Identifier
    
    	/** Update method body */
    	updateMethodBody(
    		node: ts.MethodDeclaration, // Method to update
    		/** Function that will update the method code */
    		callBack: (args: any, body: ts.Statement[]) => ts.Statement[],
    		/** Same as previous, but faster for code prepend */
    		prepend?: (args: any, body: ts.Statement[]) => ts.Statement[]
    	) : ts.MethodDeclaration
    
    	/** Create "if" statement */
    	if(
    		condition: string | ts.Expression,
    		thenStatement: string | ts.Statement,
    		elseStatement?: string | ts.Statement
    	): ts.IfStatement
    
    	/** Create Object access expression */
    	objAccess(expr: string | ts.Expression, ...args: string[]): ts.Expression
    
    	/** Create binary expression */
    	binaryExpression(
    		leftExpr: string | ts.Expression,
    		operator: '=' | '==' | '===' | '!=' | '!==' | '&' | '|',
    		rightExpr: string | ts.Expression | boolean | number
    	): ts.BinaryExpression
    
    	/** Create variable */
    	createVar(varname: ts.Identifier, value: ts.Expression | string) : ts.VariableStatement
    }

    Example

    In this example, we will create a macro hasPermission that adds permission check code to target methods. The macro will be used as follow:

    export class MyClass{
    
    	@hasPermission(User.Permissions.ANY_PERMISSION))
    	doWhatEver(...args: any[]){
    		// Logic
    	}
    }

    We will define the macro as follow:

    import { AnnotationMacro, MacroAnnotationNode, MacroUtils } from 'tt-model';
    
    export const hasPermission = AnnotationMacro(
    	function(
    		node: MacroAnnotationNode,
    		utils: MacroUtils,
    		// Any extra argument will be considered as the macro argument
    		permissions: YourPermissionType
    	){
    		// Add your logic here, modify the node and return it
    		// Assert node is of the correct type (method or class or attribute)
    		// Throw error if any
    		node = utils.updateMethodBody(node, undefined, function ([parent, args, ctx], body) {
    			// Push check instruction to the method body
    			body.push(
    				utils.if(
    					utils.binaryExpression(
    						utils.objAccess(ctx, 'session', 'isAuthenticated'),
    						'===', false
    					),
    					'throw new Error("Unauthorized Access.")'
    				)
    			);
    			// Add any other required instructions using "utils" or "typescript compiler api"
    			// return the body
    			return body;
    		});
    		// Return resulting node
    		return node;
    	}
    );

    Compile schema

    Use as GraphQL schema:

    Just use a Glob selector to load schema from all target files.

    // Add those lines in the file "anyName.ts" where you need the schema:
    import {Model} from 'tt-model';
    
    /*
    Use a GLOB selector to select all files that contains schema definitions
    (entities, resolvers, ...)
    "Glob selector" must be added as "STATIC string" to "Model.scanGraphQL" method
    Remember this will be replaced at compile time with generated schema!
    */
    const schema = Model.scanGraphQL('../model/**/*.ts, ./schema/**/*.ts');
    
    //* Or use multiple STATIC strings
    const schema = Model.scanGraphQL('../model/**/*.ts', './schema/**/*.ts');
    
    //* Then just generate your graphQL server with pre-compiled schema
    const server = new ApolloServer({
    	schema: schema,
    	// Other fields
    });

    Static strings means "hard codded strings" and not strings from variables or any dynamic sources.
    This is because the line will be replaced at compile time with generated schema.

    Use for REST or any other purpose ( like file validations? )

    Will be available in next release.

    Compile the code

    You can compile code to either TypeScript or JavaScript. use tt-model-compiler at https://www.npmjs.com/package/tt-model-compiler

    Contribute

    Authors

    License

    MIT License

    Install

    npm i tt-model

    DownloadsWeekly Downloads

    5

    Version

    0.1.2

    License

    MIT

    Unpacked Size

    122 kB

    Total Files

    64

    Last publish

    Collaborators

    • rafikalid2