How to combine TypeScript and SimpleSchema?

Hey, I am new to TS.

Is there an example of how to derive an interface from a SimpleSchema?

1 Like

This might help?

I also found this, but did not understand much.

The guy wants the d file though.

2 Likes

Hi there)

I was wondering if you found a way to use those two together?

Otherwise it seems a little bit like typing same things twice.

2 Likes

Hey guys,

an old topic but still hot.

Any existing solution yet?

2 Likes

Add a types/ folder to your app.

Add it to your tsconfig.json, too:

"typeRoots": ["./types"],

Put this in types/simpl-schema/index.d.ts:

declare module 'simpl-schema' {

  import { Mongo } from 'meteor/mongo';
// Type definitions for simpl-schema

  type Integer = RegExp;

  type SchemaType =
    | String
    | Number
    | Integer
    | Boolean
    | Object
    | ArrayLike<any>
    | SchemaDefinition<any>
    | Date
    | SimpleSchema
    | SimpleSchemaGroup;

  type SimpleSchemaGroup = { definitions: ArrayLike<{ type: SchemaType }> };

  interface CleanOption {
    filter?: boolean;
    autoConvert?: boolean;
    removeEmptyStrings?: boolean;
    trimStrings?: boolean;
    getAutoValues?: boolean;
    isModifier?: boolean;
    extendAutoValueContext?: boolean;
  }

  export type AutoValueFunctionSelf<T> = {
    key: string;
    closestSubschemaFieldName: string | null;
    isSet: boolean;
    unset: () => void;
    value: T | Mongo.Modifier<T>;
    operator: string;
    field(otherField: string): {
      isSet: boolean;
      value: any;
      operator: string;
    };
    siblingField(otherField: string): {
      isSet: boolean;
      value: any;
      operator: string;
    };
    parentField(otherField: string): {
      isSet: boolean;
      value: any;
      operator: string;
    };
  }

  type ValidationError = {
    name: string,
    type: string,
    value: any,
  };

  type AutoValueFunction<T> = (
    this: AutoValueFunctionSelf<T>,
  ) => T | undefined;

  interface ValidationFunctionSelf<T> {
    value: T;
    key: string;
    genericKey: string;
    definition: SchemaDefinition<T>;
    isSet: boolean;
    operator: any;
    validationContext: ValidationContext;
    field: (fieldName: string) => any;
    siblingField: (fieldName: string) => any;
    addValidationErrors: (errors: ValidationError[]) => {};
  }

  type ValidationFunction = (
    this: ValidationFunctionSelf<any>
  ) => string | undefined;

  export interface SchemaDefinition<T> {
    type: SchemaType;
    label?: string | Function;
    optional?: boolean | Function;
    min?: number | boolean | Date | Function;
    max?: number | boolean | Date | Function;
    minCount?: number | Function;
    maxCount?: number | Function;
    allowedValues?: any[] | Function;
    decimal?: boolean;
    exclusiveMax?: boolean;
    exclusiveMin?: boolean;
    regEx?: RegExp | RegExp[];
    custom?: ValidationFunction;
    blackbox?: boolean;
    autoValue?: AutoValueFunction<T>;
    defaultValue?: any;
    trim?: boolean;

    // allow custom extensions
    [key: string]: any;
  }

  interface EvaluatedSchemaDefinition {
    type: ArrayLike<{ type: SchemaType }>;
    label?: string;
    optional?: boolean;
    min?: number | boolean | Date;
    max?: number | boolean | Date;
    minCount?: number;
    maxCount?: number;
    allowedValues?: any[];
    decimal?: boolean;
    exclusiveMax?: boolean;
    exclusiveMin?: boolean;
    regEx?: RegExp | RegExp[];
    blackbox?: boolean;
    defaultValue?: any;
    trim?: boolean;

    // allow custom extensions
    [key: string]: any;
  }

  interface ValidationOption {
    modifier?: boolean;
    upsert?: boolean;
    clean?: boolean;
    filter?: boolean;
    upsertextendedCustomContext?: boolean;
  }

  interface SimpleSchemaValidationContext {
    validate(obj: any, options?: ValidationOption): boolean;

    validateOne(doc: any, keyName: string, options?: ValidationOption): boolean;

    resetValidation(): void;

