npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

scrapql

v0.0.34

Published

The design of ScrapQL was partially motivated by experimentation with GraphQL. One key idea behind GraphQL is that the requirements for an API can not be known on forehand and therefore it is a good idea to create a general API that lets the caller be in

Downloads

178

Readme

ScrapQL

The design of ScrapQL was partially motivated by experimentation with GraphQL. One key idea behind GraphQL is that the requirements for an API can not be known on forehand and therefore it is a good idea to create a general API that lets the caller be in control of things. Since we don't know which type sytem the caller will be using GraphQL implements it's own type system in GraphQL Schema language specifically created for the purpose. Because of the generality of the interface the types are also necessary for describing relations between different data items. GraphQL is also designed to avoid costly roundtrips between the application and the server room. The application sends "an order" for various "items" and the server takes care of collecting those items before returning them all at once to the application.

ScrapQL bears some resemblance to GraphQL but is also quite different. ScrapQL attempts to preserve the roundtrip properties of GraphQL but is intended for well known data exchange in situations where the backend code can easily be modified based on needs of the frontend of the application and the main goal is to bundle and deliver the data based on a few variables. ScrapQL is implemented in TypeScript and makes use of TypeScript's native type system. The user is adviced to define the query types with io-ts that provides type safe measures for (de)serialing the data types to (and from) JSON. The JSON can be passed over the wire in any shape or form. The ScrapQL library is used to define and iterate the query/result structure.

Tutorial

In this tutorial we explain how you can use ScrapQL in a simple project with customers and profit reports. We will use the following database mock throughout the tutorial.

const example: any = {
  customers: {
    c001: {
      name: 'Scrooge McDuck',
      age: 75,
    },
    c002: {
      name: 'Magica De Spell',
      age: 35,
    },
  },
  reports: {
    2017: {
      profit: 500,
    },
    2018: {
      profit: 100,
    },
    2019: {
      profit: 10,
    },
  },
};

type Json =
    | string
    | number
    | boolean
    | null
    | { [p: string]: Json }
    | Array<Json>;

interface Database {
  getCustomer: (c: string) => Promise<undefined|Json>;
  getReport: (y: string) => Promise<Json>;
}

const db: Database = {
  getCustomer: (customerId) => Promise.resolve(example.customers[customerId]),
  getReport: (year) => Promise.resolve(example.reports[year] || { profit: 0 }),
};

Define Data Types

import * as t from 'io-ts';

const CustomerId = t.string;
type CustomerId = t.TypeOf<typeof CustomerId>;

const Customer = t.type({
  name: t.string,
  age: t.number,
});
type Customer = t.TypeOf<typeof Customer>;

const Year = t.string;
type Year = t.TypeOf<typeof Year>;

const Report = t.type({
  profit: t.number,
});
type Report = t.TypeOf<typeof Report>;

Define Query Driver


import { Ctx, Ctx0, Wsp, Wsp0, Existence } from 'scrapql';
import * as scrapql from 'scrapql';

import * as P from 'maasglobal-prelude-ts';
// ^ or import the stuff directly from fp-ts

const Err = t.array(t.string);
type Err = t.TypeOf<typeof Err>;

const Get = t.literal(true)
type Get = t.TypeOf<typeof Get>;

type Resolvers = scrapql.Resolvers<{
  readonly fetchReport: (a: Get, b: Ctx<[Year]>) => P.TaskEither<Err, Report>;
  readonly fetchCustomer: (a: Get, b: Ctx<[CustomerId]>, c: { customer: Customer }) => P.TaskEither<Err, Customer>;
  readonly checkCustomerExistence: (a: CustomerId) => P.TaskEither<Err, P.Option<{ customer: Customer }>>;
}>

type Reporters = scrapql.Reporters<{
  readonly receiveReport: (a: Report, b: Ctx<[Get, Year]> ) => P.Task<void>;
  readonly receiveCustomer: (a: Customer, b: Ctx<[Get, CustomerId]>) => P.Task<void>;
  readonly learnCustomerExistence: (a: Existence, b: Ctx<[CustomerId]>) => P.Task<void>;
}>

type Driver = Resolvers & Reporters

Define Query Literals


// name and version from package.json
const packageName = 'scrapql-example-app';
const packageVersion = '0.0.1';

const QUERY_PROTOCOL= `${packageName}/${packageVersion}/scrapql/query`;
const RESULT_PROTOCOL = `${packageName}/${packageVersion}/scrapql/result`;

type Version = scrapql.LiteralBundle<
  Err,
  Ctx0,
  Wsp0,
  Resolvers,
  Reporters,
  typeof QUERY_PROTOCOL,
  typeof RESULT_PROTOCOL
>;
const Version: Version = scrapql.literal.bundle({
  Err,
  QueryPayload: t.literal(QUERY_PROTOCOL),
  ResultPayload: t.literal(RESULT_PROTOCOL),
});

Define Query Leafs

