@crombolo/server 0.0.0-stage → 0.9.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.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +69 -0
  3. package/package.json +41 -4
  4. package/src/access.ts +114 -0
  5. package/src/adapters/assistant.ts +229 -0
  6. package/src/adapters/blueprints.ts +163 -0
  7. package/src/adapters/bundles.ts +432 -0
  8. package/src/adapters/content.ts +783 -0
  9. package/src/adapters/dictionary.ts +76 -0
  10. package/src/adapters/documents.ts +637 -0
  11. package/src/adapters/elements.ts +403 -0
  12. package/src/adapters/file-system.ts +195 -0
  13. package/src/adapters/media.ts +347 -0
  14. package/src/adapters/member-groups.ts +90 -0
  15. package/src/adapters/members.ts +261 -0
  16. package/src/adapters/public-access.ts +116 -0
  17. package/src/adapters/redirects.ts +73 -0
  18. package/src/adapters/references.ts +67 -0
  19. package/src/adapters/schema-files.ts +204 -0
  20. package/src/adapters/schema-proposals.ts +180 -0
  21. package/src/adapters/template-files.ts +58 -0
  22. package/src/adapters/template-query.ts +127 -0
  23. package/src/adapters/users.ts +963 -0
  24. package/src/assistant.ts +487 -0
  25. package/src/backup.ts +52 -0
  26. package/src/bundles.ts +508 -0
  27. package/src/component-snippets.ts +163 -0
  28. package/src/components.ts +266 -0
  29. package/src/config.ts +548 -0
  30. package/src/content-type-udt.ts +256 -0
  31. package/src/database.ts +109 -0
  32. package/src/diagnostics.ts +143 -0
  33. package/src/dictionary-transfer.ts +123 -0
  34. package/src/domains.ts +384 -0
  35. package/src/editor-types.ts +108 -0
  36. package/src/email-http.ts +79 -0
  37. package/src/email-notice.ts +36 -0
  38. package/src/email-postmark.ts +78 -0
  39. package/src/email-resend.ts +69 -0
  40. package/src/email-ses.ts +307 -0
  41. package/src/email.ts +402 -0
  42. package/src/extensions-notice.ts +35 -0
  43. package/src/file-intake.ts +84 -0
  44. package/src/form-files.ts +241 -0
  45. package/src/form-submit.ts +397 -0
  46. package/src/form-workflows.ts +397 -0
  47. package/src/forms.ts +170 -0
  48. package/src/git-github.ts +171 -0
  49. package/src/git-routes.ts +154 -0
  50. package/src/git.ts +88 -0
  51. package/src/imaging.ts +434 -0
  52. package/src/index.ts +62 -0
  53. package/src/jobs.ts +140 -0
  54. package/src/listen.ts +36 -0
  55. package/src/log-expressions.ts +524 -0
  56. package/src/log-viewer.ts +190 -0
  57. package/src/logging.ts +126 -0
  58. package/src/marketplace.ts +297 -0
  59. package/src/media-files.ts +276 -0
  60. package/src/media-store-azure.ts +218 -0
  61. package/src/media-store-s3.ts +113 -0
  62. package/src/media-store.ts +248 -0
  63. package/src/member-auth.ts +384 -0
  64. package/src/models-builder.ts +106 -0
  65. package/src/no-nodes.ts +64 -0
  66. package/src/not-implemented.ts +44 -0
  67. package/src/oembed.ts +144 -0
  68. package/src/ports.ts +553 -0
  69. package/src/redirects.ts +132 -0
  70. package/src/schema-classify.ts +82 -0
  71. package/src/schema-store.ts +145 -0
  72. package/src/schema.ts +388 -0
  73. package/src/seo.ts +371 -0
  74. package/src/server-events.ts +131 -0
  75. package/src/server.ts +1579 -0
  76. package/src/status.ts +246 -0
  77. package/src/udt.ts +168 -0
  78. package/src/value-schemas.ts +64 -0
  79. package/src/view-runtime.ts +42 -0
  80. package/src/zip.ts +152 -0
  81. 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
+ }