Skip to content

Latest commit

 

History

History
185 lines (165 loc) · 5.63 KB

Roadmap.md

File metadata and controls

185 lines (165 loc) · 5.63 KB

Roadmap

  • CI

  • Linting

  • cli

    • Docs
  • parse: Parse ThinQL queries to ASTs

    • Unit tests
    • Generate docs and/or use TypeScript
  • model: Define resource property names and value data validations, function signatures, and joins on related resources

    • APIs
    • Unit tests
    import {
      Func,
      Model,
      Schema,
      Type,
      // convenient built-in types
      booleanType,
      nullType,
      numberType,
      requiredArgumentType,
      stringType,
    } from '@thinql/model'
    
    const id = new Type({
      comparisonOperators: ['=', '!='],
      pattern: /^[1-9]+\d*$/,
    })
    
    const number = new Type({
      comparisonOperators: ['=', '!=', '>', '>=', '<', '<='],
      pattern: /^-?\d*\.?\d*$/,
      // default: transform: value => value.content
      transform: value => parseInt(value.content, 10),
    })
    
    const dateString = new Type({
      comparisonOperators: ['=', '!=', '>', '>=', '<', '<='],
      pattern: /^\d{4}-\d{2}-\d{2}$/,
    })
    
    const permissionSlug = new Type({
      comparisonOperators: ['=', '!='],
      // async is supported but like SQL this can be a runtime error and not
      // necessary to validate as the query is composed. parsing execution
      // errors might be more efficient than making validation queries but
      // provides a less direct way to troubleshoot. also, note that this query
      // of course cannot be atomic.
      transform: async value => {
        const permission = await findPermissionBySlug(value.content)
        if (permission) {
          return value.content
        } else {
          throw new Error('The permission slug is invalid')
        }
      },
    })
    
    const string = new Type({
      comparisonOperators: ['=', '!=', '*='],
    })
    
    // extend from all of the classes (essentially just Object.assign their configs, but also runs transform calls sequentially)
    const email = string.extend({
      pattern: /^.+?@.+$/,
      transform: value => value.content.trim().toLowerCase(),
    })
    
    const requiredArgument = new Type({
      transform: value => {
        // value can literally only be undefined in the case of fewer arguments passed to a function than were in the signature
        if (value === undefined) {
          throw new Error('argument is required')
        } else {
          return value.content
        }
      },
    })
    
    const propertyArgument = requiredArgument.extend({
      transform: property => {
        // Schema#properties can be used to recursively find property names even on relationships
        if (!get(user.properties, property.content)) {
          throw new Error('property is invalid')
        } else {
          return property.content
        }
      },
    })
    
    const user = new Schema({
      id: { title: 'User ID', desc: 'The User ID', type: id },
      firstName: { title: 'First name', desc: "The user's first name", type: string },
      owner: () => ({ label: 'Owner', type: user }), // for self-references, use CB style like node graphql
      pet: () => ({ label: 'Pet', type: pet }),
    })
    
    const pet = new Schema({
      name: { title: 'Name', desc: "The pet's name", examples: ['Paco'], type: string },
      owner: { label: 'Owner', type: user },
    })
    
    const length = new OperandFunction({
      arguments: [
        { name: 'property', desc: 'The property whose string length is being computed', type: propertyArgument }
      ],
      desc: 'Get the length of the value of the given property',
      returnType: number,
    })
    
    const isActive = new StandaloneFunction({
      desc: 'Users who are active',
      // knex.QueryBuilder indicates it can be called directly in the query
      // Otherwise it may be a Type if it returns a string, or another value here.
      // it will just be a primitive or an instanceof check, so:
      // null, undefined, Number, String, RegExp, Date, Object, etc.
      returnType: StandaloneFunction.QueryBuilder,
    })
    
    const now = new StandaloneFunction({
      desc: 'Get the current date/time instance',
      returnType: Date,
    })
    
    const getDate = new StandaloneFunction({
      arguments: [
        { name: 'date', desc: 'The date object', type: Date },
      ],
      desc: 'Get a date string (YYYY-MM-DD) from the date/time instance',
      returnType: dateString,
    }
    
    export default new Model({
      fullTextSearch: false /* disable */ | true /* no restrictions, default */ | Type,
      functions: {
        getDate,
        isActive,
        length,
        now,
      },
      schema: user,
    })
  • conform: Validate and transform the AST of a ThinQL query based on models

    • Create validator
    • Unit tests
    const thinqlMiddleware = model => async (req, res, next) => {
      if (!req.query.q) {
        next()
        return
      }
    
      let ast = parse(req.query.q)
    
      try {
        ast = await conform(ast, model)
      } catch (err) {
        if (err.code === '@thinql/model') {
          res.status(400).send(err.toString())
        } else {
          next(err)
        }
        return
      }
    
      res.locals.thinql = ast
      next()
    })
  • monarch-language: Provide highlighting and IntelliSense ThinQL query editing with Monaco based on models

    • Define syntax
    • Add IntelliSense property and function suggestions when tied to a model
    • Support nested properties for related resources
  • to-knex: Transform a ThinQL AST to a Knex.js query

    • Create transformer
    • Unit tests
  • sandbox: Generate and examine ASTs, write and test models, try the Monaco editor, etc. in a GitHub Pages environment.

    • Basic input textarea -> AST output
    • Code textarea that can be eval()'d to define functions, models, schemas, and types in JS code
    • Once the model is valid, you can additionally validate and conform the query AST
    • Embed a Monaco editor that uses the defined model to provide support