type GetCustomer = scrapql.LeafBundle<
  Err,
  Ctx<[CustomerId]>,
  Wsp<{ customer: Customer }>,
  Resolvers,
  Reporters,
  Get,
  Customer
>;
const GetCustomer: GetCustomer = scrapql.leaf.bundle({
  Err,
  QueryPayload: Get,
  ResultPayload: Customer,
  queryConnector: (r: Resolvers) => r.fetchCustomer,
  queryPayloadCombiner: (_w, r) => P.Either_.right(r),
  queryPayloadExamplesArray: [Get.value],
  resultConnector: (r: Reporters) => r.receiveCustomer,
  resultPayloadCombiner: (_w, r) => P.Either_.right(r),
  resultPayloadExamplesArray: [{
      name: 'Scrooge McDuck',
      age: 75,
    }],
});

type GetReport = scrapql.LeafBundle<
  Err,
  Ctx<[Year]>,
  Wsp0,
  Resolvers,
  Reporters,
  Get,
  Report
>;
const GetReport: GetReport = scrapql.leaf.bundle({
  Err,
  QueryPayload: Get,
  ResultPayload: Report,
  queryConnector: (r: Resolvers) => r.fetchReport,
  queryPayloadCombiner: (_w, r) => P.Either_.right(r),
  queryPayloadExamplesArray: [Get.value],
  resultConnector: (r: Reporters) => r.receiveReport,
  resultPayloadCombiner: (_w, r) => P.Either_.right(r),
  resultPayloadExamplesArray: [{
    profit: 500,
  }],
});

Define Query Structure

type CustomerOps = scrapql.PropertiesBundle<{
  get: GetCustomer;
}>;
const CustomerOps: CustomerOps = scrapql.properties.bundle({
  get: GetCustomer,
});

type Customers = scrapql.IdsBundle<
  Err,
  Ctx0,
  Wsp0,
  Resolvers,
  Reporters,
  CustomerId,
  t.TypeOf<typeof CustomerOps['Query']>,
  t.TypeOf<typeof CustomerOps['Result']>
>;
const Customers: Customers = scrapql.ids.bundle({
  id: {
    Id: CustomerId,
    idExamples: ['c001', 'c999'],
  },
  item: CustomerOps,
  queryConnector: (r: Resolvers) => r.checkCustomerExistence,
  resultConnector: (r: Reporters) => r.learnCustomerExistence,
});


type ReportOps = scrapql.PropertiesBundle<{
  get: GetReport;
}>;
const ReportOps: ReportOps = scrapql.properties.bundle({
  get: GetReport,
});

type Reports = scrapql.KeysBundle<
  Err,
  Ctx0,
  Wsp0,
  Resolvers,
  Reporters,
  Year,
  t.TypeOf<typeof ReportOps['Query']>,
  t.TypeOf<typeof ReportOps['Result']>
>;
const Reports: Reports = scrapql.keys.bundle({
  key: {
    Key: Year,
    keyExamples: ['1999', '2004'],
  },
  item: ReportOps,
});

type Root = scrapql.PropertiesBundle<{
  protocol: Version,
  reports: Reports,
  customers: Customers,
}>;
const Root: Root = scrapql.properties.bundle({
  protocol: Version,
  reports: Reports,
  customers: Customers,
});

const Query = Root.Query;
type Query = t.TypeOf<typeof Query>;

const Result = Root.Result;
type Result = t.TypeOf<typeof Result>;

You can use the query validator to validate JSON queries as follows.

import { validator } from 'io-ts-validator';

const rawQuery: Json = {
  protocol: QUERY_PROTOCOL,
  reports: [
    [2018, {get: true}],
    [3030, {get: true}],
  ],
  customers: [
    ['c002', {get: true}],
    ['c007', {get: true}],
  ],
};

const exampleQuery: Query = validator(Query).decodeSync(rawQuery);
const wireQuery: string = validator(Query, 'json').encodeSync(exampleQuery);

Implement Query Resolvers

const resolvers: Resolvers = {
  fetchReport: (_queryArgs, [year]) => P.pipe(
    () => db.getReport(year),
    P.Task_.map(validator(Report).decodeEither),
  ),

  fetchCustomer: (_queryArgs, [_customerId], { customer }) => P.pipe(
    customer,  // cached by checkCustomerExistence
    P.TaskEither_.right,
  ),

  checkCustomerExistence: (customerId) => P.pipe(
    () => db.getCustomer(customerId),
    P.Task_.map((nullable) => P.pipe(
      P.Option_.fromNullable(nullable),
      P.Option_.map(validator(Customer).decodeEither),
      P.Option_.map(P.Either_.map((customer) => ({ customer }))),
      P.Option_.sequence(P.Either_.Applicative),
    )),
  ),

};

You can now process a query as follows.

import * as ruins from 'ruins-ts';

async function generateExampleOutput(input: string) {
  const query: Query = await validator(Query, 'json').decodePromise(input);
  const queryProcessor = scrapql.processQuery(Root, resolvers);
  const result = await ruins.fromTaskEither(queryProcessor(query));
  console.log(result);
}