    isValid(): boolean;

    invalidKeys(): { name: string; type: string; value?: any }[];

    addInvalidKeys(errors: ValidationError[]): void;

    keyIsInvalid(name: any): boolean;

    keyErrorMessage(name: any): string;

    getErrorObject(): any;
  }

  class ValidationContext {
    constructor(ss: any);

    addValidationErrors(errors: ValidationError[]): void;

    clean(...args: any[]): any;

    getErrorForKey(key: any, ...args: any[]): any;

    isValid(): any;

    keyErrorMessage(key: any, ...args: any[]): any;

    keyIsInvalid(key: any, ...args: any[]): any;

    reset(): void;

    setValidationErrors(errors: ValidationError): void;

    validate(obj: any, ...args: any[]): any;

    validationErrors(): any;
  }

  interface MongoObjectStatic {
    forEachNode(func: Function, options?: { endPointsOnly: boolean }): void;

    getValueForPosition(position: string): any;

    setValueForPosition(position: string, value: any): void;

    removeValueForPosition(position: string): void;

    getKeyForPosition(position: string): any;

    getGenericKeyForPosition(position: string): any;

    getInfoForKey(key: string): any;

    getPositionForKey(key: string): string;

    getPositionsForGenericKey(key: string): string[];

    getValueForKey(key: string): any;

    addKey(key: string, val: any, op: string): any;

    removeGenericKeys(keys: string[]): void;

    removeGenericKey(key: string): void;

    removeKey(key: string): void;

    removeKeys(keys: string[]): void;

    filterGenericKeys(test: Function): void;

    setValueForKey(key: string, val: any): void;

    setValueForGenericKey(key: string, val: any): void;

    getObject(): any;

    getFlatObject(options?: { keepArrays?: boolean }): any;

    affectsKey(key: string): any;

    affectsGenericKey(key: string): any;

    affectsGenericKeyImplicit(key: string): any;
  }

  interface MongoObject {
    expandKey(val: any, key: string, obj: any): void;
  }

  class SimpleSchema {
    static Integer: Integer;
    static RegEx: {
      Email: RegExp;
      EmailWithTLD: RegExp;
      Domain: RegExp;
      WeakDomain: RegExp;
      IP: RegExp;
      IPv4: RegExp;
      IPv6: RegExp;
      Url: RegExp;
      Id: RegExp;
      ZipCode: RegExp;
      Phone: RegExp;
    };
    debug: boolean;

    constructor(
      schema: { [key: string]: SchemaDefinition<any> | SchemaType } | any[],
      options?: any | { humanizeAutoLabels?: boolean; tracker?: any; check?: any },
    );

    /**
     * Returns whether the obj is a SimpleSchema object.
     * @param {Object} [obj] An object to test
     * @returns {Boolean} True if the given object appears to be a SimpleSchema instance
     */
    static isSimpleSchema(obj: any): boolean;

    static oneOf(...schemas: SchemaType[]): SchemaType;

    // If you need to allow properties other than those listed above, call this from your app or package
    static extendOptions(allowedOptionFields: string[]): void;

    static setDefaultMessages(messages: {
      messages: { [key: string]: { [key: string]: string } };
    }): void;

    namedContext(name?: string): SimpleSchemaValidationContext;

    addDocValidator(validator: (doc: any) => ValidationError[]): any;

    /**
     * @method SimpleSchema#pick
     * @param {[fields]} The list of fields to pick to instantiate the subschema
     * @returns {SimpleSchema} The subschema
     */
    pick(...fields: string[]): SimpleSchema;

    /**
     * @method SimpleSchema#omit
     * @param {[fields]} The list of fields to omit to instantiate the subschema
     * @returns {SimpleSchema} The subschema
     */
    omit(...fields: string[]): SimpleSchema;

    /**
     * Extends this schema with another schema, key by key.
     *
     * @param {SimpleSchema|Object} schema
     * @returns The SimpleSchema instance (chainable)
     */
    extend(
      schema:
        | Partial<SchemaDefinition<any>>
        | SimpleSchema
        | { [key: string]: Partial<SchemaDefinition<any>> },
    ): SimpleSchema;

    clean(doc: any, options?: CleanOption): any;

