Skip to content

refactor(database): Add typescript database implementation [Part 2/3] #62

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jun 28, 2017
1 change: 0 additions & 1 deletion gulp/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ module.exports = {
babel: {
plugins: [
require('babel-plugin-add-module-exports'),
require('babel-plugin-minify-dead-code-elimination')
],
presets: [
[require('babel-preset-env'), {
Expand Down
58 changes: 12 additions & 46 deletions gulp/tasks/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ function compileIndvES2015ModulesToBrowser() {
'firebase-app': './src/app.ts',
'firebase-storage': './src/storage.ts',
'firebase-messaging': './src/messaging.ts',
'firebase-database': './src/database.ts',
},
output: {
path: path.resolve(__dirname, './dist/browser'),
Expand Down Expand Up @@ -192,27 +193,6 @@ function compileIndvES2015ModulesToBrowser() {
.pipe(gulp.dest(`${config.paths.outDir}/browser`));
}

function compileSDKES2015ToBrowser() {
return gulp.src('./dist/es2015/firebase.js')
.pipe(webpackStream({
plugins: [
new webpack.DefinePlugin({
TARGET_ENVIRONMENT: JSON.stringify('browser')
})
]
}, webpack))
.pipe(sourcemaps.init({ loadMaps: true }))
.pipe(through.obj(function(file, enc, cb) {
// Dont pipe through any source map files as it will be handled
// by gulp-sourcemaps
var isSourceMap = /\.map$/.test(file.path);
if (!isSourceMap) this.push(file);
cb();
}))
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest(`${config.paths.outDir}/browser`));
}

function buildBrowserFirebaseJs() {
return gulp.src('./dist/browser/*.js')
.pipe(sourcemaps.init({ loadMaps: true }))
Expand All @@ -222,32 +202,18 @@ function buildBrowserFirebaseJs() {
}

function buildAltEnvFirebaseJs() {
const envs = [
'browser',
'node',
'react-native'
];

const streams = envs.map(env => {
const babelConfig = Object.assign({}, config.babel, {
plugins: [
['inline-replace-variables', {
'TARGET_ENVIRONMENT': env
}],
...config.babel.plugins
]
});
return gulp.src('./dist/es2015/firebase.js')
.pipe(sourcemaps.init({ loadMaps: true }))
.pipe(babel(babelConfig))
.pipe(rename({
suffix: `-${env}`
}))
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest(`${config.paths.outDir}/cjs`));
const babelConfig = Object.assign({}, config.babel, {
plugins: config.babel.plugins
});

return merge(streams);
return gulp.src([
'./dist/es2015/firebase-browser.js',
'./dist/es2015/firebase-node.js',
'./dist/es2015/firebase-react-native.js',
])
.pipe(sourcemaps.init({ loadMaps: true }))
.pipe(babel(babelConfig))
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest(`${config.paths.outDir}/cjs`));
}

function copyPackageContents() {
Expand Down
65 changes: 65 additions & 0 deletions src/database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import firebase from './app';
import { FirebaseApp, FirebaseNamespace } from "./app/firebase_app";
import { Database } from "./database/api/Database";
import { Query } from "./database/api/Query";
import { Reference } from "./database/api/Reference";
import { enableLogging } from "./database/core/util/util";
import { RepoManager } from "./database/core/RepoManager";
import * as INTERNAL from './database/api/internal';
import * as TEST_ACCESS from './database/api/test_access';
import { isNodeSdk } from "./utils/environment";

export function registerDatabase(instance) {
// Register the Database Service with the 'firebase' namespace.
const namespace = instance.INTERNAL.registerService(
'database',
app => RepoManager.getInstance().databaseFromApp(app),
// firebase.database namespace properties
{
Reference,
Query,
Database,
enableLogging,
INTERNAL,
ServerValue: Database.ServerValue,
TEST_ACCESS
}
);

if (isNodeSdk()) {
module.exports = namespace;
}
}

/**
* Extensions to the FirebaseApp and FirebaseNamespaces interfaces
*/
declare module './app/firebase_app' {
interface FirebaseApp {
database?(): Database
}
}

declare module './app/firebase_app' {
interface FirebaseNamespace {
database?(app: FirebaseApp): Database
}
}

registerDatabase(firebase);
168 changes: 168 additions & 0 deletions src/database/api/DataSnapshot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { validateArgCount, validateCallback } from '../../utils/validation';
import { validatePathString } from '../core/util/validation';
import { Path } from '../core/util/Path';
import { PRIORITY_INDEX } from '../core/snap/indexes/PriorityIndex';
import { Node } from '../core/snap/Node';
import { Reference } from './Reference';
import { Index } from '../core/snap/indexes/Index';
import { ChildrenNode } from '../core/snap/ChildrenNode';

/**
* Class representing a firebase data snapshot. It wraps a SnapshotNode and
* surfaces the public methods (val, forEach, etc.) we want to expose.
*/
export class DataSnapshot {
/**
* @param {!Node} node_ A SnapshotNode to wrap.
* @param {!Reference} ref_ The ref of the location this snapshot came from.
* @param {!Index} index_ The iteration order for this snapshot
*/
constructor(private readonly node_: Node,
private readonly ref_: Reference,
private readonly index_: Index) {
}

/**
* Retrieves the snapshot contents as JSON. Returns null if the snapshot is
* empty.
*
* @return {*} JSON representation of the DataSnapshot contents, or null if empty.
*/
val(): any {
validateArgCount('DataSnapshot.val', 0, 0, arguments.length);
return this.node_.val();
}

/**
* Returns the snapshot contents as JSON, including priorities of node. Suitable for exporting
* the entire node contents.
* @return {*} JSON representation of the DataSnapshot contents, or null if empty.
*/
exportVal(): any {
validateArgCount('DataSnapshot.exportVal', 0, 0, arguments.length);
return this.node_.val(true);
}

// Do not create public documentation. This is intended to make JSON serialization work but is otherwise unnecessary
// for end-users
toJSON(): any {
// Optional spacer argument is unnecessary because we're depending on recursion rather than stringifying the content
validateArgCount('DataSnapshot.toJSON', 0, 1, arguments.length);
return this.exportVal();
}

/**
* Returns whether the snapshot contains a non-null value.
*
* @return {boolean} Whether the snapshot contains a non-null value, or is empty.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed over chat, we should figure out if we want to keep the type annotations in our comment or if it is sufficient to use the types from TypeScript.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just chiming in since I was thinking about this the other day. Type annotations in the comments only make sense when using a tool like Closure compiler to do type checking, but it's not necessary anymore with TypeScript. Besides, I bet the types in the comments won't always be updated when the code changes so eventually it might do more harm than good.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy w/ defaulting to typescripts types. Just was trying to keep the delta small where possible.

That said, I think I'm going to merge #66 as I've been going through that and it addresses many of the type issues you've mentioned.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition I agree with @jsayol, that the divergence in types is probably a maintenance burden going forward. Let's consolidate after we've validated the code is good.

*/
exists(): boolean {
validateArgCount('DataSnapshot.exists', 0, 0, arguments.length);
return !this.node_.isEmpty();
}

/**
* Returns a DataSnapshot of the specified child node's contents.
*
* @param {!string} childPathString Path to a child.
* @return {!DataSnapshot} DataSnapshot for child node.
*/
child(childPathString: string): DataSnapshot {
validateArgCount('DataSnapshot.child', 0, 1, arguments.length);
// Ensure the childPath is a string (can be a number)
childPathString = String(childPathString);
validatePathString('DataSnapshot.child', 1, childPathString, false);

const childPath = new Path(childPathString);
const childRef = this.ref_.child(childPath);
return new DataSnapshot(this.node_.getChild(childPath), childRef, PRIORITY_INDEX);
}

/**
* Returns whether the snapshot contains a child at the specified path.
*
* @param {!string} childPathString Path to a child.
* @return {boolean} Whether the child exists.
*/
hasChild(childPathString: string): boolean {
validateArgCount('DataSnapshot.hasChild', 1, 1, arguments.length);
validatePathString('DataSnapshot.hasChild', 1, childPathString, false);

const childPath = new Path(childPathString);
return !this.node_.getChild(childPath).isEmpty();
}

/**
* Returns the priority of the object, or null if no priority was set.
*
* @return {string|number|null} The priority.
*/
getPriority(): string | number | null {
validateArgCount('DataSnapshot.getPriority', 0, 0, arguments.length);

// typecast here because we never return deferred values or internal priorities (MAX_PRIORITY)
return /**@type {string|number|null} */ <string | number | null>(this.node_.getPriority().val());
}

/**
* Iterates through child nodes and calls the specified action for each one.
*
* @param {function(!DataSnapshot)} action Callback function to be called
* for each child.
* @return {boolean} True if forEach was canceled by action returning true for
* one of the child nodes.
*/
forEach(action: (d: DataSnapshot) => any): boolean {
validateArgCount('DataSnapshot.forEach', 1, 1, arguments.length);
validateCallback('DataSnapshot.forEach', 1, action, false);

if (this.node_.isLeafNode())
return false;

const childrenNode = /**@type {ChildrenNode} */ <ChildrenNode>(this.node_);
// Sanitize the return value to a boolean. ChildrenNode.forEachChild has a weird return type...
return !!childrenNode.forEachChild(this.index_, (key, node) => {
return action(new DataSnapshot(node, this.ref_.child(key), PRIORITY_INDEX));
});
}

/**
* Returns whether this DataSnapshot has children.
* @return {boolean} True if the DataSnapshot contains 1 or more child nodes.
*/
hasChildren(): boolean {
validateArgCount('DataSnapshot.hasChildren', 0, 0, arguments.length);

if (this.node_.isLeafNode())
return false;
else
return !this.node_.isEmpty();
}

get key() {
return this.ref_.getKey();
}

/**
* Returns the number of children for this DataSnapshot.
* @return {number} The number of children that this DataSnapshot contains.
*/
numChildren(): number {
validateArgCount('DataSnapshot.numChildren', 0, 0, arguments.length);

return this.node_.numChildren();
}

/**
* @return {Reference} The Firebase reference for the location this snapshot's data came from.
*/
getRef(): Reference {
validateArgCount('DataSnapshot.ref', 0, 0, arguments.length);

return this.ref_;
}

get ref() {
return this.getRef();
}
}
Loading