@levante-framework/firekit
Version:
A library to facilitate Firebase authentication and Firestore interaction for LEVANTE apps
370 lines (369 loc) • 16.1 kB
JavaScript
;
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;