tamedevil 0.0.0-0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/LICENSE.md +20 -0
- package/README.md +315 -0
- package/dist/index.d.ts +198 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +724 -0
- package/dist/index.js.map +1 -0
- package/dist/reservedWords.d.ts +2 -0
- package/dist/reservedWords.d.ts.map +1 -0
- package/dist/reservedWords.js +100 -0
- package/dist/reservedWords.js.map +1 -0
- package/package.json +56 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.raw = exports.undefined = exports.tmp = exports.tempVar = exports.te = exports.substring = exports.subcomment = exports.set = exports.run = exports.reservedWords = exports.ref = exports.optionalGet = exports.literal = exports.lit = exports.join = exports.isTE = exports.identifier = exports.get = exports.eval = exports.dangerousKey = exports.compile = exports.canRepresentAsIdentifier = exports.isSafeObjectPropertyName = exports.toJSON = exports.stringifyString = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const lru_1 = tslib_1.__importDefault(require("@graphile/lru"));
|
|
6
|
+
const reservedWords_js_1 = require("./reservedWords.js");
|
|
7
|
+
Object.defineProperty(exports, "reservedWords", { enumerable: true, get: function () { return reservedWords_js_1.reservedWords; } });
|
|
8
|
+
function exportAs(thing, exportName) {
|
|
9
|
+
const existingExport = thing.$$export;
|
|
10
|
+
if (existingExport) {
|
|
11
|
+
if (existingExport.exportName !== exportName) {
|
|
12
|
+
throw new Error(`Attempted to export same thing under multiple names '${existingExport.exportName}' and '${exportName}'`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
Object.defineProperty(thing, "$$export", {
|
|
17
|
+
value: { moduleName: "tamedevil", exportName },
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return thing;
|
|
21
|
+
}
|
|
22
|
+
const isDev = process.env.GRAPHILE_ENV === "development";
|
|
23
|
+
/**
|
|
24
|
+
* This is the secret to our safety; since this is a symbol it cannot be faked
|
|
25
|
+
* in a JSON payload and it cannot be constructed with a new Symbol (even with
|
|
26
|
+
* the same argument), so external data cannot make itself trusted.
|
|
27
|
+
*/
|
|
28
|
+
const $$type = Symbol("tamedevil-type");
|
|
29
|
+
/**
|
|
30
|
+
* This helps us to avoid GC overhead of allocating new raw nodes all the time
|
|
31
|
+
* when they're likely to be the same values over and over. The average raw
|
|
32
|
+
* string is likely to be around 20 bytes; allowing for 50 bytes once this has
|
|
33
|
+
* been turned into an object, 10000 would mean 500kB which seems an acceptable
|
|
34
|
+
* amount of memory to consume for this.
|
|
35
|
+
*/
|
|
36
|
+
const CACHE_RAW_NODES = new lru_1.default({ maxLength: 10000 });
|
|
37
|
+
function makeRawNode(text, exportName) {
|
|
38
|
+
const n = CACHE_RAW_NODES.get(text);
|
|
39
|
+
if (n) {
|
|
40
|
+
return n;
|
|
41
|
+
}
|
|
42
|
+
if (typeof text !== "string") {
|
|
43
|
+
throw new Error(`[tamedevil] Invalid argument to makeRawNode - expected string, but received '${String(text)}'`);
|
|
44
|
+
}
|
|
45
|
+
const newNode = {
|
|
46
|
+
[$$type]: "RAW",
|
|
47
|
+
t: text,
|
|
48
|
+
};
|
|
49
|
+
if (exportName) {
|
|
50
|
+
exportAs(newNode, exportName);
|
|
51
|
+
}
|
|
52
|
+
Object.freeze(newNode);
|
|
53
|
+
CACHE_RAW_NODES.set(text, newNode);
|
|
54
|
+
return newNode;
|
|
55
|
+
}
|
|
56
|
+
// Simple function to help V8 optimize it.
|
|
57
|
+
function makeRefNode(rawValue) {
|
|
58
|
+
return Object.freeze({
|
|
59
|
+
[$$type]: "REF",
|
|
60
|
+
v: rawValue,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function makeTemporaryVariableNode(symbol) {
|
|
64
|
+
return Object.freeze({
|
|
65
|
+
[$$type]: "VARIABLE",
|
|
66
|
+
s: symbol,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function makeIndentNode(content) {
|
|
70
|
+
return Object.freeze({
|
|
71
|
+
[$$type]: "INDENT",
|
|
72
|
+
c: content[$$type] === "QUERY" ? content : makeQueryNode([content]),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function makeQueryNode(nodes) {
|
|
76
|
+
return Object.freeze({
|
|
77
|
+
[$$type]: "QUERY",
|
|
78
|
+
n: nodes,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function isTE(node) {
|
|
82
|
+
return (typeof node === "object" &&
|
|
83
|
+
node !== null &&
|
|
84
|
+
typeof node[$$type] === "string");
|
|
85
|
+
}
|
|
86
|
+
exports.isTE = isTE;
|
|
87
|
+
function enforceValidNode(node, where) {
|
|
88
|
+
if (isTE(node)) {
|
|
89
|
+
return node;
|
|
90
|
+
}
|
|
91
|
+
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.`);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Accepts an te`...` expression and compiles it out to TE text with
|
|
95
|
+
* placeholders, and the values to substitute for these values.
|
|
96
|
+
*/
|
|
97
|
+
function compile(fragment) {
|
|
98
|
+
/**
|
|
99
|
+
* Values hold the JavaScript values that are represented in the query string
|
|
100
|
+
* by placeholders. They are eager because they were provided before compile
|
|
101
|
+
* time.
|
|
102
|
+
*/
|
|
103
|
+
const refs = Object.create(null);
|
|
104
|
+
let refCount = 0;
|
|
105
|
+
const refMap = new Map();
|
|
106
|
+
const makeRef = (value) => {
|
|
107
|
+
const existingIdentifier = refMap.get(value);
|
|
108
|
+
if (existingIdentifier) {
|
|
109
|
+
return existingIdentifier;
|
|
110
|
+
}
|
|
111
|
+
refCount++;
|
|
112
|
+
// Arbitrary
|
|
113
|
+
if (refCount > 65535) {
|
|
114
|
+
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.");
|
|
115
|
+
}
|
|
116
|
+
const identifier = `_$$_ref_${refCount}`;
|
|
117
|
+
refMap.set(value, identifier);
|
|
118
|
+
refs[identifier] = value;
|
|
119
|
+
return identifier;
|
|
120
|
+
};
|
|
121
|
+
const varMap = new Map();
|
|
122
|
+
let tmpCounter = 0;
|
|
123
|
+
const getVar = (sym) => {
|
|
124
|
+
const existing = varMap.get(sym);
|
|
125
|
+
if (existing) {
|
|
126
|
+
return existing;
|
|
127
|
+
}
|
|
128
|
+
const varName = `_$_tmp${tmpCounter++}`;
|
|
129
|
+
varMap.set(sym, varName);
|
|
130
|
+
return varName;
|
|
131
|
+
};
|
|
132
|
+
function print(untrustedInput, indent = 0) {
|
|
133
|
+
/**
|
|
134
|
+
* Join this to generate the TE query
|
|
135
|
+
*/
|
|
136
|
+
const teFragments = [];
|
|
137
|
+
const trustedInput = enforceValidNode(untrustedInput, ``);
|
|
138
|
+
const items = trustedInput[$$type] === "QUERY"
|
|
139
|
+
? expandQueryNodes(trustedInput)
|
|
140
|
+
: [trustedInput];
|
|
141
|
+
const itemCount = items.length;
|
|
142
|
+
for (let itemIndex = 0; itemIndex < itemCount; itemIndex++) {
|
|
143
|
+
const item = enforceValidNode(items[itemIndex], `item ${itemIndex}`);
|
|
144
|
+
switch (item[$$type]) {
|
|
145
|
+
case "RAW": {
|
|
146
|
+
if (item.t === "") {
|
|
147
|
+
// No need to add blank raw text!
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
// IMPORTANT: this **must not** mangle primitives. Fortunately they're single line so it should be fine.
|
|
151
|
+
teFragments.push(isDev ? item.t.replace(/\n/g, "\n" + " ".repeat(indent)) : item.t);
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
case "REF": {
|
|
155
|
+
const identifier = makeRef(item.v);
|
|
156
|
+
teFragments.push(identifier);
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
case "VARIABLE": {
|
|
160
|
+
const identifier = getVar(item.s);
|
|
161
|
+
teFragments.push(identifier);
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
case "INDENT": {
|
|
165
|
+
if (!isDev) {
|
|
166
|
+
throw new Error("INDENT nodes only allowed in development mode");
|
|
167
|
+
}
|
|
168
|
+
teFragments.push("\n" +
|
|
169
|
+
" ".repeat(indent + 1) +
|
|
170
|
+
print(item.c, indent + 1) +
|
|
171
|
+
"\n" +
|
|
172
|
+
" ".repeat(indent));
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
default: {
|
|
176
|
+
const never = item;
|
|
177
|
+
// This cannot happen
|
|
178
|
+
throw new Error(`Unsupported node found in TE: ${String(never)}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return teFragments.join("");
|
|
183
|
+
}
|
|
184
|
+
let str = print(fragment);
|
|
185
|
+
const variables = [];
|
|
186
|
+
for (const varName of varMap.values()) {
|
|
187
|
+
variables.push(`let ${varName};`);
|
|
188
|
+
}
|
|
189
|
+
if (variables.length > 0) {
|
|
190
|
+
str = variables.join("\n") + "\n" + str;
|
|
191
|
+
}
|
|
192
|
+
const string = isDev ? str.replace(/\n\s*\n/g, "\n") : str;
|
|
193
|
+
return {
|
|
194
|
+
string,
|
|
195
|
+
refs,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
exports.compile = compile;
|
|
199
|
+
// LRU not necessary
|
|
200
|
+
const CACHE_SIMPLE_FRAGMENTS = new Map();
|
|
201
|
+
/**
|
|
202
|
+
* A template string tag that creates a `TE` query out of some strings and
|
|
203
|
+
* some values. Use this to construct all PostgreTE queries to avoid TE
|
|
204
|
+
* injection.
|
|
205
|
+
*
|
|
206
|
+
* Note that using this function, the user *must* specify if they are injecting
|
|
207
|
+
* raw text. This makes a TE injection vulnerability harder to create.
|
|
208
|
+
*/
|
|
209
|
+
const teBase = function te(strings, ...values) {
|
|
210
|
+
if (!Array.isArray(strings) || !strings.raw) {
|
|
211
|
+
throw new Error("[tamedevil] te should be used as a template literal, not a function call.");
|
|
212
|
+
}
|
|
213
|
+
const stringsLength = strings.length;
|
|
214
|
+
const first = strings[0];
|
|
215
|
+
// Reduce memory churn with a cache
|
|
216
|
+
if (stringsLength === 1) {
|
|
217
|
+
if (first === "") {
|
|
218
|
+
return blankNode;
|
|
219
|
+
}
|
|
220
|
+
let node = CACHE_SIMPLE_FRAGMENTS.get(first);
|
|
221
|
+
if (!node) {
|
|
222
|
+
node = makeRawNode(first);
|
|
223
|
+
CACHE_SIMPLE_FRAGMENTS.set(first, node);
|
|
224
|
+
}
|
|
225
|
+
return node;
|
|
226
|
+
}
|
|
227
|
+
// Special case te`${...}` - just return the node directly
|
|
228
|
+
if (stringsLength === 2 && strings[0] === "" && strings[1] === "") {
|
|
229
|
+
return values[0];
|
|
230
|
+
}
|
|
231
|
+
const items = [];
|
|
232
|
+
let currentText = "";
|
|
233
|
+
for (let i = 0, l = stringsLength; i < l; i++) {
|
|
234
|
+
const text = strings[i];
|
|
235
|
+
if (typeof text !== "string") {
|
|
236
|
+
throw new Error("[tamedevil] te must be invoked as a template literal, not a function call.");
|
|
237
|
+
}
|
|
238
|
+
currentText += text;
|
|
239
|
+
if (i < l - 1) {
|
|
240
|
+
const rawVal = values[i];
|
|
241
|
+
const valid = enforceValidNode(rawVal, `template literal placeholder ${i}`);
|
|
242
|
+
if (valid[$$type] === "RAW") {
|
|
243
|
+
currentText += valid.t;
|
|
244
|
+
}
|
|
245
|
+
else if (valid[$$type] === "QUERY") {
|
|
246
|
+
const nodes = expandQueryNodes(valid);
|
|
247
|
+
const nodeCount = nodes.length;
|
|
248
|
+
for (let nodeIndex = 0; nodeIndex < nodeCount; nodeIndex++) {
|
|
249
|
+
const node = nodes[nodeIndex];
|
|
250
|
+
if (node[$$type] === "RAW") {
|
|
251
|
+
currentText += node.t;
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
if (currentText !== "") {
|
|
255
|
+
items.push(makeRawNode(currentText));
|
|
256
|
+
currentText = "";
|
|
257
|
+
}
|
|
258
|
+
items.push(node);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
if (currentText !== "") {
|
|
264
|
+
items.push(makeRawNode(currentText));
|
|
265
|
+
currentText = "";
|
|
266
|
+
}
|
|
267
|
+
items.push(valid);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (currentText !== "") {
|
|
272
|
+
items.push(makeRawNode(currentText));
|
|
273
|
+
currentText = "";
|
|
274
|
+
}
|
|
275
|
+
return items.length === 1 ? items[0] : makeQueryNode(items);
|
|
276
|
+
};
|
|
277
|
+
let rawWarningOutput = false;
|
|
278
|
+
/**
|
|
279
|
+
* Creates a TE item for a raw code string. Just plain ol‘ raw code. This
|
|
280
|
+
* method is dangerous though because it involves no escaping, so proceed with
|
|
281
|
+
* caution! It's very very rarely warranted - there is likely a safer way of
|
|
282
|
+
* achieving your goal. DO NOT USE THIS WITH UNTRUSTED INPUT!
|
|
283
|
+
*/
|
|
284
|
+
function raw(text) {
|
|
285
|
+
if (!rawWarningOutput) {
|
|
286
|
+
rawWarningOutput = true;
|
|
287
|
+
try {
|
|
288
|
+
throw new Error("te.raw first invoked here");
|
|
289
|
+
}
|
|
290
|
+
catch (e) {
|
|
291
|
+
console.warn(`[tamedevil] WARNING: you're using the te.raw escape hatch, usage of this API is rarely required and is highly discouraged. Please be sure this is what you intend. ${e.stack}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (typeof text !== "string") {
|
|
295
|
+
throw new Error(`[tamedevil] te.raw must be passed a string, but it was passed '${String(text)}'.`);
|
|
296
|
+
}
|
|
297
|
+
return makeRawNode(text);
|
|
298
|
+
}
|
|
299
|
+
exports.raw = raw;
|
|
300
|
+
/**
|
|
301
|
+
* Creates a TE item for a value that will be included in our final query.
|
|
302
|
+
* This value will be added in a way which avoids TE injection.
|
|
303
|
+
*/
|
|
304
|
+
function ref(val) {
|
|
305
|
+
return makeRefNode(val);
|
|
306
|
+
}
|
|
307
|
+
exports.ref = ref;
|
|
308
|
+
const blankNode = makeRawNode(``, "blank");
|
|
309
|
+
const undefinedNode = makeRawNode(`undefined`, "undefined");
|
|
310
|
+
exports.undefined = undefinedNode;
|
|
311
|
+
/**
|
|
312
|
+
* A regexp that matches the first character that might need escaping in a JSON
|
|
313
|
+
* string. ("Might" because we'd rather be safe.)
|
|
314
|
+
*
|
|
315
|
+
* Unsafe:
|
|
316
|
+
* - `\\`
|
|
317
|
+
* - `"`
|
|
318
|
+
* - control characters
|
|
319
|
+
* - surrogates
|
|
320
|
+
*/
|
|
321
|
+
// eslint-disable-next-line no-control-regex
|
|
322
|
+
// const forbiddenCharacters = /["\\\u0000-\u001f\ud800-\udfff]/;
|
|
323
|
+
/**
|
|
324
|
+
* A 'short string' has a length less than or equal to this, and can
|
|
325
|
+
* potentially have JSON.stringify skipped on it if it doesn't contain any of
|
|
326
|
+
* the forbiddenCharacters. To prevent the forbiddenCharacters regexp running
|
|
327
|
+
* for a long time, we cap the length of string we test.
|
|
328
|
+
*/
|
|
329
|
+
const MAX_SHORT_STRING_LENGTH = 200; // TODO: what should this be?
|
|
330
|
+
const BACKSLASH_CODE = "\\".charCodeAt(0);
|
|
331
|
+
const QUOTE_CODE = '"'.charCodeAt(0);
|
|
332
|
+
// Bizarrely this seems to be faster than the regexp approach
|
|
333
|
+
function stringifyString(value) {
|
|
334
|
+
const l = value.length;
|
|
335
|
+
if (l > MAX_SHORT_STRING_LENGTH) {
|
|
336
|
+
return JSON.stringify(value);
|
|
337
|
+
}
|
|
338
|
+
// Scan through for disallowed charcodes
|
|
339
|
+
for (let i = 0; i < l; i++) {
|
|
340
|
+
const code = value.charCodeAt(i);
|
|
341
|
+
if (code === BACKSLASH_CODE ||
|
|
342
|
+
code === QUOTE_CODE ||
|
|
343
|
+
(code & 0xffe0) === 0 || // equivalent to `code <= 0x001f`
|
|
344
|
+
(code & 0xc000) !== 0 // Not quite equivalent to `code >= 0xd800`, but good enough for our purposes
|
|
345
|
+
) {
|
|
346
|
+
// Backslash, quote, control character or surrogate
|
|
347
|
+
return JSON.stringify(value);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return `"${value}"`;
|
|
351
|
+
}
|
|
352
|
+
exports.stringifyString = stringifyString;
|
|
353
|
+
// TODO: more optimal stringifier
|
|
354
|
+
// TODO: rename to jsonStringify?
|
|
355
|
+
const toJSON = (value) => {
|
|
356
|
+
if (value == null)
|
|
357
|
+
return "null";
|
|
358
|
+
if (value === true)
|
|
359
|
+
return "true";
|
|
360
|
+
if (value === false)
|
|
361
|
+
return "false";
|
|
362
|
+
const t = typeof value;
|
|
363
|
+
if (t === "number")
|
|
364
|
+
return "" + value;
|
|
365
|
+
if (t === "string") {
|
|
366
|
+
return stringifyString(value);
|
|
367
|
+
}
|
|
368
|
+
return JSON.stringify(value);
|
|
369
|
+
};
|
|
370
|
+
exports.toJSON = toJSON;
|
|
371
|
+
/**
|
|
372
|
+
* If the value is simple will inline it into the query, otherwise will defer
|
|
373
|
+
* to `te.ref`.
|
|
374
|
+
*/
|
|
375
|
+
function lit(val) {
|
|
376
|
+
if (val === undefined) {
|
|
377
|
+
return undefinedNode;
|
|
378
|
+
}
|
|
379
|
+
else if (val === null ||
|
|
380
|
+
typeof val === "string" ||
|
|
381
|
+
typeof val === "boolean" ||
|
|
382
|
+
(typeof val === "number" && Number.isFinite(val))) {
|
|
383
|
+
/*
|
|
384
|
+
* Prior to ECMAScript 2019, JSON wasn't truly a subset of JS - it was possible
|
|
385
|
+
* to encode characters in JSON that JS couldn't parse via `eval` (notably
|
|
386
|
+
* `\u2028` and friends), however as of ES2019 JSON is now a subset of JS, so
|
|
387
|
+
* JSON.stringify is safe.
|
|
388
|
+
*
|
|
389
|
+
* https://github.com/tc39/proposal-json-superset
|
|
390
|
+
*/
|
|
391
|
+
const primitive = val;
|
|
392
|
+
return makeRawNode((0, exports.toJSON)(primitive));
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
return ref(val);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
exports.lit = lit;
|
|
399
|
+
exports.literal = lit;
|
|
400
|
+
/**
|
|
401
|
+
* If you're building a string and you want to inject untrusted content into it
|
|
402
|
+
* without opening yourself to code injection attacks, this is the method for
|
|
403
|
+
* you. Example:
|
|
404
|
+
*
|
|
405
|
+
* ```js
|
|
406
|
+
* const code = te`const str = "abc${te.substring(untrusted, '"')}123";`
|
|
407
|
+
* ```
|
|
408
|
+
*/
|
|
409
|
+
function substring(text, stringType) {
|
|
410
|
+
// Quick scan to see if it's safe to use verbatim
|
|
411
|
+
const l = text.length;
|
|
412
|
+
if (l < MAX_SHORT_STRING_LENGTH) {
|
|
413
|
+
const stringTypeCode = stringType.charCodeAt(0);
|
|
414
|
+
let verbatim = true;
|
|
415
|
+
for (let i = 0; i < l; i++) {
|
|
416
|
+
const code = text.charCodeAt(i);
|
|
417
|
+
if (code === BACKSLASH_CODE ||
|
|
418
|
+
code === stringTypeCode ||
|
|
419
|
+
(code & 0xffe0) === 0 || // equivalent to `code <= 0x001f`
|
|
420
|
+
(code & 0xc000) !== 0 // Not quite equivalent to `code >= 0xd800`, but good enough for our purposes
|
|
421
|
+
) {
|
|
422
|
+
// Backslash, quote, control character or surrogate
|
|
423
|
+
verbatim = false;
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (verbatim) {
|
|
428
|
+
return makeRawNode(text);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
// Not safe to use verbatim, so let's escape it
|
|
432
|
+
// This'll escape most things that need escaping - backslashes, fancy characters, double quotes, etc.
|
|
433
|
+
const jsonStringified = JSON.stringify(text);
|
|
434
|
+
// But we're already in a string so we don't want the quote marks
|
|
435
|
+
const inner = jsonStringified.substring(1, jsonStringified.length - 1);
|
|
436
|
+
// And if we're not inside a `"` we'll need to escape our string type.
|
|
437
|
+
const escaped = stringType === '"'
|
|
438
|
+
? inner // "" strings already escapes
|
|
439
|
+
: stringType === "'"
|
|
440
|
+
? inner.replace(/'/g, "\\'") // '' strings need `'` escaped too (`\` has already been escaped)
|
|
441
|
+
: inner.replace(/[`$]/g, "\\$&"); // `` strings need both '`' and `$` to be escaped
|
|
442
|
+
// Finally return a raw node
|
|
443
|
+
return makeRawNode(escaped);
|
|
444
|
+
}
|
|
445
|
+
exports.substring = substring;
|
|
446
|
+
/**
|
|
447
|
+
* Escapes `content` so that it can be safely embedded in a multiline comment.
|
|
448
|
+
*/
|
|
449
|
+
function subcomment(content) {
|
|
450
|
+
return makeRawNode(String(content).replace(/\*\//g, "* /"));
|
|
451
|
+
}
|
|
452
|
+
exports.subcomment = subcomment;
|
|
453
|
+
const disallowedKeys = [
|
|
454
|
+
...Object.getOwnPropertyNames(Object.prototype),
|
|
455
|
+
...Object.getOwnPropertySymbols(Object.prototype),
|
|
456
|
+
];
|
|
457
|
+
/**
|
|
458
|
+
* Is safe to set as the key of a POJO (without a null prototype).
|
|
459
|
+
*/
|
|
460
|
+
const isSafeObjectPropertyName = (key) => (typeof key === "number" ||
|
|
461
|
+
typeof key === "symbol" ||
|
|
462
|
+
(typeof key === "string" &&
|
|
463
|
+
/^(?:[0-9a-z$]|_[a-z0-9$])[a-z0-9_$]*$/i.test(key))) &&
|
|
464
|
+
!disallowedKeys.includes(key);
|
|
465
|
+
exports.isSafeObjectPropertyName = isSafeObjectPropertyName;
|
|
466
|
+
/**
|
|
467
|
+
* Can represent as an identifier rather than a string key
|
|
468
|
+
*
|
|
469
|
+
* @remarks
|
|
470
|
+
* Doesn't allow it to start with two underscores.
|
|
471
|
+
*/
|
|
472
|
+
const canRepresentAsIdentifier = (key) => Number.isFinite(key) ||
|
|
473
|
+
(typeof key === "string" &&
|
|
474
|
+
(key === "_" || /^(?:[a-z$]|_[a-z0-9$])[a-z0-9_$]*$/i.test(key)));
|
|
475
|
+
exports.canRepresentAsIdentifier = canRepresentAsIdentifier;
|
|
476
|
+
function isValidVariableName(name) {
|
|
477
|
+
if (reservedWords_js_1.reservedWords.has(name)) {
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name)) {
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
return true;
|
|
484
|
+
}
|
|
485
|
+
function identifier(name) {
|
|
486
|
+
if (!isValidVariableName(name)) {
|
|
487
|
+
throw new Error(`Invalid identifier name '${name}'`);
|
|
488
|
+
}
|
|
489
|
+
return makeRawNode(name);
|
|
490
|
+
}
|
|
491
|
+
exports.identifier = identifier;
|
|
492
|
+
// TODO: rename to `ensureSafeKey` or `safeKeyOrThrow` or something?
|
|
493
|
+
/**
|
|
494
|
+
* IMPORTANT: It's strongly recommended that instead of defining an object via
|
|
495
|
+
* `const obj = { ${te.dangerousKey(untrustedKey)}: value }` you instead use
|
|
496
|
+
* `const obj = Object.create(null);` and then set the properties on the resulting
|
|
497
|
+
* object via `${obj}[${te.lit(untrustedKey)}] = value;` - this prevents attacks such as
|
|
498
|
+
* **prototype polution** since properties like `__proto__` are not special on
|
|
499
|
+
* null-prototype objects, whereas they can cause havok in regular `{}` objects.
|
|
500
|
+
*/
|
|
501
|
+
function dangerousKey(key) {
|
|
502
|
+
if ((0, exports.isSafeObjectPropertyName)(key)) {
|
|
503
|
+
if ((0, exports.canRepresentAsIdentifier)(key)) {
|
|
504
|
+
return makeRawNode(String(key));
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
return makeRawNode(JSON.stringify(key));
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
throw new Error(`Forbidden object key: ${JSON.stringify(key)}; consider using 'Object.create(null)' and assigning properties using te.lit.`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
exports.dangerousKey = dangerousKey;
|
|
515
|
+
function canAccessViaDot(str) {
|
|
516
|
+
return (str.length < MAX_SHORT_STRING_LENGTH &&
|
|
517
|
+
/^[$_a-zA-Z][$_a-zA-Z0-9]*$/.test(str));
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Accesses the key of an object either via `.` or `[]` as appropriate;
|
|
521
|
+
* `obj${te.get(key)}` would become `obj.foo` or `obj["1foo"]` as
|
|
522
|
+
* appropriate.
|
|
523
|
+
*/
|
|
524
|
+
function get(key) {
|
|
525
|
+
return typeof key === "string" && canAccessViaDot(key)
|
|
526
|
+
? // ._mySimpleProperty
|
|
527
|
+
te `.${makeRawNode(key)}`
|
|
528
|
+
: // ["@@meaning"]
|
|
529
|
+
te `[${te.lit(key)}]`;
|
|
530
|
+
}
|
|
531
|
+
exports.get = get;
|
|
532
|
+
/**
|
|
533
|
+
* Accesses the key of an object via optional-chaining:
|
|
534
|
+
* `obj${te.optionalGet(key)}` would become `obj?.foo` or `obj?.["1foo"]` as
|
|
535
|
+
* appropriate.
|
|
536
|
+
*/
|
|
537
|
+
function optionalGet(key) {
|
|
538
|
+
return typeof key === "string" && canAccessViaDot(key)
|
|
539
|
+
? // ?._mySimpleProperty
|
|
540
|
+
te `?.${makeRawNode(key)}`
|
|
541
|
+
: // ?.["@@meaning"]
|
|
542
|
+
te `?.[${te.lit(key)}]`;
|
|
543
|
+
}
|
|
544
|
+
exports.optionalGet = optionalGet;
|
|
545
|
+
// TODO: rename this. 'leftSet'? 'leftAccess'? 'safeAccess'?
|
|
546
|
+
/**
|
|
547
|
+
* Sets the key of an object either via `.` or `[]` as appropriate;
|
|
548
|
+
* `obj${te.set(key)}` would become `obj.foo` or `obj["1foo"]` as
|
|
549
|
+
* appropriate.
|
|
550
|
+
*
|
|
551
|
+
* If the object you're setting properties on has a `null` prototype
|
|
552
|
+
* (`Object.create(null)`) then you can set `hasNullPrototype` to true and all
|
|
553
|
+
* keys are allowed. If this is not the case, then an error will be thrown on
|
|
554
|
+
* certain potentially dangerous keys such as `__proto__` or `constructor`.
|
|
555
|
+
*/
|
|
556
|
+
function set(key, hasNullPrototype = false) {
|
|
557
|
+
if (!hasNullPrototype && disallowedKeys.includes(key)) {
|
|
558
|
+
throw new Error(`Attempted to set '${String(key)}' on an object that isn't declared as having a null prototype. This could be unsafe.`);
|
|
559
|
+
}
|
|
560
|
+
return typeof key === "string" && canAccessViaDot(key)
|
|
561
|
+
? // ._mySimpleProperty
|
|
562
|
+
te `.${makeRawNode(key)}`
|
|
563
|
+
: // ["@@meaning"]
|
|
564
|
+
te `[${te.lit(key)}]`;
|
|
565
|
+
}
|
|
566
|
+
exports.set = set;
|
|
567
|
+
/**
|
|
568
|
+
* @experimental
|
|
569
|
+
*/
|
|
570
|
+
function tempVar(symbol = Symbol()) {
|
|
571
|
+
return makeTemporaryVariableNode(symbol);
|
|
572
|
+
}
|
|
573
|
+
exports.tempVar = tempVar;
|
|
574
|
+
function tmp(obj, callback) {
|
|
575
|
+
const varName = te.tempVar();
|
|
576
|
+
return te `(${varName} = ${obj}, ${callback(varName)})`;
|
|
577
|
+
}
|
|
578
|
+
exports.tmp = tmp;
|
|
579
|
+
function run(fragmentOrStrings, ...values) {
|
|
580
|
+
if ("raw" in fragmentOrStrings) {
|
|
581
|
+
return run(te(fragmentOrStrings, ...values));
|
|
582
|
+
}
|
|
583
|
+
if (values.length > 0) {
|
|
584
|
+
throw new Error("Invalid call to `te.run`");
|
|
585
|
+
}
|
|
586
|
+
const fragment = fragmentOrStrings;
|
|
587
|
+
const compiled = compile(fragment);
|
|
588
|
+
const argNames = Object.keys(compiled.refs);
|
|
589
|
+
const argValues = Object.values(compiled.refs);
|
|
590
|
+
try {
|
|
591
|
+
return newFunction(...argNames, compiled.string)(...argValues);
|
|
592
|
+
}
|
|
593
|
+
catch (e) {
|
|
594
|
+
// TODO: improve this!
|
|
595
|
+
console.error(`Error occurred during code generation:`);
|
|
596
|
+
console.error(e);
|
|
597
|
+
console.error("Function definition:");
|
|
598
|
+
console.error(compiled.string);
|
|
599
|
+
throw new Error(`Error occurred during code generation.`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
exports.eval = run;
|
|
603
|
+
exports.run = run;
|
|
604
|
+
/** Because `new Function` retains the scope, we do it at top level to avoid capturing extra values */
|
|
605
|
+
function newFunction(...args) {
|
|
606
|
+
return new Function(...args);
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Join some TE items together, optionally separated by a string. Useful when
|
|
610
|
+
* dealing with lists of TE items, for example a dynamic list of columns or
|
|
611
|
+
* variadic TE function arguments.
|
|
612
|
+
*/
|
|
613
|
+
function join(items, separator = "") {
|
|
614
|
+
if (!Array.isArray(items)) {
|
|
615
|
+
throw new Error(`[tamedevil] Invalid te.join call - the first argument should be an array, but it was '${String(items)}'.`);
|
|
616
|
+
}
|
|
617
|
+
if (typeof separator !== "string") {
|
|
618
|
+
throw new Error(`[tamedevil] Invalid separator passed to te.join - must be a string, but we received '${String(separator)}'`);
|
|
619
|
+
}
|
|
620
|
+
// Short circuit joins of size <= 1
|
|
621
|
+
if (items.length === 0) {
|
|
622
|
+
return blankNode;
|
|
623
|
+
}
|
|
624
|
+
else if (items.length === 1) {
|
|
625
|
+
const rawNode = items[0];
|
|
626
|
+
const node = enforceValidNode(rawNode, `join item ${0}`);
|
|
627
|
+
return node;
|
|
628
|
+
}
|
|
629
|
+
const hasSeparator = separator.length > 0;
|
|
630
|
+
let currentText = "";
|
|
631
|
+
const currentItems = [];
|
|
632
|
+
for (let i = 0, l = items.length; i < l; i++) {
|
|
633
|
+
const rawNode = items[i];
|
|
634
|
+
const addSeparator = i > 0 && hasSeparator;
|
|
635
|
+
const node = enforceValidNode(rawNode, `join item ${i}`);
|
|
636
|
+
if (addSeparator) {
|
|
637
|
+
currentText += separator;
|
|
638
|
+
}
|
|
639
|
+
if (node[$$type] === "QUERY") {
|
|
640
|
+
for (const innerNode of expandQueryNodes(node)) {
|
|
641
|
+
if (innerNode[$$type] === "RAW") {
|
|
642
|
+
currentText += innerNode.t;
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
if (currentText !== "") {
|
|
646
|
+
currentItems.push(makeRawNode(currentText));
|
|
647
|
+
currentText = "";
|
|
648
|
+
}
|
|
649
|
+
currentItems.push(innerNode);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
else if (node[$$type] === "RAW") {
|
|
654
|
+
currentText += node.t;
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
if (currentText !== "") {
|
|
658
|
+
currentItems.push(makeRawNode(currentText));
|
|
659
|
+
currentText = "";
|
|
660
|
+
}
|
|
661
|
+
currentItems.push(node);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (currentText !== "") {
|
|
665
|
+
currentItems.push(makeRawNode(currentText));
|
|
666
|
+
currentText = "";
|
|
667
|
+
}
|
|
668
|
+
return currentItems.length === 1
|
|
669
|
+
? currentItems[0]
|
|
670
|
+
: makeQueryNode(currentItems);
|
|
671
|
+
}
|
|
672
|
+
exports.join = join;
|
|
673
|
+
function expandQueryNodes(node) {
|
|
674
|
+
return node.n;
|
|
675
|
+
}
|
|
676
|
+
function indent(fragmentOrStrings, ...values) {
|
|
677
|
+
const fragment = "raw" in fragmentOrStrings
|
|
678
|
+
? te(fragmentOrStrings, ...values)
|
|
679
|
+
: fragmentOrStrings;
|
|
680
|
+
if (!isDev) {
|
|
681
|
+
return fragment;
|
|
682
|
+
}
|
|
683
|
+
return makeIndentNode(fragment);
|
|
684
|
+
}
|
|
685
|
+
function indentIf(condition, fragment) {
|
|
686
|
+
return isDev && condition ? makeIndentNode(fragment) : fragment;
|
|
687
|
+
}
|
|
688
|
+
const te = teBase;
|
|
689
|
+
exports.te = te;
|
|
690
|
+
exports.default = te;
|
|
691
|
+
const attributes = {
|
|
692
|
+
te,
|
|
693
|
+
ref,
|
|
694
|
+
reference: ref,
|
|
695
|
+
lit,
|
|
696
|
+
literal: lit,
|
|
697
|
+
substring,
|
|
698
|
+
subcomment,
|
|
699
|
+
join,
|
|
700
|
+
identifier,
|
|
701
|
+
dangerousKey,
|
|
702
|
+
get,
|
|
703
|
+
optionalGet,
|
|
704
|
+
set,
|
|
705
|
+
tmp,
|
|
706
|
+
tempVar,
|
|
707
|
+
run,
|
|
708
|
+
eval: run,
|
|
709
|
+
compile,
|
|
710
|
+
indent,
|
|
711
|
+
indentIf,
|
|
712
|
+
undefined: undefinedNode,
|
|
713
|
+
blank: blankNode,
|
|
714
|
+
isTE,
|
|
715
|
+
reservedWords: reservedWords_js_1.reservedWords,
|
|
716
|
+
raw,
|
|
717
|
+
};
|
|
718
|
+
Object.entries(attributes).forEach(([exportName, value]) => {
|
|
719
|
+
if (!value.$$export) {
|
|
720
|
+
exportAs(value, exportName);
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
Object.assign(teBase, attributes);
|
|
724
|
+
//# sourceMappingURL=index.js.map
|