generateExampleOutput(wireQuery);

The result object should look as follows.

const rawResult: Json = {
  protocol: 'scrapql-example-app/0.0.1/scrapql/result',
  reports: [
    [2018, {
      get: {
        _tag: 'Right',  // get success
        right: { profit: 100 }
      },
    }],
    [3030, {
      get: {
        _tag: 'Right',  // get success
        right: { profit: 0 }
      },
    }],
  ],
  customers: [
    ['c002', {
      _tag: 'Right',  // identity check success
      right: {
        _tag: 'Some',  // customer exists
        some: {
          get: {
            _tag: 'Right',  // get success
            right: {
              name: 'Magica De Spell',
              age: '35',
            },
          },
        },
      },
    }],
    ['c007', {
      _tag: 'Right',  // identity check success
      right: {
        _tag: 'None',  // customer does not exist
      },
    }],
  ],
};

We can now use the result validator to encode the result as JSON.

const exampleResult: Result = validator(Result).decodeSync(rawResult);
const wireResult: string = validator(Result, 'json').encodeSync(exampleResult);

It all comes together as the following query processor.

async function wireQueryProcessor(input: string): Promise<string> {
  const queryProcessor = scrapql.processQuery(Root, resolvers);
  const query: Query = await validator(Query, 'json').decodePromise(input);
  const result: Result = await ruins.fromTaskEither(queryProcessor(query));
  const output: string = await validator(Result, 'json').encodePromise(result);
  return output;
}

Implement Result Reporters

const reporters: Reporters = {

  receiveReport: (report, [_query, year]) => () => {
    return Promise.resolve(console.log(year, report));
  },

  receiveCustomer: (customer, [_query, customerId]) => () => {
    return Promise.resolve(console.log(customerId, customer));
  },

  learnCustomerExistence: (existence, [customerId]) => () => {
    return Promise.resolve(console.log(customerId, existence ? 'known customer' : 'unknown customer'));
  },

};

Define Request and Response formats

import { either as tEither } from 'io-ts-types/lib/either';

const Request = <D extends t.Mixed>(DataC: D) => DataC
type Request<D> = D

const Response = tEither
type Response<E, D> = P.Either<E, D>

Implement Client and Server

import * as Console_ from 'fp-ts/lib/Console';


async function server(request: string): Promise<string> {
  const main = P.pipe(
    // validate request
    validator(Request(Query), 'json').decodeEither(request),
    P.TaskEither_.fromEither,
    // process query
    P.TaskEither_.chain((query: Query) => P.pipe(
      query,
      scrapql.processQuery(Root, resolvers),
    )),
    // log status
    P.Task_.chainFirst((response: Response<Err, Result>) => P.pipe(
      response,
      P.Either_.fold(
        (errors) => Console_.error(['Error!'].concat(errors).join('\n')),
        (_output) => Console_.log('Success!'),
      ),
      P.Task_.fromIO,
    )),
    // encode response
    P.Task_.map((response: Response<Err, Result>) => 
      validator(Response(Err, Result), 'json').encodeEither(response)
    ),
  );
  return ruins.fromTaskEither(main);
}

async function client(query: Query): Promise<void> {
  const main = P.pipe(
    // encode request
    validator(Request(Query), 'json').encodeEither(query),
    P.TaskEither_.fromEither,
    // call server
    P.TaskEither_.chain((request) => P.pipe(
      P.TaskEither_.tryCatch(() => server(request), (reason) => [String(reason)]),
    )),
    // validate response
    P.TaskEither_.chainEitherK((body: string) =>
      validator(Response(Err, Result), 'json').decodeEither(body),
    ),
    // acknowledge server-side errors
    P.TaskEither_.chain((response: Response<Err, Result>) => P.Task_.of(response)),
    // process result
    P.TaskEither_.chain((result: Result) => P.pipe(
      result,
      scrapql.processResult( Root, reporters ),
      P.TaskEither_.fromTask,
    )),
  );
  return ruins.fromTaskEither(main);
}

Utilities

The package also contains some utilities for related generic data structures.

Dict

The scrapql dictionary type Dict<K, V> = Array<[K, V]> is similar to TypeScript's native record type Record<K, V> but accepts arbitrary values as keys. This is useful for scrapql since it allows us to use entire queries as keys while storing query results as values. The dictionary data structure is inspired by Haskell's lookup function that is defined for a list of pairs. The related scrapql utility package is similar to fp-ts Record utils.

NonEmptyList

The scrapql non-empty list type NonEmptyList<A> = () => Generator<A, A, undefined> is similar to TypeScript's native array type Array<A> and fp-ts non-empty array type NonEmptyArray<A> but generates values on demand. The lazy approach is useful while combining scrapql query and result examples. The lazy list lets us read a few example query combinations, letting us ignore the exponential amount of possible example combinations. The non-empty list data structure is inspired by Python's itertools module. The related scrapql utility package is similar to fp-ts NonEmptyArray utils.