Coder Social home page Coder Social logo

mondrian-framework / mondrian-framework Goto Github PK

View Code? Open in Web Editor NEW
8.0 8.0 1.0 7.37 MB

Framework for building modular, type-safe and documented backends.

Home Page: https://mondrianframework.com

License: Apache License 2.0

TypeScript 98.21% JavaScript 0.79% CSS 0.94% Dockerfile 0.06%
documentation graphql model-driven-development modular openapi prisma runtime-validation schema-validation static-type type-inference type-safety typescript

mondrian-framework's Introduction

CI codecov

Mondrian

Homepage

CI Tools

1 minute spinup example

Prerequisite:

  • Node >= 20
git clone https://github.com/mondrian-framework/mondrian-framework.git
cd mondrian-framework
npm run spinup

Then query your endpoint with graphql or rest:

curl --location --globoff 'http://localhost:4000/graphql' \
--header 'Content-Type: application/json' \
--data-raw '{"query":"mutation register { user { register(input: { email: \"[email protected]\", password: \"12345\", firstName: \"John\", lastName: \"Wick\" }) { ... on MyUser { id } ... on RegisterFailure { code } } } }" }'

How it works

Mondrian allows you to define a data model in an intuitive human-readable way. In addition to model fields, types, possibly new scalars and relationships, you can utilize a wide range of validity rules or create new and reusable ones. Once the model is defined, the framework provides a set of fully automatic translation features to major standards: JSONSchema (OpenAPI), GraphQL and Protobuf. graphql-example

Usage example

In this section, we’ll walk through an example of how to use the Mondrian framework in TypeScript. We’ll create a simple registration function, add typed errors, and serve it through a REST API.

For this example we'll need to install this packages:

npm i @mondrian-framework/model \
      @mondrian-framework/module \
      @mondrian-framework/rest-fastify \
      @mondrian-framework/graphql-yoga \
      fastify

Build functions

In our first example, we'll guide you through creating a registration function using the Mondrian framework. This function, written in TypeScript, accepts an email and password as input and outputs a JSON web token:

import { model, result } from '@mondrian-framework/model'
import { functions } from '@mondrian-framework/module'

const register = functions
  .define({
    input: model.object({ email: model.email(), password: model.string() }),
    output: model.object({ jwt: model.string() }),
  })
  .implement({
    async body({ input: { email, password } }) {
      // weak password check
      if (password.length < 3) {
        throw new Error('Weak password.')
      }
      // register logic ...
      return result.ok({ jwt: '...' })
    },
  })

Congratulations! You've just implemented your initial Mondrian function. To enhance error handling, let's explore a more advanced example where we introduce typed errors:

import { model, result } from '@mondrian-framework/model'
import { functions, error } from '@mondrian-framework/module'

const errors = error.define({
  weakPassword: { message: 'The password is weak', details: model.object({ reason: model.string() }) },
  emailAlreadyUsed: { message: 'This email is already used' },
})

const register = functions
  .define({
    input: model.object({ email: model.email(), password: model.string() }),
    output: model.object({ jwt: model.string({ minLength: 3 }) }),
    errors,
  })
  .implement({
    async body({ input: { email, password } }) {
      if (false /* weak password logic */) {
        return result.fail({ weakPassword: { details: { reason: 'Some reason' } } })
      }
      if (false /* email check logic */) {
        return result.fail({ emailAlreadyUsed: {} })
      }
      // register logic ...
      return result.ok({ jwt: '...' })
    },
  })

Build module

Here's how you can build the Mondrian module using TypeScript:

import { result } from '@mondrian-framework/model'
import { module } from '@mondrian-framework/module'

//instantiate the Mondrian module
const moduleInstance = module.build({
  name: 'my-module',
  functions: { register },
})

This snippet showcases how to instantiate the Mondrian module, incorporating the functions you've defined.

Serve module REST

Now, let's move on to serving the module as a REST API endpoint. The following TypeScript code demonstrates the mapping of functions to methods and how to start the server:

import { serve, rest } from '@mondrian-framework/rest-fastify'
import { fastify } from 'fastify'

