@crombolo/server 0.0.0-stage → 0.10.0
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/LICENSE +21 -0
- package/NOTICE +69 -0
- package/package.json +41 -4
- package/src/access.ts +114 -0
- package/src/adapters/assistant.ts +229 -0
- package/src/adapters/blueprints.ts +163 -0
- package/src/adapters/bundles.ts +507 -0
- package/src/adapters/content.ts +783 -0
- package/src/adapters/dictionary.ts +76 -0
- package/src/adapters/documents.ts +637 -0
- package/src/adapters/elements.ts +403 -0
- package/src/adapters/file-system.ts +195 -0
- package/src/adapters/media.ts +347 -0
- package/src/adapters/member-groups.ts +90 -0
- package/src/adapters/members.ts +261 -0
- package/src/adapters/public-access.ts +116 -0
- package/src/adapters/redirects.ts +73 -0
- package/src/adapters/references.ts +67 -0
- package/src/adapters/schema-files.ts +204 -0
- package/src/adapters/schema-proposals.ts +180 -0
- package/src/adapters/template-files.ts +58 -0
- package/src/adapters/template-query.ts +127 -0
- package/src/adapters/users.ts +963 -0
- package/src/assistant.ts +487 -0
- package/src/backup.ts +52 -0
- package/src/bundle-install.ts +417 -0
- package/src/bundle-sections.ts +424 -0
- package/src/bundle-tables.ts +161 -0
- package/src/bundles.ts +609 -0
- package/src/component-snippets.ts +163 -0
- package/src/components.ts +266 -0
- package/src/config.ts +556 -0
- package/src/content-type-udt.ts +256 -0
- package/src/database.ts +148 -0
- package/src/diagnostics.ts +143 -0
- package/src/dictionary-transfer.ts +123 -0
- package/src/domains.ts +384 -0
- package/src/editor-types.ts +108 -0
- package/src/email-http.ts +79 -0
- package/src/email-notice.ts +36 -0
- package/src/email-postmark.ts +78 -0
- package/src/email-resend.ts +69 -0
- package/src/email-ses.ts +307 -0
- package/src/email.ts +402 -0
- package/src/extensions-notice.ts +35 -0
- package/src/file-intake.ts +84 -0
- package/src/form-files.ts +241 -0
- package/src/form-submit.ts +397 -0
- package/src/form-workflows.ts +397 -0
- package/src/forms.ts +170 -0
- package/src/git-github.ts +171 -0
- package/src/git-routes.ts +154 -0
- package/src/git.ts +88 -0
- package/src/imaging.ts +434 -0
- package/src/index.ts +65 -0
- package/src/jobs.ts +140 -0
- package/src/listen.ts +36 -0
- package/src/log-expressions.ts +524 -0
- package/src/log-viewer.ts +190 -0
- package/src/logging.ts +126 -0
- package/src/marketplace.ts +382 -0
- package/src/media-files.ts +276 -0
- package/src/media-store-azure.ts +218 -0
- package/src/media-store-s3.ts +113 -0
- package/src/media-store.ts +248 -0
- package/src/member-auth.ts +384 -0
- package/src/models-builder.ts +106 -0
- package/src/no-nodes.ts +64 -0
- package/src/not-implemented.ts +44 -0
- package/src/oembed.ts +144 -0
- package/src/ports.ts +553 -0
- package/src/redirects.ts +132 -0
- package/src/schema-classify.ts +82 -0
- package/src/schema-store.ts +145 -0
- package/src/schema.ts +388 -0
- package/src/seo.ts +371 -0
- package/src/server-events.ts +131 -0
- package/src/server.ts +1642 -0
- package/src/status.ts +275 -0
- package/src/tar.ts +85 -0
- package/src/udt.ts +168 -0
- package/src/value-schemas.ts +64 -0
- package/src/view-runtime.ts +42 -0
- package/src/zip.ts +152 -0
- package/README.md +0 -3
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The filter language of Umbraco's log viewer: Serilog.Expressions, over
|
|
3
|
+
* events read from the log files. This covers what the viewer's saved
|
|
4
|
+
* searches and its filter box use — `@Level`, `@Message`, `@MessageTemplate`,
|
|
5
|
+
* `@Exception`, `@Timestamp` and properties (with `.member`, `[n]` and the
|
|
6
|
+
* any-element wildcard `[?]`), comparisons, `like`, `in`, `is [not] null`,
|
|
7
|
+
* `and`/`or`/`not`, the `ci` modifier, and the functions `Has`, `Not`,
|
|
8
|
+
* `StartsWith`, `EndsWith`, `Contains`, `IndexOf`, `Length`, `ToLower`,
|
|
9
|
+
* `ToUpper`. As in Serilog, a missing property is undefined, and only a result
|
|
10
|
+
* that is exactly true matches.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** An event as the filter sees it. */
|
|
14
|
+
export interface LogEvent {
|
|
15
|
+
timestamp: string
|
|
16
|
+
level: string
|
|
17
|
+
messageTemplate: string
|
|
18
|
+
message: string
|
|
19
|
+
exception: string | null
|
|
20
|
+
properties: Record<string, unknown>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type Node =
|
|
24
|
+
| { kind: 'literal'; value: unknown }
|
|
25
|
+
| { kind: 'name'; name: string }
|
|
26
|
+
| { kind: 'member'; target: Node; name: string }
|
|
27
|
+
| { kind: 'index'; target: Node; index: Node | 'any' }
|
|
28
|
+
| { kind: 'call'; name: string; args: Node[]; ci: boolean }
|
|
29
|
+
| { kind: 'unary'; op: 'not' | '-'; operand: Node }
|
|
30
|
+
| { kind: 'binary'; op: string; left: Node; right: Node; ci: boolean }
|
|
31
|
+
| { kind: 'is'; operand: Node; negated: boolean }
|
|
32
|
+
| { kind: 'array'; items: Node[] }
|
|
33
|
+
|
|
34
|
+
/** A value that stands for "any element of" a collection, from `[?]`. */
|
|
35
|
+
class AnyOf {
|
|
36
|
+
constructor(readonly values: unknown[]) {}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const UNDEFINED = undefined
|
|
40
|
+
|
|
41
|
+
type Token = { type: 'num' | 'str' | 'id' | 'op' | 'punct'; value: string }
|
|
42
|
+
|
|
43
|
+
function tokenize(source: string): Token[] | undefined {
|
|
44
|
+
const tokens: Token[] = []
|
|
45
|
+
let i = 0
|
|
46
|
+
while (i < source.length) {
|
|
47
|
+
const c = source[i] as string
|
|
48
|
+
if (/\s/.test(c)) {
|
|
49
|
+
i += 1
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
if (c === "'") {
|
|
53
|
+
let value = ''
|
|
54
|
+
i += 1
|
|
55
|
+
for (;;) {
|
|
56
|
+
if (i >= source.length) return undefined
|
|
57
|
+
if (source[i] === "'") {
|
|
58
|
+
if (source[i + 1] === "'") {
|
|
59
|
+
value += "'"
|
|
60
|
+
i += 2
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
i += 1
|
|
64
|
+
break
|
|
65
|
+
}
|
|
66
|
+
value += source[i]
|
|
67
|
+
i += 1
|
|
68
|
+
}
|
|
69
|
+
tokens.push({ type: 'str', value })
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
const number = /^\d+(\.\d+)?/.exec(source.slice(i))
|
|
73
|
+
if (number) {
|
|
74
|
+
tokens.push({ type: 'num', value: number[0] })
|
|
75
|
+
i += number[0].length
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
const identifier = /^@?[A-Za-z_][A-Za-z0-9_]*/.exec(source.slice(i))
|
|
79
|
+
if (identifier) {
|
|
80
|
+
tokens.push({ type: 'id', value: identifier[0] })
|
|
81
|
+
i += identifier[0].length
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
const operator = /^(<>|<=|>=|=|<|>|\+|-|\*|\/|%)/.exec(source.slice(i))
|
|
85
|
+
if (operator) {
|
|
86
|
+
tokens.push({ type: 'op', value: operator[0] })
|
|
87
|
+
i += operator[0].length
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
if ('()[],.?'.includes(c)) {
|
|
91
|
+
tokens.push({ type: 'punct', value: c })
|
|
92
|
+
i += 1
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
return undefined
|
|
96
|
+
}
|
|
97
|
+
return tokens
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
class Parser {
|
|
101
|
+
#tokens: Token[]
|
|
102
|
+
#at = 0
|
|
103
|
+
|
|
104
|
+
constructor(tokens: Token[]) {
|
|
105
|
+
this.#tokens = tokens
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
get done() {
|
|
109
|
+
return this.#at >= this.#tokens.length
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#peek(): Token | undefined {
|
|
113
|
+
return this.#tokens[this.#at]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#keyword(word: string): boolean {
|
|
117
|
+
const token = this.#peek()
|
|
118
|
+
if (token?.type === 'id' && token.value.toLowerCase() === word) {
|
|
119
|
+
this.#at += 1
|
|
120
|
+
return true
|
|
121
|
+
}
|
|
122
|
+
return false
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#punct(value: string): boolean {
|
|
126
|
+
const token = this.#peek()
|
|
127
|
+
if (token?.type === 'punct' && token.value === value) {
|
|
128
|
+
this.#at += 1
|
|
129
|
+
return true
|
|
130
|
+
}
|
|
131
|
+
return false
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#expect(value: string) {
|
|
135
|
+
if (!this.#punct(value)) throw new SyntaxError(`Expected '${value}'`)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
expression(): Node {
|
|
139
|
+
return this.#or()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#or(): Node {
|
|
143
|
+
let left = this.#and()
|
|
144
|
+
while (this.#keyword('or'))
|
|
145
|
+
left = { kind: 'binary', op: 'or', left, right: this.#and(), ci: false }
|
|
146
|
+
return left
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
#and(): Node {
|
|
150
|
+
let left = this.#not()
|
|
151
|
+
while (this.#keyword('and'))
|
|
152
|
+
left = { kind: 'binary', op: 'and', left, right: this.#not(), ci: false }
|
|
153
|
+
return left
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#not(): Node {
|
|
157
|
+
const token = this.#peek()
|
|
158
|
+
const next = this.#tokens[this.#at + 1]
|
|
159
|
+
// `not` as an operator, not the `Not(...)` function
|
|
160
|
+
if (
|
|
161
|
+
token?.type === 'id' &&
|
|
162
|
+
token.value.toLowerCase() === 'not' &&
|
|
163
|
+
!(next?.type === 'punct' && next.value === '(' && token.value === 'Not')
|
|
164
|
+
) {
|
|
165
|
+
this.#at += 1
|
|
166
|
+
return { kind: 'unary', op: 'not', operand: this.#not() }
|
|
167
|
+
}
|
|
168
|
+
return this.#comparison()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
#comparison(): Node {
|
|
172
|
+
let left = this.#additive()
|
|
173
|
+
for (;;) {
|
|
174
|
+
const token = this.#peek()
|
|
175
|
+
if (token?.type === 'op' && ['=', '<>', '<', '<=', '>', '>='].includes(token.value)) {
|
|
176
|
+
this.#at += 1
|
|
177
|
+
const right = this.#additive()
|
|
178
|
+
left = { kind: 'binary', op: token.value, left, right, ci: this.#keyword('ci') }
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
if (this.#keyword('is')) {
|
|
182
|
+
const negated = this.#keyword('not')
|
|
183
|
+
if (!this.#keyword('null')) throw new SyntaxError('Expected null')
|
|
184
|
+
left = { kind: 'is', operand: left, negated }
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
const save = this.#at
|
|
188
|
+
const negated = this.#keyword('not')
|
|
189
|
+
if (this.#keyword('like')) {
|
|
190
|
+
const right = this.#additive()
|
|
191
|
+
const like: Node = { kind: 'binary', op: 'like', left, right, ci: this.#keyword('ci') }
|
|
192
|
+
left = negated ? { kind: 'unary', op: 'not', operand: like } : like
|
|
193
|
+
continue
|
|
194
|
+
}
|
|
195
|
+
if (this.#keyword('in')) {
|
|
196
|
+
const right = this.#additive()
|
|
197
|
+
const within: Node = { kind: 'binary', op: 'in', left, right, ci: this.#keyword('ci') }
|
|
198
|
+
left = negated ? { kind: 'unary', op: 'not', operand: within } : within
|
|
199
|
+
continue
|
|
200
|
+
}
|
|
201
|
+
this.#at = save
|
|
202
|
+
return left
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
#additive(): Node {
|
|
207
|
+
let left = this.#multiplicative()
|
|
208
|
+
for (;;) {
|
|
209
|
+
const token = this.#peek()
|
|
210
|
+
if (token?.type === 'op' && (token.value === '+' || token.value === '-')) {
|
|
211
|
+
this.#at += 1
|
|
212
|
+
left = { kind: 'binary', op: token.value, left, right: this.#multiplicative(), ci: false }
|
|
213
|
+
} else return left
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
#multiplicative(): Node {
|
|
218
|
+
let left = this.#unary()
|
|
219
|
+
for (;;) {
|
|
220
|
+
const token = this.#peek()
|
|
221
|
+
if (token?.type === 'op' && ['*', '/', '%'].includes(token.value)) {
|
|
222
|
+
this.#at += 1
|
|
223
|
+
left = { kind: 'binary', op: token.value, left, right: this.#unary(), ci: false }
|
|
224
|
+
} else return left
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
#unary(): Node {
|
|
229
|
+
const token = this.#peek()
|
|
230
|
+
if (token?.type === 'op' && token.value === '-') {
|
|
231
|
+
this.#at += 1
|
|
232
|
+
return { kind: 'unary', op: '-', operand: this.#unary() }
|
|
233
|
+
}
|
|
234
|
+
return this.#postfix(this.#primary())
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
#postfix(node: Node): Node {
|
|
238
|
+
let current = node
|
|
239
|
+
for (;;) {
|
|
240
|
+
if (this.#punct('.')) {
|
|
241
|
+
const name = this.#peek()
|
|
242
|
+
if (name?.type !== 'id') throw new SyntaxError('Expected a member name')
|
|
243
|
+
this.#at += 1
|
|
244
|
+
current = { kind: 'member', target: current, name: name.value }
|
|
245
|
+
} else if (this.#punct('[')) {
|
|
246
|
+
if (this.#punct('?')) {
|
|
247
|
+
this.#expect(']')
|
|
248
|
+
current = { kind: 'index', target: current, index: 'any' }
|
|
249
|
+
} else {
|
|
250
|
+
const index = this.expression()
|
|
251
|
+
this.#expect(']')
|
|
252
|
+
current = { kind: 'index', target: current, index }
|
|
253
|
+
}
|
|
254
|
+
} else return current
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
#primary(): Node {
|
|
259
|
+
const token = this.#peek()
|
|
260
|
+
if (!token) throw new SyntaxError('Unexpected end')
|
|
261
|
+
this.#at += 1
|
|
262
|
+
if (token.type === 'num') return { kind: 'literal', value: Number(token.value) }
|
|
263
|
+
if (token.type === 'str') return { kind: 'literal', value: token.value }
|
|
264
|
+
if (token.type === 'punct' && token.value === '(') {
|
|
265
|
+
const inner = this.expression()
|
|
266
|
+
this.#expect(')')
|
|
267
|
+
return inner
|
|
268
|
+
}
|
|
269
|
+
if (token.type === 'punct' && token.value === '[') {
|
|
270
|
+
const items: Node[] = []
|
|
271
|
+
if (!this.#punct(']')) {
|
|
272
|
+
do items.push(this.expression())
|
|
273
|
+
while (this.#punct(','))
|
|
274
|
+
this.#expect(']')
|
|
275
|
+
}
|
|
276
|
+
return { kind: 'array', items }
|
|
277
|
+
}
|
|
278
|
+
if (token.type === 'id') {
|
|
279
|
+
const lower = token.value.toLowerCase()
|
|
280
|
+
if (lower === 'true') return { kind: 'literal', value: true }
|
|
281
|
+
if (lower === 'false') return { kind: 'literal', value: false }
|
|
282
|
+
if (lower === 'null') return { kind: 'literal', value: null }
|
|
283
|
+
if (this.#punct('(')) {
|
|
284
|
+
const args: Node[] = []
|
|
285
|
+
if (!this.#punct(')')) {
|
|
286
|
+
do args.push(this.expression())
|
|
287
|
+
while (this.#punct(','))
|
|
288
|
+
this.#expect(')')
|
|
289
|
+
}
|
|
290
|
+
return { kind: 'call', name: token.value.toLowerCase(), args, ci: this.#keyword('ci') }
|
|
291
|
+
}
|
|
292
|
+
return { kind: 'name', name: token.value }
|
|
293
|
+
}
|
|
294
|
+
throw new SyntaxError(`Unexpected '${token.value}'`)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const BUILT_INS: Record<string, keyof LogEvent> = {
|
|
299
|
+
'@l': 'level',
|
|
300
|
+
'@level': 'level',
|
|
301
|
+
'@m': 'message',
|
|
302
|
+
'@message': 'message',
|
|
303
|
+
'@mt': 'messageTemplate',
|
|
304
|
+
'@messagetemplate': 'messageTemplate',
|
|
305
|
+
'@x': 'exception',
|
|
306
|
+
'@exception': 'exception',
|
|
307
|
+
'@t': 'timestamp',
|
|
308
|
+
'@timestamp': 'timestamp',
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const text = (value: unknown, ci: boolean) =>
|
|
312
|
+
typeof value === 'string' ? (ci ? value.toLowerCase() : value) : value
|
|
313
|
+
|
|
314
|
+
function equal(a: unknown, b: unknown, ci: boolean): boolean {
|
|
315
|
+
if (typeof a === 'string' && typeof b === 'string') return text(a, ci) === text(b, ci)
|
|
316
|
+
if (a === null || b === null) return a === b
|
|
317
|
+
if (typeof a === 'object' || typeof b === 'object') return JSON.stringify(a) === JSON.stringify(b)
|
|
318
|
+
return a === b
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function like(value: unknown, pattern: unknown, ci: boolean): boolean | undefined {
|
|
322
|
+
if (typeof value !== 'string' || typeof pattern !== 'string') return UNDEFINED
|
|
323
|
+
const regex = pattern
|
|
324
|
+
.split('')
|
|
325
|
+
.map((c) => (c === '%' ? '.*' : c === '_' ? '.' : c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')))
|
|
326
|
+
.join('')
|
|
327
|
+
return new RegExp(`^${regex}$`, ci ? 'is' : 's').test(value)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Applies `op` to each element of an any-of operand; true when any is. */
|
|
331
|
+
function anyOf(left: unknown, right: unknown, op: (l: unknown, r: unknown) => unknown): unknown {
|
|
332
|
+
if (left instanceof AnyOf) return left.values.some((l) => op(l, right) === true)
|
|
333
|
+
if (right instanceof AnyOf) return right.values.some((r) => op(left, r) === true)
|
|
334
|
+
return op(left, right)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function evaluate(node: Node, event: LogEvent): unknown {
|
|
338
|
+
switch (node.kind) {
|
|
339
|
+
case 'literal':
|
|
340
|
+
return node.value
|
|
341
|
+
case 'array':
|
|
342
|
+
return node.items.map((item) => evaluate(item, event))
|
|
343
|
+
case 'name': {
|
|
344
|
+
const builtIn = BUILT_INS[node.name.toLowerCase()]
|
|
345
|
+
if (builtIn) return event[builtIn] ?? UNDEFINED
|
|
346
|
+
if (node.name.toLowerCase() === '@p' || node.name.toLowerCase() === '@properties')
|
|
347
|
+
return event.properties
|
|
348
|
+
return node.name in event.properties ? event.properties[node.name] : UNDEFINED
|
|
349
|
+
}
|
|
350
|
+
case 'member': {
|
|
351
|
+
const target = evaluate(node.target, event)
|
|
352
|
+
if (target && typeof target === 'object' && node.name in target)
|
|
353
|
+
return (target as Record<string, unknown>)[node.name]
|
|
354
|
+
return UNDEFINED
|
|
355
|
+
}
|
|
356
|
+
case 'index': {
|
|
357
|
+
const target = evaluate(node.target, event)
|
|
358
|
+
if (node.index === 'any') return Array.isArray(target) ? new AnyOf(target) : UNDEFINED
|
|
359
|
+
const index = evaluate(node.index, event)
|
|
360
|
+
if (Array.isArray(target) && typeof index === 'number') return target[index]
|
|
361
|
+
if (target && typeof target === 'object' && typeof index === 'string')
|
|
362
|
+
return (target as Record<string, unknown>)[index]
|
|
363
|
+
return UNDEFINED
|
|
364
|
+
}
|
|
365
|
+
case 'is': {
|
|
366
|
+
const value = evaluate(node.operand, event)
|
|
367
|
+
const isNull = value === null || value === UNDEFINED
|
|
368
|
+
return node.negated ? !isNull : isNull
|
|
369
|
+
}
|
|
370
|
+
case 'unary': {
|
|
371
|
+
const value = evaluate(node.operand, event)
|
|
372
|
+
if (node.op === '-') return typeof value === 'number' ? -value : UNDEFINED
|
|
373
|
+
return typeof value === 'boolean' ? !value : UNDEFINED
|
|
374
|
+
}
|
|
375
|
+
case 'binary': {
|
|
376
|
+
if (node.op === 'and') {
|
|
377
|
+
const left = evaluate(node.left, event)
|
|
378
|
+
if (left === false) return false
|
|
379
|
+
const right = evaluate(node.right, event)
|
|
380
|
+
if (right === false) return false
|
|
381
|
+
return left === true && right === true ? true : UNDEFINED
|
|
382
|
+
}
|
|
383
|
+
if (node.op === 'or') {
|
|
384
|
+
const left = evaluate(node.left, event)
|
|
385
|
+
if (left === true) return true
|
|
386
|
+
const right = evaluate(node.right, event)
|
|
387
|
+
if (right === true) return true
|
|
388
|
+
return left === false && right === false ? false : UNDEFINED
|
|
389
|
+
}
|
|
390
|
+
const left = evaluate(node.left, event)
|
|
391
|
+
const right = evaluate(node.right, event)
|
|
392
|
+
return anyOf(left, right, (l, r) => {
|
|
393
|
+
if (l === UNDEFINED || r === UNDEFINED) return UNDEFINED
|
|
394
|
+
switch (node.op) {
|
|
395
|
+
case '=':
|
|
396
|
+
return equal(l, r, node.ci)
|
|
397
|
+
case '<>':
|
|
398
|
+
return !equal(l, r, node.ci)
|
|
399
|
+
case 'like':
|
|
400
|
+
return like(l, r, node.ci)
|
|
401
|
+
case 'in':
|
|
402
|
+
return Array.isArray(r) ? r.some((item) => equal(l, item, node.ci)) : UNDEFINED
|
|
403
|
+
default:
|
|
404
|
+
break
|
|
405
|
+
}
|
|
406
|
+
const comparable =
|
|
407
|
+
(typeof l === 'number' && typeof r === 'number') ||
|
|
408
|
+
(typeof l === 'string' && typeof r === 'string')
|
|
409
|
+
if (!comparable) return UNDEFINED
|
|
410
|
+
const a = l as number | string
|
|
411
|
+
const b = r as number | string
|
|
412
|
+
switch (node.op) {
|
|
413
|
+
case '<':
|
|
414
|
+
return a < b
|
|
415
|
+
case '<=':
|
|
416
|
+
return a <= b
|
|
417
|
+
case '>':
|
|
418
|
+
return a > b
|
|
419
|
+
case '>=':
|
|
420
|
+
return a >= b
|
|
421
|
+
case '+':
|
|
422
|
+
return typeof a === 'number' && typeof b === 'number' ? a + b : `${a}${b}`
|
|
423
|
+
case '-':
|
|
424
|
+
return typeof a === 'number' && typeof b === 'number' ? a - b : UNDEFINED
|
|
425
|
+
case '*':
|
|
426
|
+
return typeof a === 'number' && typeof b === 'number' ? a * b : UNDEFINED
|
|
427
|
+
case '/':
|
|
428
|
+
return typeof a === 'number' && typeof b === 'number' && b !== 0 ? a / b : UNDEFINED
|
|
429
|
+
case '%':
|
|
430
|
+
return typeof a === 'number' && typeof b === 'number' && b !== 0 ? a % b : UNDEFINED
|
|
431
|
+
default:
|
|
432
|
+
return UNDEFINED
|
|
433
|
+
}
|
|
434
|
+
})
|
|
435
|
+
}
|
|
436
|
+
case 'call': {
|
|
437
|
+
const args = node.args.map((arg) => evaluate(arg, event))
|
|
438
|
+
const [first, second] = args
|
|
439
|
+
switch (node.name) {
|
|
440
|
+
case 'has':
|
|
441
|
+
case 'isdefined':
|
|
442
|
+
return first instanceof AnyOf
|
|
443
|
+
? first.values.length > 0
|
|
444
|
+
: first !== UNDEFINED && first !== null
|
|
445
|
+
case 'not':
|
|
446
|
+
return typeof first === 'boolean' ? !first : UNDEFINED
|
|
447
|
+
case 'startswith':
|
|
448
|
+
return anyOf(first, second, (a, b) =>
|
|
449
|
+
typeof a === 'string' && typeof b === 'string'
|
|
450
|
+
? (text(a, node.ci) as string).startsWith(text(b, node.ci) as string)
|
|
451
|
+
: UNDEFINED,
|
|
452
|
+
)
|
|
453
|
+
case 'endswith':
|
|
454
|
+
return anyOf(first, second, (a, b) =>
|
|
455
|
+
typeof a === 'string' && typeof b === 'string'
|
|
456
|
+
? (text(a, node.ci) as string).endsWith(text(b, node.ci) as string)
|
|
457
|
+
: UNDEFINED,
|
|
458
|
+
)
|
|
459
|
+
case 'contains':
|
|
460
|
+
return anyOf(first, second, (a, b) =>
|
|
461
|
+
typeof a === 'string' && typeof b === 'string'
|
|
462
|
+
? (text(a, node.ci) as string).includes(text(b, node.ci) as string)
|
|
463
|
+
: UNDEFINED,
|
|
464
|
+
)
|
|
465
|
+
case 'indexof':
|
|
466
|
+
return anyOf(first, second, (a, b) =>
|
|
467
|
+
typeof a === 'string' && typeof b === 'string'
|
|
468
|
+
? (text(a, node.ci) as string).indexOf(text(b, node.ci) as string)
|
|
469
|
+
: UNDEFINED,
|
|
470
|
+
)
|
|
471
|
+
case 'length':
|
|
472
|
+
return typeof first === 'string' || Array.isArray(first) ? first.length : UNDEFINED
|
|
473
|
+
case 'tolower':
|
|
474
|
+
return typeof first === 'string' ? first.toLowerCase() : UNDEFINED
|
|
475
|
+
case 'toupper':
|
|
476
|
+
return typeof first === 'string' ? first.toUpperCase() : UNDEFINED
|
|
477
|
+
default:
|
|
478
|
+
throw new SyntaxError(`Unknown function '${node.name}'`)
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** A compiled expression, or undefined when it is not one — which the viewer turns into a text search. */
|
|
485
|
+
export function compileExpression(source: string): ((event: LogEvent) => boolean) | undefined {
|
|
486
|
+
const tokens = tokenize(source)
|
|
487
|
+
if (!tokens || tokens.length === 0) return undefined
|
|
488
|
+
let tree: Node
|
|
489
|
+
try {
|
|
490
|
+
const parser = new Parser(tokens)
|
|
491
|
+
tree = parser.expression()
|
|
492
|
+
if (!parser.done) return undefined
|
|
493
|
+
// An unknown function is a compile error in Serilog, so find one now
|
|
494
|
+
evaluate(tree, {
|
|
495
|
+
timestamp: '',
|
|
496
|
+
level: '',
|
|
497
|
+
messageTemplate: '',
|
|
498
|
+
message: '',
|
|
499
|
+
exception: null,
|
|
500
|
+
properties: {},
|
|
501
|
+
})
|
|
502
|
+
} catch {
|
|
503
|
+
return undefined
|
|
504
|
+
}
|
|
505
|
+
return (event) => {
|
|
506
|
+
try {
|
|
507
|
+
return evaluate(tree, event) === true
|
|
508
|
+
} catch {
|
|
509
|
+
return false
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Umbraco's filter: a single word with no operator characters, or anything that
|
|
516
|
+
* does not compile, is a search for that text in the rendered message.
|
|
517
|
+
*/
|
|
518
|
+
export function logFilter(expression: string | null | undefined): (event: LogEvent) => boolean {
|
|
519
|
+
const source = expression?.trim() ?? ''
|
|
520
|
+
if (!source) return () => true
|
|
521
|
+
const plainWord = !/\s/.test(source) && !/[()+=*<>%-]/.test(source)
|
|
522
|
+
const compiled = plainWord ? undefined : compileExpression(source)
|
|
523
|
+
return compiled ?? ((event) => event.message.includes(source))
|
|
524
|
+
}
|