firebase-bolt
Version:
Firebase Bolt Security and Modeling Language Compiler
1,338 lines (1,330 loc) • 748 kB
JavaScript
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
"use strict";
exports.__esModule = true;
/*
* AST builders for Firebase Rules Language.
*
* Copyright 2015 Google Inc. All Rights Reserved.
*
* 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.
*/
var util = require("./util");
var logger = require("./logger");
var errors = {
typeMismatch: "Unexpected type: ",
duplicatePathPart: "A path component name is duplicated: "
};
;
;
var PathPart = /** @class */ (function () {
// "label", undefined - static path part
// "$label", X - variable path part
// X, !undefined - variable path part
function PathPart(label, variable) {
if (label[0] === '$' && variable === undefined) {
variable = label;
}
if (variable && label[0] !== '$') {
label = '$' + label;
}
this.label = label;
this.variable = variable;
}
return PathPart;
}());
exports.PathPart = PathPart;
var PathTemplate = /** @class */ (function () {
function PathTemplate(parts) {
if (parts === void 0) { parts = []; }
this.parts = parts.map(function (part) {
if (util.isType(part, 'string')) {
return new PathPart(part);
}
else {
return part;
}
});
}
PathTemplate.prototype.copy = function () {
var result = new PathTemplate();
result.push(this);
return result;
};
PathTemplate.prototype.getLabels = function () {
return this.parts.map(function (part) { return part.label; });
};
// Mapping from variables to JSON labels
PathTemplate.prototype.getScope = function () {
var result = {};
this.parts.forEach(function (part) {
if (part.variable) {
if (result[part.variable]) {
throw new Error(errors.duplicatePathPart + part.variable);
}
result[part.variable] = literal(part.label);
}
});
return result;
};
PathTemplate.prototype.push = function (temp) {
util.extendArray(this.parts, temp.parts);
};
PathTemplate.prototype.pop = function (temp) {
var _this = this;
temp.parts.forEach(function (part) {
_this.parts.pop();
});
};
PathTemplate.prototype.length = function () {
return this.parts.length;
};
PathTemplate.prototype.getPart = function (i) {
if (i > this.parts.length || i < -this.parts.length) {
var l = this.parts.length;
throw new Error("Path reference out of bounds: " + i +
" [" + -l + " .. " + l + "]");
}
if (i < 0) {
return this.parts[this.parts.length + i];
}
return this.parts[i];
};
return PathTemplate;
}());
exports.PathTemplate = PathTemplate;
;
var Schema = /** @class */ (function () {
function Schema() {
}
Schema.isGeneric = function (schema) {
return schema.params !== undefined && schema.params.length > 0;
};
return Schema;
}());
exports.Schema = Schema;
;
exports.string = valueGen('String');
exports.boolean = valueGen('Boolean');
exports.number = valueGen('Number');
exports.array = valueGen('Array');
exports.neg = opGen('neg', 1);
exports.not = opGen('!', 1);
exports.mult = opGen('*');
exports.div = opGen('/');
exports.mod = opGen('%');
exports.add = opGen('+');
exports.sub = opGen('-');
exports.eq = opGen('==');
exports.lt = opGen('<');
exports.lte = opGen('<=');
exports.gt = opGen('>');
exports.gte = opGen('>=');
exports.ne = opGen('!=');
exports.and = opGen('&&');
exports.or = opGen('||');
exports.ternary = opGen('?:', 3);
exports.value = opGen('value', 1);
function variable(name) {
return { type: 'var', valueType: 'Any', name: name };
}
exports.variable = variable;
function literal(name) {
return { type: 'literal', valueType: 'Any', name: name };
}
exports.literal = literal;
function nullType() {
return { type: 'Null', valueType: 'Null' };
}
exports.nullType = nullType;
function reference(base, prop) {
return {
type: 'ref',
valueType: 'Any',
base: base,
accessor: prop
};
}
exports.reference = reference;
var reIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_]*$/;
function isIdentifierStringExp(exp) {
return exp.type === 'String' && reIdentifier.test(exp.value);
}
exports.isIdentifierStringExp = isIdentifierStringExp;
// Shallow copy of an expression (so it can be modified and preserve
// immutability of the original expression).
function copyExp(exp) {
exp = util.extend({}, exp);
switch (exp.type) {
case 'op':
case 'call':
var opExp = exp;
opExp.args = util.copyArray(opExp.args);
return opExp;
case 'union':
var unionExp = exp;
unionExp.types = util.copyArray(unionExp.types);
return unionExp;
case 'generic':
var genericExp = exp;
genericExp.params = util.copyArray(genericExp.params);
return genericExp;
default:
return exp;
}
}
exports.copyExp = copyExp;
// Make a (shallow) copy of the base expression, setting (or removing) it's
// valueType.
//
// valueType is a string indicating the type of evaluating an expression (e.g.
// 'Snapshot') - used to know when type coercion is needed in the context
// of parent expressions.
function cast(base, valueType) {
var result = copyExp(base);
result.valueType = valueType;
return result;
}
exports.cast = cast;
function call(ref, args) {
if (args === void 0) { args = []; }
return { type: 'call', valueType: 'Any', ref: ref, args: args };
}
exports.call = call;
// Return empty string if not a function.
function getFunctionName(exp) {
if (exp.ref.type === 'ref') {
return '';
}
return exp.ref.name;
}
exports.getFunctionName = getFunctionName;
// Return empty string if not a (simple) method call -- ref.fn()
function getMethodName(exp) {
if (exp.ref.type === 'var') {
return exp.ref.name;
}
if (exp.ref.type !== 'ref') {
return '';
}
return getPropName(exp.ref);
}
exports.getMethodName = getMethodName;
function getPropName(ref) {
if (ref.accessor.type !== 'String') {
return '';
}
return ref.accessor.value;
}
exports.getPropName = getPropName;
// TODO: Type of function signature does not fail this declaration?
function builtin(fn) {
return { type: 'builtin', valueType: 'Any', fn: fn };
}
exports.builtin = builtin;
function snapshotVariable(name) {
return cast(variable(name), 'Snapshot');
}
exports.snapshotVariable = snapshotVariable;
function snapshotParent(base) {
if (base.valueType !== 'Snapshot') {
throw new Error(errors.typeMismatch + "expected Snapshot");
}
return cast(call(reference(cast(base, 'Any'), exports.string('parent'))), 'Snapshot');
}
exports.snapshotParent = snapshotParent;
function ensureValue(exp) {
if (exp.valueType === 'Snapshot') {
return snapshotValue(exp);
}
return exp;
}
exports.ensureValue = ensureValue;
// ref.val()
function snapshotValue(exp) {
return call(reference(cast(exp, 'Any'), exports.string('val')));
}
exports.snapshotValue = snapshotValue;
// Ensure expression is a boolean (when used in a boolean context).
function ensureBoolean(exp) {
exp = ensureValue(exp);
if (isCall(exp, 'val')) {
exp = exports.eq(exp, exports.boolean(true));
}
return exp;
}
exports.ensureBoolean = ensureBoolean;
function isCall(exp, methodName) {
return exp.type === 'call' && exp.ref.type === 'ref' &&
exp.ref.accessor.type === 'String' &&
exp.ref.accessor.value === methodName;
}
exports.isCall = isCall;
// Return value generating function for a given Type.
function valueGen(typeName) {
return function (val) {
return {
type: typeName,
valueType: typeName,
value: val // The (constant) value itself.
};
};
}
function regexp(pattern, modifiers) {
if (modifiers === void 0) { modifiers = ""; }
switch (modifiers) {
case "":
case "i":
break;
default:
throw new Error("Unsupported RegExp modifier: " + modifiers);
}
return {
type: 'RegExp',
valueType: 'RegExp',
value: pattern,
modifiers: modifiers
};
}
exports.regexp = regexp;
function cmpValues(v1, v2) {
if (v1.type !== v2.type) {
return false;
}
return v1.value === v2.value;
}
function isOp(opType, exp) {
return exp.type === 'op' && exp.op === opType;
}
// Return a generating function to make an operator exp node.
function opGen(opType, arity) {
if (arity === void 0) { arity = 2; }
return function () {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
if (args.length !== arity) {
throw new Error("Operator has " + args.length +
" arguments (expecting " + arity + ").");
}
return op(opType, args);
};
}
exports.andArray = leftAssociateGen('&&', exports.boolean(true), exports.boolean(false));
exports.orArray = leftAssociateGen('||', exports.boolean(false), exports.boolean(true));
// Create an expression builder function which operates on arrays of values.
// Returns new expression like v1 op v2 op v3 ...
//
// - Any identityValue's in array input are ignored.
// - If zeroValue is found - just return zeroValue.
//
// Our function re-orders top-level op in array elements to the resulting
// expression is left-associating. E.g.:
//
// [a && b, c && d] => (((a && b) && c) && d)
// (NOT (a && b) && (c && d))
function leftAssociateGen(opType, identityValue, zeroValue) {
return function (a) {
var i;
function reducer(result, current) {
if (result === undefined) {
return current;
}
return op(opType, [result, current]);
}
// First flatten all top-level op values to one flat array.
var flat = [];
for (i = 0; i < a.length; i++) {
flatten(opType, a[i], flat);
}
var result = [];
for (i = 0; i < flat.length; i++) {
// Remove identifyValues from array.
if (cmpValues(flat[i], identityValue)) {
continue;
}
// Just return zeroValue if found
if (cmpValues(flat[i], zeroValue)) {
return zeroValue;
}
result.push(flat[i]);
}
if (result.length === 0) {
return identityValue;
}
// Return left-associative expression of opType.
return result.reduce(reducer);
};
}
// Flatten the top level tree of op into a single flat array of expressions.
function flatten(opType, exp, flat) {
var i;
if (flat === undefined) {
flat = [];
}
if (!isOp(opType, exp)) {
flat.push(exp);
return flat;
}
for (i = 0; i < exp.args.length; i++) {
flatten(opType, exp.args[i], flat);
}
return flat;
}
exports.flatten = flatten;
function op(opType, args) {
return {
type: 'op',
valueType: 'Any',
op: opType,
args: args // Arguments to the operator Array<exp>
};
}
exports.op = op;
// Warning: NOT an expression type!
function method(params, body) {
return {
params: params,
body: body
};
}
exports.method = method;
function typeType(typeName) {
return { type: "type", valueType: "type", name: typeName };
}
exports.typeType = typeType;
function unionType(types) {
return { type: "union", valueType: "type", types: types };
}
exports.unionType = unionType;
function genericType(typeName, params) {
return { type: "generic", valueType: "type", name: typeName, params: params };
}
exports.genericType = genericType;
var Symbols = /** @class */ (function () {
function Symbols() {
this.functions = {};
this.paths = [];
this.schema = {};
}
Symbols.prototype.register = function (map, typeName, name, object) {
if (map[name]) {
logger.error("Duplicated " + typeName + " definition: " + name + ".");
}
else {
map[name] = object;
}
return map[name];
};
Symbols.prototype.registerFunction = function (name, params, body) {
return this.register(this.functions, 'functions', name, method(params, body));
};
Symbols.prototype.registerPath = function (template, isType, methods) {
if (methods === void 0) { methods = {}; }
isType = isType || typeType('Any');
var p = {
template: template.copy(),
isType: isType,
methods: methods
};
this.paths.push(p);
return p;
};
Symbols.prototype.registerSchema = function (name, derivedFrom, properties, methods, params) {
if (properties === void 0) { properties = {}; }
if (methods === void 0) { methods = {}; }
if (params === void 0) { params = []; }
derivedFrom = derivedFrom || typeType(Object.keys(properties).length > 0 ? 'Object' : 'Any');
var s = {
derivedFrom: derivedFrom,
properties: properties,
methods: methods,
params: params
};
return this.register(this.schema, 'schema', name, s);
};
Symbols.prototype.isDerivedFrom = function (type, ancestor) {
var _this = this;
if (ancestor === 'Any') {
return true;
}
switch (type.type) {
case 'type':
case 'generic':
var simpleType = type;
if (simpleType.name === ancestor) {
return true;
}
if (simpleType.name === 'Any') {
return false;
}
var schema = this.schema[simpleType.name];
if (!schema) {
return false;
}
return this.isDerivedFrom(schema.derivedFrom, ancestor);
case 'union':
return type.types
.map(function (subType) { return _this.isDerivedFrom(subType, ancestor); })
.reduce(util.or);
default:
throw new Error("Unknown type: " + type.type);
}
};
return Symbols;
}());
exports.Symbols = Symbols;
var JS_OPS = {
'value': { rep: "", p: 18 },
'neg': { rep: "-", p: 15 },
'!': { p: 15 },
'*': { p: 14 },
'/': { p: 14 },
'%': { p: 14 },
'+': { p: 13 },
'-': { p: 13 },
'<': { p: 11 },
'<=': { p: 11 },
'>': { p: 11 },
'>=': { p: 11 },
'in': { p: 11 },
'==': { p: 10 },
"!=": { p: 10 },
'&&': { p: 6 },
'||': { p: 5 },
'?:': { p: 4 },
',': { p: 0 }
};
// From an AST, decode as an expression (string).
function decodeExpression(exp, outerPrecedence) {
if (outerPrecedence === undefined) {
outerPrecedence = 0;
}
var innerPrecedence = precedenceOf(exp);
var result = '';
switch (exp.type) {
case 'Boolean':
case 'Number':
result = JSON.stringify(exp.value);
break;
case 'String':
result = util.quoteString(exp.value);
break;
// RegExp assumed to be in pre-quoted format.
case 'RegExp':
var regexp_1 = exp;
result = '/' + regexp_1.value + '/';
if (regexp_1.modifiers !== '') {
result += regexp_1.modifiers;
}
break;
case 'Array':
result = '[' + decodeArray(exp.value) + ']';
break;
case 'Null':
result = 'null';
break;
case 'var':
case 'literal':
result = exp.name;
break;
case 'ref':
var expRef = exp;
if (isIdentifierStringExp(expRef.accessor)) {
result = decodeExpression(expRef.base, innerPrecedence) + '.' + expRef.accessor.value;
}
else {
result = decodeExpression(expRef.base, innerPrecedence) +
'[' + decodeExpression(expRef.accessor) + ']';
}
break;
case 'call':
var expCall = exp;
result = decodeExpression(expCall.ref) + '(' + decodeArray(expCall.args) + ')';
break;
case 'builtin':
result = decodeExpression(exp);
break;
case 'op':
var expOp = exp;
var rep = JS_OPS[expOp.op].rep === undefined ? expOp.op : JS_OPS[expOp.op].rep;
if (expOp.args.length === 1) {
result = rep + decodeExpression(expOp.args[0], innerPrecedence);
}
else if (expOp.args.length === 2) {
result =
decodeExpression(expOp.args[0], innerPrecedence) +
' ' + rep + ' ' +
// All ops are left associative - so nudge the innerPrecendence
// down on the right hand side to force () for right-associating
// operations.
decodeExpression(expOp.args[1], innerPrecedence + 1);
if ((innerPrecedence >= outerPrecedence) && ((expOp.op === '&&') || (expOp.op === '||'))) {
result = '(' + result + ')';
}
}
else if (expOp.args.length === 3) {
result =
decodeExpression(expOp.args[0], innerPrecedence) + ' ? ' +
decodeExpression(expOp.args[1], innerPrecedence) + ' : ' +
decodeExpression(expOp.args[2], innerPrecedence);
}
break;
case 'type':
result = exp.name;
break;
case 'union':
result = exp.types.map(decodeExpression).join(' | ');
break;
case 'generic':
var genericType_1 = exp;
return genericType_1.name + '<' + decodeArray(genericType_1.params) + '>';
default:
result = "***UNKNOWN TYPE*** (" + exp.type + ")";
break;
}
if (innerPrecedence < outerPrecedence) {
result = '(' + result + ')';
}
return result;
}
exports.decodeExpression = decodeExpression;
function decodeArray(args) {
return args.map(decodeExpression).join(', ');
}
function precedenceOf(exp) {
var result;
switch (exp.type) {
case 'op':
result = JS_OPS[exp.op].p;
break;
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence
// lists call as 17 and ref as 18 - but how could they be anything other than left to right?
// http://www.scriptingmaster.com/javascript/operator-precedence.asp - agrees.
case 'call':
result = 18;
break;
case 'ref':
result = 18;
break;
default:
result = 19;
break;
}
return result;
}
},{"./logger":3,"./util":7}],2:[function(require,module,exports){
"use strict";
/*
* Copyright 2015 Google Inc. All Rights Reserved.
*
* 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.
*/
exports.__esModule = true;
// TODO(koss): After node 0.10 leaves LTS - remove polyfilled Promise library.
if (typeof Promise === 'undefined') {
require('es6-promise').polyfill();
}
var parser = require('./rules-parser');
var generator = require("./rules-generator");
var astImport = require("./ast");
exports.FILE_EXTENSION = 'bolt';
exports.ast = astImport;
exports.parse = parser.parse;
exports.Generator = generator.Generator;
exports.decodeExpression = exports.ast.decodeExpression;
exports.generate = generator.generate;
},{"./ast":1,"./rules-generator":5,"./rules-parser":6,"es6-promise":8}],3:[function(require,module,exports){
"use strict";
exports.__esModule = true;
/*
* Copyright 2015 Google Inc. All Rights Reserved.
*
* 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.
*/
var lastError;
var lastMessage;
var errorCount;
var silenceOutput;
var DEBUG = false;
var getContext = function () { return ({}); };
reset();
function reset() {
lastError = undefined;
lastMessage = undefined;
errorCount = 0;
silenceOutput = false;
}
exports.reset = reset;
function setDebug(debug) {
if (debug === void 0) { debug = true; }
DEBUG = debug;
}
exports.setDebug = setDebug;
function silent(f) {
if (f === void 0) { f = true; }
silenceOutput = f;
}
exports.silent = silent;
function setContext(fn) {
getContext = fn;
}
exports.setContext = setContext;
function error(s) {
var err = errorString(s);
// De-dup identical messages
if (err === lastMessage) {
return;
}
lastMessage = err;
lastError = lastMessage;
if (!silenceOutput) {
console.error(lastError);
if (DEBUG) {
var e = new Error("Stack trace");
console.error(e.stack);
}
}
errorCount += 1;
}
exports.error = error;
function warn(s) {
var err = errorString(s);
// De-dup identical messages
if (err === lastMessage) {
return;
}
lastMessage = err;
if (!silenceOutput) {
console.warn(lastMessage);
}
}
exports.warn = warn;
function getLastMessage() {
return lastMessage;
}
exports.getLastMessage = getLastMessage;
function errorString(s) {
var ctx = getContext();
if (ctx.line !== undefined && ctx.column !== undefined) {
return 'bolt:' + ctx.line + ':' + ctx.column + ': ' + s;
}
else {
return 'bolt: ' + s;
}
}
function hasErrors() {
return errorCount > 0;
}
exports.hasErrors = hasErrors;
function errorSummary() {
if (errorCount === 1) {
return lastError;
}
if (errorCount !== 0) {
return "Fatal errors: " + errorCount;
}
return "";
}
exports.errorSummary = errorSummary;
},{}],4:[function(require,module,exports){
"use strict";
exports.__esModule = true;
/*
* Copyright 2015 Google Inc. All Rights Reserved.
*
* 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.
*/
var parser = require('./rules-parser');
function parseExpression(expression) {
var result = parser.parse('function f() {return ' + expression + ';}');
return result.functions.f.body;
}
exports.parseExpression = parseExpression;
},{"./rules-parser":6}],5:[function(require,module,exports){
"use strict";
exports.__esModule = true;
/*
* Copyright 2015 Google Inc. All Rights Reserved.
*
* 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.
*/
var util = require("./util");
var ast = require("./ast");
var logger_1 = require("./logger");
var parser = require('./rules-parser');
var parse_util_1 = require("./parse-util");
var errors = {
badIndex: "The index function must return a String or an array of Strings.",
noPaths: "Must have at least one path expression.",
nonObject: "Type contains properties and must extend 'Object'.",
missingSchema: "Missing definition for type.",
recursive: "Recursive function call.",
mismatchParams: "Incorrect number of function arguments.",
generateFailed: "Could not generate JSON: ",
noSuchType: "No type definition for: ",
badSchemaMethod: "Unsupported method name in type statement: ",
badPathMethod: "Unsupported method name in path statement: ",
badWriteAlias: "Cannot have both a write() method and a write-aliasing method: ",
coercion: "Cannot convert value: ",
undefinedFunction: "Undefined function: ",
application: "Bolt application error: ",
invalidGeneric: "Invalid generic schema usage: ",
invalidMapKey: "Map<Key, T> - Key must derive from String type.",
invalidWildChildren: "Types can have at most one $wild property and cannot mix with other properties.",
invalidPropertyName: "Property names cannot contain any of: . $ # [ ] / or control characters: "
};
var INVALID_KEY_REGEX = /[\[\].#$\/\u0000-\u001F\u007F]/;
;
var builtinSchemaNames = ['Any', 'Null', 'String', 'Number', 'Boolean', 'Object'];
// Method names allowed in Bolt files.
var valueMethods = ['length', 'includes', 'startsWith', 'beginsWith', 'endsWith',
'replace', 'toLowerCase', 'toUpperCase', 'test', 'contains',
'matches'];
// TODO: Make sure users don't call internal methods...make private to impl.
var snapshotMethods = ['parent', 'child', 'hasChildren', 'val', 'isString', 'isNumber',
'isBoolean'].concat(valueMethods);
var writeAliases = {
'create': parse_util_1.parseExpression('prior(this) == null'),
'update': parse_util_1.parseExpression('prior(this) != null && this != null'),
'delete': parse_util_1.parseExpression('prior(this) != null && this == null')
};
// Usage:
// json = bolt.generate(bolt-text)
function generate(symbols) {
if (typeof symbols === 'string') {
symbols = parser.parse(symbols);
}
var gen = new Generator(symbols);
return gen.generateRules();
}
exports.generate = generate;
// Symbols contains:
// functions: {}
// schema: {}
// paths: {}
var Generator = /** @class */ (function () {
function Generator(symbols) {
this.symbols = symbols;
this.validators = {};
this.rules = {};
this.errorCount = 0;
this.runSilently = false;
this.allowUndefinedFunctions = false;
this.keyIndex = 0;
// TODO: globals should be part of this.symbols (nested scopes)
this.globals = {
"root": ast.call(ast.variable('@root'))
};
this.registerBuiltinSchema();
}
// Return Firebase compatible Rules JSON for a the given symbols definitions.
Generator.prototype.generateRules = function () {
var _this = this;
this.errorCount = 0;
var paths = this.symbols.paths;
var schema = this.symbols.schema;
var name;
paths.forEach(function (path) {
_this.validateMethods(errors.badPathMethod, path.methods, ['validate', 'read', 'write', 'index']);
});
for (name in schema) {
if (!util.arrayIncludes(builtinSchemaNames, name)) {
this.validateMethods(errors.badSchemaMethod, schema[name].methods, ['validate', 'read', 'write']);
}
}
if (paths.length === 0) {
this.fatal(errors.noPaths);
}
paths.forEach(function (path) { return _this.updateRules(path); });
this.convertExpressions(this.rules);
if (this.errorCount !== 0) {
throw new Error(errors.generateFailed + this.errorCount + " errors.");
}
util.deletePropName(this.rules, '.scope');
util.pruneEmptyChildren(this.rules);
return {
rules: this.rules
};
};
Generator.prototype.validateMethods = function (m, methods, allowed) {
var _this = this;
if (util.arrayIncludes(allowed, 'write')) {
allowed = allowed.concat(Object.keys(writeAliases));
}
for (var method in methods) {
if (!util.arrayIncludes(allowed, method)) {
logger_1.warn(m + util.quoteString(method) +
" (allowed: " + allowed.map(util.quoteString).join(', ') + ")");
}
}
if ('write' in methods) {
Object.keys(writeAliases).forEach(function (alias) {
if (alias in methods) {
_this.fatal(errors.badWriteAlias + alias);
}
});
}
};
Generator.prototype.registerBuiltinSchema = function () {
var self = this;
var thisVar = ast.variable('this');
function registerAsCall(name, methodName) {
self.symbols.registerSchema(name, ast.typeType('Any'), undefined, {
validate: ast.method(['this'], ast.call(ast.reference(ast.cast(thisVar, 'Any'), ast.string(methodName))))
});
}
this.symbols.registerSchema('Any', ast.typeType('Any'), undefined, {
validate: ast.method(['this'], ast.boolean(true))
});
registerAsCall('Object', 'hasChildren');
// Because of the way firebase treats Null values, there is no way to
// write a validation rule, that will EVER be called with this == null
// (firebase allows values to be deleted no matter their validation rules).
// So, comparing this == null will always return false -> that is what
// we do here, which will be optimized away if ORed with other validations.
this.symbols.registerSchema('Null', ast.typeType('Any'), undefined, {
validate: ast.method(['this'], ast.boolean(false))
});
self.symbols.registerSchema('String', ast.typeType('Any'), undefined, {
validate: ast.method(['this'], ast.call(ast.reference(ast.cast(thisVar, 'Any'), ast.string('isString')))),
includes: ast.method(['this', 's'], ast.call(ast.reference(ast.value(thisVar), ast.string('contains')), [ast.value(ast.variable('s'))])),
startsWith: ast.method(['this', 's'], ast.call(ast.reference(ast.value(thisVar), ast.string('beginsWith')), [ast.value(ast.variable('s'))])),
endsWith: ast.method(['this', 's'], ast.call(ast.reference(ast.value(thisVar), ast.string('endsWith')), [ast.value(ast.variable('s'))])),
replace: ast.method(['this', 's', 'r'], ast.call(ast.reference(ast.value(thisVar), ast.string('replace')), [ast.value(ast.variable('s')), ast.value(ast.variable('r'))])),
test: ast.method(['this', 'r'], ast.call(ast.reference(ast.value(thisVar), ast.string('matches')), [ast.call(ast.variable('@RegExp'), [ast.variable('r')])]))
});
registerAsCall('Number', 'isNumber');
registerAsCall('Boolean', 'isBoolean');
this.symbols.registerFunction('@RegExp', ['r'], ast.builtin(this.ensureType.bind(this, 'RegExp')));
var map = this.symbols.registerSchema('Map', ast.typeType('Any'), undefined, undefined, ['Key', 'Value']);
map.getValidator = this.getMapValidator.bind(this);
};
// type Map<Key, Value> => {
// $key: {
// '.validate': $key instanceof Key and this instanceof Value;
// '.validate': 'newData.hasChildren()'
// }
// Key must derive from String
Generator.prototype.getMapValidator = function (params) {
var keyType = params[0];
var valueType = params[1];
if (keyType.type !== 'type' || !this.symbols.isDerivedFrom(keyType, 'String')) {
throw new Error(errors.invalidMapKey + " (" + ast.decodeExpression(keyType) + " does not)");
}
var validator = {};
var index = this.uniqueKey();
validator[index] = {};
extendValidator(validator, this.ensureValidator(ast.typeType('Object')));
// First validate the key (omit terminal String type validation).
while (keyType.name !== 'String') {
var schema = this.symbols.schema[keyType.name];
if (schema.methods['validate']) {
var exp = this.partialEval(schema.methods['validate'].body, { 'this': ast.literal(index) });
extendValidator(validator[index], { '.validate': [exp] });
}
keyType = schema.derivedFrom;
}
extendValidator(validator[index], this.ensureValidator(valueType));
return validator;
};
Generator.prototype.uniqueKey = function () {
this.keyIndex += 1;
return '$key' + this.keyIndex;
};
// Collection schema has exactly one $wildchild property
Generator.prototype.isCollectionSchema = function (schema) {
var props = Object.keys(schema.properties);
var result = props.length === 1 && props[0][0] === '$';
return result;
};
// Ensure we have a definition for a validator for the given schema.
Generator.prototype.ensureValidator = function (type) {
var key = ast.decodeExpression(type);
if (!this.validators[key]) {
this.validators[key] = { '.validate': ast.literal('***TYPE RECURSION***') };
var allowSave = this.allowUndefinedFunctions;
this.allowUndefinedFunctions = true;
this.validators[key] = this.createValidator(type);
this.allowUndefinedFunctions = allowSave;
}
return this.validators[key];
};
Generator.prototype.createValidator = function (type) {
var _this = this;
switch (type.type) {
case 'type':
return this.createValidatorFromSchemaName(type.name);
case 'union':
var union_1 = {};
type.types.forEach(function (typePart) {
// Make a copy
var singleType = extendValidator({}, _this.ensureValidator(typePart));
mapValidator(singleType, ast.andArray);
extendValidator(union_1, singleType);
});
mapValidator(union_1, ast.orArray);
return union_1;
case 'generic':
var genericType = type;
return this.createValidatorFromGeneric(genericType.name, genericType.params);
default:
throw new Error(errors.application + "invalid internal type: " + type.type);
}
};
Generator.prototype.createValidatorFromGeneric = function (schemaName, params) {
var schema = this.symbols.schema[schemaName];
if (schema === undefined || !ast.Schema.isGeneric(schema)) {
throw new Error(errors.noSuchType + schemaName + " (generic)");
}
var schemaParams = schema.params;
if (params.length !== schemaParams.length) {
throw new Error(errors.invalidGeneric + " expected <" + schemaParams.join(', ') + ">");
}
// Call custom validator, if given.
if (schema.getValidator) {
return schema.getValidator(params);
}
var bindings = {};
for (var i = 0; i < params.length; i++) {
bindings[schemaParams[i]] = params[i];
}
// Expand generics and generate validator from schema.
schema = this.replaceGenericsInSchema(schema, bindings);
return this.createValidatorFromSchema(schema);
};
Generator.prototype.replaceGenericsInSchema = function (schema, bindings) {
var _this = this;
var expandedSchema = {
derivedFrom: this.replaceGenericsInExp(schema.derivedFrom, bindings),
properties: {},
methods: {}
};
var props = Object.keys(schema.properties);
props.forEach(function (prop) {
expandedSchema.properties[prop] =
_this.replaceGenericsInExp(schema.properties[prop], bindings);
});
var methods = Object.keys(schema.methods);
methods.forEach(function (methodName) {
expandedSchema.methods[methodName] = _this.replaceGenericsInMethod(schema.methods[methodName], bindings);
});
return expandedSchema;
};
Generator.prototype.replaceGenericsInExp = function (exp, bindings) {
var self = this;
function replaceGenericsInArray(exps) {
return exps.map(function (expPart) {
return self.replaceGenericsInExp(expPart, bindings);
});
}
switch (exp.type) {
case 'op':
case 'call':
var opType = ast.copyExp(exp);
opType.args = replaceGenericsInArray(opType.args);
return opType;
case 'type':
var simpleType = exp;
return bindings[simpleType.name] || simpleType;
case 'union':
var unionType = exp;
return ast.unionType(replaceGenericsInArray(unionType.types));
case 'generic':
var genericType = exp;
return ast.genericType(genericType.name, replaceGenericsInArray(genericType.params));
default:
return exp;
}
};
Generator.prototype.replaceGenericsInMethod = function (method, bindings) {
var expandedMethod = {
params: method.params,
body: method.body
};
expandedMethod.body = this.replaceGenericsInExp(method.body, bindings);
return expandedMethod;
};
Generator.prototype.createValidatorFromSchemaName = function (schemaName) {
var schema = this.symbols.schema[schemaName];
if (!schema) {
throw new Error(errors.noSuchType + schemaName);
}
if (ast.Schema.isGeneric(schema)) {
throw new Error(errors.noSuchType + schemaName + " used as non-generic type.");
}
return this.createValidatorFromSchema(schema);
};
Generator.prototype.createValidatorFromSchema = function (schema) {
var _this = this;
var hasProps = Object.keys(schema.properties).length > 0 &&
!this.isCollectionSchema(schema);
if (hasProps && !this.symbols.isDerivedFrom(schema.derivedFrom, 'Object')) {
this.fatal(errors.nonObject + " (is " + ast.decodeExpression(schema.derivedFrom) + ")");
return {};
}
var validator = {};
if (!(schema.derivedFrom.type === 'type' &&
schema.derivedFrom.name === 'Any')) {
extendValidator(validator, this.ensureValidator(schema.derivedFrom));
}
var requiredProperties = [];
var wildProperties = 0;
Object.keys(schema.properties).forEach(function (propName) {
if (propName[0] === '$') {
wildProperties += 1;
if (INVALID_KEY_REGEX.test(propName.slice(1))) {
_this.fatal(errors.invalidPropertyName + propName);
}
}
else {
if (INVALID_KEY_REGEX.test(propName)) {
_this.fatal(errors.invalidPropertyName + propName);
}
}
if (!validator[propName]) {
validator[propName] = {};
}
var propType = schema.properties[propName];
if (propName[0] !== '$' && !_this.isNullableType(propType)) {
requiredProperties.push(propName);
}
extendValidator(validator[propName], _this.ensureValidator(propType));
});
if (wildProperties > 1 || wildProperties === 1 && requiredProperties.length > 0) {
this.fatal(errors.invalidWildChildren);
}
if (requiredProperties.length > 0) {
// this.hasChildren(requiredProperties)
extendValidator(validator, { '.validate': [hasChildrenExp(requiredProperties)] });
}
// Disallow $other properties by default
if (hasProps) {
validator['$other'] = {};
extendValidator(validator['$other'], { '.validate': ast.boolean(false) });
}
this.extendValidationMethods(validator, schema.methods);
return validator;
};
Generator.prototype.isNullableType = function (type) {
var result = this.symbols.isDerivedFrom(type, 'Null') ||
this.symbols.isDerivedFrom(type, 'Map');
return result;
};
// Update rules based on the given path expression.
Generator.prototype.updateRules = function (path) {
var i;
var location = util.ensureObjectPath(this.rules, path.template.getLabels());
var exp;
extendValidator(location, this.ensureValidator(path.isType));
location['.scope'] = path.template.getScope();
this.extendValidationMethods(location, path.methods);
// Write indices
if (path.methods['index']) {
switch (path.methods['index'].body.type) {
case 'String':
exp = ast.array([path.methods['index'].body]);
break;
case 'Array':
exp = path.methods['index'].body;
break;
default:
this.fatal(errors.badIndex);
return;
}
var indices = [];
for (i = 0; i < exp.value.length; i++) {
if (exp.value[i].type !== 'String') {
this.fatal(errors.badIndex + " (not " + exp.value[i].type + ")");
}
else {
indices.push(exp.value[i].value);
}
}
// TODO: Error check not over-writing index rules.
location['.indexOn'] = indices;
}
};
Generator.prototype.extendValidationMethods = function (validator, methods) {
var writeMethods = [];
['create', 'update', 'delete'].forEach(function (method) {
if (method in methods) {
writeMethods.push(ast.andArray([writeAliases[method], methods[method].body]));
}
});
if (writeMethods.length !== 0) {
extendValidator(validator, { '.write': ast.orArray(writeMethods) });
}
['validate', 'read', 'write'].forEach(function (method) {
if (method in methods) {
var methodValidator = {};
methodValidator['.' + method] = methods[method].body;
extendValidator(validator, methodValidator);
}
});
};
// Return union validator (||) over each schema
Generator.prototype.unionValidators = function (schema) {
var union = {};
schema.forEach(function (typeName) {
// First and the validator terms for a single type
// Todo extend to unions and generics
var singleType = extendValidator({}, this.ensureValidator(typeName));
mapValidator(singleType, ast.andArray);
extendValidator(union, singleType);
}.bind(this));
mapValidator(union, ast.orArray);
return union;
};
// Convert expressions to text, and at the same time, apply pruning operations
// to remove no-op rules.
Generator.prototype.convertExpressions = function (validator) {
var _this = this;
var methodThisIs = { '.validate': 'newData',
'.read': 'data',
'.write': 'newData' };
function hasWildcardSibling(path) {
var parts = path.getLabels();
var childPart = parts.pop();
var parent = util.deepLookup(validator, parts);
if (parent === undefined) {
return false;
}
for (var _i = 0, _a = Object.keys(parent); _i < _a.length; _i++) {
var prop = _a[_i];
if (prop === childPart) {
continue;
}
if (prop[0] === '$') {
return true;
}
}
return false;
}
mapValidator(validator, function (value, prop, scope, path) {
if (prop in methodThisIs) {
var result = _this.getExpressionText(ast.andArray(collapseHasChildren(value)), methodThisIs[prop], scope, path);
// Remove no-op .read or .write rule if no sibling wildcard props.
if ((prop === '.read' || prop === '.write') && result === 'false') {
if (!hasWildcardSibling(path)) {
return undefined;
}
}
// Remove no-op .validate rule if no sibling wildcard props.
if (prop === '.validate' && result === 'true') {
if (!hasWildcardSibling(path)) {
return undefined;
}
}
return result;
}
return value;
});
};
Generator.prototype.getExpressionText = function (exp, thisIs, scope, path) {
if (!('type' in exp)) {
throw new Error(errors.application + "Not an expression: " + util.prettyJSON(exp));
}
// First evaluate w/o binding of this to specific location.
this.allowUndefinedFunctions = true;
scope = util.extend({}, scope, { 'this': ast.cast(ast.call(ast.variable('@getThis')), 'Snapshot') });
exp = this.partialEval(exp, scope);
// Now re-evaluate the flattened expression.
this.allowUndefinedFunctions = false;
this.thisIs = thisIs;
this.symbols.registerFunction('@getThis', [], ast.builtin(this.getThis.bind(this)));
this.symbols.registerFunction('@root', [], ast.builtin(this.getRootReference.bind(this, path)));
this.symbols.registerFunction('prior', ['exp'], ast.builtin(this.prior.bind(this)));
this.symbols.registerFunction('key', [], ast.builtin(this.getKey.bind(this, path.length() === 0 ? '' : path.getPart(-1).label)));
exp = this.partialEval(exp);
delete this.symbols.functions['@getThis'];
delete this.symbols.functions['@root'];
delete this.symbols.functions['prior'];
delete this.symbols.functions['key'];
// Top level expressions should never be to a snapshot reference - should
// always evaluate to a boolean.
exp = ast.ensureBoolean(exp);
return ast.decodeExpression(exp);
};
/*
* Wrapper for partialEval debugging.
*/
Generator.prototype.partialEval = function (exp, params, functionCalls) {
if (params === void 0) { params = {}; }
if (functionCalls === void 0) { functionCalls = {}; }
// Wrap real call for debugging.
var result = this.partialEvalReal(exp, params, functionCalls);
// console.log(ast.decodeExpression(exp) + " => " + ast.decodeExpression(result));
return result;
};
// Partial evaluation of expressions - copy of expression tree (immutable).
//
// - Expand inline function calls.
// - Replace local and global variables with their values.
// - Expand snapshot references using child('ref').
// - Coerce snapshot references to values as needed.
Generator.prototype.partialEvalReal = function (exp, params, functionCalls) {
if (params === void 0) { params = {}; }
if (functionCalls === void 0) { functionCalls = {}; }
var self = this;
function subExpression(exp2) {
return self.partialEval(exp2, params, functionCalls);
}
function valueExpression(exp2) {
return ast.ensureValue(subExpression(exp2));
}
function booleanExpression(exp2) {
return ast.ensureBoolean(subExpression(exp2));
}
function lookupVar(exp2) {
// TODO: Unbound variable access should be a