UNPKG

@levante-framework/firekit

Version:

A library to facilitate Firebase authentication and Firestore interaction for LEVANTE apps

370 lines (369 loc) 16.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getTreeTableOrgs = exports.crc32String = exports.mergeGameParams = exports.emptyOrgList = exports.emptyOrg = exports.initializeFirebaseProject = exports.AuthPersistence = exports.safeInitializeApp = exports.replaceValues = exports.removeUndefined = exports.removeNull = void 0; const app_1 = require("firebase/app"); const auth_1 = require("firebase/auth"); const functions_1 = require("firebase/functions"); const storage_1 = require("firebase/storage"); const performance_1 = require("firebase/performance"); const difference_1 = __importDefault(require("lodash/difference")); const get_1 = __importDefault(require("lodash/get")); const isEmpty_1 = __importDefault(require("lodash/isEmpty")); const isEqual_1 = __importDefault(require("lodash/isEqual")); const isPlainObject_1 = __importDefault(require("lodash/isPlainObject")); const mergeWith_1 = __importDefault(require("lodash/mergeWith")); const remove_1 = __importDefault(require("lodash/remove")); const vue_1 = require("vue"); const crc_32_1 = require("crc-32"); const firestore_1 = require("firebase/firestore"); /** Remove null attributes from an object * @function * @param {Object} obj - Object to remove null attributes from * @returns {Object} Object with null attributes removed */ const removeNull = (obj) => { return Object.fromEntries(Object.entries(obj).filter(([_, v]) => v !== null)); }; exports.removeNull = removeNull; /** Remove undefined attributes from an object * @function * @param {Object} obj - Object to remove undefined attributes from * @returns {Object} Object with undefined attributes removed */ const removeUndefined = (obj) => { return Object.fromEntries(Object.entries(obj).filter(([_, v]) => v !== undefined)); }; exports.removeUndefined = removeUndefined; /** Recursively replace values in an object * @function * @param {Object} obj - Object to recursively replace values in * @param {unknown} valueToReplace - Value to replace * @param {unknown} replacementValue - Replacement value * @returns {Object} Object with values recursively replaced */ const replaceValues = (obj, valueToReplace = undefined, replacementValue = null) => { return Object.fromEntries(Object.entries(obj).map(([key, value]) => { if ((0, isPlainObject_1.default)(value)) { return [key, (0, exports.replaceValues)(value, valueToReplace, replacementValue)]; } return [key, value === valueToReplace ? replacementValue : value]; })); }; exports.replaceValues = replaceValues; const safeInitializeApp = (config, name) => { try { const app = (0, app_1.getApp)(name); if (!(0, isEqual_1.default)(app.options, config)) { throw new Error(`There is an existing firebase app named ${name} with different configuration options.`); } return app; // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error) { if (error.code === 'app/no-app') { return (0, app_1.initializeApp)(config, name); } else { throw error; } } }; exports.safeInitializeApp = safeInitializeApp; var AuthPersistence; (function (AuthPersistence) { AuthPersistence["local"] = "local"; AuthPersistence["session"] = "session"; AuthPersistence["none"] = "none"; })(AuthPersistence = exports.AuthPersistence || (exports.AuthPersistence = {})); const initializeFirebaseProject = async (config, name, emulatorConfig, authPersistence = AuthPersistence.session, markRawConfig = {}) => { const optionallyMarkRaw = (productKey, productInstance) => { if ((0, get_1.default)(markRawConfig, productKey)) { return (0, vue_1.markRaw)(productInstance); } else { return productInstance; } }; if (emulatorConfig) { console.log('Initializing Firebase emulator', emulatorConfig); const app = (0, app_1.initializeApp)({ projectId: emulatorConfig ? 'demo-emulator' : config.projectId, apiKey: config.apiKey }, name); const auth = optionallyMarkRaw('auth', (0, auth_1.getAuth)(app)); const db = optionallyMarkRaw('db', (0, firestore_1.getFirestore)(app)); const functions = optionallyMarkRaw('functions', (0, functions_1.getFunctions)(app)); const storage = optionallyMarkRaw('storage', (0, storage_1.getStorage)(app)); (0, firestore_1.connectFirestoreEmulator)(db, emulatorConfig.firestore.host, emulatorConfig.firestore.port); (0, functions_1.connectFunctionsEmulator)(functions, emulatorConfig.functions.host, emulatorConfig.functions.port); const originalInfo = console.info; // eslint-disable-next-line @typescript-eslint/no-empty-function console.info = () => { }; (0, auth_1.connectAuthEmulator)(auth, `http://${emulatorConfig.auth.host}:${emulatorConfig.auth.port}`); console.info = originalInfo; return { firebaseApp: app, auth, db, functions, storage, }; } else { // const { siteKey, debugToken, ...appConfig } = config as LiveFirebaseConfig; const { ...appConfig } = config; const app = (0, exports.safeInitializeApp)(appConfig, name); // Initialize App Check with reCAPTCHA provider before calling any other Firebase services // Get the App Check token for use in Axios calls to Firebase from the client // const appCheck = initializeAppCheckWithRecaptcha(app, siteKey, debugToken); // const { token: appCheckToken } = await getToken(appCheck); let performance = undefined; try { performance = (0, performance_1.getPerformance)(app); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error) { if (error.code !== 'performance/FB not default') { throw error; } } const kit = { firebaseApp: app, // appCheckToken: appCheckToken, auth: optionallyMarkRaw('auth', (0, auth_1.getAuth)(app)), db: optionallyMarkRaw('db', (0, firestore_1.getFirestore)(app)), functions: optionallyMarkRaw('functions', (0, functions_1.getFunctions)(app)), storage: optionallyMarkRaw('storage', (0, storage_1.getStorage)(app)), perf: performance, }; // Auth state persistence is set with ``setPersistence`` and specifies how a // user session is persisted on a device. We choose in session persistence by // default because many students will access the ROAR on shared devices in the // classroom. if (authPersistence === AuthPersistence.session) { await (0, auth_1.setPersistence)(kit.auth, auth_1.browserSessionPersistence); } else if (authPersistence === AuthPersistence.local) { await (0, auth_1.setPersistence)(kit.auth, auth_1.browserLocalPersistence); } else if (authPersistence === AuthPersistence.none) { await (0, auth_1.setPersistence)(kit.auth, auth_1.inMemoryPersistence); } return kit; } }; exports.initializeFirebaseProject = initializeFirebaseProject; const emptyOrg = () => { return { current: [], all: [], dates: {}, }; }; exports.emptyOrg = emptyOrg; const emptyOrgList = () => { return { districts: [], schools: [], classes: [], groups: [], families: [], }; }; exports.emptyOrgList = emptyOrgList; /** * Merge new game parameters with old parameters with constraints * * The constraints are: * - no new parameters may be added, * - no old parameters may be removed, * - any parameters that have been changed must have had ``null`` values in ``oldParams`` * * @param oldParams - Old game parameters * @param newParams - New game parameters * @returns merged game parameters */ const mergeGameParams = (oldParams, newParams) => { let keysAdded = false; const customizer = (oldValue, newValue, key) => { if (oldValue === null) { return newValue; } if ((0, isEqual_1.default)(oldValue, newValue)) { return newValue; } if (oldValue === undefined && newValue !== undefined) { keysAdded = true; return newValue; } else { throw new Error(`Attempted to change previously non-null value with key ${key}`); } }; const merged = (0, mergeWith_1.default)({ ...oldParams }, newParams, customizer); const differentKeys = (0, difference_1.default)(Object.keys(merged), Object.keys(newParams)); if (!(0, isEmpty_1.default)(differentKeys)) { throw new Error(`Detected deleted keys: ${differentKeys.join(', ')}`); } return { keysAdded, merged, }; }; exports.mergeGameParams = mergeGameParams; const crc32String = (inputString) => { const modulo = (a, b) => { return a - Math.floor(a / b) * b; }; const toUint32 = (x) => { return modulo(x, Math.pow(2, 32)); }; return toUint32((0, crc_32_1.str)(inputString)).toString(16); }; exports.crc32String = crc32String; const treeTableFormat = (orgs, orgType, startIndex = 0) => { return orgs.map((element, index) => ({ key: (index + startIndex).toString(), data: { ...element, orgType, }, })); }; const getTreeTableOrgs = (inputOrgs) => { const { districts = [], schools = [], classes = [], groups = [], families = [] } = inputOrgs; const ttDistricts = treeTableFormat(districts, 'district'); const ttSchools = treeTableFormat(schools, 'school'); const ttClasses = treeTableFormat(classes, 'class'); let topLevelOrgs = []; if (districts.length) { topLevelOrgs = ttDistricts; for (const _school of ttSchools) { const districtId = _school.data.districtId; const districtIndex = topLevelOrgs.findIndex((district) => district.data.id === districtId); // This will return all classes for this school and also remove them from the classes array. // At the end, we will add any left over classes as orphaned classes const classesForThisSchool = (0, remove_1.default)(ttClasses, (c) => c.data.schoolId === _school.data.id); if (districtIndex !== -1) { const _district = topLevelOrgs[districtIndex]; if (_district.children === undefined) { topLevelOrgs[districtIndex].children = [ { ..._school, key: `${_district.key}-0`, // This next pattern is a bit funky. It conditionally adds a children field // but only if there are any classes for this school. ...(classesForThisSchool.length > 0 && { children: classesForThisSchool.map((element, index) => ({ key: `${_district.key}-0-${index}`, data: element.data, })), }), }, ]; } else { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion topLevelOrgs[districtIndex].children.push({ ..._school, key: `${_district.key}-${_district.children.length}`, ...(classesForThisSchool.length > 0 && { children: classesForThisSchool.map((element, index) => ({ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion key: `${_district.key}-${_district.children.length}-${index}`, data: element.data, })), }), }); } } else { topLevelOrgs.push({ ..._school, key: `${topLevelOrgs.length}`, ...(classesForThisSchool.length > 0 && { children: classesForThisSchool.map((element, index) => ({ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion key: `${topLevelOrgs.length}-${index}`, data: element.data, })), }), }); } } // We have now gone through all of the schools and removed any classes that // belong to the supplied schools. If there are any schools left, they // should either be direct descendants of a district (rare) or they should // be at the top level. for (const _class of ttClasses) { const districtId = _class.data.districtId; const districtIndex = topLevelOrgs.findIndex((district) => district.data.id === districtId); if (districtIndex !== -1) { // Add this class as a direct descendant of the district const _district = topLevelOrgs[districtIndex]; if (_district.children === undefined) { topLevelOrgs[districtIndex].children = [ { key: `${_district.key}-0`, data: _class.data, }, ]; } else { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion topLevelOrgs[districtIndex].children.push({ key: `${_district.key}-${_district.children.length}`, data: _class.data, }); } } else { // Add this class to the top-level orgs topLevelOrgs.push({ key: `${topLevelOrgs.length}`, data: _class.data, }); } } } else if (schools.length) { topLevelOrgs = ttSchools; for (const _class of ttClasses) { const schoolId = _class.data.schoolId; const schoolIndex = topLevelOrgs.findIndex((school) => school.data.id === schoolId); if (schoolIndex !== -1) { const _school = topLevelOrgs[schoolIndex]; if (_school.children === undefined) { topLevelOrgs[schoolIndex].children = [ { ..._class, key: `${_school.key}-0`, }, ]; } else { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion topLevelOrgs[schoolIndex].children.push({ ..._class, key: `${_school.key}-${_school.children.length}`, }); } } else { topLevelOrgs.push({ ..._class, key: `${topLevelOrgs.length}`, }); } } } else if (classes.length) { topLevelOrgs = ttClasses; } const ttGroups = treeTableFormat(groups, 'group', topLevelOrgs.length); topLevelOrgs.push(...ttGroups); const ttFamilies = treeTableFormat(families, 'family', topLevelOrgs.length); topLevelOrgs.push(...ttFamilies); return topLevelOrgs; }; exports.getTreeTableOrgs = getTreeTableOrgs;