tamedevil
Version:
Build and evaluate JavaScript strings safely via tagged template literals
978 lines • 34.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.reservedWords = exports.undefined = exports.te = exports.Idents = exports.isSafeObjectPropertyName = exports.toJSON = exports.stringifyJSON = void 0;
exports.stringifyString = stringifyString;
exports.EXPORTABLE = EXPORTABLE;
exports.isForbidden = isForbidden;
exports.batch = batch;
exports.cache = cache;
exports.compile = compile;
exports.dangerousKey = safeKeyOrThrow;
exports.safeKeyOrThrow = safeKeyOrThrow;
exports.dangerouslyIncludeRawCode = dangerouslyIncludeRawCode;
exports.debug = debug;
exports.eval = run;
exports.run = run;
exports.get = get;
exports.identifier = identifier;
exports.isTE = isTE;
exports.join = join;
exports.lit = lit;
exports.literal = lit;
exports.optionalGet = optionalGet;
exports.ref = ref;
exports.runExportable = runExportable;
exports.runInBatch = runInBatch;
exports.set = set;
exports.subcomment = subcomment;
exports.substring = substring;
exports.tempVar = tempVar;
exports.tmp = tmp;
const reservedWords_ts_1 = require("./reservedWords.js");
Object.defineProperty(exports, "reservedWords", { enumerable: true, get: function () { return reservedWords_ts_1.reservedWords; } });
const thereCanBeOnlyOne_ts_1 = require("./thereCanBeOnlyOne.js");
/**
* For compatibility with graphile-export
*/
function exportAs(thing, exportName) {
const existingExport = thing.$$export;
if (existingExport) {
if (existingExport.exportName !== exportName) {
throw new Error(`Attempted to export same thing under multiple names '${existingExport.exportName}' and '${exportName}'`);
}
}
else {
Object.defineProperty(thing, "$$export", {
value: { moduleName: "tamedevil", exportName },
});
}
return thing;
}
const isDev = typeof process !== "undefined" && process.env.GRAPHILE_ENV === "development";
function makeRawNode(text) {
return {
[thereCanBeOnlyOne_ts_1.$$type]: "RAW",
t: text,
};
}
function makeExportedRawNode(text, exportName) {
const newNode = {
[thereCanBeOnlyOne_ts_1.$$type]: "RAW",
t: text,
};
exportAs(newNode, exportName);
return newNode;
}
// Simple function to help V8 optimize it.
function makeRefNode(rawValue, name) {
return Object.freeze({
[thereCanBeOnlyOne_ts_1.$$type]: "REF",
v: rawValue,
n: name,
});
}
// Simple function to help V8 optimize it.
function makeTemporaryVariableNode(symbol) {
return Object.freeze({
[thereCanBeOnlyOne_ts_1.$$type]: "VARIABLE",
s: symbol,
});
}
// Simple function to help V8 optimize it.
function makeIndentNode(content) {
return Object.freeze({
[thereCanBeOnlyOne_ts_1.$$type]: "INDENT",
c: content[thereCanBeOnlyOne_ts_1.$$type] === "QUERY" ? content : makeQueryNode([content]),
});
}
// Simple function to help V8 optimize it.
function makeQueryNode(nodes) {
return Object.freeze({
[thereCanBeOnlyOne_ts_1.$$type]: "QUERY",
n: nodes,
});
}
/**
* Returns true if the given node is a TE node, false otherwise.
*/
function isTE(node) {
return (typeof node === "object" &&
node !== null &&
typeof node[thereCanBeOnlyOne_ts_1.$$type] === "string");
}
function debug(expression) {
const result = te.compile(expression);
const argNames = Object.keys(result.refs);
const fn = newFunction(...argNames, result.string);
const string = fn.toString();
const lines = string.split(/\r?\n/);
const maxLineLength = String(lines.length).length;
const output = lines.map((line, i) => `${String(i + 1).padStart(maxLineLength, " ")}: ${line}`);
console.log(`Outputting expression from tamedevil:`);
console.dir(result.refs);
console.log(output.join("\n"));
}
function enforceValidNode(node, where) {
if (isTE(node)) {
return node;
}
throw new Error(`[tamedevil] Invalid expression. Expected an TE item${where ? ` at ${where}` : ""} but received '${String(node)}'. This may mean that there is an issue in the TE expression where a dynamic value was not escaped via 'te.ref(...)', an embedded string wasn't wrapped with 'te.string(...)', or a TE expression was added without using the \`te\` tagged template literal.`);
}
function findAvailableName(refs, suggestedName) {
if (!(suggestedName in refs)) {
return suggestedName;
}
for (let i = 0; i < 1000000; i++) {
const name = suggestedName + i;
if (!(name in refs)) {
return name;
}
}
throw new Error("Failed to find an available variable name to use");
}
const makeRef = (refs, refMap, value, suggestedName) => {
const existingIdentifier = refMap.get(value);
if (existingIdentifier) {
return existingIdentifier;
}
const refCount = refMap.size + 1;
// Arbitrary
if (refCount > 65535) {
throw new Error("[tamedevil] This TE statement would contain too many placeholders; tamedevil supports at most 65535 placeholders. To solve this, consider passing multiple values in using a single array or object.");
}
const identifier = suggestedName
? findAvailableName(refs, suggestedName)
: `_$$_ref_${refCount}`;
refMap.set(value, identifier);
refs[identifier] = value;
return identifier;
};
const getVar = (varMap, sym) => {
const existing = varMap.get(sym);
if (existing) {
return existing;
}
const tmpCounter = varMap.size + 1;
const varName = `_$_tmp${tmpCounter}`;
varMap.set(sym, varName);
return varName;
};
function serialize(item, refs, refMap, varMap, indent = 0) {
let str = "";
if (item == null) {
enforceValidNode(item);
}
switch (item[thereCanBeOnlyOne_ts_1.$$type]) {
case "QUERY": {
for (const listItem of item.n) {
str += serialize(listItem, refs, refMap, varMap, indent);
}
break;
}
case "RAW": {
if (item.t === "") {
// No need to add blank raw text!
break;
}
// IMPORTANT: this **must not** mangle primitives. Fortunately they're all single line so it should be fine.
str += isDev ? item.t.replace(/\n/g, "\n" + " ".repeat(indent)) : item.t;
break;
}
case "REF": {
const identifier = makeRef(refs, refMap, item.v, item.n);
str += identifier;
break;
}
case "VARIABLE": {
const identifier = getVar(varMap, item.s);
str += identifier;
break;
}
case "INDENT": {
if (!isDev) {
throw new Error("INDENT nodes only allowed in development mode");
}
str += "\n" + " ".repeat(indent + 1);
str += serialize(item.c, refs, refMap, varMap, indent + 1);
str += "\n" + " ".repeat(indent);
break;
}
default: {
const never = item;
// This cannot happen
throw new Error(`Unsupported node found in TE: ${String(enforceValidNode(never))}`);
}
}
return str;
}
/**
* Accepts an te`...` expression and compiles it out to the function body
* `string` and the `refs` that need to be passed via the closure.
*/
function compile(fragment) {
/**
* Values hold the JavaScript values that are represented in the query string
* by placeholders. They are eager because they were provided before compile
* time.
*/
const refs = Object.create(null);
const refMap = new Map();
const varMap = new Map();
/**
* Join this to generate the TE string
*/
let str = serialize(fragment, refs, refMap, varMap);
let variables = "";
for (const varName of varMap.values()) {
variables = variables + `let ${varName};\n`;
}
if (variables.length > 0) {
str = variables + "\n" + str;
}
const string = isDev ? str.replace(/\n\s*\n/g, "\n") : str;
return {
string,
refs,
};
}
/**
* A template string tag function that creates a `TE` query out of some strings and
* some TE values. Use this to prepare all dynamically executed code code
* injection attacks.
*
* Note that this function enforces that every passed placeholder is a TE node,
* preventing potentially untrusted raw text from being injected into the code
* to be evaluated, thereby seriously reducing the risk of code injection
* attacks. Care must still be taken in the building and management of these
* code strings, however, since no parsing takes place - so it is up to the
* coder to use the right TE helper in the right positions in the constructed
* code.
*/
const teBase = function te(strings, ...values) {
if (!Array.isArray(strings) || !strings.raw) {
throw new Error("[tamedevil] te should be used as a template literal, not a function call.");
}
const stringsLength = strings.length;
// Reduce memory churn with a cache
if (stringsLength === 1) {
const first = strings[0];
if (first === "") {
return blankNode;
}
else if (first === "undefined") {
return undefinedNode;
}
return makeRawNode(first);
}
// Special case te`${...}` - just return the node directly
if (stringsLength === 2 && strings[0] === "" && strings[1] === "") {
const v = values[0];
return v[thereCanBeOnlyOne_ts_1.$$type] !== undefined ? v : enforceValidNode(v);
}
const items = [];
let lastRawNode = null;
let currentText = "";
const addText = () => {
if (lastRawNode === null || lastRawNode.t !== currentText) {
lastRawNode = makeRawNode(currentText);
}
items.push(lastRawNode);
currentText = "";
};
const finalStringIndex = stringsLength - 1;
for (let i = 0; i < stringsLength; i++) {
const text = strings[i];
currentText += text;
if (i !== finalStringIndex) {
const v = values[i];
const valid = v[thereCanBeOnlyOne_ts_1.$$type] !== undefined
? v
: enforceValidNode(v, `literal placeholder ${i}`);
if (valid[thereCanBeOnlyOne_ts_1.$$type] === "RAW") {
lastRawNode = valid;
currentText += valid.t;
}
else if (valid[thereCanBeOnlyOne_ts_1.$$type] === "QUERY") {
const nodes = valid.n;
const nodeCount = nodes.length;
for (let nodeIndex = 0; nodeIndex < nodeCount; nodeIndex++) {
const node = nodes[nodeIndex];
if (node[thereCanBeOnlyOne_ts_1.$$type] === "RAW") {
lastRawNode = node;
currentText += node.t;
}
else {
if (currentText !== "") {
/*#__INLINE__*/ addText();
}
items.push(node);
}
}
}
else {
if (currentText !== "") {
/*#__INLINE__*/ addText();
}
items.push(valid);
}
}
}
if (currentText !== "") {
/*#__INLINE__*/ addText();
}
return items.length === 1 ? items[0] : makeQueryNode(items);
};
const teCacheCache = Object.create(null);
function cache(strings) {
if (strings.length !== 1) {
throw new Error(`te.cache can currently only be used with a static string`);
}
const str = strings[0];
const existing = teCacheCache[str];
if (existing)
return existing;
const node = makeRawNode(str);
teCacheCache[str] = node;
return node;
}
let rawWarningOutput = false;
/**
* Creates a TE node for a raw code string. Just plain ol‘ raw code - EXTREMELY
* DANGEROUS.
*
* This method is dangerous because it involves no escaping, so proceed with
* caution!
*
* It's very very rarely warranted to use this - there is likely a safer way of
* achieving your goal. DO NOT USE THIS WITH UNTRUSTED INPUT!
*/
function dangerouslyIncludeRawCode(text) {
if (!rawWarningOutput) {
rawWarningOutput = true;
try {
throw new Error("te.dangerouslyIncludeRawCode first invoked here");
}
catch (e) {
console.warn(`[tamedevil] WARNING: you're using the te.dangerouslyIncludeRawCode escape hatch, usage of this API is rarely required and is highly discouraged due to the potential security ramifications. Please be sure this is what you intend. ${e.stack}`);
}
}
if (typeof text !== "string") {
throw new Error(`[tamedevil] te.dangerouslyIncludeRawCode must be passed a string, but it was passed '${String(text)}'.`);
}
return makeRawNode(text);
}
/**
* Creates a TE node for an arbitrary JS object that will be made available
* (via a closure) to the final code that will be executed. This value is not
* serialized or similar, it is passed by reference.
*/
function ref(val, name) {
if (name != null) {
if (!isValidVariableName(name)) {
throw new Error(`Invalid variable name '${name}'`);
}
return makeRefNode(val, name);
}
else {
return makeRefNode(val);
}
}
const blankNode = makeExportedRawNode(``, "blank");
const undefinedNode = makeExportedRawNode(`undefined`, "undefined");
exports.undefined = undefinedNode;
/**
* A regexp that matches the first character that might need escaping in a JSON
* string. ("Might" because we'd rather be safe.)
*
* Unsafe:
* - `\\`
* - `"`
* - control characters
* - surrogates
*/
// eslint-disable-next-line no-control-regex
// const forbiddenCharacters = /["\\\u0000-\u001f\ud800-\udfff]/;
/**
* A 'short string' has a length less than or equal to this, and can
* potentially have JSON.stringify skipped on it if it doesn't contain any of
* the forbiddenCharacters. To prevent the forbiddenCharacters regexp running
* for a long time, we cap the length of string we test.
*/
const MAX_SHORT_STRING_LENGTH = 200; // TODO: what should this be?
const BACKSLASH_CODE = "\\".charCodeAt(0);
const DOLLAR_CODE = "$".charCodeAt(0);
const QUOTE_CODE = '"'.charCodeAt(0);
/**
* Similar to `JSON.stringify(string)`, but faster. Bizarrely this seems to be
* faster than the regexp approach
*/
function stringifyString(value) {
const l = value.length;
if (l > MAX_SHORT_STRING_LENGTH) {
return JSON.stringify(value);
}
// Scan through for disallowed charcodes
for (let i = 0; i < l; i++) {
const code = value.charCodeAt(i);
if (code === BACKSLASH_CODE ||
code === QUOTE_CODE ||
(code & 0xffe0) === 0 || // equivalent to `code <= 0x001f`
(code & 0xc000) !== 0 // Not quite equivalent to `code >= 0xd800`, but good enough for our purposes
) {
// Backslash, quote, control character or surrogate
return JSON.stringify(value);
}
}
return `"${value}"`;
}
// PERF: more optimal stringifier
/**
* Equivalent to JSON.stringify, but typically faster.
*/
const stringifyJSON = (value) => {
if (value == null)
return "null";
if (value === true)
return "true";
if (value === false)
return "false";
const t = typeof value;
if (t === "number")
return "" + value;
if (t === "string") {
return stringifyString(value);
}
return JSON.stringify(value);
};
exports.stringifyJSON = stringifyJSON;
/** @deprecated Use stringifyJSON instead */
exports.toJSON = exports.stringifyJSON;
/**
* If the value is simple this will stringify it and inject it directly into
* the code to be evaluated, otherwise will defer to `te.ref`.
*/
function lit(val) {
if (val === undefined) {
return undefinedNode;
}
else if (val === null ||
typeof val === "string" ||
typeof val === "boolean" ||
(typeof val === "number" && Number.isFinite(val))) {
/*
* Prior to ECMAScript 2019, JSON wasn't truly a subset of JS - it was possible
* to encode characters in JSON that JS couldn't parse via `eval` (notably
* `\u2028` and friends), however as of ES2019 JSON is now a subset of JS, so
* JSON.stringify is safe.
*
* https://github.com/tc39/proposal-json-superset
*/
const primitive = val;
return makeRawNode((0, exports.stringifyJSON)(primitive));
}
else {
return ref(val);
}
}
/**
* If you're building a string and you want to inject untrusted content into it
* without opening yourself to code injection attacks nor doing awkward string
* concatenation, this is the method for you. It's essential that you pass the
* correct type of string that you're embedding into via the `stringType`
* parameter.
*
* Example:
*
* ```js
* const code = te`const str = "abc${te.substring(untrusted, '"')}123";`
* ```
*/
function substring(text, stringType) {
if (typeof text !== "string" || typeof stringType !== "string") {
throw new Error(`Invalid call to te.substring`);
}
// Quick scan to see if it's safe to use verbatim
const l = text.length;
if (l < MAX_SHORT_STRING_LENGTH) {
const stringTypeCode = stringType.charCodeAt(0);
let verbatim = true;
for (let i = 0; i < l; i++) {
const code = text.charCodeAt(i);
if (code === BACKSLASH_CODE ||
code === DOLLAR_CODE ||
code === stringTypeCode ||
(code & 0xffe0) === 0 || // equivalent to `code <= 0x001f`
(code & 0xc000) !== 0 // Not quite equivalent to `code >= 0xd800`, but good enough for our purposes
) {
// Backslash, dollar, quote, control character or surrogate
verbatim = false;
break;
}
}
if (verbatim) {
return makeRawNode(text);
}
}
// Not safe to use verbatim, so let's escape it
// This'll escape most things that need escaping - backslashes, fancy characters, double quotes, etc.
const jsonStringified = JSON.stringify(text);
// But we're already in a string so we don't want the quote marks
const inner = jsonStringified.substring(1, jsonStringified.length - 1);
// And if we're not inside a `"` we'll need to escape our string type.
const escaped = stringType === '"'
? inner // "" strings already escapes
: stringType === "'"
? inner.replace(/'/g, "\\'") // '' strings need `'` escaped too (`\` has already been escaped)
: inner.replace(/[`$]/g, "\\$&"); // `` strings need both '`' and `$` to be escaped (`\` has already been escaped)
// Finally return a raw node
return makeRawNode(escaped);
}
/**
* Escapes `content` so that it can be safely embedded in a multiline comment.
*/
function subcomment(content) {
return makeRawNode(String(content).replace(/\*\//g, "* /"));
}
/**
* Values that cannot safely be used as the keys on a POJO (`{}`) due to
* security or similar concerns. For example `__proto__` is disallowed.
*/
const disallowedKeys = [
...Object.getOwnPropertyNames(Object.prototype),
...Object.getOwnPropertySymbols(Object.prototype),
];
if (!disallowedKeys.includes("__proto__")) {
// If you're running with `--disable-proto=delete` we still want to check
// you're not trying to set __proto__.
disallowedKeys.push("__proto__");
}
/**
* Returns true if the given key is safe to set as the key of a POJO (without a
* null prototype), false otherwise.
*/
const isSafeObjectPropertyName = (key) => typeof key === "number" ||
((typeof key === "symbol" ||
(typeof key === "string" &&
/^(?:[0-9a-z$]|_[a-z0-9$])[a-z0-9_$]*$/i.test(key))) &&
!disallowedKeys.includes(key));
exports.isSafeObjectPropertyName = isSafeObjectPropertyName;
exportAs(exports.isSafeObjectPropertyName, "isSafeObjectPropertyName");
/**
* Returns true if you can use the given key as the key to a POJO without
* requiring quote marks or square braces.
*
* @remarks
* Doesn't allow it to start with two underscores.
*/
const canBeObjectKeyWithoutQuotes = (key) => Number.isFinite(key) ||
(typeof key === "string" &&
(key === "_" || /^(?:[a-z$]|_[a-z0-9$])[a-z0-9_$]*$/i.test(key)));
/**
* If this returns true then the given `name` is allowed to be a variable name
* in JavaScript. If it returns false then it may or may not be safe to use it
* as a variable name.
*/
function isValidVariableName(name) {
if (reservedWords_ts_1.reservedWords.has(name)) {
return false;
}
if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name)) {
return false;
}
return true;
}
/**
* Asserts that the given `name` is allowed to be used as an identifier
* (variable name, function name, etc); and if so it returns a TE node
* representing this identifier. If disallowed, will throw an error.
*/
function identifier(name) {
if (typeof name !== "string" || !isValidVariableName(name)) {
throw new Error(`Invalid identifier name '${name}'`);
}
return makeRawNode(name);
}
/**
* Checks that the given `key` is not explicitly disallowed as a key on a POJO and returns a TE node representing it. If disallowed, an error will be thrown. Useful for building easy to read objects:
*
* ```js
* const obj = te.run`return { ${te.identifier("frogs")}: "frogs" }`;
* ```
*
* IMPORTANT: It's strongly recommended that instead of defining an object via
* `const obj = { ${te.safeKeyOrThrow(untrustedKey)}: value }` you instead use
* `const obj = Object.create(null);` and then set the properties on the resulting
* object via `${obj}${te.set(untrustedKey, true)} = value;` - this prevents
* attacks such as **prototype polution** since properties like `__proto__` are
* not special on null-prototype objects, whereas they can cause havok in
* regular `{}` objects (POJOs).
*/
function safeKeyOrThrow(key) {
if ((0, exports.isSafeObjectPropertyName)(key)) {
if (canBeObjectKeyWithoutQuotes(key)) {
return makeRawNode(String(key));
}
else {
return makeRawNode(JSON.stringify(key));
}
}
else {
throw new Error(`Forbidden object key: ${JSON.stringify(key)}; consider using 'Object.create(null)' and assigning properties using te.lit.`);
}
}
function canAccessViaDot(str) {
return (str.length < MAX_SHORT_STRING_LENGTH &&
/^[$_a-zA-Z][$_a-zA-Z0-9]*$/.test(str));
}
/**
* Accesses the key of an object either via `.` or `[]` as appropriate;
* `obj${te.get(key)}` would become `obj.foo` or `obj["1foo"]` as
* appropriate.
*
* DO NOT USE THIS FOR ASSIGNMENT!
*/
function get(key) {
return typeof key === "string" && canAccessViaDot(key)
? // ._mySimpleProperty
makeRawNode(`.${key}`)
: // ["@@meaning"]
typeof key === "string" ||
(typeof key === "number" && Number.isFinite(key))
? makeRawNode(`[${(0, exports.stringifyJSON)(key)}]`)
: te `[${te.lit(key)}]`;
}
/**
* Accesses the key of an object via optional-chaining:
* `obj${te.optionalGet(key)}` would become `obj?.foo` or `obj?.["1foo"]` as
* appropriate.
*
* DO NOT USE THIS FOR ASSIGNMENT!
*/
function optionalGet(key) {
return typeof key === "string" && canAccessViaDot(key)
? // ?._mySimpleProperty
makeRawNode(`?.${key}`)
: // ?.["@@meaning"]
typeof key === "string" ||
(typeof key === "number" && Number.isFinite(key))
? makeRawNode(`?.[${(0, exports.stringifyJSON)(key)}]`)
: te `?.[${te.lit(key)}]`;
}
/**
* Access to the key of an object either via `.` or `[]` as appropriate ready
* for being assigned to; `obj${te.set(key)}` would become `obj.foo` or
* `obj["1foo"]` as appropriate. Note this is different to `get` because it
* performs additional checks.
*
* If the object you're setting properties on has a `null` prototype
* (`Object.create(null)`) then you can set `hasNullPrototype` to true and all
* keys are allowed. If this is not the case, then an error will be thrown on
* certain potentially dangerous keys such as `__proto__` or `constructor`.
*
* ENHANCE: consider aliasing this as 'leftAccess' (accessing a property, to
* be done on the left side of an assignment).
*/
function set(key, hasNullPrototype = false) {
if (!hasNullPrototype && disallowedKeys.includes(key)) {
throw new Error(`Attempted to set '${String(key)}' on an object that isn't declared as having a null prototype. This could be unsafe.`);
}
return typeof key === "string" && canAccessViaDot(key)
? // ._mySimpleProperty
makeRawNode(`.${key}`)
: // ["@@meaning"]
typeof key === "string" ||
(typeof key === "number" && Number.isFinite(key))
? makeRawNode(`[${(0, exports.stringifyJSON)(key)}]`)
: te `[${te.lit(key)}]`;
}
/**
* @experimental
*/
function tempVar(symbol = Symbol()) {
if (typeof symbol !== "symbol") {
throw new Error(`Invalid call to te.tempVar`);
}
return makeTemporaryVariableNode(symbol);
}
/**
* @experimental
*/
function tmp(obj, callback) {
const trustedObj = obj[thereCanBeOnlyOne_ts_1.$$type] !== undefined ? obj : enforceValidNode(obj);
// ENHANCEMENT: we should be able to reuse these tempvars between tmp calls
// that aren't nested (or are nested at the same level).
const varName = te.tempVar();
return te `(${varName} = ${trustedObj}, ${callback(varName)})`;
}
/** For compatibility with graphile-export */
function EXPORTABLE(factory, args, nameHint) {
const forbiddenIndex = args.findIndex(isForbidden);
if (forbiddenIndex >= 0) {
throw new Error(`${nameHint ?? "Anonymous"} EXPORTABLE call's args[${forbiddenIndex}] is not allowed to be exported.`);
}
const fn = factory(...args);
if (((typeof fn === "object" && fn !== null) || typeof fn === "function") &&
!("$exporter$factory" in fn)) {
Object.defineProperties(fn, {
$exporter$args: { value: args },
$exporter$factory: { value: factory },
$exporter$name: { writable: true, value: nameHint },
});
}
return fn;
}
function isForbidden(thing) {
return ((typeof thing === "object" || typeof thing === "function") &&
thing !== null &&
"$$export" in thing &&
thing.$$export === false);
}
function _runCore(fragmentOrStrings, exportable = false) {
const fragment = fragmentOrStrings[thereCanBeOnlyOne_ts_1.$$type] !== undefined
? fragmentOrStrings
: enforceValidNode(fragmentOrStrings);
const compiled = compile(fragment);
const argNames = Object.keys(compiled.refs);
const argValues = Object.values(compiled.refs);
if (isDev && activeBatch) {
throw new Error(`te.run called, but batch is active - recommend you use runInBatch`);
}
try {
const fn = newFunction(...argNames, compiled.string);
if (exportable) {
// eslint-disable-next-line graphile-export/exhaustive-deps
return EXPORTABLE(fn, argValues);
}
else {
return fn(...argValues);
}
}
catch (e) {
// ERRORS: improve this!
console.error(`Error occurred during code generation:`);
console.error(e);
console.error("Function definition:");
console.error(compiled.string);
throw new Error(`Error occurred during code generation.`);
}
}
function run(fragmentOrStrings, ...values) {
if ("raw" in fragmentOrStrings) {
// Turn a template literal run into a run just passing a TE fragment
return run(te(fragmentOrStrings, ...values));
}
if (values.length > 0) {
throw new Error("Invalid call to `te.run`");
}
return _runCore(fragmentOrStrings);
}
function runExportable(fragmentOrStrings, ...values) {
if ("raw" in fragmentOrStrings) {
// Turn a template literal run into a run just passing a TE fragment
return runExportable(te(fragmentOrStrings, ...values));
}
if (values.length > 0) {
throw new Error("Invalid call to `te.runExportable`");
}
return _runCore(fragmentOrStrings, true);
}
let activeBatch = null;
function runInBatch(fragment, callback) {
if (!activeBatch) {
throw new Error(`te.runInBatch failed - there's no active batch`);
}
activeBatch.push({ fragment, callback });
}
function batch(callback) {
if (activeBatch) {
throw new Error(`te.batch failed - there's already a batch in progress`);
}
let batch;
try {
activeBatch = [];
callback();
batch = activeBatch;
}
finally {
activeBatch = null;
}
if (batch.length === 0) {
return;
}
const finalCode = te `return [\n${te.join(batch.map((entry) => entry.fragment), ",\n")}\n];`;
const result = te.run(finalCode);
for (let i = 0, l = batch.length; i < l; i++) {
batch[i].callback(result[i]);
}
}
/** Because `new Function` retains the scope, we do it at top level to avoid capturing extra values */
function newFunction(...argNamesAndCode) {
return new Function(...argNamesAndCode);
}
/**
* Join some TE items together, optionally separated by a string. Useful when
* dealing with lists of TE nodes, for example a dynamic list of object
* properties.
*/
function join(items, separator = "") {
if (!Array.isArray(items)) {
throw new Error(`[tamedevil] Invalid te.join call - the first argument should be an array, but it was '${String(items)}'.`);
}
if (typeof separator !== "string") {
throw new Error(`[tamedevil] Invalid separator passed to te.join - must be a string, but we received '${String(separator)}'`);
}
// Short circuit joins of size <= 1
if (items.length === 0) {
return blankNode;
}
else if (items.length === 1) {
const rawNode = items[0];
const node = rawNode[thereCanBeOnlyOne_ts_1.$$type] !== undefined
? rawNode
: enforceValidNode(rawNode, `join item ${0}`);
return node;
}
const hasSeparator = separator.length > 0;
let currentText = "";
const currentItems = [];
for (let i = 0, l = items.length; i < l; i++) {
const rawNode = items[i];
const node = rawNode[thereCanBeOnlyOne_ts_1.$$type] !== undefined
? rawNode
: enforceValidNode(rawNode, `join item ${i}`);
const addSeparator = i > 0 && hasSeparator;
if (addSeparator) {
currentText += separator;
}
if (node[thereCanBeOnlyOne_ts_1.$$type] === "QUERY") {
for (const innerNode of node.n) {
if (innerNode[thereCanBeOnlyOne_ts_1.$$type] === "RAW") {
currentText += innerNode.t;
}
else {
if (currentText !== "") {
currentItems.push(makeRawNode(currentText));
currentText = "";
}
currentItems.push(innerNode);
}
}
}
else if (node[thereCanBeOnlyOne_ts_1.$$type] === "RAW") {
currentText += node.t;
}
else {
if (currentText !== "") {
currentItems.push(makeRawNode(currentText));
currentText = "";
}
currentItems.push(node);
}
}
if (currentText !== "") {
currentItems.push(makeRawNode(currentText));
currentText = "";
}
return currentItems.length === 1
? currentItems[0]
: makeQueryNode(currentItems);
}
function indent(fragmentOrStrings, ...values) {
const fragment = "raw" in fragmentOrStrings
? te(fragmentOrStrings, ...values)
: fragmentOrStrings[thereCanBeOnlyOne_ts_1.$$type] !== undefined
? fragmentOrStrings
: enforceValidNode(fragmentOrStrings);
if (!isDev) {
return fragment;
}
return makeIndentNode(fragment);
}
/**
* If the given condition is true, the fragment will be wrapped with an indent
* node, otherwise it will be returned verbatim.
*/
function indentIf(condition, fragment) {
const trusted = fragment[thereCanBeOnlyOne_ts_1.$$type] !== undefined ? fragment : enforceValidNode(fragment);
return isDev && condition ? makeIndentNode(trusted) : trusted;
}
/**
* Makes safe identifiers
*
* @experimental
*/
class Idents {
constructor() {
// Initialized with forbidden words
this.idents = new Set(reservedWords_ts_1.reservedWords);
}
/**
* Don't allow using these identifiers (presumably because they're already in use).
*/
forbid(identifiers) {
for (const identifier in identifiers) {
this.idents.add(identifier);
}
}
// TODO: we need a proper understanding of lexical scope so we can generate truly safe identifiers
makeSafeIdentifier(str) {
const { idents } = this;
const safe = "i_" + str.replace(/[^a-zA-Z0-9_$]+/g, "").replace(/_+/, "_");
let ident = undefined;
for (let i = 1; i < 10000; i++) {
const val = safe + (i > 1 ? String(i) : "");
if (!idents.has(val)) {
ident = val;
break;
}
}
if (!ident) {
throw new Error("Too many identifiers!");
}
idents.add(ident);
return ident;
}
}
exports.Idents = Idents;
const te = teBase;
exports.te = te;
exports.default = te;
const attributes = {
te,
cache,
ref,
reference: ref,
lit,
literal: lit,
substring,
subcomment,
join,
identifier,
safeKeyOrThrow,
dangerousKey: safeKeyOrThrow,
get,
optionalGet,
set,
tmp,
tempVar,
run,
eval: run,
runExportable,
runInBatch,
batch,
compile,
indent,
indentIf,
undefined: undefinedNode,
blank: blankNode,
isTE,
debug,
dangerouslyIncludeRawCode,
};
Object.entries(attributes).forEach(([exportName, value]) => {
if (!value.$$export) {
exportAs(value, exportName);
}
});
Object.assign(teBase, attributes);
//# sourceMappingURL=index.js.map