    /**
     * @param {String} [key] One specific or generic key for which to get the schema.
     * @returns {Object} The entire schema object or just the definition for one key.
     *
     * Note that this returns the raw, unevaluated definition object. Use `getDefinition`
     * if you want the evaluated definition, where any properties that are functions
     * have been run to produce a result.
     */
    schema<T>(key?: string): SchemaDefinition<T> | { [key: string]: SchemaDefinition<T> };

    /**
     * @returns {Object} The entire schema object with subschemas merged. This is the
     * equivalent of what schema() returned in SimpleSchema < 2.0
     *
     * Note that this returns the raw, unevaluated definition object. Use `getDefinition`
     * if you want the evaluated definition, where any properties that are functions
     * have been run to produce a result.
     */
    mergedSchema(): { [key: string]: SchemaDefinition<any> };

    /**
     * Returns the evaluated definition for one key in the schema
     *
     * @param {String} key Generic or specific schema key
     * @param {Array(String)} [propList] Array of schema properties you need; performance optimization
     * @param {Object} [functionContext] The context to use when evaluating schema options that are functions
     * @returns {Object} The schema definition for the requested key
     */
    getDefinition(
      key: string,
      propList?: ArrayLike<string>,
      functionContext?: any,
    ): EvaluatedSchemaDefinition;

    /**
     * Returns a string identifying the best guess data type for a key. For keys
     * that allow multiple types, the first type is used. This can be useful for
     * building forms.
     *
     * @param {String} key Generic or specific schema key
     * @returns {String} A type string. One of:
     *  string, number, boolean, date, object, stringArray, numberArray, booleanArray,
     *  dateArray, objectArray
     */
    getQuickTypeForKey(
      key: string,
    ):
      | 'string'
      | 'number'
      | 'boolean'
      | 'date'
      | 'object'
      | 'stringArray'
      | 'numberArray'
      | 'booleanArray'
      | 'dateArray'
      | 'objectArray'
      | undefined;

    /**
     * Given a key that is an Object, returns a new SimpleSchema instance scoped to that object.
     *
     * @param {String} key Generic or specific schema key
     */
    getObjectSchema(key: string): SimpleSchema;

    // Returns an array of all the autovalue functions, including those in subschemas all the
    // way down the schema tree
    autoValueFunctions(): AutoValueFunction<any>[];

    // Returns an array of all the blackbox keys, including those in subschemas
    blackboxKeys(): ArrayLike<string>;

    // Check if the key is a nested dot-syntax key inside of a blackbox object
    keyIsInBlackBox(key: string): boolean;

    /**
     * Change schema labels on the fly, causing mySchema.label computation
     * to rerun. Useful when the user changes the language.
     *
     * @param {Object} labels A dictionary of all the new label values, by schema key.
     */
    labels(labels: { [key: string]: string }): void;

    /**
     * Gets a field's label or all field labels reactively.
     *
     * @param {String} [key] The schema key, specific or generic.
     *   Omit this argument to get a dictionary of all labels.
     * @returns {String} The label
     */
    label(key: any): any;

    /**
     * Gets a field's property
     *
     * @param {String} [key] The schema key, specific or generic.
     *   Omit this argument to get a dictionary of all labels.
     * @param {String} [prop] Name of the property to get.
     *
     * @returns {any} The property value
     */
    get(key?: string, prop?: string): any;

    // shorthand for getting defaultValue
    defaultValue(key): any;

    messages(messages: any): void;

    // Returns a string message for the given error type and key. Passes through
    // to message-box pkg.
    messageForError(type: any, key: any, def: any, value: any): string;

    // Returns true if key is explicitly allowed by the schema or implied
    // by other explicitly allowed keys.
    // The key string should have $ in place of any numeric array positions.
    allowsKey(key: any): boolean;

    newContext(): ValidationContext;

    /**
     * Returns all the child keys for the object identified by the generic prefix,
     * or all the top level keys if no prefix is supplied.
     *
     * @param {String} [keyPrefix] The Object-type generic key for which to get child keys. Omit for
     *   top-level Object-type keys
     * @returns {[[Type]]} [[Description]]
     */
    objectKeys(keyPrefix?: any): any[];

    /**
     * @param obj {Object|Object[]} Object or array of objects to validate.
     * @param [options] {Object} Same options object that ValidationContext#validate takes
     *
     * Throws an Error with name `ClientError` and `details` property containing the errors.
     */
    validate(obj: any, options?: ValidationOption): void;