//Define the mapping of Functions<->Methods
const api = rest.build({
  module: moduleInstance,
  version: 2,
  functions: {
    register: [
      {
        method: 'put',
        path: '/user',
        errorCodes: { weakPassword: 400, emailAlreadyUsed: 401 },
        version: { max: 1 },
      },
      {
        method: 'post',
        path: '/login',
        errorCodes: { weakPassword: 400, emailAlreadyUsed: 403 },
        version: { min: 2 },
      },
    ],
  },
})

//Start the server
const server = fastify()
serve({ server, api, context: async ({}) => ({}), options: { introspection: { path: '/openapi' } } })
server.listen({ port: 4000 }).then((address) => {
  console.log(`Server started at address ${address}/openapi`)
})

By enabling REST introspection, you can explore your API using the Swagger documentation at http://localhost:4000/openapi. swagger-example

Serve module GRAPHQL

You can serve the module also as a GraphQL endpoint with the following code:

import { serveWithFastify, graphql } from '@mondrian-framework/graphql-yoga'
import { fastify } from 'fastify'

//Define the mapping of Functions<->Methods
const api = graphql.build({
  module: moduleInstance,
  functions: {
    register: { type: 'mutation' },
  },
})

//Start the server
const server = fastify()
serveWithFastify({ server, api, context: async ({}) => ({}), options: { introspection: true } })
server.listen({ port: 4000 }).then((address) => {
  console.log(`Server started at address ${address}/graphql`)
})

Enabling GraphQL introspection allows you to explore your API using the Yoga schema navigator at http://localhost:4000/graphql Nothing stops you from exposing the module with both a GraphQL and a REST endpoint.

graphql-example

Prisma integration

This framework has a strong integration with prisma type-system and enable you to expose a graph of your data in a seamless-way.

Schema.prisma

model User {
  id         String       @id @default(auto()) @map("_id") @db.ObjectId
  email      String       @unique
  password   String
  posts      Post[]
}

model Post {
  id          String         @id @default(auto()) @map("_id") @db.ObjectId
  content     String
  authorId    String         @db.ObjectId
  author      User           @relation(fields: [authorId], references: [id])
}

types.ts

const User = () =>
  model.entity({
    id: model.string(),
    email: model.string(),
    //passowrd omitted, you can expose a subset of field
    posts: model.array(Post),
  })
const Post = () =>
  model.entity({
    id: model.string(),
    content: model.string(),
    author: User,
  })

const getUsers = functions
  .define({
    output: model.array(User),
    retrieve: { select: true, where: true, orderBy: true, skip: true, limit: true },
  })
  .implement({
    body: async ({ retrieve }) => result.ok(await prismaClient.user.findMany(retrieve)), //retrieve type match Prisma generated types
  })

By exposing the function as GraphQL endpoint we can navigate the relation between User and Post.

image

Graph security

In this configuration, we have created a data breach. In fact, by retrieving users with the getUsers query, we are exposing the entire graph to every caller. To resolve this problem, we can (and in some cases should) implement a first level of security on the function that checks if the caller is an authenticated user. We can do this as follows:

import { model, result } from '@mondrian-framework/model'
import { functions, provider, error } from '@mondrian-framework/module'

const { unauthorized } = error.define({ unauthorized: { message: 'Not authenticated!' } })

const authProvider = provider.build({
  errors: { unauthorized },
  body: async ({ authorization }: { authorization?: string }) => {
    if (!authorization) {
      return result.fail({ unauthorized: {} })
    }
    const userId = await verifyToken(authorization)
    if (!userId) {
      return result.fail({ unauthorized: {} })
    }
    return result.ok({ userId })
  },
})

const getUsers = functions
  .define({
    output: model.array(User),
    errors: { unauthorized },
    retrieve: { select: true, where: true, orderBy: true, skip: true, limit: true },
  })
  .use({ providers: { auth: authProvider } })
  .implement({
    body: async ({ retrieve, auth: { userId } }) => {
      const users = await prismaClient.user.findMany(retrieve)
      return result.ok(users)
    },
  })

A problem remains... What if a logged-in user selects all user passwords?! Or maybe traverses the graph and selects some private fields? Mondrian-Framework natively supports a layer of security that can be used to secure the graph. This mechanism is applied every time we call a function with some retrieve capabilities and for the protected types (defined by you). In the following example, we show how to define such a level of security:

