diff --git a/src/firebaseApi.js b/src/firebaseApi.js deleted file mode 100644 index 0ca4a1384a0..00000000000 --- a/src/firebaseApi.js +++ /dev/null @@ -1,35 +0,0 @@ -const api = require("./api"); - -const API_VERSION = "v1beta1"; - -function _list(nextPageToken, projects) { - projects = projects || []; - - let path = `/${API_VERSION}/projects?page_size=100`; - if (nextPageToken) { - path += `&page_token=${nextPageToken}`; - } - - return api - .request("GET", path, { - auth: true, - origin: api.firebaseApiOrigin, - }) - .then((response) => { - projects = projects.concat(response.body.results); - if (response.body.nextPageToken) { - return _list(response.body.nextPageToken, projects); - } - return projects; - }); -} - -exports.listProjects = () => _list(); - -exports.getProject = (projectId) => - api - .request("GET", `/${API_VERSION}/projects/${projectId}`, { - auth: true, - origin: api.firebaseApiOrigin, - }) - .then((response) => response.body); diff --git a/src/firebaseApi.ts b/src/firebaseApi.ts new file mode 100644 index 00000000000..97fe85eff01 --- /dev/null +++ b/src/firebaseApi.ts @@ -0,0 +1,49 @@ +import * as api from "./api"; + +const API_VERSION = "v1beta1"; + +/** + * Represents the FirebaseProject resource returned from calling + * `projects.get` in Firebase Management API: + * https://firebase.google.com/docs/projects/api/reference/rest/v1beta1/projects#FirebaseProject + */ +export interface FirebaseProject { + projectId: string; + projectNumber: number; + displayName: string; + name: string; + resources: { + hostingSite?: string; + realtimeDatabaseInstance?: string; + storageBucket?: string; + locationId?: string; + }; +} + +export async function listProjects( + nextPageToken?: string, + projectsList: FirebaseProject[] = [] +): Promise { + let path = `/${API_VERSION}/projects?page_size=100`; + if (nextPageToken) { + path += `&page_token=${nextPageToken}`; + } + + const response = await api.request("GET", path, { + auth: true, + origin: api.firebaseApiOrigin, + }); + projectsList = projectsList.concat(response.body.results); + if (response.body.nextPageToken) { + return listProjects(response.body.nextPageToken, projectsList); + } + return projectsList; +} + +export async function getProject(projectId: string): Promise { + const response = await api.request("GET", `/${API_VERSION}/projects/${projectId}`, { + auth: true, + origin: api.firebaseApiOrigin, + }); + return response.body; +} diff --git a/src/init/features/index.js b/src/init/features/index.js index 846b80745ff..24567d2b80e 100644 --- a/src/init/features/index.js +++ b/src/init/features/index.js @@ -5,7 +5,7 @@ module.exports = { firestore: require("./firestore").doSetup, functions: require("./functions"), hosting: require("./hosting"), - storage: require("./storage"), + storage: require("./storage").doSetup, // always runs, sets up .firebaserc - project: require("./project"), + project: require("./project").doSetup, }; diff --git a/src/init/features/project.js b/src/init/features/project.js deleted file mode 100644 index 29f89e6a607..00000000000 --- a/src/init/features/project.js +++ /dev/null @@ -1,125 +0,0 @@ -"use strict"; - -var clc = require("cli-color"); - -var _ = require("lodash"); -var firebaseApi = require("../../firebaseApi"); -var { promptOnce } = require("../../prompt"); -var logger = require("../../logger"); -var utils = require("../../utils"); - -var NO_PROJECT = "[don't setup a default project]"; -var NEW_PROJECT = "[create a new project]"; - -/** - * Get the user's desired project, prompting if necessary. - * Returns an object with three fields: - * - * { - * id: project ID [required] - * label: project display label [optional] - * instance: project database instance [optional] - * } - */ -function _getProject(options) { - // The user passed in a --project flag directly, so no need to - // load all projects. - if (options.project) { - return firebaseApi - .getProject(options.project) - .then(function(project) { - var id = project.projectId; - var name = project.displayName; - return { - id: id, - label: id + " (" + name + ")", - instance: _.get(project, "resources.realtimeDatabaseInstance"), - }; - }) - .catch(function(e) { - return utils.reject("Error getting project " + options.project, { original: e }); - }); - } - - // Load all projects and prompt the user to choose. - return firebaseApi.listProjects().then(function(projects) { - var choices = projects.filter((project) => !!project).map((project) => { - return { - name: project.projectId + " (" + project.displayName + ")", - value: project.projectId, - }; - }); - choices = _.orderBy(choices, ["name"], ["asc"]); - choices.unshift({ name: NO_PROJECT, value: NO_PROJECT }); - choices.push({ name: NEW_PROJECT, value: NEW_PROJECT }); - - if (choices.length >= 25) { - utils.logBullet( - "Don't want to scroll through all your projects? If you know your project ID, " + - "you can initialize it directly using " + - clc.bold("firebase init --project ") + - ".\n" - ); - } - - return promptOnce({ - type: "list", - name: "id", - message: "Select a default Firebase project for this directory:", - validate: function(answer) { - if (!_.includes(choices, answer)) { - return "Must specify a Firebase to which you have access."; - } - return true; - }, - choices: choices, - }).then(function(id) { - if (id === NEW_PROJECT || id === NO_PROJECT) { - return { id: id }; - } - - const project = projects.find((p) => p.projectId === id); - const label = choices.find((p) => p.value === id).name; - return { - id: id, - label: label, - instance: _.get(project, "resources.realtimeDatabaseInstance"), - }; - }); - }); -} - -module.exports = function(setup, config, options) { - setup.project = {}; - - logger.info(); - logger.info("First, let's associate this project directory with a Firebase project."); - logger.info( - "You can create multiple project aliases by running " + clc.bold("firebase use --add") + ", " - ); - logger.info("but for now we'll just set up a default project."); - logger.info(); - - if (_.has(setup.rcfile, "projects.default")) { - utils.logBullet(".firebaserc already has a default project, skipping"); - setup.projectId = _.get(setup.rcfile, "projects.default"); - return undefined; - } - - return _getProject(options).then(function(project) { - if (project.id === NEW_PROJECT) { - setup.createProject = true; - return; - } else if (project.id === NO_PROJECT) { - return; - } - - utils.logBullet("Using project " + project.label); - - // write "default" alias and activate it immediately - _.set(setup.rcfile, "projects.default", project.id); - setup.projectId = project.id; - setup.instance = project.instance; - utils.makeActiveProject(config.projectDir, project.id); - }); -}; diff --git a/src/init/features/project.ts b/src/init/features/project.ts new file mode 100644 index 00000000000..cf34d69f5f1 --- /dev/null +++ b/src/init/features/project.ts @@ -0,0 +1,149 @@ +import * as clc from "cli-color"; +import * as _ from "lodash"; + +import * as Config from "../../config"; +import * as FirebaseError from "../../error"; +import { FirebaseProject, getProject, listProjects } from "../../firebaseApi"; +import * as logger from "../../logger"; +import { promptOnce, Question } from "../../prompt"; +import * as utils from "../../utils"; + +const NO_PROJECT = "[don't setup a default project]"; +const NEW_PROJECT = "[create a new project]"; + +/** + * Used in init flows to keep information about the project - basically + * a shorter version of {@link FirebaseProject} with some additional fields. + */ +export interface ProjectInfo { + id: string; // maps to FirebaseProject.projectId + label?: string; + instance?: string; // maps to FirebaseProject.resources.realtimeDatabaseInstance + location?: string; // maps to FirebaseProject.resources.locationId +} + +/** + * Get the user's desired project, prompting if necessary. + * @returns A {@link ProjectInfo} object. + */ +async function getProjectInfo(options: any): Promise { + if (options.project) { + return selectProjectFromOptions(options); + } + return selectProjectFromList(options); +} + +/** + * Selects project when --project is passed in. + * @param options Command line options. + * @returns A {@link FirebaseProject} object. + */ +async function selectProjectFromOptions(options: any): Promise { + let project: FirebaseProject; + try { + project = await getProject(options.project); + } catch (e) { + throw new FirebaseError(`Error getting project ${options.project}: ${e}`); + } + const projectId = project.projectId; + const name = project.displayName; + return { + id: projectId, + label: `${projectId} (${name})`, + instance: _.get(project, "resources.realtimeDatabaseInstance"), + }; +} + +/** + * Presents user with list of projects to choose from and gets project + * information for chosen project. + * @param options Command line options. + * @returns A {@link FirebaseProject} object. + */ +async function selectProjectFromList(options: any): Promise { + let project: FirebaseProject | undefined; + const projects: FirebaseProject[] = await listProjects(); + let choices = projects.filter((p: FirebaseProject) => !!p).map((p) => { + return { + name: `${p.projectId} (${p.displayName})`, + value: p.projectId, + }; + }); + choices = _.orderBy(choices, ["name"], ["asc"]); + choices.unshift({ name: NO_PROJECT, value: NO_PROJECT }); + choices.push({ name: NEW_PROJECT, value: NEW_PROJECT }); + + if (choices.length >= 25) { + utils.logBullet( + `Don't want to scroll through all your projects? If you know your project ID, ` + + `you can initialize it directly using ${clc.bold( + "firebase init --project " + )}.\n` + ); + } + const projectId: string = await promptOnce({ + type: "list", + name: "id", + message: "Select a default Firebase project for this directory:", + validate: (answer: any) => { + if (!_.includes(choices, answer)) { + return `Must specify a Firebase project to which you have access.`; + } + return true; + }, + choices, + } as Question); + if (projectId === NEW_PROJECT || projectId === NO_PROJECT) { + return { id: projectId }; + } + + project = projects.find((p) => p.projectId === projectId); + const pId = choices.find((p) => p.value === projectId); + const label = pId ? pId.name : ""; + + return { + id: projectId, + label, + instance: _.get(project, "resources.realtimeDatabaseInstance"), + }; +} + +/** + * Sets up the default project if provided and writes .firebaserc file. + * @param setup A helper object to use for the rest of the init features. + * @param config Configuration for the project. + * @param options Command line options. + */ +export async function doSetup(setup: any, config: Config, options: any): Promise { + setup.project = {}; + + logger.info(); + logger.info(`First, let's associate this project directory with a Firebase project.`); + logger.info( + `You can create multiple project aliases by running ${clc.bold("firebase use --add")}, ` + ); + logger.info(`but for now we'll just set up a default project.`); + logger.info(); + + if (_.has(setup.rcfile, "projects.default")) { + utils.logBullet(`.firebaserc already has a default project, skipping`); + setup.projectId = _.get(setup.rcfile, "projects.default"); + return; + } + + const projectInfo: ProjectInfo = await getProjectInfo(options); + if (projectInfo.id === NEW_PROJECT) { + setup.createProject = true; + return; + } else if (projectInfo.id === NO_PROJECT) { + return; + } + + utils.logBullet(`Using project ${projectInfo.label}`); + + // write "default" alias and activate it immediately + _.set(setup.rcfile, "projects.default", projectInfo.id); + setup.projectId = projectInfo.id; + setup.instance = projectInfo.instance; + utils.makeActiveProject(config.projectDir, projectInfo.id); +} diff --git a/src/init/features/storage.js b/src/init/features/storage.ts similarity index 59% rename from src/init/features/storage.js rename to src/init/features/storage.ts index 08843999cea..d0b6f409e99 100644 --- a/src/init/features/storage.js +++ b/src/init/features/storage.ts @@ -1,17 +1,15 @@ -"use strict"; +import * as clc from "cli-color"; +import * as fs from "fs"; -var clc = require("cli-color"); -var fs = require("fs"); +import * as logger from "../../logger"; +import { prompt } from "../../prompt"; -var { prompt } = require("../../prompt"); -var logger = require("../../logger"); - -var RULES_TEMPLATE = fs.readFileSync( +const RULES_TEMPLATE = fs.readFileSync( __dirname + "/../../../templates/init/storage/storage.rules", "utf8" ); -module.exports = function(setup, config) { +export async function doSetup(setup: any, config: any): Promise { setup.config.storage = {}; logger.info(); @@ -20,14 +18,13 @@ module.exports = function(setup, config) { logger.info("and publish them with " + clc.bold("firebase deploy") + "."); logger.info(); - return prompt(setup.config.storage, [ + await prompt(setup.config.storage, [ { type: "input", name: "rules", message: "What file should be used for Storage Rules?", default: "storage.rules", }, - ]).then(function() { - return config.writeProjectFile(setup.config.storage.rules, RULES_TEMPLATE); - }); -}; + ]); + config.writeProjectFile(setup.config.storage.rules, RULES_TEMPLATE); +}