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