import { result } from '@mondrian-framework/model'
import { module, security } from '@mondrian-framework/module'

const moduleInstance = module.build({
  name: 'my-module',
  version: '0.0.0',
  functions: myFunctions,
  policies({ auth: { userId } }: { auth: { userId?: string } }) {
    if (userId != null) {
      return (
        security
          //On entity "User" a logged user can read anything if it's selecting it's user
          //otherwise can read the "id" and the "email"
          .on(User)
          .allows({ selection: true, restriction: { id: { equals: userId } } })
          .allows({ selection: { id: true, email: true } })
          //On entity "Post" a logged user can read anything on every post
          .on(Post)
          .allows({ selection: true })
      )
    } else {
      // On unauthenticated caller we left visible only id of both "User" and "Post" entities
      return security
        .on(User)
        .allows({ selection: { id: true } })
        .on(Post)
        .allows({ selection: { id: true } })
    }
  },
})

This feature offers some more functionality that you can read about in the official documentation, or you can take a peek inside the example package where we define some more complex security policies (packages/example/src/core/security-policies.ts).

mondrian-framework's People

Contributors

edobrb avatar giacomocavalieri avatar minox86 avatar renovate[bot] avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

edobrb

mondrian-framework's Issues

`withDefault` should also be considered in decoding

The default is very useful when used inside input types. For example:

const QueryInput = types.object({
  search: types.string().optional(),
  skip: types.number().optional().withDefault(0),
  take: types.number().optional().withDefault(20),
})

