Node People Magic

    @puppybits/read-write
    TypeScript icon, indicating that this package has built-in type declarations

    0.0.1 • Public • Published

    Read/Write

    [dark launch: pre-release]

    The fastest, production-ready DX & UX for NoSQL/Firestore.

    • Instant UI. Change data synchronously. No Thunks, no Sagas, no Axios, no GraphQL mutations/subscriptions.
    • Zero-Redux Redux. Write Redux without Redux. No reducers, no slices, no selectors, no entity mappers, no normalization.
    • The One Test. The One test rules them all. Why write seperate unit, integration, visual/storybook & property-based tests? The One test validates each layer seperately & together. No boilerplate, no stubs, no mocks, no spys.
    • Offline-first NoSQL. Firestore ACID-compliant transactions with live subscriptions.

    [![License][license-image]][license-url]

    API Basics

    Read

    useRead({ path, ...query })

    Query & load & subscribe to live updates from Firestore.

    const tasks = useRead({ 
      path: 'tasks', 
      where: [
        ['status', '==', 'done'],
        ['assignee', '==', myUID]
      ],
      orderBy: ['createdAt', 'desc'],
    });

    @see Advanced Read

    Write

    createMutate({ action, read, write })

    Create a Redux action creator to create, update & delete data. Mutations synchronously update the Redux store. This makes React components feel instant while data persistence are eventually consistent.

    const archiveAction = createMutate({ 
      action: 'ArchiveTask', 
    
      read: (taskId) => ({ taskId: () => taskId }), 
      
      write: ({ taskId }) => ({ 
        path:'tasks', 
        id: taskId, 
        archived: true 
      }),
    });

    @see Advanced Write

    createMutate returns an Action Creator. When the action creator is dispatched it return a promise that will execute when Firestore accepts or rejects the mutation.

    import { archiveAction } from './mutations';
    
    const ReactComponent = () => {
      return <div role="button" onClick={() => {
        useDispatch(archiveAction('task-one'))
          .then(() => alert('task archived.'));
      }} />
    }

    Testing

    Unit Tests

    it.each([{ payload, results }])(...shouldPass)

    Zero bolierplate testing. No mocks or spies; just data.

    import { archiveAction } from '../mutations';
    
    it.each({
      setup: [{ path: 'tasks', id: '99', archived: false, }],
      
      payload: { taskId: '99' },
    
      results: [{ path: 'tasks', id: '99', archived: true, }],
    
     })(...shouldPass(archiveAction));

    it.each([{ payload, returned }])(...shouldFail)

    Switch results for returned to run failure checks.

    it.each([{
      payload: { taskId: 'not-valid-id' },
    
      returned: new Error('Document not found.'),
    
    }])(...shouldFail(archiveAction));

    @see Jest Test

    Intergration Tests

    Automatically upgrade unit tests to intergration with just a boolean. Integration tests loads setup data into the database, tests access rules then validates the final mutation in the database.

    (Coming Soon): Parallelized intergation tests

    import { archiveAction } from '../mutations';
    
    const RUN_AS_INTEGRATION = true; // or use the env var READWRITE_INTEGRATION=true
    
    it.each({
      setup: [{ path: 'tasks', id: '99', archived: false, }],
      
      payload: { taskId: '99' },
    
      results: [{ path: 'tasks', id: '99', archived: true, }],
    
     })(...shouldPass(archiveAction, RUN_AS_INTEGRATION));

    @see Jest Test

    StoryBook Tests

    it.each([{ payload, component, results }])(...shouldPass)

    Unit tests can generate storybook tests with a pre and a post for the mutation by adding a component property to the test.

    import { archiveAction } from '../mutations';
    
    it.each({
      setup: [{ path: 'tasks', id: '99', archived: false, }],
      
      payload: { taskId: '99' },
    
      component: 'path/to/component',
    
      results: [{ path: 'tasks', id: '99', archived: true, }],
    
     })(...shouldPass(archiveAction));

    @see Storybook Test

    Typescript QuickCheck Tests

    it.each([{ payload, results }])(...shouldPass)

    Test for the unknown. Unit tests have one major flaw, they can only test the known; not unknown cases.

    It's impossible for you to find the unknown. But it is possible to let the unknowns find you.

    This is what Haskell-style QuickCheck does. QuickCheck systems are analogous to property-based testing or fuzzing. When data is generated on each run a single test can test and validate all possible permutations of pass/fail conditions for a mutation. When a test fails it will throw with the exact data to expose new cases that the code didn't handle.

    import { generate } from 'TypescriptDecoder';
    import { archiveAction } from '../mutations';
    
    const task = generate('Task', { archived: false });
    
    it.each({
      setup: [task],
      
      payload: { taskId: task.id },
    
      results: [{ ...task, archived: true, }],
    
     })(...shouldPass(archiveAction));

    @coming-soon: TypeCheck Tests Working to extract it from our codebase

    The One Test (QuickType + Unit + Intergration + Storybook)

    Why write multiple tests when you could write one?

    import { generate } from 'TypescriptDecoder';
    import { archiveAction } from '../mutations';
    
    const task = generate('Task', { archived: false });
    
    it.each({
      setup: [task],
      
      payload: { taskId: task.id },
    
      component: 'path/to/component',
    
      results: [{ ...task, archived: true, }],
    
     })(...shouldPass(archiveAction, true));

    @coming-soon: Docs for The One Tests

    Example Project

    Read/Write Notes

    Run yarn && yarn start

    Alternatives

    Looking for options to work with Firestore? Check out these other libraries:

    Documentation

    API Documentation

    Code deep-dives

    Design Fundamentals

    Setup

    1. Add the libraries to your project.
    yarn add read-write firebase @reduxjs/toolkit redux
    
    1. Include the firestore/firebase reducers and thunk middleware.
    import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';
    import {
      getFirebase,
      getFirestore,
      firebaseReducer,
      firestoreReducer,
    } from 'read-write';
    
    import firebase from 'firebase/compat/app';
    
    // Create store with reducers and initial state
    export const store = configureStore({
      // Add Firebase to reducers
      reducer: combineReducers({ 
        firebase: firebaseReducer,
        firestore: firestoreReducer,
      }),
      middleware: (getDefaultMiddleware) =>
        getDefaultMiddleware({
          thunk: {
            extraArgument: { getFirestore, getFirebase },
          },
        }),
    });
    
    export type AppDispatch = typeof store.dispatch;
    export type RootState = ReturnType<typeof store.getState>;
    export type AppThunk<ReturnType = void> = ThunkAction<
      ReturnType,
      RootState,
      unknown,
      Action<string>
    >;

    hooks.ts

    import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
    import type { AppDispatch, RootState } from './store';
    
    export const useAppDispatch = () => useDispatch<AppDispatch>();
    export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
    1. Initialize Firebase and pass store to your component's context using react-redux's Provider:
    import React from 'react';
    import { render } from 'react-dom';
    import App from './App';
    import { store } from './app/store';
    import { Provider } from 'react-redux';
    import {
      ReactReduxFirebaseProvider,
      createFirestoreInstance,
    } from 'read-write';
    
    import firebase from 'firebase/compat/app';
    import 'firebase/compat/firestore';
    import 'firebase/compact/auth';
    
    const firebaseApp = firebase.initializeApp({
      authDomain: process.env.REACT_APP_FIREBASE_authDomain,
      databaseURL: process.env.REACT_APP_FIREBASE_databaseUrl,
      projectId: process.env.REACT_APP_FIREBASE_projectId,
    });
    
    render(
      <Provider store={store}>
          <ReactReduxFirebaseProvider
            firebase={firebaseApp}
            dispatch={store.dispatch}
            createFirestoreInstance={createFirestoreInstance}
          >
            <App />
          </ReactReduxFirebaseProvider>
        </Provider>,
      document.querySelector('body'),
    );

    Future Roadmap

    v1.0 - in progress

    • [x] lib: read & write data with optimistic commits
    • [x] lib: 100% support for all Firestore features
    • [x] lib: hooks return query results from firestore
    • [x] lib: hooks return picks & partials of firestore data
    • [x] lib: hooks alternative solution for createSelector
    • [x] lib: hooks validatin of rendering performance
    • [x] lib: cache reducer synchronous, optimistic reads
    • [x] lib: cache reducer synchronous, optimistic database writes
    • [x] lib: cache reducer synchronous updates all affected queries upon mutation
    • [x] lib: cache reducer performance speed ups by minimizing Immer changes
    • [x] lib: mutation support Redux enhancers for global data
    • [x] lib: mutation support basic writes for Firestore
    • [x] lib: mutation support nested field updates
    • [x] lib: mutation force read providers to be idempotent
    • [x] lib: mutation support for all FieldValue types (top-level only)
    • [x] lib: mutation batches accept an infinite number of writes, chunk into 500 and fold in results
    • [x] lib: mutation transactions run synchronous, optimistic and are ACID-compliant (online-only)
    • [x] tests: support data-driven unit tests
    • [x] tests: data-driven intergration tests with Firestore emulator
    • [x] tests: data-driven storybook tests are written to disk
    • [ ] todo tests: switch intergration tests to run parallelized
    • [x] DX: add readwrite:cache profiling for Redux store changes
    • [x] DX: add readwrite:profile profiling for data load phases timings
    • [ ] in progress docs: document public API layer
    • [ ] todo testing: increase code coverage from 90% to 100%

    future

    • docs: document internal processes
    • lib: remove redux-firebase, redux, redux-toolkit dependencies
    • lib: create redux-compatable layer but don't use Redux
    • lib: reduce lib deployment size
    • lib: support CSP channel streaming reducers with max runtime buffer
    • lib: support hard delete
    • lib: remove deprecated populates
    • lib: support custom returned results in dispatch promise
    • lib: queries support Firestore Document Refs in pagination
    • lib: allow custom config
    • lib: cache reducer performance boost on reprocessing by exclude on where clauses
    • lib: refactor cache reduce & mutation to be agnostic for any NoSQL
    • tests: export Typescript Decoders from our interal project
    • tests: move test cases to pure JSON
    • tests: add auth into intergration tests for Firestore rules
    • tests: create API to better integrate visual tests into storybook
    • tests: setup support for standard redux reducers and selectors
    • tests: QuickType support relational ids
    • add your feature request here

    Install

    npm i @puppybits/read-write

    DownloadsWeekly Downloads

    2

    Version

    0.0.1

    License

    MIT

    Unpacked Size

    3.22 MB

    Total Files

    171

    Last publish

    Collaborators

    • puppybits