Skip to content

Commit 03dc410

Browse files
authored
Add --non-interactive flag to views generator script (#75)
* Add --no-interactive flag to views generator script * address review feedback * small fixes * address latest review feedback round * rename util -> schema-loader-utils, let -> const
1 parent 353e127 commit 03dc410

27 files changed

+304
-61
lines changed

firestore-bigquery-export/scripts/gen-schema-view/package.json

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@firebaseextensions/fs-bq-schema-views",
3-
"version": "0.1.1",
3+
"version": "0.1.2",
44
"description": "Generate strongly-typed BigQuery Views based on raw JSON",
55
"main": "./lib/index.js",
66
"repository": {
@@ -26,19 +26,23 @@
2626
"author": "Jan Wyszynski <wyszynski@google.com>",
2727
"license": "Apache-2.0",
2828
"dependencies": {
29+
"commander": "3.0.2",
30+
"glob": "7.1.5",
2931
"@google-cloud/bigquery": "^2.1.0",
32+
"fs-find": "^0.4.0",
3033
"firebase-admin": "^7.1.1",
3134
"firebase-functions": "^2.2.1",
3235
"sql-formatter": "^2.3.3",
3336
"generate-schema": "^2.6.0",
34-
"@firebaseextensions/firestore-bigquery-change-tracker": "^0.1.3",
3537
"inquirer": "^6.4.0"
3638
},
3739
"devDependencies": {
40+
"@types/mocha": "^5.2.5",
3841
"rimraf": "^2.6.3",
3942
"nyc": "^14.0.0",
40-
"mocha": "^5.0.5",
41-
"typescript": "^3.4.5",
43+
"mocha": "^5.2.0",
44+
"mocha-typescript": "*",
45+
"typescript": "^3.5.2",
4246
"ts-node": "^7.0.1"
4347
}
4448
}

firestore-bigquery-export/scripts/gen-schema-view/src/index.ts

Lines changed: 98 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,13 @@
1717
*/
1818

1919
import * as bigquery from "@google-cloud/bigquery";
20+
import * as program from "commander";
2021
import * as firebase from "firebase-admin";
2122
import * as inquirer from "inquirer";
22-
import * as path from "path";
2323

24-
import { existsSync, readdirSync } from "fs";
24+
import { FirestoreBigQuerySchemaViewFactory, FirestoreSchema } from "./schema";
2525

26-
import {
27-
FirestoreBigQuerySchemaViewFactory,
28-
FirestoreSchema,
29-
} from "./schema";
26+
import { readSchemas } from "./schema-loader-utils";
3027

3128
const BIGQUERY_VALID_CHARACTERS = /^[a-zA-Z0-9_]+$/;
3229
const FIRESTORE_VALID_CHARACTERS = /^[^\/]+$/;
@@ -41,86 +38,139 @@ const validateInput = (value: any, name: string, regex: RegExp) => {
4138
return true;
4239
};
4340

41+
function collect(value, previous) {
42+
return previous.concat([value]);
43+
}
44+
45+
program
46+
.name("gen-schema-views")
47+
.option(
48+
"--non-interactive",
49+
"Parse all input from command line flags instead of prompting the caller.",
50+
false
51+
)
52+
.option(
53+
"-P, --project <project>",
54+
"Firebase Project ID for project containing Cloud Firestore database."
55+
)
56+
.option(
57+
"-d, --dataset <dataset>",
58+
"The ID of the BigQuery dataset containing a raw Cloud Firestore document changelog."
59+
)
60+
.option(
61+
"-t, --table-name-prefix <table-name-prefix>",
62+
"A common prefix for the names of all views generated by this script."
63+
)
64+
.option(
65+
"-f, --schema-files <schema-files>",
66+
"A collection of files from which to read schemas.",
67+
collect,
68+
[]
69+
);
70+
4471
const questions = [
4572
{
4673
message: "What is your Firebase project ID?",
47-
name: "projectId",
74+
name: "project",
4875
type: "input",
4976
validate: (value) =>
5077
validateInput(value, "project ID", FIRESTORE_VALID_CHARACTERS),
5178
},
5279
{
5380
message:
5481
"What is the ID of the BigQuery dataset the raw changelog lives in? (The dataset and the raw changelog must already exist!)",
55-
name: "datasetId",
82+
name: "dataset",
5683
type: "input",
5784
validate: (value) =>
58-
validateInput(value, "dataset", BIGQUERY_VALID_CHARACTERS),
85+
validateInput(value, "dataset ID", BIGQUERY_VALID_CHARACTERS),
5986
},
6087
{
6188
message:
62-
"What is the name of the Cloud Firestore Collection that you would like to generate a schema view for?",
63-
name: "collectionName",
89+
"What is the name of the Cloud Firestore collection for which you want to generate a schema view?",
90+
name: "tableNamePrefix",
6491
type: "input",
6592
validate: (value) =>
66-
validateInput(value, "dataset", BIGQUERY_VALID_CHARACTERS),
93+
validateInput(value, "table name prefix", BIGQUERY_VALID_CHARACTERS),
6794
},
6895
{
6996
message:
70-
"Have you installed all your desired schemas in ./schemas/*.json?",
71-
name: "confirmed",
72-
type: "confirm",
73-
}
97+
"Where should this script look for schema definitions? (Enter a comma-separated list of, optionally globbed, paths to files or directories).",
98+
name: "schemaFiles",
99+
type: "input",
100+
},
74101
];
75102