const function = buildFunction({
  input: QueryInput,
  output: ...,
  async apply({ input }) {
    // input.skip & input.take should be always defined
    //...
})

it would be cool if we implement 2 slightly different variants of Infer:

// type used by the function implementation
type QueryInput = types.InferInput<typeof QueryInput> // { search?: string, skip: number, take: number }

// type used by the caller
type QueryInput = types.Infer<typeof QueryInput> // { search?: string, skip?: number, take?: number }

Maybe this would be clearer if we reintroduce the DefaultDecorator that we'll be called DefaultType and used as follows:

const QueryInput = types.object({
  search: types.string().optional(),
  skip: types.number().default(0),
  take: types.number().default(20),
})

To me the latter communicates the intent more clearly instead of optional().withDefault(...)

Error handling design

IMPERATIVA:
In rest:

  • bisogno di poter cambiare lo stato http in caso di errore

In graphql:

  • poter mappare l'eccessione in un GraphqlError

FUNZIONALE:

ogni ritorno è wrappato in un tipo
type Result<T, E> = { pass: true, result: T } | { pass: false, error: E }

obbligo a definire sempre 2 tipi per ogni operazione API (T, E)

cons:

  • alcune operazioni potrebbe non avere bisogno di E
  • alcune operazioni potrebbero comunque lanciare eccezzione

PROPOSTA:

l'uso di un Wrapper Result è a discrezione dello sviluppatore. Le eccezzioni che escono dal resolver devono essere gestite in qualche modo e non lanciare un 500.
Obbligatorietà di definire una gestione dell'errore dentro al ModuleRunnerOptions

Add an `Arbitrary` instances for the Mondrian types

It could greatly help to have an Arbitrary implementation for each of the Mondrian types just to make the testing of useful properties easier.

Also, we could think about exposing those implementations to the framework user to help them property test their own Mondrian-based models without having to rewrite their own implementations.

Types utils

Record
Implement a utility function that resemble Record

Take this example:

const Currency = m.enum(['USD', 'EUR', 'GBP'])

//Feature request:
const Value = m.record(Currency, m.integer()) 

//Should be equivalent to:
const Value = m.object({
    USD: m.integer(),
    EUR: m.integer(),
    GBP: m.integer(),
})

m.record should accept an enum type or (maybe?) a union of literals, and a type for the properties

Pick
rename m.select in m.pick (https://www.typescriptlang.org/docs/handbook/utility-types.html#picktype-keys)

Omit
Inverse of m.pick (https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys)

const User = () => m.object({
    id: m.string(),
    name: m.string(),
    referrer: m.reference(m.optional(User)),
})

const SafeUser = m.omit(User, { referrer: { referrer: true } }) // { id: string, name: string, referrer: { id: string, name: string } )

Bonus:
OmitReference
m.omitReference(...object...) should automatically remove all reference fields

const User = () => m.object({
    id: m.string(),
    name: m.string(),
    referrer: m.reference(m.optional(User)),
})

const SafeUser = m.omitReference(User) // { id: string, name: string )

Opentelemetry instrumentation

When we'll be confident enough of the current opentelemetry instrumentation we can proceed with these further instrumentations:

  • aws-sqs
  • cron
  • direct
  • graphql
  • module
  • rest

This is a continuation of #51

Improve number generators' options

The generator for number types currently does not generate the minimum/maximum/exclusiveMinimum/exclusiveMaximum options. Previously we were naive in the implementation and this resulted in generators that would output invalid types.
I couldn't figure out a nice way to do it so I'm opening this issue as a future reminder to put more work into it.

Thinking about this, the options generator should generate non sensical options (fundamental to properly test the implementations relying on the options) but the fromType generator should refuse to generate a type for those options since it would be impossible (however, if it detects options for which it's impossible to generate a number it should throw an exception, not hang forever like it previously did)

Perfomance improvement

Remove ts-pattern dependency as some quick test asserts a > 3x slowdown

  • encode
  • decode
  • validate

`merge`, `omit`, `pick` and `partial` not building with recursive type

const User = () =>
    types.object({
      email: types.string(),
      password: types.string(),
      friend: types.partial(User),
})
//'User' implicitly has return type 'any' because it does not have a return type annotation
// and is referenced directly or indirectly in one of its return expressions.ts(7023)

Change `DefaultDecorator` definition

I'm writing docs and tests for the DefaultDecorator and I was thinking it could make sense to change its definition:

export interface DefaultDecorator<T extends LazyType = Type> extends Type {
  kind: 'default-decorator'
  type: T
  opts: {
    name?: string
    description?: string
    default?: Infer<T> | (() => Infer<T>)
  }
}

Right now its opts field is not optional (making it the only exception among all the other Types, whose opts field is always optional). Moreover the default field is optional which to me doesn't make sense since this is used to decorate a type with a value.

I propose we change its definition to the following:

export interface DefaultDecorator<T extends LazyType = Type> extends Type {
  kind: 'default-decorator'
  type: T
  default: Infer<T> | (() => Infer<T>)
  opts?: {
    name?: string
    description?: string 
  }
}

In my opinion this would have a couple advantages:

  • a DefaultDecorator must have a default value by construction
  • the default is no longer tied to the options but directly to the interface making it obvious how it is something specifically related to DefaultDecorator and not a generic option that may also appear on other types
  • opts can now be turned back to an optional field making it the same as all other type definitions

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

Rate-Limited

These updates are currently rate-limited. Click on a checkbox below to force their creation now.

  • chore(deps): update dependency @fast-check/vitest to v0.1.1
  • chore(deps): update dependency @types/node to v20.12.7
  • chore(deps): update dependency typescript to v5.4.5
  • fix(deps): update dependency @types/aws-lambda to v8.10.137
  • fix(deps): update font awesome to v6.5.2 (@fortawesome/fontawesome-svg-core, @fortawesome/free-solid-svg-icons)
  • chore(deps): update dependency graphql-yoga to v5.3.0
  • chore(deps): update dependency rollup to v4.17.2
  • fix(deps): update dependency @graphql-tools/utils to v10.2.0
  • fix(deps): update dependency @opentelemetry/instrumentation-fastify to v0.36.0
  • fix(deps): update dependency @opentelemetry/instrumentation-graphql to v0.40.0
  • fix(deps): update dependency fast-check to v3.18.0
  • fix(deps): update dependency swagger-ui-dist to v5.17.2
  • fix(deps): update opentelemetry-js monorepo to ^0.51.0 (@opentelemetry/api-logs, @opentelemetry/instrumentation-http, @opentelemetry/sdk-node)
  • fix(deps): update prisma monorepo to v5.13.0 (@prisma/client, @prisma/instrumentation, prisma)
  • fix(deps): update react monorepo to v18.3.1 (react, react-dom)
  • 🔐 Create all rate-limited PRs at once 🔐

Open

These updates have all been created already. Click a checkbox below to force a retry/rebase of any.

Detected dependencies

dockerfile
Dockerfile
  • public.ecr.aws/lambda/nodejs 20-arm64
github-actions
.github/workflows/ci-checks.yml
  • actions/checkout v4
  • actions/setup-node v4
  • codecov/codecov-action v3
.github/workflows/deploy-docs.yml
  • actions/checkout v4
  • actions/setup-node v4
  • chetan/invalidate-cloudfront-action v2
npm
package.json
  • @changesets/cli 2.27.1
  • @trivago/prettier-plugin-sort-imports 4.3.0
  • prettier 3.2.5
  • rimraf 5.0.5
  • typescript 5.4.4
  • vitest 0.34.6
  • @types/node 20.12.5
  • @vitest/coverage-v8 0.34.6
  • rollup 4.14.1
  • node >=20.9
packages/aws-lambda-rest/package.json
  • aws-lambda 1.0.7
  • @types/aws-lambda 8.10.136
  • lambda-api 1.0.3
packages/aws-lambda-sqs/package.json
  • aws-lambda 1.0.7
  • @types/aws-lambda 8.10.136
packages/aws-sqs/package.json
  • @aws-sdk/client-sqs ^3.529.1
packages/ci-tools/package.json
  • @aws-sdk/client-s3 ^3.529.1
  • @graphql-inspector/core 5.0.2
  • @pb33f/openapi-changes 0.0.55
  • fastify ^4.26.2
packages/cli-commander/package.json
  • commander 11.1.0
packages/cli/package.json
  • commander 11.1.0
packages/cron/package.json
  • node-cron 3.0.3
  • @types/node-cron 3.0.11
packages/direct/package.json
  • fastify ^4.26.2
  • @opentelemetry/sdk-node ^0.49.1 || ^0.50.0
packages/docs/package.json
  • @docusaurus/core 3.0.0
  • @docusaurus/preset-classic 3.0.0
  • @mdx-js/react ^3.0.0
  • clsx ^2.1.0
  • prism-react-renderer ^2.3.1
  • react ^18.0.0
  • react-dom ^18.0.0
  • react-github-btn 1.4.0
  • @fortawesome/fontawesome-svg-core 6.5.1
  • @fortawesome/free-solid-svg-icons 6.5.1
  • @fortawesome/react-fontawesome 0.2.0
  • @docusaurus/theme-mermaid 3.0.0
  • @docusaurus/module-type-aliases 3.0.0
  • @docusaurus/tsconfig 3.0.0
  • @docusaurus/types 3.0.0
  • node >=18.0
packages/example/package.json
  • @prisma/client 5.10.2
  • prisma 5.10.2
  • jsonwebtoken 9.0.2
  • rest 2.0.0
  • @opentelemetry/sdk-node 0.50.0
  • @opentelemetry/api-logs 0.50.0
  • @redis/client 1.5.14
  • @fastify/cors 9.0.1
  • @opentelemetry/instrumentation-http 0.50.0
  • @opentelemetry/instrumentation-fastify 0.34.0
  • @prisma/instrumentation 5.10.2
  • @opentelemetry/instrumentation-graphql 0.38.1
  • @types/jsonwebtoken 9.0.6
packages/graphql-yoga/package.json
  • graphql-yoga ^5.1.1
  • fastify ^4.26.2
packages/graphql/package.json
  • @graphql-tools/schema ^10.0.3
  • @graphql-tools/utils ^10.1.0
  • graphql ^16.8.1
  • graphql-yoga ^5.1.1
packages/model/package.json
  • fast-check ^3.16.0
  • jsonwebtoken ^9.0.2
  • bignumber.js ^9.1.2
  • @types/jsonwebtoken ^9.0.6
  • @fast-check/vitest ^0.1.0
  • @vitest/coverage-v8 ^0.34.6
packages/module/package.json
  • @opentelemetry/sdk-node ^0.50.0
  • @opentelemetry/api-logs ^0.50.0
packages/provider/rate-limiter/package.json
  • @redis/client ^1.5.14
  • @opentelemetry/sdk-node ^0.49.1 || ^0.50.0
packages/rest-fastify/package.json
  • fastify ^4.26.2
  • @fastify/static ^7.0.1
  • @types/swagger-ui-dist 3.30.4
packages/rest/package.json
  • openapi-types ^12.1.3
  • swagger-ui-dist ^5.11.10
  • @opentelemetry/sdk-node ^0.49.1 || ^0.50.0
packages/utils/package.json

  • Check this box to trigger a request for Renovate to run again on this repository

Add `sensitive` option

const t = types.object({ username: types.string(), password: types.string().sensitive() })
const t = types.object({ username: types.string(), password: types.string({ sensitive: true }) })

Also add an option on encoder

// or 'show' (default is 'show')
t.encode(value, { sensitiveInformationStrategy: 'hide' }) // { "username": "Jon", "password": null }

A sensitive type always encode as null if sensitiveInformationStrategy is 'hide'

Add a `minItems` to the `ArrayDecorator` type

It could be useful to add a minItems field in the ArrayDecorator opts
This would make sense as defining non empty structures could be quite useful; moreover it would align the ArrayDecoration definition with other definitions like NumberType that define both an upper and lower limit on their values.

As far as I can tell right now this would require:

  • adding a field to the interface
  • updating the validation step to check the length of the array

Use classes to create types

Right now each time we call a type constructor we allocate a new object with all the needed functions as closures. We could resort to using a private class instead

`subProjection` should be renamed `subProjectionType`

Also we need an implementation of subProjection with works with instance of projection.

const sub1 = subProjection({ field: true }, 'field') //true
const sub2 = subProjection(true, 'field') //true
const sub3 = subProjection({ field2: true }, 'field') //undefined

JWT could accept the payload type

const payloadType = object({ userId: string() })
const t = jwt(payloadType)

const result = decode(t, ...)
if(result.success) {
 const  { payload, jwt } = result.value
 //payload is of type Infer<typeof payloadType>
 //jwt is the original jwt
}
`

Trim result value on `projection.respectProjection`

It would be useful if the returned value of this function were result.Result<types.InferPartial<T>, projection.Error[]> and it's behaviour to remove all the fields that are not required by the projection.

This is useful when we implement a function without caring about the projection by returning always the complete output type. A user calling the function from rest or from graphql could anyways specify the projection and it would be cool if the output is "trimmed" in order to sent to the network only the minimum required data.
I am aware that graphql-yoga server does the same thing so on the graphql side this wouldn't be needed. On the rest/local side this is still relevant.

If we consider to implement this I suggest something like this:

export function respectsProjection<T extends types.Type>(
  type: T,
  projection: projection.FromType<T>,
  value: types.InferPartial<T>,
  options?: { trim?: boolean /*if false returns exactly `value`, if true trim `value` to `projection` */}
): result.Result<types.InferPartial<T>, projection.Error[]> 

The options makes sense only if there is a non-negligible difference in performance, otherwise we could go without the option and always trim.

Support for `OpenTelemetry`

We could add a new package called opentelemetry where we can give an utility for instrument a Mondrian module.

import { module } from '@mondrian-framework/module'
import { opentelemetry } from '@mondrian-framework/opentelemetry'

const m = module.build({ functions: { ... }, ... })
opentelemetry.instrument({ module: m })

For the first step I think we could just wrap every functions in order to create a span and a metric signal for every execution.

As second step we could include the log signal.

Some useful documentation:
https://opentelemetry.io/docs/instrumentation/js/manual/
https://opentelemetry.io/docs/concepts/semantic-conventions/

Add enum `TypeKind`

enum TypeKind = {
  String = 0,
  Number = 1,
  ...
  Object = 6,
  Custom = 7
}
  • Improve memory performance
  • Decoder, Encoder, Validate could remove the if/else cascade

Add `projection.decode` function

//inside model/projection.ts
function decode<T extends types.Type>(type: T, value: unknown): decoder.Result<FromType<T>>

Used to decode a value into a valid T projection

Add `rate-limiting` capability

We can get inspiration from CloudFlare solution.

Feature requests:

  • possible to define multiple limits per function based on a custom logic (for example the login function must be limited with a maximum 20 requests per hour by ip-email combination, and with a maximum of 20 request per minute by ip independently)
  • possible to define the strategy: leaky-bucket or sliding-window
  • possible to define the memory support: redis or local

An example could be:

const myModule = m.module({
 name: 'example',
 version: '1.0.0',
 functions: {
   definitions: functions,
   options: {
     login: {
       rateLimits: [
         {
           description: 'Maximum 20 attempts per hour', // could be exported to openapi & graphql
           rate: '20/hour',
           key: (context, input) => `login-${input.email}-${context.request.ip}`,
           strategy: undefined // default is set to 'sliding-window'
         },
         {
           rate: '20/minute',
           key: (context, input) => `login-${context.request.ip}`,
           strategy: 'leaky-bucket'
         },
       ],
     },
   },
 },
 options: {
   rateLimit: {
     strategy: 'sliding-window',
     support: ratelimiter.redis({ endpoint: '...' })
   }
 }
})

As a first step we could only implement the sliding-windows strategy described by CloudFlare

Add an options chapter in the doc site

We should add a page related to types' options:

  • explain how they work
  • show how Mondrian types are immutable (changing options returns a new type)
  • describe the basic options shared by all types
  • go into more depth about the options of each type

Add `tuple` type

const t = types.tuple(types.number(), types.string(), types.boolean())
type T = types.Infer<typeof t> // [number, string, boolean]

Add `Project` type

export type Project<T extends types.Type, P extends projection.FromType<T>> = never // A subset of types.Infer<T> based on P

`projection.subProjection` not compiling with recursive types

This occurs only when the projection type is of type projection.FromType<T> and not a specific type. However, this usecase is the most common while implementing a function.

This snippet gives: Type of property 'friends' circularly references itself in mapped type... ts(2615)

const user = () => types.object({ name: types.string(), friends: types.array(user) })
type UserProjection = projection.FromType<typeof user>
const userPorjection: UserProjection = { name: true, friends: { name: true } }
const friendsProjection = projection.subProjection(userPorjection, ['friends'])

Remove `ts-pattern` dependency

We used ts-pattern on our encoder/decoder/validator but removed it, right now the only place where it's used is the in the arbitrary definition but could easily be replaced by a "simple" chain of if-else (an example of such transformation can be seen here)

Packages dashboard for #25

In order to merge #25 we need to update this packages:

  • utils
  • model
  • module
  • prisma
  • rest
  • rest-fastify
  • graphql
  • graphql-yoga
  • aws-sqs
  • aws-lambda-sqs
  • aws-lambda-rest
  • cron
  • example

Change decoder.Result type and fix implementation for 'allErrors' reporting strategy

const type = types.object({ a: types.string(), b: types.string({ minLength: 2 }) })
const result = decoder.decode(type, { b: '1' }, { errorReportingStrategy: 'allErrors' })
expect(result.isOk).toBe(false)
if(!result.isOk) {
  expect(result.error.length).toBe(2) //instead is 1 but i would expect to get 2
}

In this example i would expect to get a decoder error and one validation error. In order to do this we must change the return of decode from

result.Result<types.Infer<T>, validator.Error[] | decoder.Error[]>

to

result.Result<types.Infer<T>, (validator.Error | decoder.Error)[]>

To me this would also simplify the user experience, for example I encounter this case:

const results: decoder.Result<unknown>[] = ...
const allErrors = results.flatMap(r => r.match(() => [], (e) => e)) //this would not be possible with (validator.Error[] | decoder.Error[])
if(results.some(r => !r.isOk)) {
  console.log(allErrors)
}

Let sdk accept an operationId

When using a module through sdk utility I would like to pass an operationId. This is useful if inside a function I'm using another module. In that way i can correlate the different calls.

Improve documentation for `ArrayDecorator`'s `maxItems`

Right now it's not immediately apparent from the documentation of ArrayDecorator if its maxItems option is considered as an inclusive or exclusive upper limit.

As far as I can tell this would require:

  • inspecting the validation step to see if the number is included or not in the upper limit
  • updating the documentation with a better explanation of the expected behavior

Take advantage of new classes

Since we now create the model using classes, it would be a really nice update to move all relevant methods directly into the types' interface rather than in separate functions. This could make things easier for the someone who's more practical with OO languages and intellisense could help a lot.

For example, instead of

import { decoder } from @mondrian
decoder.decode(model, value)

One could simply:

model.decode(value)

This would be relevant for:

  • equality checks with areSameType
  • encoding
  • decoding
  • validation

Everything is already implemented, it would just be a matter of moving each branch of the if/then/elses to the relevant class

Versioning

sarebbe molto potente se si potesse cambiare il modello e opzionalmente si volesse continuare a supportare anche quello vecchio.
Opzioni:

  • Usare git+tag per avere più deploy contemporanei di uno stesso modulo a versioni diverse. Svantaggio: i resolver di versioni vecchie non possono essere aggiornati
  • Lasciare all'utente la gestione
  • ... ???

bonus: uno stesso resolver che risponde a più path (o query o mutation) diverse
questo serve nel caso in cui si migra un path

Builder methods should accept options

Right now the builder methods do not accept options, they should in order to be equivalent to the corresponding functions:

types.number().array() // <- cannot pass array options
types.array(types.number(), arrayOptions)

Add minimum projection required to check union variants

Something like this:

export type UnionType<
  Ts extends Types,
  P extends { [K in keyof Ts]: Infer<projection.FromType<Ts[K]>> } | undefined,
> = {
  readonly kind: 'union'
  readonly variants: Ts
  readonly variantsChecks?: {
    projection?: P
    is: {
      [K in keyof Ts]: P extends undefined
        ? (_: Infer<UnionType<Ts, undefined>>) => boolean
        : P extends Infer<projection.FromType<UnionType<Ts, undefined>>>
        ? (_: Infer<projection.ProjectedType<UnionType<Ts, undefined>, P>>) => boolean
        : never
    }
  }
  readonly options?: UnionTypeOptions

  optional(): OptionalType<UnionType<Ts, P>>
  nullable(): NullableType<UnionType<Ts, P>>
  array(): ArrayType<'immutable', UnionType<Ts, P>>
  reference(): ReferenceType<UnionType<Ts, P>>
  setOptions(options: UnionTypeOptions): UnionType<Ts, P>
  updateOptions(options: UnionTypeOptions): UnionType<Ts, P>
  setName(name: string): UnionType<Ts, P>
}

const u = union(
  {
    A: object({ type: literal('a'), n: number() }),
    B: object({ type: literal('b'), n: number() }),
    C: object({ c: string() }),
  },
  {
    projection: {
      A: { type: true },
      B: { type: true },
      C: { c: true },
    },
    is: {
      A: (v) => !('c' in v) && v.type === 'a',
      B: (v) => !('c' in v) && v.type === 'b',
      C: (v) => 'c' in v,
    },
  },
)

I'm not sure if projection could be undefiend.

Add a contributing guide

We should make it easier for new people to start contributing to the framework:

  • Add a contributing guide:
    • Best practices to follow
    • Enforce in the CI that code is properly tested and has sufficient coverage
    • Enforce in the CI that code is properly formatted
    • Add "good first issues" labels
  • Maybe in the docs we could have a whole section dedicated to this
  • There should be a high level description of the project's structure, what each module does and why
  • We should document our design choices somewhere (the above mentioned section in the docs could be a good place to do so!) so that we do not waste time in future bike shedding

Function composition

F1 = I1 -> O1
F2 = I2 -> O2
f.compose(F1, F2, P1)

O1(P1) must extends O2

what about context

Add `validator.validatePartial`.

export function validatePartial<T extends types.Type>(
  type: T,
  value: types.InferPartial<T>,
  options?: Partial<validator.Options>,
): validator.Result {
  //TODO
}

Benchmarks for the encoding and decoding

Right now I've implemented the encoding and decoding parts using TS pattern that can be undoubtedly useful in writing nice looking conditional code. However, this could introduce an overhead we may not accept in the encoding/decoding process.

We should do a benchmark to see the difference between the TS-pattern version and the pure typescript nested ifs version

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.