    /**
     * @param obj {Object} Object to validate.
     * @param [options] {Object} Same options object that ValidationContext#validate takes
     *
     * Returns a Promise that resolves with the errors
     */
    validateAndReturnErrorsPromise(
      obj: any,
      options?: ValidationOption,
    ): Promise<ArrayLike<Error>>;

    validator(options?: ValidationOption): (args: { [key: string]: any }) => void;
  }

  export default SimpleSchema;
}

Add this to types/meteor-mongo.d.ts:

declare module 'meteor/mongo' {
  import SimpleSchema from 'simpl-schema';

  module Mongo {
    export interface Collection<T> {
      schema: SimpleSchema;

      attachSchema(schema: SimpleSchema): void;

      attachJSONSchema(schema: any): void;

      helpers(methods: object): void;

      _name: string;
    }
  }
}

The definitions arenā€™t perfect, but work for us.

5 Likes

That is awesome to have type definitions for the SimpleSchema library. I hope we have a way to include these type definitions directly in the Meteor packages soon.

And I think that the original poster was asking about something slightly different which is being able to generate a Typescript interface from the SimpleSchema.

Hereā€™s an example of a package that does something similar with another schema library: https://www.npmjs.com/package/joi-extract-type

export const jobOperatorRoleSchema = Joi.object({
  id: Joi.string().required(),
  user_id: Joi.string().required(),
  job_id: Joi.string().required(),
  role: Joi.string().valid(['recruiter', 'requester']),
  pipeline_rules: Joi.array().items(rule),
});
type extractComplexType = Joi.extractType<typeof jobOperatorRoleSchema>;
export const extractedComplexType: extractComplexType = {
  id: '2015',
  user_id: '102',
  job_id: '52',
  role: 'admin',
  pipeline_rules: [extractedRule],
};

Being able to do something like that with SimpleSchema would be amazing since all Mongo operations could return an interface automatically derived from the SimpleSchema for example.

4 Likes

@hexsprite yes, that would be really AWESOME!!! This way weā€™d have an elegant mapping-solution from a mongoDb-collection2-schema up to a typescript interface to further use where neededā€¦

@aldeed what are your thoughts on this?

Made my day!!! Thanks a lot

Hi!

I happened to have the same issue (donā€™t want to both code the schema and the interfaces) and ended up in this forum a couple of months ago.
Since I didnā€™t find a solution, I decided to write my first npm package!

It generates Typescript interfaces based on simpl-schema definitions.
Itā€™s highly experimental but I have been using it for some days (!) on my hobby project and it serves the purpose just fine.

It still lacks a lot of polishing but if people start using it and / or contribute, that will motivate me to improve it.

Please tell me what you think!

7 Likes

Hi, this is cool, I managed to make it work with ValidatedMethod!

Now, Iā€™d prefer not to define all my method parameter schemas in a single file but directly where the ValidatedMethod is written, because itā€™s tedious to import both the schema and the type:

import { schemas } from '/server/core/method-params-schemas'
import { MyMethodParams } from '/server/core/method-params-schemas-generated-types'

