@pinpt/data-manager
TypeScript icon, indicating that this package has built-in type declarations

0.0.180 • Public • Published
Pinpoint

Pinpoint Data Manager for React

Overview

This package provides a set of web browser based JavaScript utilities for use with Pinpoint data.

Install

This package targets the web browser and should be installed as a node module in a React project:

npm install @pinpt/data-manager

Goals

The following are goals of this package:

  • provide a high level set of capabilities that are lightweight
  • don't require a huge set of external dependencies (right now, zero external dependencies)
  • do as much work in the browser on a web worker thread vs the UI thread
  • provide a set of reusable frameworks for building other services which sit on top of Event API and GraphQL API services
  • provide a better separation between the presentation and the data and hide the complexity and details of how to acquire the data

Components

This project exposes the following major components:

  • GraphQL client: a zero dependency, lightweight GraphQL client
  • Event API client: a zero dependency, lightweight WebSocket client for communicating with the Event API
  • WebWorker: a reusable abstract base class for writing web workers
  • Cache: a caching system for syncing data and maintaining a database in the browser's IndexDB
  • Data Manager: a data management system for abstracting away how data is managed

All of these components are exposed so that they can be used directly. However, the app will interact mainly with the provided React Context and Hook.

Usage

DataManager Context

The DataManager exposes a React Context component which should be used in the application as follows:

function App() {
  return (
    <DataManager.Provider
      graphAPI="https://graph.api.edge.pinpoint.com/graphql"
      eventAPI="wss://event.api.edge.pinpoint.com/ws"
      deviceID={window.PinpointEvent.deviceid()}
      sessionID={window.PinpointEvent.sessionid()}
      customerID={window.PinpointAuth.customer.id}
      userID={window.PinpointAuth.user.id}
    >
      <div className="App">
        <App />
      </div>
    </DataManager.Provider>
  );
}

The properties should be dynamically passed instead of hard coded. The deviceID and sessionID should be passed from the event-api beacon using the values from window.PinpointEvent.deviceid() and window.PinpointEvent.sessionid().

Hook

The useData hook will expose data from pluggable data managers in a generic way. The features of the hook:

  • handle both findOne and findMany type data access
  • automatically (unless disabled) cache fetched data and use cached data on subsequent fetches
  • cache data between page reloads
  • optimistically fetch data after returning cached data immediately (unless disabled)
  • automatically trigger a re-render when data is updated behind the scenes w/o requiring any intervention by the app
  • support automatic pagination
  • ability to turn off caching
  • automatically sync low cardinality data in the browser for immediate and offline data access
  • perform most operations in a background web worker to offline from the UI render thread

The hook takes the following arguments:

  • model: (string) - the name of the model such as admin.User
  • dataprops?: (undefined | string | FilterQuery | Function | Promise) - pass a string with the _id to do a findOne or pass a FilterQuery to do a findMany. pass or skip the parameter if you want to load later.
  • opts?: (FindOptions | Function) - optional opts for controlling the query, can be callback function which returns FindOptions (use with dependencies)

The FilterQuery has the following properties:

  • filters: (string[]) - the filter expression
  • params?: (any[]) - the filter parameters to fill in the placeholders from the filter expression

The DataOptionalProps interface has the following properties:

  • cache?: (undefined | false) - set to false to turn off caching of data in the request. defaults to true.
  • optimistic?: (undefined | false | number) - set to false to turn off optimistic refetching of cached data. defaults to true. set to a positive number to control the number of milliseconds before triggering the refresh
  • paginate?: (PaginateOptions) - set the a value to paginate for fetchMany requests

The PaginateOptions interface has the following properties:

  • max?: (number) - set to a number to control the per request page size. defaults to 100 if not provided.

The useData hook will return an object with the following properties:

  • data: (any) - the result data, which is null if not found
  • loading: (boolean) - true if the data is loading, false if loaded
  • pending: (boolean) - true if the data is pending because no parameters were passed
  • cached: (boolean) - true if the data returned was from the cache and false if from the network
  • offline: (boolean) - true if the data returned was from the cache and you're offline
  • error: (Error) - null if no error or an Error if an error fetching the data
  • pagination: (DataPagination) - if the query was paginated, the pagination details or null if not
  • setID: (val: string) => void - change the ID of the query dynamically
  • setFilter: (val: FilterQuery) => void - change the filter of the query dynamically

The DataPagination interface has the following properties:

  • offset: number - the offset number in the total result set
  • totalCount: number - the total number of records matching the query
  • hasNextPage: boolean - true if there is more data beyond the current result going forward
  • hasPreviousPage: boolean - true if there is more data beyond the current result going backwards
  • startCursor: string - the opaque cursor for the beginning of the page
  • endCursor: string - the opaque cursor for the end of the page
  • next: (append?: boolean, position?: 'before' | 'after') => Promise - a function to advance the page forwards. if append = false, will prepend (position=before) or append (position=after) to the previous page
  • previous: () => Promise - a function to advance the page backwards