76-
async function run(): Promise<number> {
77-
const schemaDirectory = [process.cwd(), "schemas"].join('/');
78-
const schemaDirExists = existsSync(schemaDirectory);
79-
if (!schemaDirExists) {
80-
console.log(`Expected directory "${schemaDirectory}" not found!`);
81-
process.exit(1);
82-
}
103+
interface CliConfig {
104+
projectId: string;
105+
datasetId: string;
106+
tableNamePrefix: string;
107+
schemas: { [schemaName: string]: FirestoreSchema };
108+
}
83109

84-
const {
85-
projectId,
86-
datasetId,
87-
collectionName,
88-
confirmed
89-
} = await inquirer.prompt(questions);
110+
async function run(): Promise<number> {
111+
// Get all configuration options via inquirer prompt or commander flags.
112+
const config: CliConfig = await parseConfig();
90113

91114
// Set project ID so it can be used in BigQuery intialization
92-
process.env.PROJECT_ID = projectId;
93-
// BigQuery aactually requires this variable to set the project correctly.
94-
process.env.GOOGLE_CLOUD_PROJECT = projectId;
115+
process.env.PROJECT_ID = config.projectId;
116+
// BigQuery actually requires this variable to set the project correctly.
117+
process.env.GOOGLE_CLOUD_PROJECT = config.projectId;
95118

96119
// Initialize Firebase
97120
firebase.initializeApp({
98121
credential: firebase.credential.applicationDefault(),
99-
databaseURL: `https://${projectId}.firebaseio.com`,
122+
databaseURL: `https://${config.projectId}.firebaseio.com`,
100123
});
101124

102125
// @ts-ignore string not assignable to enum
103-
const schemas: { [schemaName: string]: FirestoreSchema} = readSchemas(schemaDirectory);
104-
if (Object.keys(schemas).length === 0) {
105-
console.log(`Found no schemas in ${schemaDirectory}!`);
126+
if (Object.keys(config.schemas).length === 0) {
127+
console.log(`No schema files found!`);
106128
}
107129
const viewFactory = new FirestoreBigQuerySchemaViewFactory();
108-
109-
for (const schemaName in schemas) {
110-
await viewFactory.initializeSchemaViewResources(datasetId, collectionName, schemaName, schemas[schemaName]);
130+
for (const schemaName in config.schemas) {
131+
await viewFactory.initializeSchemaViewResources(
132+
config.datasetId,
133+
config.tableNamePrefix,
134+
schemaName,
135+
config.schemas[schemaName]
136+
);
111137
}
112138
return 0;
113-
};
139+
}
114140

115-
function readSchemas(directory: string): { [schemaName: string]: FirestoreSchema } {
116-
let results = {};
117-
let files = readdirSync(directory);
118-
let schemaNames = files.map(fileName => path.basename(fileName).split('.').slice(0, -1).join('.').replace(/-/g,'_'));
119-
for (var i = 0; i < files.length; i++) {
120-
const schema: FirestoreSchema = require([directory, files[i]].join('/'));
121-
results[schemaNames[i]] = schema;
141+
async function parseConfig(): Promise<CliConfig> {
142+
program.parse(process.argv);
143+
if (program.nonInteractive) {
144+
if (
145+
program.project === undefined ||
146+
program.dataset === undefined ||
147+
program.tableNamePrefix === undefined ||
148+
program.schemaFiles.length === 0
149+
) {
150+
program.outputHelp();
151+
process.exit(1);
152+
}
153+
return {
154+
projectId: program.project,
155+
datasetId: program.dataset,
156+
tableNamePrefix: program.tableNamePrefix,
157+
schemas: readSchemas(program.schemaFiles)
158+
};
122159
}
123-
return results;
160+
const {
161+
project,
162+
dataset,
163+
tableNamePrefix,
164+
schemaFiles,
165+
} = await inquirer.prompt(questions);
166+
return {
167+
projectId: project,
168+
datasetId: dataset,
169+
tableNamePrefix: tableNamePrefix,
170+
schemas: readSchemas(
171+
schemaFiles.split(",").map((schemaFileName) => schemaFileName.trim()),
172+
),
173+
};
124174
}
125175

126176
run()
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2019 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as glob from "glob";
18+
import * as path from "path";
19+
20+
import { FirestoreSchema } from "./schema";
21+
22+
import { existsSync, readdirSync, lstatSync } from "fs";
23+
24+
export function readSchemas(
25+
globs: string[]
26+
): { [schemaName: string]: FirestoreSchema } {
27+
let schemas = {};
28+
const expanded = expandGlobs(globs);
29+
for (var i = 0; i < expanded.length; i++) {
30+
const dirent = resolveFilePath(expanded[i]);
31+
const stats = lstatSync(dirent);
32+
if (stats.isDirectory()) {
33+
const directorySchemas = readSchemasFromDirectory(dirent);
34+
for (const schemaName in directorySchemas) {
35+
if (schemas.hasOwnProperty(schemaName)) {
36+
warnDuplicateSchemaName(schemaName);
37+
}
38+
schemas[schemaName] = directorySchemas[schemaName];
39+
}
40+
} else {
41+
const schemaName = filePathToSchemaName(dirent);
42+
if (schemas.hasOwnProperty(schemaName)) {
43+
warnDuplicateSchemaName(schemaName);
44+
}
45+
schemas[schemaName] = readSchemaFromFile(dirent);
46+
}
47+
}
48+
return schemas;
49+
}
50+
51+
function warnDuplicateSchemaName(schemaName: string) {
52+
console.log(
53+
`Found multiple schema files named ${schemaName}! Only the last one will be used to create a schema view!`
54+
);
55+
}
56+
57+
function resolveFilePath(filePath: string): string {
58+
if (filePath.startsWith(".") || !filePath.startsWith("/")) {
59+
return [process.cwd(), filePath].join("/");
60+
}
61+
return filePath;
62+
}
63+
64+
function expandGlobs(globs: string[]): string[] {
65+
let results = [];
66+
for (var i = 0; i < globs.length; i++) {
67+
const globResults = glob.sync(globs[i]);
68+
results = results.concat(globResults);
69+
}
70+
return results;
71+
}
72+
73+
function readSchemasFromDirectory(
74+
directory: string
75+
): { [schemaName: string]: FirestoreSchema } {
76+
let results = {};
77+
const files = readdirSync(directory);
78+
const schemaNames = files.map((fileName) => filePathToSchemaName(fileName));
79+
for (var i = 0; i < files.length; i++) {
80+
const schema: FirestoreSchema = readSchemaFromFile(
81+
[directory, files[i]].join("/")
82+
);
83+
results[schemaNames[i]] = schema;
84+
}
85+
return results;
86+
}
87+
88+
function readSchemaFromFile(file: string): FirestoreSchema {
89+
return require(file);
90+
}
91+
92+
export function filePathToSchemaName(filePath: string): string {
93+
return path
94+
.basename(filePath)
95+
.split(".")
96+
.slice(0, -1)
97+
.join(".")
98+
.replace(/-/g, "_");
99+
}

firestore-bigquery-export/scripts/gen-schema-view/src/schema.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,14 @@ export class FirestoreBigQuerySchemaViewFactory {
7272
*/
7373
async initializeSchemaViewResources(
7474
datasetId: string,
75-
collectionName: string,
75+
tableNamePrefix: string,
7676
schemaName: string,
7777
firestoreSchema: FirestoreSchema,
7878
): Promise<bigquery.Table> {
79-
const rawChangeLogTableName = changeLog(raw(collectionName));
80-
const latestRawViewName = latest(raw(collectionName));
81-
const changeLogSchemaViewName = changeLog(schema(collectionName, schemaName));
82-
const latestSchemaViewName = latest(schema(collectionName, schemaName));
79+
const rawChangeLogTableName = changeLog(raw(tableNamePrefix));
80+
const latestRawViewName = latest(raw(tableNamePrefix));
81+
const changeLogSchemaViewName = changeLog(schema(tableNamePrefix, schemaName));
82+
const latestSchemaViewName = latest(schema(tableNamePrefix, schemaName));
8383
const dataset = this.bq.dataset(datasetId);
8484

8585
const udfNames = Object.keys(udfs);
@@ -181,7 +181,7 @@ export const buildSchemaViewQuery = (
181181
}
182182

183183
/**
184-
* Given a firestore schema which may contain values for any type present
184+
* Given a Cloud Firestore schema which may contain values for any type present
185185
* in the Firestore document proto, return a list of clauses that may be
186186
* used to extract schema values from a JSON string and convert them into
187187
* the corresponding BigQuery type.
@@ -302,7 +302,7 @@ const processLeafField = (
302302
const longitude = jsonExtract(dataFieldName, extractPrefix, field, `._longitude`, transformer);
303303
/*
304304
* We return directly from this branch because it's the only one that
305-
* generate multiple selector clauses.
305+
* generates multiple selector clauses.
306306
*/
307307
fieldNameToSelector[qualifyFieldName(prefix, field.name)] =
308308
`${firestoreGeopoint(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

0 commit comments

Comments
 (0)