UNPKG

@taraai/read-write

Version:

Synchronous NoSQL/Firestore for React

413 lines (354 loc) 14.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = setCache; exports.shouldPass = shouldPass; var _shouldPassFail = require("./__mock__/shouldPassFail"); var _app = _interopRequireDefault(require("firebase/compat/app")); require("firebase/compat/firestore"); require("firebase/auth"); var _createFirestoreInstance = _interopRequireWildcard(require("../createFirestoreInstance")); var _reducer = _interopRequireDefault(require("../reducer")); var _mutate = _interopRequireDefault(require("../utils/mutate")); var _reduxThunk = _interopRequireDefault(require("redux-thunk")); var _toolkit = require("@reduxjs/toolkit"); var _isEmpty = _interopRequireDefault(require("lodash/isEmpty")); var _isFunction = _interopRequireDefault(require("lodash/isFunction")); var _merge = _interopRequireDefault(require("lodash/merge")); var _pick = _interopRequireDefault(require("lodash/pick")); var _kebabCase = _interopRequireDefault(require("lodash/kebabCase")); var _startCase = _interopRequireDefault(require("lodash/startCase")); var _reactRedux = require("react-redux"); var _react = _interopRequireDefault(require("react")); var _react2 = require("@testing-library/react"); var _query = require("../utils/query"); var _constants = require("../constants"); var _fs = require("fs"); var _perf_hooks = require("perf_hooks"); var _debug = _interopRequireDefault(require("debug")); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const { useFirestore } = require('../redux-firebase/useFirebase'); const { mutationWriteOutput } = require('../reducers/utils/mutate'); const { wrapInDispatch } = require('../utils/actions'); const info = (0, _debug.default)('readwrite:*'); const verbose = (0, _debug.default)('readwrite:debug'); const removeColors = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g; const noop = () => null; function setupFirestore(databaseURL, enhancers, sideEffects) { let preload = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : []; const store = (0, _toolkit.configureStore)({ reducer: { firestore: _reducer.default }, middleware: [_reduxThunk.default.withExtraArgument({ getFirestore: _createFirestoreInstance.getFirestore, ...enhancers })], preloadedState: { firestore: { cache: { database: preload.reduce((normalized, data) => (0, _merge.default)(normalized, { [data.path]: { [data.id]: data } }), {}), databaseOverrides: {} } } } }); const wasStarted = _app.default.apps.length > 0; const app = function () { return wasStarted ? _app.default.apps[0] : _app.default.initializeApp({ projectId: 'demo-read-write', ...(databaseURL ? { authDomain: 'localhost:9099', databaseURL } : {}) }); }.bind(this)(); const extendedFirestoreInstance = (0, _createFirestoreInstance.default)(app, { userProfile: 'users', useFirestoreForProfile: true }, store.dispatch); extendedFirestoreInstance.setListeners = queryOpts => { const unsubscribe = () => null; queryOpts.forEach(query => { const meta = (0, _query.getQueryConfig)(query); store.dispatch({ type: _constants.actionTypes.SET_LISTENER, meta, payload: { name: (0, _query.getQueryName)(meta) } }); }); return unsubscribe; }; useFirestore.mockReturnValue(extendedFirestoreInstance); if (!wasStarted) _app.default.firestore().useEmulator('localhost', 8080); wrapInDispatch.mockImplementation((dispatcher, action) => { return sideEffects(dispatcher, store.getState, action); }); return [extendedFirestoreInstance, store, app]; } async function loadCollection(firestore, messages) { let fragment = {}; await Promise.all(messages.map(_ref => { let { path } = _ref; return firestore.collection(path).get(); })).then(collection => { const frag = collection.map(snap => { return snap.docs.reduce((db, doc) => ({ ...db, [doc.ref.parent.path]: { [doc.id]: { id: doc.id, path: doc.ref.parent.path, ...doc.data() } } }), {}); }); fragment = { ...fragment, ...frag[0] }; }); return fragment; } async function cleanFirestore(firestore, messages) { const batch = firestore.batch(); messages.filter(Boolean).forEach(_ref2 => { let { path, id } = _ref2; batch.delete(firestore.doc(`${path}/${id}`)); }); return batch.commit(); } function shouldPass(actionCreatorFn) { let useEmulator = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; const isIntegration = undefined || (typeof useEmulator === 'boolean' ? useEmulator : arguments[2] || false); const type = isIntegration ? '[integration]' : '[unit]'; const testSuiteName = typeof actionCreatorFn === 'string' ? `${type}: ${actionCreatorFn}` : `${type}: ${actionCreatorFn.typePrefix || ''} %# $testname should pass.`; const actionCreator = (0, _isFunction.default)(useEmulator) ? useEmulator : actionCreatorFn; return [testSuiteName, async _ref3 => { let { payload, writes: writesExpected, results: resultsExpected, returned: returnExpected, component, globals, setup, testname } = _ref3; const profiles = [{ name: 'start', time: _perf_hooks.performance.now(), delta: 0 }]; if (setup && !(Array.isArray(setup) && setup.every(_ref4 => { let { path, id } = _ref4; return !(0, _isEmpty.default)(path) && !(0, _isEmpty.default)(id); }))) { throw new Error(`'setup' must be an { path:string; id: string; ...any}[] but received ${JSON.stringify(setup)}.`); } const databaseURL = typeof isIntegration === 'string' ? isIntegration : isIntegration && 'localhost:8080' || null; const log = []; let cache = {}; let customDispatcher = (dispatcher, getState, action) => { const mutationPromise = (0, _shouldPassFail.dispatchActual)(dispatcher, action, { extras: globals }); cache = getState().firestore.cache; log.push({ event: 'cache', data: getState().firestore.cache }); return mutationPromise; }; const [firestore, store, firebaseApp] = setupFirestore(databaseURL, globals, customDispatcher, setup); profiles.push({ name: 'firestore-init', time: _perf_hooks.performance.now(), delta: _perf_hooks.performance.now() - profiles[profiles.length - 1].time }); if (setup) { await (0, _mutate.default)({ firestore: () => firestore }, setup); } profiles.push({ name: 'firestore-preload', time: _perf_hooks.performance.now(), delta: _perf_hooks.performance.now() - profiles[profiles.length - 1].time }); let element; let elementName; let preComponent; if (component) {} let writeReceived = null; mutationWriteOutput.mockImplementation((writes, db) => { writeReceived = Array.isArray(writesExpected) ? writes : writes[0]; verbose.enabled && verbose(`test-name: "${testname}"\nwrite: ${JSON.stringify(writeReceived, null, 2)}`); return (0, _shouldPassFail.mutationWriteOutputActual)(writes, db); }); verbose.enabled && verbose(`test-name: "${testname}"\npayload: ${JSON.stringify(payload, null, 2)}`); const dispatched = store.dispatch(actionCreator(payload)).then(_toolkit.unwrapResult); const returnRevived = await expect(dispatched).resolves.not.toThrow(); profiles.push({ name: 'action-dispatched', time: _perf_hooks.performance.now(), delta: _perf_hooks.performance.now() - profiles[profiles.length - 1].time }); const postComponent = element && element.container && (0, _react2.prettyDOM)(element.container).replace(removeColors, ''); (0, _fs.writeFile)(`stories/${(0, _startCase.default)(elementName)}_${(0, _kebabCase.default)(testname)}.stories.tsx`, `import React from 'react';\nexport default {\n\t` + `title: '${(0, _startCase.default)(elementName)}',\n};\n\n` + `export const Default = () => (${preComponent});\n\n` + `export const After = () => (${postComponent});`, (done, err) => null); if (element && info.enabled) { info(`\nstories/${snakeCase(testname)}_mutation.stories.tsx`, `${prettyFormat.format(element.toJSON(), { plugins: [prettyFormat.plugins.ReactTestComponent], printFunctionName: false, highlight: false })}`, (done, err) => null); } if (writesExpected !== undefined) { expect(writeReceived).toStrictEqual(writesExpected); } if (resultsExpected !== undefined) { const { firestore: diskExpected = resultsExpected, cache: memoryExpected = resultsExpected } = resultsExpected; verbose.enabled && verbose(`test-name: "${testname}"\nstore: ${JSON.stringify(memoryExpected, null, 2)}`); Object.keys(memoryExpected).forEach(path => Object.keys(memoryExpected[path]).forEach(id => { const documentExpected = memoryExpected[path][id]; const keys = Object.keys(documentExpected); const optimistic = { ...(cache.database && cache.database[path] && cache.database[path][id] || {}), ...(cache.databaseOverrides && cache.databaseOverrides[path] && cache.databaseOverrides[path][id] || {}) }; const documentCached = (0, _pick.default)(optimistic, keys); expect(documentCached).toStrictEqual(documentExpected); })); profiles.push({ name: 'cache-validated', time: _perf_hooks.performance.now(), delta: _perf_hooks.performance.now() - profiles[profiles.length - 1].time }); if (databaseURL) { const queries = []; Object.keys(diskExpected).forEach(path => { const collection = diskExpected[path]; Object.keys(collection).forEach(id => { queries.push({ path, id }); }); }); const database = await loadCollection(firestore, queries); Object.keys(diskExpected).forEach(path => Object.keys(diskExpected[path]).forEach(id => { const keys = Object.keys(diskExpected[path][id]); const documentSavedToFirestore = (0, _pick.default)(database[path][id] || {}, keys); const documentExpected = (0, _pick.default)(diskExpected[path][id] || {}, keys); expect(documentExpected).toStrictEqual(documentSavedToFirestore); })); } profiles.push({ name: 'firestore-validated', time: _perf_hooks.performance.now(), delta: _perf_hooks.performance.now() - profiles[profiles.length - 1].time }); } if (returnExpected !== undefined) { expect(returnExpected).toStrictEqual(returnRevived); } await cleanFirestore(firestore, [...(setup || []), ...(Array.isArray(writeReceived) ? writeReceived : [writeReceived])]); profiles.push({ name: 'firestore-cleaned', time: _perf_hooks.performance.now(), delta: _perf_hooks.performance.now() - profiles[profiles.length - 1].time }); await firebaseApp.delete(); for (var i in firestore) { firestore[i] = null; } if (info.enabled) { info(`\ntest-name: "${testname}"\n`, profiles.slice(1).map(_ref5 => { let { name, delta } = _ref5; return `${name}: ${delta.toFixed(2)}ms `; }).join('\n'), '\n'); } }]; } function setCache(_ref6) { let { firebaseAuth = { isEmpty: true, isLoaded: false }, firebaseProfile = { isEmpty: true, isLoaded: false }, ...documents } = _ref6; let middlewares = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; const keys = Object.keys(aliases); documents.map(); const normalizedDocuments = keys.reduce((obj, key) => { const list = aliases[key]; const { path } = list[0]; list.forEach(item => { obj[list[0].path] = { [item.id]: item }; }); return obj; }, {}); const initialState = { firebase: { auth: firebaseAuth, profile: firebaseProfile }, firestore: { cache: { database: normalizedDocuments, databaseOverrides: {}, ...keys.reduce((obj, alias) => ({ ...obj, [alias]: { ordered: aliases[alias].map(_ref7 => { let { path, id } = _ref7; return [path, id]; }), path: aliases[alias] && aliases[alias][0] && aliases[alias][0].path || 'unset', via: 'memory' } }), {}) } } }; const store = (0, _toolkit.configureStore)(middlewares); return store(initialState); }