npm package: @jmorecroft67/io-ts-types
Description: An extended `io-ts` `Schemable` interface adding decimals, dates and ints, plus a handful of implementations for filter condition schema generation, filter function generation and protobuf serialization and proto file generation.
Author: Jess Morecroft
Latest version: 1.0.1

## io_ts_types

An extended `io-ts` `Schemable` interface adding decimals, dates and ints, plus a handful of implementations for filter condition schema generation, filter function generation and protobuf serialization and proto file generation.

Before trying out this library you should be familiar with the `Schemable` interfaces and usage patterns, discussed [here](https://github.com/gcanti/io-ts/blob/master/Schema.md).

### `Schemable` and `SchemableLight`

Both `Schemable` and `SchemableLight` interfaces add the additional functions `decimal`, `date` and `int`, which are not included in thaae `io-ts` `Schemable` interfaces. The `SchemableLight` interfaces are otherwise a subset of the `Schemable` interfaces and apart from the primitive functions only include the functions `literal`, `nullable`, `array` and `struct`. This reduces the effort for implementations of `SchemableLight`, but because the interfaces are a subset of the `Schemable` interfaces schemas defined using `SchemableLight` are still compatible with all `Schemable` implementations too. The motivation for `SchemableLight` was the `schemas` and `filter` implementations, which operate on schemas of database entities that do not require the additional functions in `Schemable`.

### `schemas`

The `schemas` module is a `SchemableLight` implementation for generating a filter condition schema from an entity schema. The generated filter condition schema is designed to be a compatible subset of a Prisma query `where` clause interface for a similarly defined database entity. Further, the filter condition schema defines the condition object that is used as part of the input to filter functions generated by our `filter` module. In this way we're able to generate a filter condition schema from an entity schema that we can use for querying a database and for filtering notifications of entity changes coming from a database - effectively the building blocks for a live query implementation.

Here's an example of generating a condition schema for a simple bank account entity, then using that schema to decode an input using an extended version of the `io-ts` decoder. As shown, this particular input fails to decode for a number of reasons.

```typescript
import {
  schemable as S,
  schemas as SS,
  decoder as De
} from '@jmorecroft67/io-ts-types';
import * as D from 'io-ts/Decoder';
import * as E from 'fp-ts/Either';
import { pipe } from 'fp-ts/function';

const accountSchemas = SS.struct({
  id: SS.int,
  name: SS.string,
  balance: SS.decimal,
  updatedAt: SS.date,
  createdAt: SS.date
});

const untypedCondition = {
  id: {
    not: {
      equals: '2'
    }
  },
  name: {
    startsWith: 1
  },
  balance: {
    gt: 'large number'
  },
  createdAt: {
    gt: true
  }
};

const { decode } = S.interpreter(De.Schemable)(accountSchemas.spec);

const condition = pipe(decode(untypedCondition), E.mapLeft(D.draw));

if (E.isLeft(condition)) {
  console.log('decode failed');
  console.log(condition.left);
} else {
  console.log('decode succeeded');
}
```

```
decode failed
optional property "name"
└─ optional property "startsWith"
   └─ cannot decode 1, should be string
optional property "balance"
└─ optional property "gt"
   └─ cannot decode "large number", should be Decimal
optional property "createdAt"
└─ optional property "gt"
   └─ cannot decode true, should be Date
```

Changing this code so the input is more palatable, as shown, and we are then able to decode.

```typescript
import {
  schemable as S,
  schemas as SS,
  decoder as De,
  types
} from '@jmorecroft67/io-ts-types';
import * as D from 'io-ts/Decoder';
import * as E from 'fp-ts/Either';
import { pipe } from 'fp-ts/function';
import Decimal from 'decimal.js';

const accountSchemas = SS.struct({
  id: SS.int,
  name: SS.string,
  balance: SS.decimal,
  updatedAt: SS.date,
  createdAt: SS.date
});

const typedCondition: SS.SpecTypeOf<typeof accountSchemas> = {
  id: {
    not: {
      equals: 2 as types.Int
    }
  },
  name: {
    startsWith: 'A'
  },
  balance: {
    gt: new Decimal('1.23')
  },
  createdAt: {
    gt: new Date(2022, 1, 1)
  }
};

const { decode } = S.interpreter(De.Schemable)(accountSchemas.spec);

// In this case condition will be the same type as typedCondition,
// so this decode is not at required but for demonstation purposes.
const condition = pipe(decode(typedCondition), E.mapLeft(D.draw));

if (E.isLeft(condition)) {
  console.log('decode failed');
  console.log(condition.left);
} else {
  console.log('decode succeeded');
}
```

```
decode succeeded
```

### `filter`

The `filter` module is a `SchemableLight` implementation that acts as a function generator. It will generate a function that takes as input a filter condition object and an entity being filtered. The function returns a `Left` (not filtered) or `Right` (filtered), with the result in both cases being a list of reasons as to why the entity was or was not filtered. The filter condition object must adhere to the type prescribed by the `Spec` type defined in the `Schemas` module.

Here's an example of a condition being applied and failing to match against an entity:

```typescript
import {
  schemableLight as SL,
  schemas as S,
  filter as F,
  types
} from '@jmorecroft67/io-ts-types';
import * as E from 'fp-ts/Either';
import Decimal from 'decimal.js';
import { pipe } from 'fp-ts/lib/function';

const wrap: <T>(
  filter: F.Filter<T>
) => (
  algType: 'failFast' | 'checkAll'
) => (spec: S.Spec<T>, obj: T) => E.Either<string[], string[]> =
  (filter) => (algType) => (spec, obj) =>
    pipe(
      filter(algType)(spec, obj)(),
      E.bimap(
        (i) => i.map((j) => j()),
        (i) => i.map((j) => j())
      )
    );

const personSchema = SL.make((S) =>
  S.struct({
    id: S.int,
    name: S.string,
    favouriteNumber: S.literal(7, 42),
    savings: S.decimal,
    pets: S.array(S.string),
    gender: S.nullable(S.literal('male', 'female')),
    died: S.nullable(S.date),
    createdAt: S.date
  })
);

type Person = SL.TypeOf<typeof personSchema>;
type Spec = S.Spec<Person>;

const condition: Spec = {
  id: {
    not: {
      equals: 123 as types.Int
    }
  },
  name: {
    startsWith: 'B'
  },
  favouriteNumber: {
    notIn: [42]
  },
  savings: {
    gt: new Decimal(100)
  },
  pets: {
    hasEvery: ['rex', 'bluey'],
    hasSome: ['bingo', 'louie']
  },
  gender: {
    in: ['female']
  },
  died: {
    equals: null
  },
  createdAt: {
    gt: new Date(Date.UTC(2000, 1, 1))
  },
  AND: [
    {
      id: {
        lt: 50 as types.Int
      }
    },
    {
      id: {
        gt: 0 as types.Int
      }
    }
  ],
  OR: [
    {
      name: {
        in: ['Betty', 'Beatrice']
      }
    },
    {
      name: {
        in: ['Bianca', 'Barb']
      }
    }
  ],
  NOT: [
    {
      gender: {
        in: ['female']
      }
    },
    {
      savings: {
        gt: new Decimal(200)
      }
    }
  ]
};

const filter = wrap(SL.interpreter(F.Schemable)(personSchema));

const alan: Person = {
  id: 123 as types.Int,
  name: 'Alan',
  favouriteNumber: 42,
  savings: new Decimal(99.9),
  pets: ['rex', 'sparkle'],
  gender: 'male',
  died: new Date(Date.UTC(2020, 1, 1)),
  createdAt: new Date(Date.UTC(1999, 1, 1))
};

const result1 = filter('checkAll')(condition, alan);
const result2 = filter('failFast')(condition, alan);

console.log('result1', result1);
console.log('result2', result2);
```

```
result1 {
  _tag: 'Left',
  left: [
    'id: 123 = 123',
    'name: Alan does not start with B',
    'favouriteNumber: 42 in 42',
    'savings: 99.9 !> 100',
    'pets: rex,sparkle does not have at least one item in bingo,louie',
    'pets: rex,sparkle does not have every item in rex,bluey',
    'gender: male not in female',
    'died: Sat Feb 01 2020 10:00:00 GMT+1000 (Australian Eastern Standard Time) != null',
    'createdAt: Mon Feb 01 1999 10:00:00 GMT+1000 (Australian Eastern Standard Time) !> Tue Feb 01 2000 10:00:00 GMT+1000 (Australian Eastern Standard Time)',
    'id: 123 !< 50',
    'name: Alan not in Betty,Beatrice',
    'name: Alan not in Bianca,Barb'
  ]
}
result2 { _tag: 'Left', left: [ 'id: 123 = 123' ] }
```

Here's a matching condition and the consequent output for the above program using this condition.

```typescript
const condition: Spec = {
  id: {
    equals: 123 as types.Int
  },
  name: {
    startsWith: 'A'
  },
  favouriteNumber: {
    not: {
      in: [7]
    }
  },
  savings: {
    gt: new Decimal(99)
  },
  pets: {
    hasSome: ['rex', 'bingo'],
    hasEvery: ['rex', 'sparkle']
  },
  gender: {
    in: ['male']
  },
  died: {
    equals: new Date(Date.UTC(2020, 1, 1))
  },
  createdAt: {
    lt: new Date(Date.UTC(2000, 1, 1))
  },
  AND: [
    {
      id: {
        gt: 50 as types.Int
      }
    },
    {
      id: {
        lt: 200 as types.Int
      }
    }
  ],
  OR: [
    {
      name: {
        in: ['Betty', 'Beatrice']
      }
    },
    {
      name: {
        in: ['Alfred', 'Alan']
      }
    }
  ],
  NOT: [
    {
      gender: {
        in: ['female']
      }
    },
    {
      savings: {
        gt: new Decimal(200)
      }
    }
  ]
};
```

```
result1 {
  _tag: 'Right',
  right: [
    'id: 123 = 123',
    'name: Alan starts with A',
    'favouriteNumber: 42 not in 7',
    'savings: 99.9 > 99',
    'pets: rex,sparkle has at least one item in rex,bingo',
    'pets: rex,sparkle has every item in rex,sparkle',
    'gender: male in male',
    'died: Sat Feb 01 2020 10:00:00 GMT+1000 (Australian Eastern Standard Time) = Sat Feb 01 2020 10:00:00 GMT+1000 (Australian Eastern Standard Time)',
    'createdAt: Mon Feb 01 1999 10:00:00 GMT+1000 (Australian Eastern Standard Time) < Tue Feb 01 2000 10:00:00 GMT+1000 (Australian Eastern Standard Time)',
    'id: 123 > 50',
    'id: 123 < 200',
    'name: Alan in Alfred,Alan',
    'gender: male not in female',
    'savings: 99.9 !> 200'
  ]
}
result2 {
  _tag: 'Right',
  right: [
    'id: 123 = 123',
    'name: Alan starts with A',
    'favouriteNumber: 42 not in 7',
    'savings: 99.9 > 99',
    'pets: rex,sparkle has at least one item in rex,bingo',
    'pets: rex,sparkle has every item in rex,sparkle',
    'gender: male in male',
    'died: Sat Feb 01 2020 10:00:00 GMT+1000 (Australian Eastern Standard Time) = Sat Feb 01 2020 10:00:00 GMT+1000 (Australian Eastern Standard Time)',
    'createdAt: Mon Feb 01 1999 10:00:00 GMT+1000 (Australian Eastern Standard Time) < Tue Feb 01 2000 10:00:00 GMT+1000 (Australian Eastern Standard Time)',
    'id: 123 > 50',
    'id: 123 < 200',
    'name: Alan in Alfred,Alan',
    'gender: male not in female'
  ]
}
```

### `protobuf-codec`

The `protobuf-codec` module is a `Schemable` implementation that provides functions for encoding and decoding between JS objects and the protobuf binary format, plus a function for generating a `protobufjs.Type` object that may itself be used to generate a protobuf file. This provides a convenient mechanism for using protobuf serialisation and deserialisation just by defining a regular schema in code, without bothering with field numbers and without having to define protobuf files. Because we are implementing our extended `Schemable` interface, it also handles the types we use for `date`, `int` and `decimal`.

Here's an example of our `protobuf-codec` module in action:

```typescript
import 'jest-extended';
import { protobufCodec as SPC } from '@jmorecroft67/io-ts-types';
import * as protobufjs from 'protobufjs';
import { pipe } from 'fp-ts/lib/function';
import Decimal from 'decimal.js';

const codecMaker = pipe(
  SPC.struct({
    myNum: SPC.number,
    myString: SPC.string,
    myBool: SPC.boolean,
    myDate: SPC.date,
    myLiteral: SPC.literal('hello', 'world')
  }),
  SPC.intersect(
    SPC.partial({
      myDecimal: SPC.decimal
    })
  )
);

const pbType = new protobufjs.Type('MyMessage');
pbType.add(new protobufjs.Field('myNum', 1, 'double'));
pbType.add(new protobufjs.Field('myString', 2, 'string'));
pbType.add(new protobufjs.Field('myBool', 3, 'bool'));
pbType.add(new protobufjs.Field('myDate', 4, 'int64'));
pbType.add(new protobufjs.Field('myLiteral', 5, 'int32'));
pbType.add(new protobufjs.Field('myDecimal', 6, 'string'));

const codec = codecMaker(SPC.makeContext());

// object encoded by us, decoded by protobufjs
// note in the output the encoding of our special types
// - literal uses the index of the literal in our literal definition array,
//   so 0 in this case
// - date uses the epoch value as an int64
// - decimal uses the string representation
const buf = codec
  .encodeDelimited({
    myBool: true,
    myDate: new Date(2000, 1, 1),
    myLiteral: 'hello',
    myNum: 123,
    myString: 'xxxx',
    myDecimal: new Decimal('123.45')
  })
  .finish();
const obj1 = pbType.decodeDelimited(buf);
console.log('obj1', obj1);

// object encoded by us, decoded by us
const buf2 = codec
  .encodeDelimited({
    myBool: true,
    myDate: new Date(2000, 1, 1),
    myLiteral: 'hello',
    myNum: 123,
    myString: 'xxxx',
    myDecimal: new Decimal('123.45')
  })
  .finish();
const obj2 = codec.decodeDelimited(buf2);
console.log('obj2', obj2);

// object encoded by protobufjs, decoded by us
const buf3 = pbType
  .encodeDelimited({
    myBool: true,
    myDate: 0,
    myLiteral: 'hello',
    myNum: 123,
    myString: 'xxxx',
    myDecimal: '123.45'
  })
  .finish();
const obj3 = codec.decodeDelimited(buf3);
console.log('obj3', obj3);
```

```
obj1 MyMessage {
  myNum: 123,
  myString: 'xxxx',
  myBool: true,
  myDate: Long { low: 139427584, high: 221, unsigned: false },
  myLiteral: 0,
  myDecimal: '123.45'
}
obj2 {
  _tag: 'Right',
  right: {
    myNum: 123,
    myString: 'xxxx',
    myBool: true,
    myDate: 2000-01-31T14:00:00.000Z,
    myLiteral: 'hello',
    myDecimal: 123.45
  }
}
obj3 {
  _tag: 'Right',
  right: {
    myNum: 123,
    myString: 'xxxx',
    myBool: true,
    myDate: 1970-01-01T00:00:00.000Z,
    myLiteral: 'hello',
    myDecimal: 123.45
  }
}
```

Here's an example of how we deal with errors, which we handle similarly to the regular decoder provided in `io-ts`:

```typescript
import { protobufCodec as SPC } from '@jmorecroft67/io-ts-types';
import * as protobufjs from 'protobufjs';
import { pipe } from 'fp-ts/lib/function';
import * as E from 'fp-ts/Either';
import * as D from 'io-ts/Decoder';

const codecMaker = pipe(
  SPC.struct({
    myNum: SPC.number,
    myString: SPC.string,
    myBool: SPC.boolean,
    myDate: SPC.date,
    myLiteral: SPC.literal('hello', 'world')
  }),
  SPC.intersect(
    SPC.partial({
      myDecimal: SPC.decimal
    })
  )
);

const pbType = new protobufjs.Type('MyMessage');
pbType.add(new protobufjs.Field('myNum', 1, 'double'));
pbType.add(new protobufjs.Field('myString', 2, 'string'));
pbType.add(new protobufjs.Field('myBool', 3, 'bool'));
pbType.add(new protobufjs.Field('myDate', 4, 'int64'));
pbType.add(new protobufjs.Field('myLiteral', 5, 'int32'));
pbType.add(new protobufjs.Field('myDecimal', 6, 'string'));

const codec = codecMaker(SPC.makeContext());

// object encoded by protobufjs, decoded by us
const buf = pbType
  .encodeDelimited({
    myBool: true,
    myDate: 0,
    myDecimal: '123.45'
  })
  .finish();
const obj = codec.decodeDelimited(buf);
if (E.isLeft(obj)) {
  console.log('obj', D.draw(obj.left));
}
```

```
obj required property "myNum"
└─ cannot decode undefined, should be defined
required property "myString"
└─ cannot decode undefined, should be defined
required property "myLiteral"
└─ cannot decode undefined, should be defined
```