Gathering detailed insights and metrics for @jmorecroft67/io-ts-types
Gathering detailed insights and metrics for @jmorecroft67/io-ts-types
Gathering detailed insights and metrics for @jmorecroft67/io-ts-types
Gathering detailed insights and metrics for @jmorecroft67/io-ts-types
npm install @jmorecroft67/io-ts-types
Typescript
Module System
Node Version
NPM Version
Cumulative downloads
Total Downloads
Last Day
0%
1
Compared to previous day
Last Week
150%
5
Compared to previous week
Last Month
-64.6%
23
Compared to previous month
Last Year
13.4%
339
Compared to previous year
6
1
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.
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.
1import { 2 schemable as S, 3 schemas as SS, 4 decoder as De 5} from '@jmorecroft67/io-ts-types'; 6import * as D from 'io-ts/Decoder'; 7import * as E from 'fp-ts/Either'; 8import { pipe } from 'fp-ts/function'; 9 10const accountSchemas = SS.struct({ 11 id: SS.int, 12 name: SS.string, 13 balance: SS.decimal, 14 updatedAt: SS.date, 15 createdAt: SS.date 16}); 17 18const untypedCondition = { 19 id: { 20 not: { 21 equals: '2' 22 } 23 }, 24 name: { 25 startsWith: 1 26 }, 27 balance: { 28 gt: 'large number' 29 }, 30 createdAt: { 31 gt: true 32 } 33}; 34 35const { decode } = S.interpreter(De.Schemable)(accountSchemas.spec); 36 37const condition = pipe(decode(untypedCondition), E.mapLeft(D.draw)); 38 39if (E.isLeft(condition)) { 40 console.log('decode failed'); 41 console.log(condition.left); 42} else { 43 console.log('decode succeeded'); 44}
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.
1import { 2 schemable as S, 3 schemas as SS, 4 decoder as De, 5 types 6} from '@jmorecroft67/io-ts-types'; 7import * as D from 'io-ts/Decoder'; 8import * as E from 'fp-ts/Either'; 9import { pipe } from 'fp-ts/function'; 10import Decimal from 'decimal.js'; 11 12const accountSchemas = SS.struct({ 13 id: SS.int, 14 name: SS.string, 15 balance: SS.decimal, 16 updatedAt: SS.date, 17 createdAt: SS.date 18}); 19 20const typedCondition: SS.SpecTypeOf<typeof accountSchemas> = { 21 id: { 22 not: { 23 equals: 2 as types.Int 24 } 25 }, 26 name: { 27 startsWith: 'A' 28 }, 29 balance: { 30 gt: new Decimal('1.23') 31 }, 32 createdAt: { 33 gt: new Date(2022, 1, 1) 34 } 35}; 36 37const { decode } = S.interpreter(De.Schemable)(accountSchemas.spec); 38 39// In this case condition will be the same type as typedCondition, 40// so this decode is not at required but for demonstation purposes. 41const condition = pipe(decode(typedCondition), E.mapLeft(D.draw)); 42 43if (E.isLeft(condition)) { 44 console.log('decode failed'); 45 console.log(condition.left); 46} else { 47 console.log('decode succeeded'); 48}
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:
1import { 2 schemableLight as SL, 3 schemas as S, 4 filter as F, 5 types 6} from '@jmorecroft67/io-ts-types'; 7import * as E from 'fp-ts/Either'; 8import Decimal from 'decimal.js'; 9import { pipe } from 'fp-ts/lib/function'; 10 11const wrap: <T>( 12 filter: F.Filter<T> 13) => ( 14 algType: 'failFast' | 'checkAll' 15) => (spec: S.Spec<T>, obj: T) => E.Either<string[], string[]> = 16 (filter) => (algType) => (spec, obj) => 17 pipe( 18 filter(algType)(spec, obj)(), 19 E.bimap( 20 (i) => i.map((j) => j()), 21 (i) => i.map((j) => j()) 22 ) 23 ); 24 25const personSchema = SL.make((S) => 26 S.struct({ 27 id: S.int, 28 name: S.string, 29 favouriteNumber: S.literal(7, 42), 30 savings: S.decimal, 31 pets: S.array(S.string), 32 gender: S.nullable(S.literal('male', 'female')), 33 died: S.nullable(S.date), 34 createdAt: S.date 35 }) 36); 37 38type Person = SL.TypeOf<typeof personSchema>; 39type Spec = S.Spec<Person>; 40 41const condition: Spec = { 42 id: { 43 not: { 44 equals: 123 as types.Int 45 } 46 }, 47 name: { 48 startsWith: 'B' 49 }, 50 favouriteNumber: { 51 notIn: [42] 52 }, 53 savings: { 54 gt: new Decimal(100) 55 }, 56 pets: { 57 hasEvery: ['rex', 'bluey'], 58 hasSome: ['bingo', 'louie'] 59 }, 60 gender: { 61 in: ['female'] 62 }, 63 died: { 64 equals: null 65 }, 66 createdAt: { 67 gt: new Date(Date.UTC(2000, 1, 1)) 68 }, 69 AND: [ 70 { 71 id: { 72 lt: 50 as types.Int 73 } 74 }, 75 { 76 id: { 77 gt: 0 as types.Int 78 } 79 } 80 ], 81 OR: [ 82 { 83 name: { 84 in: ['Betty', 'Beatrice'] 85 } 86 }, 87 { 88 name: { 89 in: ['Bianca', 'Barb'] 90 } 91 } 92 ], 93 NOT: [ 94 { 95 gender: { 96 in: ['female'] 97 } 98 }, 99 { 100 savings: { 101 gt: new Decimal(200) 102 } 103 } 104 ] 105}; 106 107const filter = wrap(SL.interpreter(F.Schemable)(personSchema)); 108 109const alan: Person = { 110 id: 123 as types.Int, 111 name: 'Alan', 112 favouriteNumber: 42, 113 savings: new Decimal(99.9), 114 pets: ['rex', 'sparkle'], 115 gender: 'male', 116 died: new Date(Date.UTC(2020, 1, 1)), 117 createdAt: new Date(Date.UTC(1999, 1, 1)) 118}; 119 120const result1 = filter('checkAll')(condition, alan); 121const result2 = filter('failFast')(condition, alan); 122 123console.log('result1', result1); 124console.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.
1const condition: Spec = { 2 id: { 3 equals: 123 as types.Int 4 }, 5 name: { 6 startsWith: 'A' 7 }, 8 favouriteNumber: { 9 not: { 10 in: [7] 11 } 12 }, 13 savings: { 14 gt: new Decimal(99) 15 }, 16 pets: { 17 hasSome: ['rex', 'bingo'], 18 hasEvery: ['rex', 'sparkle'] 19 }, 20 gender: { 21 in: ['male'] 22 }, 23 died: { 24 equals: new Date(Date.UTC(2020, 1, 1)) 25 }, 26 createdAt: { 27 lt: new Date(Date.UTC(2000, 1, 1)) 28 }, 29 AND: [ 30 { 31 id: { 32 gt: 50 as types.Int 33 } 34 }, 35 { 36 id: { 37 lt: 200 as types.Int 38 } 39 } 40 ], 41 OR: [ 42 { 43 name: { 44 in: ['Betty', 'Beatrice'] 45 } 46 }, 47 { 48 name: { 49 in: ['Alfred', 'Alan'] 50 } 51 } 52 ], 53 NOT: [ 54 { 55 gender: { 56 in: ['female'] 57 } 58 }, 59 { 60 savings: { 61 gt: new Decimal(200) 62 } 63 } 64 ] 65};
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:
1import 'jest-extended'; 2import { protobufCodec as SPC } from '@jmorecroft67/io-ts-types'; 3import * as protobufjs from 'protobufjs'; 4import { pipe } from 'fp-ts/lib/function'; 5import Decimal from 'decimal.js'; 6 7const codecMaker = pipe( 8 SPC.struct({ 9 myNum: SPC.number, 10 myString: SPC.string, 11 myBool: SPC.boolean, 12 myDate: SPC.date, 13 myLiteral: SPC.literal('hello', 'world') 14 }), 15 SPC.intersect( 16 SPC.partial({ 17 myDecimal: SPC.decimal 18 }) 19 ) 20); 21 22const pbType = new protobufjs.Type('MyMessage'); 23pbType.add(new protobufjs.Field('myNum', 1, 'double')); 24pbType.add(new protobufjs.Field('myString', 2, 'string')); 25pbType.add(new protobufjs.Field('myBool', 3, 'bool')); 26pbType.add(new protobufjs.Field('myDate', 4, 'int64')); 27pbType.add(new protobufjs.Field('myLiteral', 5, 'int32')); 28pbType.add(new protobufjs.Field('myDecimal', 6, 'string')); 29 30const codec = codecMaker(SPC.makeContext()); 31 32// object encoded by us, decoded by protobufjs 33// note in the output the encoding of our special types 34// - literal uses the index of the literal in our literal definition array, 35// so 0 in this case 36// - date uses the epoch value as an int64 37// - decimal uses the string representation 38const buf = codec 39 .encodeDelimited({ 40 myBool: true, 41 myDate: new Date(2000, 1, 1), 42 myLiteral: 'hello', 43 myNum: 123, 44 myString: 'xxxx', 45 myDecimal: new Decimal('123.45') 46 }) 47 .finish(); 48const obj1 = pbType.decodeDelimited(buf); 49console.log('obj1', obj1); 50 51// object encoded by us, decoded by us 52const buf2 = codec 53 .encodeDelimited({ 54 myBool: true, 55 myDate: new Date(2000, 1, 1), 56 myLiteral: 'hello', 57 myNum: 123, 58 myString: 'xxxx', 59 myDecimal: new Decimal('123.45') 60 }) 61 .finish(); 62const obj2 = codec.decodeDelimited(buf2); 63console.log('obj2', obj2); 64 65// object encoded by protobufjs, decoded by us 66const buf3 = pbType 67 .encodeDelimited({ 68 myBool: true, 69 myDate: 0, 70 myLiteral: 'hello', 71 myNum: 123, 72 myString: 'xxxx', 73 myDecimal: '123.45' 74 }) 75 .finish(); 76const obj3 = codec.decodeDelimited(buf3); 77console.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
:
1import { protobufCodec as SPC } from '@jmorecroft67/io-ts-types'; 2import * as protobufjs from 'protobufjs'; 3import { pipe } from 'fp-ts/lib/function'; 4import * as E from 'fp-ts/Either'; 5import * as D from 'io-ts/Decoder'; 6 7const codecMaker = pipe( 8 SPC.struct({ 9 myNum: SPC.number, 10 myString: SPC.string, 11 myBool: SPC.boolean, 12 myDate: SPC.date, 13 myLiteral: SPC.literal('hello', 'world') 14 }), 15 SPC.intersect( 16 SPC.partial({ 17 myDecimal: SPC.decimal 18 }) 19 ) 20); 21 22const pbType = new protobufjs.Type('MyMessage'); 23pbType.add(new protobufjs.Field('myNum', 1, 'double')); 24pbType.add(new protobufjs.Field('myString', 2, 'string')); 25pbType.add(new protobufjs.Field('myBool', 3, 'bool')); 26pbType.add(new protobufjs.Field('myDate', 4, 'int64')); 27pbType.add(new protobufjs.Field('myLiteral', 5, 'int32')); 28pbType.add(new protobufjs.Field('myDecimal', 6, 'string')); 29 30const codec = codecMaker(SPC.makeContext()); 31 32// object encoded by protobufjs, decoded by us 33const buf = pbType 34 .encodeDelimited({ 35 myBool: true, 36 myDate: 0, 37 myDecimal: '123.45' 38 }) 39 .finish(); 40const obj = codec.decodeDelimited(buf); 41if (E.isLeft(obj)) { 42 console.log('obj', D.draw(obj.left)); 43}
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
No vulnerabilities found.
No security vulnerabilities found.