Skip to content

Commit

Permalink
Init
Browse files Browse the repository at this point in the history
  • Loading branch information
kevin-ashton committed Aug 19, 2020
1 parent 1878dd3 commit 03c736f
Show file tree
Hide file tree
Showing 20 changed files with 3,039 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
root = true

# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true

indent_style = space
indent_size = 2
5 changes: 5 additions & 0 deletions .firebaserc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"projects": {
"default": "fir-lift"
}
}
65 changes: 65 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
dist
.idea

.vscode

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env

# next.js build output
.next
10 changes: 10 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"trailingComma": "none",
"bracketSpacing": true,
"arrowParens": "always",
"singleQuote": true
}
17 changes: 17 additions & 0 deletions firebase.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"firestore": {
"rules": "firestore.rules",
"indexes": "firestore.indexes.json"
},
"emulators": {
"firestore": {
"port": 8080
},
"database": {
"port": 9000
},
"ui": {
"enabled": true
}
}
}
40 changes: 40 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "firebase-lift",
"version": "0.0.1",
"description": "",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"test": "node dist/tests/index.js",
"ts-build": "tsc ",
"ts-watch": "tsc -w",
"start": "firebase emulators:start"
},
"devDependencies": {
"@firebase/testing": "^0.19.5",
"@types/json-stable-stringify": "^1.0.32",
"@types/lodash": "^4.14.158",
"@types/md5": "^2.2.0",
"@types/shortid": "^0.0.29",
"nano-test-runner": "^1.2.0",
"prettier": "^2.0.5",
"typescript": "^3.9.3"
},
"dependencies": {
"firebase": "^7.14.5",
"json-stable-stringify": "^1.0.1",
"lodash": "^4.17.19",
"md5": "^2.2.1",
"shortid": "^2.2.15"
},
"repository": {
"type": "git",
"url": "git+https://github.com/kevin-ashton/firebase-lift.git"
},
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/kevin-ashton/firebase-lift/issues"
},
"homepage": "https://github.com/kevin-ashton/firebase-lift#readme"
}
82 changes: 82 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Firebase Lift

Firebase provides a variety of tools that are amazing. This wraps various aspects of the api.

## Firestore

### Features
* Types on returned documents
* Types on various CRUD functions
* Types for query construction
* Ability group queries/doc fetches
* Metrics that track doc read/writes for various collections

## Limitations
* Firestore caching is always disabled
* Sub collections are not supported
* Server timestamps are not supported
* Array filters are currently not supported
* Only supports basic types string, number, array, maps. No support for geo data, timestamps, etc.
* Increment is limited to a single number increment (no jumping by multiple numbers, or decrementing)
* startAt, startAfter, endAt, endBefore are supported for values but not for firestore docs or query docs. In other words you must use a value and not a firestore document when using those filters.

## Realtime Database

### Features
* Add some types for objects/primatives

### Limitations
* Only covers part of the API. You can access the raw refs to do everything normally without types.

## Usage

```ts
import {
createRtdbLift,
createFirestoreLift,
FirestoreLiftCollection
TypedFirebaseObjectOrPrimativeRefGenerator
} from '../RTDB';
import * as firebase from 'firebase';

interface Person {
name: string;
age: number;
}

interface Book {
title: string;
year: number;
}

interface Heartbeat {
dateMs: number;
msg: string;
}

interface DeviceInfo {
dateMs: number;
dId: string;
}

const app = firebase.initializeApp(testFirebaseConfig);

const firestoreLiftExample = createFirestoreLift<ExampleFirestore>({
collections: {
Person: {
collection: 'person'
},
Book: {
collection: 'book'
}
},
firebaseApp: app,
firestoreModule: firebase.firestore
});

const rtdbLiftExample = createRtdbLift({firebaseApp: app, {
person: (null as unknown) as TypedFirebaseObjectOrPrimativeRefGenerator<Person>,
book: (null as unknown) as TypedFirebaseObjectOrPrimativeRefGenerator<Book>
}});

```
108 changes: 108 additions & 0 deletions src/BatchRunner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import * as firebase from 'firebase';
import {
BatchTask,
BatchTaskEmpty,
MagicDeleteString,
MagicIncrementString,
MagicServerTimestampString
} from './models';
import { generateFirestorePathFromObject, defaultEmptyTask } from './misc';

export class BatchRunner {
public firestoreModule: typeof firebase.firestore;
public app: firebase.app.App;

constructor(config: { firestoreModule: typeof firebase.firestore; app: firebase.app.App }) {
this.firestoreModule = config.firestoreModule;
this.app = config.app;
}

// We use a magic string for deletes so we can pass around batches of change sets to be environment agnostic
private scrubDataPreWrite(p: { obj: any; removeEmptyObjects: boolean }) {
const { obj, removeEmptyObjects } = p;

if (typeof obj === 'object') {
let keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
let k = keys[i];
if (obj[k] === MagicDeleteString) {
obj[k] = this.firestoreModule.FieldValue.delete();
} else if (obj[k] === MagicIncrementString) {
obj[k] = this.firestoreModule.FieldValue.increment(1);
} else if (obj[k] === undefined || obj[k] === null) {
//Undefined values get coerced to null by the Firestore SDK, which makes no sense for a strongly typed library like this
delete obj[k];
} else if (typeof obj[k] === 'object') {
if (removeEmptyObjects && Object.keys(obj[k]).length === 0) {
delete obj[k];
} else {
obj[k] = this.scrubDataPreWrite({ obj: obj[k], removeEmptyObjects });
}
} else {
obj[k] = obj[k];
}
}
}
if (typeof obj === 'string' && obj === MagicDeleteString) {
return this.firestoreModule.FieldValue.delete();
}
return obj;
}

async executeBatch(b: BatchTask[]): Promise<BatchTaskEmpty> {
let batch = this.firestoreModule(this.app).batch();
try {
for (let i = 0; i < b.length; i++) {
let task = b[i];
if (task.type === 'empty') {
continue;
}

if (!task.id) {
throw Error(`Unable to process item. Lacks an id. Collection: ${task.collection}. Task Type: ${task.type}`);
}
let ref = this.firestoreModule(this.app).collection(task.collection).doc(task.id);

let newObj;
switch (task.type) {
case 'add':
batch.set(ref, this.scrubDataPreWrite({ obj: task.doc, removeEmptyObjects: false }), { merge: true });
break;
case 'set':
batch.set(ref, this.scrubDataPreWrite({ obj: task.doc, removeEmptyObjects: false }), { merge: false });
break;
case 'setPath':
let p = generateFirestorePathFromObject(task.pathObj);
let newPathVal = p.path.split('.').reduce((acc, val) => {
if (acc[val] === undefined) {
throw new Error('Missing value for setPath update');
}
return acc[val];
}, task.value);
newPathVal = this.scrubDataPreWrite({ obj: newPathVal, removeEmptyObjects: false });
batch.update(ref, p.path, newPathVal);
break;
case 'update':
//firestore set merge has the very dumb default behavior of making empty objects overwriting the object entirely
newObj = this.scrubDataPreWrite({ obj: task.doc, removeEmptyObjects: true });
batch.set(ref, newObj, { merge: true });
break;
case 'delete':
batch.delete(ref);
break;
default:
// @ts-ignore
throw new Error(`Unknown BatchTask type. Type: ${task.type}`);
}
}

await batch.commit();

// Returning an empty task makes it easier for the helper functions (.add, .update) so they always return a batch type. Makes it so we don't have to check for undefined
return defaultEmptyTask;
} catch (e) {
console.error(e);
throw e;
}
}
}
Loading

0 comments on commit 03c736f

Please sign in to comment.