new ValidatedMethod({
    name: 'myMethod',
    validate: schemas['MyMethodParams'].validator(),
    run({param1, param2, param3}: MyMethodParams) {

Also, the generator takes some time and for now Iā€™m calling it manually instead. But ultimately it could work in the background when we save files having schemas.

Letā€™s keep in touch!

Yay!

So glad you found it useful.
Regarding your feedback:

  1. yes I see your point, maybe the best solution would be to process any schema that has a particular annotation, instead of looking at a single ā€œschemasā€ array? Iā€™ve never created one myself, but I guess itā€™s not too complicated
  2. itā€™s really slow, I agree. I havenā€™t investigated but I suppose itā€™s related to the usage of ts-morph that dynamically compiles the project and introspects itā€¦

On both of these issues, I must admit I probably wonā€™t investigate at least in the near future: I was happy to publish this package but donā€™t plan to spend too much time refining it. Iā€™d love people to contribute, though :wink:

Still, Iā€™ll maintain it as long as Iā€™ll be using it, and I just pushed version 1.0.3 for a fix and support of SimpleSchema.oneOf() definitions!

Again, if anybody wants to contribute Iā€™ll be happy to support it.

Thanks again for your feedback!

Thank you; on a blank project with no existing schemas I tried Joi Schemas with joi-extract-type (suggested by hexsprite above), and it works great with ValidatedMethod.
My methods look like this:

import DeclareJoiValidatedMethod from '../core/joi-method'
import * as Joi from '@hapi/joi';
import 'joi-extract-type';

const MyFirstMethodSchema = Joi.object({
    userId: Joi.string().required(),
    roles: Joi.array().items(Joi.string()).required(),
 });

 DeclareJoiValidatedMethod({
    name: 'myMethod',
    schema: MyFirstMethodSchema,
    run({userId, roles}: Joi.extractType<typeof MyFirstMethodSchema>): boolean {
      ...
      return true
    }
});

with the helper function DeclareJoiValidatedMethod inspired from this post:

export interface JoiValidatedMethodOptions<T> {
    name: string,
    schema: Joi.ObjectSchema,
    validate?(obj: Object): void,
    run(obj: Object): T;
}

export function joiValidate<T>(vmOptions: JoiValidatedMethodOptions<T>) {
    vmOptions.validate = (obj: Object) => {
        const result = Joi.validate(obj, vmOptions.schema, { stripUnknown: true });
      
        if (result.error) {
          const err = new ValidationError(result.error.details.map(err => {
            // map joi error to ValidationError
            return {
              name: err.path,
              type: err.type,
              message: err.message
            };
          }));
          throw err;
        }
          
        // call validatedMethod.run with the validated/cleaned values!
        const runFunc = vmOptions.run;
        vmOptions.run = function () {
          return runFunc.bind(this)(result.value);
        }
    }
    return vmOptions as ValidatedMethodOptionsWithMixins<string, typeof vmOptions.run>;
}

function DeclareJoiValidatedMethod<T>(optionObj: JoiValidatedMethodOptions<T>) {
  return new ValidatedMethod(joiValidate(optionObj));
}

Annoyingly, joi-extract-type doesnā€™t appear to support later versions of Joi. However, I seem to have found a solution using the io-ts package that allows for both compile time and run time checking using only a single prop schema:

import * as t from "io-ts";
import { PathReporter } from "io-ts/PathReporter";

const MethodProps = t.type({
  someProp1: t.string,
  someProp: t.number,
});
type MethodTypes = t.TypeOf<typeof MethodProps>;
const methodName = "myMethod";

const MyValidatedMethod = new ValidatedMethod<string, (...params: MethodTypes[])=> void>({
  name: methodName,
  validate(obj) {
    const isValid = MethodProps.is(obj);
    if (!isValid) {
      const decoded = MethodProps.decode(obj);
      const report = PathReporter.report(decoded);
      const error = new Meteor.Error("Fail");
      error.error = "validation-error";
      error.details = report.join(", ");
      throw error;
    }
  },
  run(params): void {
    // Some logic
    console.log(params.someProp1);
  },
});
2 Likes

Indeed. It seemed complicated to make Joi support it, but I was already using the Ajv library with Uniforms, and I found out that the latest version of Ajv (8.6) had native Typescript type inference based on JSON Type Definition (JTD) schemas. So now hereā€™s what my new methods look like:

const itemRecordSchema = {
    properties: {
        distance: {type: 'float64'}
    },
    optionalProperties: {
        tag: {type: 'string'}
    }
} as const

const reportDistancesSchema = {
    properties: {
        items: {elements: itemRecordSchema},
        userMsg: {type: 'string'}
    }
} as const

DeclareAjvValidatedMethod({
    name: 'reportDistances',
    schema: reportDistancesSchema,
    run({items, userMsg}: JTDDataType<typeof reportDistancesSchema>) {

My DeclareAjvValidatedMethod is quite similar to DeclareJoiValidatedMethod, except that it uses ajv.compile() outside the validate function, then uses the returned validator function inside it.

NB: Ajvā€™s type inference is said to require Typescript 4.2.4, which is available in Meteor 2.3.
Notice the ā€œas constā€ after schemas, it you omit them all property types will be ā€˜unknownā€™.
More info: Using with TypeScript | Ajv JSON schema validator

1 Like