Examples

Example usage for returning a specific record when you have an ID:

const UserComponent = () => {
  const { data: user, loading, error } = useData<IAdminUser>('admin.User', 'ab980e7028c8b147');
  if (loading) {
    return <div>Loading</div>;
  }
  if (error) {
    return <div>{error.message}</div>;
  }
  return (
    <div>{user?.profile.first_name} {user?.profile.last_name}</div>
  );
};

Example usage for returning multiple results with a filter:

const UserListComponent = () => {
  const { data: users, loading, error } = useData<IAdminUser[]>('admin.User', { filters: [ 'active = ?' ], params: [ true ] } );
  if (loading) {
    return <div>Loading</div>;
  }
  if (error) {
    return <div>{error.message}</div>;
  }
  return (
    <>
      <pre style={{padding: '10px'}}>{JSON.stringify(users, null, 2)}</pre>
    </>
  );
);

Example usage for returning multiple results with pagination:

const UserListComponent = () => {
  const { data: users, loading, error, pagination } = useData<IAdminUser[]>('admin.User', {}, { paginate: { max: 1 } });
  if (loading) {
    return <div>Loading</div>;
  }
  if (error) {
    return <div>{error.message}</div>;
  }
  return (
    <>
      <div>{1 + (pagination?.offset || 0)}/{pagination?.totalCount}</div>
      <div>
        <button disabled={!pagination?.hasPreviousPage} onClick={() => pagination?.previous()}>Previous</button>
        <button disabled={!pagination?.hasNextPage} onClick={() => pagination?.next()}>Next</button>
      </div>
      <pre style={{padding: '10px'}}>{JSON.stringify(users, null, 2)}</pre>
    </>
  );
);

Explicitly turning off caching:

const UserComponent = () => {
  const { data: user, loading, error } = useData<IAdminUser>('admin.User', 'ab980e7028c8b147', { cache: false });
  if (loading) {
    return <div>Loading</div>;
  }
  if (error) {
    return <div>{error.message}</div>;
  }
  return (
    <div>{user?.profile.first_name} {user?.profile.last_name}</div>
  );
};

Setup the data but fetch later dynamically (or change the props dynamically):

const UserComponent = () => {
  const { data: user, loading, pending, error, setID } = useData<IAdminUser>('admin.User');
  if (loading) {
    return <div>Loading</div>;
  }
  if (pending) {
    return <button onClick={() => setID('ab980e7028c8b147')}>Click to load</button>;
  }
  if (error) {
    return <div>{error.message}</div>;
  }
  return (
    <div>{user?.profile.first_name} {user?.profile.last_name}</div>
  );
};

Setup the data but fetch later dynamically using a callback function or Promise:

const UserComponent = () => {
  const { data: user, loading, pending, error, setID } = useData<IAdminUser>('admin.User', () => {
    return getGetSomeID(); // this can either return a promise or an immediate value
  });
  if (loading || pending) {
    return <div>Loading</div>;
  }
  if (error) {
    return <div>{error.message}</div>;
  }
  return (
    <div>{user?.profile.first_name} {user?.profile.last_name}</div>
  );
};

Create a dependency chain between different data and use dependencies to react to changes:

const DependencyComponent = () => {
  const { data: customer, current: currentCustomer } = useData<IAdminCustomer[]>('admin.Customer', {});
  const { data: subscription } = useData<IAdminCustomerSubscription>('admin.CustomerSubscription', () => {
    // use current to get the captured current value which can be used outside the render
    // since if you captured customer it would always be undefined when calling this function
    // because it would have captured the closure value for the JS value but once the render
    // function runs, the variable is out of scope and no longer valid.  this current variable
    // returned from useData will give you a function that you can invoke to get the current
    // value of the data variable that was correctly captured during the previous render
    const customer = currentCustomer();
    if (customer) {
      return {
        filters: ['customer_id = ?'],
        params: [customer[0]._id],
      }
    }
  }, [customer]);
  return (
    <>
      <div>Customer</div>
      <pre style={{padding: '10px', color: 'orange'}}>{JSON.stringify(customer, null, 2)}</pre>
      <div>Customer Subscription</div>
      <pre style={{padding: '10px', color: 'yellow'}}>{JSON.stringify(subscription, null, 2)}</pre>
    </>
  );
};

License

All code is Copyright (c) 2020 by Pinpoint Software, Inc. and proprietary. Do not redistribute.

Readme

Keywords

none

Package Sidebar

Install

npm i @pinpt/data-manager

Weekly Downloads

329

Version

0.0.180

License

Proprietary

Unpacked Size

13.4 MB

Total Files

245

Last publish

Collaborators

  • jhaynie
  • chrisbowley
  • keegandonley
  • robindiddams