map, filter, and transform data structures using GraphQL and JSONPath.
# install peerDependencies
npm install graphql graphql-anywhere graphql-tag rambda jsonpath
# install package from Github packages
npm install --registry=https://npm.pkg.github.com/specialblend @specialblend/mapql
or add .npmrc
:
registry=https://registry.npmjs.org/
@specialblend:registry=https://npm.pkg.github.com/specialblend
and run
npm install @specialblend/mapql graphql graphql-anywhere graphql-tag rambda jsonpath
import gql from "graphql-tag";
import map from "@specialblend/mapql";
const data = {
foo: {
bar: "baz",
baz: "faz"
},
// ...
};
const query = gql`
query ExampleFilter {
foo {
# bar
baz
}
}
`;
const result = map(query, data);
mapql [queryFile] [sourceFile]
queryFile
.graphql query filesourceFile
JSON or javascript file exporting object
# compact JSON output
mapql example/TransformLeases.example.graphql example/data.json > example.result.json
# pretty JSON output
PRETTY=true mapql example/TransformLeases.example.graphql example/data.json > example.result.json
GraphQL query is written in desired result structure, using GraphQL arguments and directives for remapping paths and transforming result values.
⭕ using example data:
expand example data
{
"reportMeta": {
"generated": {
"date": "12/21/2012",
"user": {
"name": "System Admin",
"email": "admin@example.com"
}
}
},
"leases": [
{
"leaseId": 1234,
"residents": [
{
"name": "Alice",
"dob": "1/1/1111",
"email": "alice@example.com"
},
{
"name": "Bob",
"dob": "12/12/1212",
"email": "bob@example.com"
}
],
"address": {
"street": "1234 Main St.",
"city": "New York City",
"stateCode": "NY",
"zipCode": "11210"
}
},
{
"leaseId": 2345,
"residents": [
{
"name": "John Smith",
"dob": "2/2/2222",
"email": "john.smith@example.com"
},
{
"name": "Jane Doe",
"dob": "11/11/1111",
"email": "jane.doe@example.com"
}
],
"address": {
"street": "1333 3rd St.",
"city": "Newark",
"stateCode": "NJ",
"zipCode": "07195"
}
},
{
"leaseId": 4567,
"residents": [
{
"name": "Alice",
"dob": "1/1/1111",
"email": "alice@example.com"
},
{
"name": "Bob",
"dob": "12/12/1212",
"email": "bob@example.com"
}
],
"address": {
"street": "1333 3rd St.",
"city": "Newark",
"stateCode": "NJ",
"zipCode": "07195"
}
}
]
}
⭕ with following query:
query GetAddresses1 {
leases(from: "leases") {
contractNumber(from: "leaseId")
address(from: "address") {
street(from: "street")
city(from: "city")
stateCode(from: "stateCode")
zipCode(from: "zipCode")
}
}
reportInfo {
date(from: "reportMeta.generated.date")
manager(from: "reportMeta.generated.user.name")
exampleVersion @const(of: "v1.2.3.4")
}
}
⭕ we get our result:
{
"leases": [
{
"contractNumber": 1234,
"address": {
"street": "1234 Main St.",
"city": "New York City",
"stateCode": "NY",
"zipCode": "11210"
}
},
{
"contractNumber": 2345,
"address": {
"street": "1333 3rd St.",
"city": "Newark",
"stateCode": "NJ",
"zipCode": "07195"
}
},
{
"contractNumber": 4567,
"address": {
"street": "1333 3rd St.",
"city": "Newark",
"stateCode": "NJ",
"zipCode": "07195"
}
}
],
"reportInfo": {
"date": "12/21/2012",
"manager": "System Admin",
"exampleVersion": "v1.2.3.4"
}
}
- ✅
foo(from:$path)
will mapfoo
field in result to JSONPath$path
. - ✅
foo @const(of: "bar")
will mapfoo
field in result to fixed value"bar"
.
⭕ let's see if we can't shorten it a little:
query GetAddresses2 {
leases @map {
contractNumber(from: "leaseId")
address @map {
street @map
city @map
stateCode @map
zipCode @map
}
}
reportInfo {
date(from: "reportMeta.generated.date")
manager(from: "reportMeta.generated.user.name")
exampleVersion @const(of: "v1.2.3.4")
}
}
- ✅
foo @map
is short forfoo(from: "foo")
.
⭕ let's go little bit shorter.
{
leases @map {
contractNumber: leaseId
address @map {
street
city
stateCode
zipCode
}
}
reportInfo(from: "reportMeta.generated") {
date
manager(from: "user.name")
exampleVersion @const(of: "v1.2.3.4")
}
}
- ✅
query
keyword and name are optional. - ✅
foo: bar
is short forfoo(from: "bar")
when path is simple field name (not JSONPath). - ✅
@map
directive is optional and implicit on leaf nodes.
By default, mapql will return head (first element) of result from JSONPath query. This can be disabled with argument head: false
, useful when expecting list of values. Default value is head: true
.
example:
const data = {
exampleObj: {
exampleStr: "hello world",
},
destinations: [
{
array_string: "foo",
},
{
array_string: "bar",
},
{
array_string: "baz",
},
],
}
query PathHead {
exampleStr
exampleStrHead(head: true)
exampleStrNoHead(head: false)
destinationsHead(from: "destinations[*].array_string", head: true)
destinationsNoHead(from: "destinations[*].array_string", head: false)
destinations(from: "destinations[*].array_string")
}
{
"destinations": "foo",
"destinationsHead": "foo",
"destinationsNoHead": [
"foo",
"bar",
"baz"
],
"exampleStr": "hello world",
"exampleStrHead": "hello world",
"exampleStrNoHead": [
"hello world"
]
}
⭕ query:
query FilterActiveLeases {
leases(filter: { match: { isActive: true } }) {
isActive
residents @map {
name
email
}
address @map {
street
city
zipCode
}
}
}
⭕ result:
expand result
{
"leases": [
{
"isActive": true,
"residents": [
{
"name": "Alice",
"email": "alice@example.com"
},
{
"name": "Bob",
"email": "bob@example.com"
}
],
"address": {
"street": "1234 Main St.",
"city": "New York City",
"zipCode": "11210"
}
},
{
"isActive": true,
"residents": [
{
"name": "Alice",
"email": "alice@example.com"
},
{
"name": "Bob",
"email": "bob@example.com"
}
],
"address": {
"street": "1333 3rd St.",
"city": "Newark",
"zipCode": "07195"
}
}
]
}
- ✅
foo(filter: { match: { bar: "baz" } })
can be used to filter fields using structured selectors.
⭕ using nested match:
query FilterByStateCode {
leases(filter: { match: { address: { stateCode: "NJ" } } }) {
residents @map {
name
email
}
address @map {
street
city
zipCode
}
}
}
⭕ query:
query TransformLeases {
leases @map {
contractNumber: leaseId @String @concat(before: "#")
address @map {
street
streetLine2 @default(to: "N/A")
city
stateCode
zipCode @parseInt
}
}
reportMetaJson: reportMeta @toJson
}
⭕ result:
{
"leases": [
{
"contractNumber": "#1234",
"address": {
"street": "1234 Main St.",
"streetLine2": "#789",
"city": "New York City",
"stateCode": "NY",
"zipCode": 11210
}
},
{
"contractNumber": "#2345",
"address": {
"street": "1333 3rd St.",
"streetLine2": "N/A",
"city": "Newark",
"stateCode": "NJ",
"zipCode": 7195
}
},
{
"contractNumber": "#4567",
"address": {
"street": "1333 3rd St.",
"streetLine2": "N/A",
"city": "Newark",
"stateCode": "NJ",
"zipCode": 7195
}
}
],
"reportMetaJson": "{\"generated\":{\"date\":\"12/21/2012\",\"user\":{\"name\":\"System Admin\",\"email\":\"admin@example.com\"}}}"
}
- ✅ directives are composable and will execute in left-to-right composition order.
⚠️ transformation directives on parent nodes are called before child nodes.
@default(to: any)
: set default value@parseInt
: cast numeric string to integer@parseFloat
cast numeric string to float@String
: cast value tostring
@Boolean
: cast value intoboolean
@toJson
: format value to JSON string@fromJson
: parse value from JSON string@concat(before?: string, after?: string)
: concatenate string. arguments not mutually exclusive.@not
: logical not.@of
: return singleton array of value.
@substr(from: $index, len: $index)
: return substring of value.@slice(from: $index, to: $index)
: slice array.@add(x: number)
: add to value.@sub(x: number)
: subtract from value.@mul(x: number)
: multiply value.@prop(_: string)
: return property of object.@path(_: string[])
: return path of object.@head
: return first element of array.@init
: return all except last element of array.@tail
: return all except first element of array.@last
: return last element of array.