@volter/twin-temporal 0.1.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.
Files changed (55) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +104 -0
  3. package/dist/src/cli.d.ts +2 -0
  4. package/dist/src/cli.js +36 -0
  5. package/dist/src/generated/hpack.json +1 -0
  6. package/dist/src/generated/temporal-api.json +1 -0
  7. package/dist/src/index.d.ts +18 -0
  8. package/dist/src/index.js +70 -0
  9. package/dist/src/temporal-budget.d.ts +31 -0
  10. package/dist/src/temporal-budget.js +50 -0
  11. package/dist/src/temporal-capabilities.d.ts +7 -0
  12. package/dist/src/temporal-capabilities.js +650 -0
  13. package/dist/src/temporal-conformance.d.ts +8 -0
  14. package/dist/src/temporal-conformance.js +54 -0
  15. package/dist/src/temporal-connector.d.ts +32 -0
  16. package/dist/src/temporal-connector.js +121 -0
  17. package/dist/src/temporal-engine.d.ts +274 -0
  18. package/dist/src/temporal-engine.js +2132 -0
  19. package/dist/src/temporal-grpc.d.ts +51 -0
  20. package/dist/src/temporal-grpc.js +124 -0
  21. package/dist/src/temporal-h2.d.ts +40 -0
  22. package/dist/src/temporal-h2.js +644 -0
  23. package/dist/src/temporal-history.d.ts +27 -0
  24. package/dist/src/temporal-history.js +85 -0
  25. package/dist/src/temporal-http.d.ts +17 -0
  26. package/dist/src/temporal-http.js +160 -0
  27. package/dist/src/temporal-json.d.ts +7 -0
  28. package/dist/src/temporal-json.js +68 -0
  29. package/dist/src/temporal-proto.d.ts +17 -0
  30. package/dist/src/temporal-proto.js +46 -0
  31. package/dist/src/temporal-server.d.ts +25 -0
  32. package/dist/src/temporal-server.js +56 -0
  33. package/dist/src/temporal-twin.d.ts +25 -0
  34. package/dist/src/temporal-twin.js +100 -0
  35. package/dist/src/temporal-visibility.d.ts +49 -0
  36. package/dist/src/temporal-visibility.js +293 -0
  37. package/package.json +59 -0
  38. package/src/cli.ts +36 -0
  39. package/src/generated/hpack.json +1 -0
  40. package/src/generated/temporal-api.json +1 -0
  41. package/src/index.ts +81 -0
  42. package/src/temporal-budget.ts +75 -0
  43. package/src/temporal-capabilities.ts +679 -0
  44. package/src/temporal-conformance.ts +52 -0
  45. package/src/temporal-connector.ts +118 -0
  46. package/src/temporal-engine.ts +2176 -0
  47. package/src/temporal-grpc.ts +139 -0
  48. package/src/temporal-h2.ts +614 -0
  49. package/src/temporal-history.ts +99 -0
  50. package/src/temporal-http.ts +146 -0
  51. package/src/temporal-json.ts +77 -0
  52. package/src/temporal-proto.ts +53 -0
  53. package/src/temporal-server.ts +67 -0
  54. package/src/temporal-twin.ts +102 -0
  55. package/src/temporal-visibility.ts +245 -0
@@ -0,0 +1,245 @@
1
+ // VISIBILITY — Temporal's list filter language (https://docs.temporal.io/list-filter), read over the
2
+ // runs' own fields. Temporal's language is a SQL WHERE subset: comparisons (`=`, `!=`, `>`, `>=`, `<`,
3
+ // `<=`), `IN (...)`, `BETWEEN a AND b`, `STARTS_WITH`, `IS NULL`/`IS NOT NULL`, combined with `AND`,
4
+ // `OR` and parentheses, then an optional `ORDER BY`. This reads that grammar and evaluates it against
5
+ // one run's system attributes (WorkflowId, WorkflowType, ExecutionStatus, StartTime, CloseTime, …)
6
+ // and its custom search attributes. What the language has and this reader does not (`ORDER BY` beyond
7
+ // StartTime/CloseTime, `GROUP BY`) is refused with INVALID_ARGUMENT naming it, never ignored.
8
+
9
+ export type VisibilityValue = string | number | boolean | null | string[];
10
+ export type VisibilityRow = Record<string, VisibilityValue | undefined>;
11
+
12
+ type Expr =
13
+ | { kind: 'and' | 'or'; left: Expr; right: Expr }
14
+ | { kind: 'not'; expr: Expr }
15
+ | { kind: 'cmp'; field: string; op: string; value: VisibilityValue }
16
+ | { kind: 'in'; field: string; values: VisibilityValue[]; negate: boolean }
17
+ | { kind: 'between'; field: string; low: VisibilityValue; high: VisibilityValue }
18
+ | { kind: 'starts'; field: string; prefix: string }
19
+ | { kind: 'null'; field: string; negate: boolean };
20
+
21
+ export class VisibilityQueryError extends Error {}
22
+
23
+ type Token = { kind: 'ident' | 'string' | 'number' | 'op' | 'lparen' | 'rparen' | 'comma'; text: string };
24
+
25
+ function tokenize(query: string): Token[] {
26
+ const tokens: Token[] = [];
27
+ let i = 0;
28
+ while (i < query.length) {
29
+ const c = query[i]!;
30
+ if (/\s/.test(c)) { i += 1; continue; }
31
+ if (c === '(') { tokens.push({ kind: 'lparen', text: c }); i += 1; continue; }
32
+ if (c === ')') { tokens.push({ kind: 'rparen', text: c }); i += 1; continue; }
33
+ if (c === ',') { tokens.push({ kind: 'comma', text: c }); i += 1; continue; }
34
+ if (c === '"' || c === "'") {
35
+ let j = i + 1;
36
+ let text = '';
37
+ while (j < query.length && query[j] !== c) {
38
+ if (query[j] === '\\' && j + 1 < query.length) { text += query[j + 1]; j += 2; continue; }
39
+ text += query[j];
40
+ j += 1;
41
+ }
42
+ if (j >= query.length) throw new VisibilityQueryError(`unterminated string in query: ${query}`);
43
+ tokens.push({ kind: 'string', text });
44
+ i = j + 1;
45
+ continue;
46
+ }
47
+ if (c === '`') {
48
+ const j = query.indexOf('`', i + 1);
49
+ if (j < 0) throw new VisibilityQueryError(`unterminated backquote in query: ${query}`);
50
+ tokens.push({ kind: 'ident', text: query.slice(i + 1, j) });
51
+ i = j + 1;
52
+ continue;
53
+ }
54
+ const op = /^(>=|<=|!=|<>|=|>|<)/.exec(query.slice(i));
55
+ if (op) { tokens.push({ kind: 'op', text: op[1] === '<>' ? '!=' : op[1]! }); i += op[1]!.length; continue; }
56
+ const number = /^-?\d+(\.\d+)?/.exec(query.slice(i));
57
+ if (number) { tokens.push({ kind: 'number', text: number[0] }); i += number[0].length; continue; }
58
+ const ident = /^[A-Za-z_][A-Za-z0-9_.]*/.exec(query.slice(i));
59
+ if (ident) { tokens.push({ kind: 'ident', text: ident[0] }); i += ident[0].length; continue; }
60
+ throw new VisibilityQueryError(`unexpected character ${JSON.stringify(c)} in query: ${query}`);
61
+ }
62
+ return tokens;
63
+ }
64
+
65
+ export type ParsedQuery = { where: Expr | null; orderBy: Array<{ field: string; desc: boolean }> };
66
+
67
+ export function parseVisibilityQuery(query: string): ParsedQuery {
68
+ const tokens = tokenize(query);
69
+ let pos = 0;
70
+ const peek = (): Token | undefined => tokens[pos];
71
+ const keyword = (word: string): boolean => {
72
+ const token = peek();
73
+ if (token?.kind === 'ident' && token.text.toUpperCase() === word) { pos += 1; return true; }
74
+ return false;
75
+ };
76
+ const expect = (kind: Token['kind']): Token => {
77
+ const token = tokens[pos];
78
+ if (!token || token.kind !== kind) throw new VisibilityQueryError(`expected ${kind} at ${token ? JSON.stringify(token.text) : 'end of query'}`);
79
+ pos += 1;
80
+ return token;
81
+ };
82
+ const literal = (): VisibilityValue => {
83
+ const token = tokens[pos];
84
+ if (!token) throw new VisibilityQueryError('expected a value at end of query');
85
+ pos += 1;
86
+ if (token.kind === 'string') return token.text;
87
+ if (token.kind === 'number') return Number(token.text);
88
+ if (token.kind === 'ident' && /^(true|false)$/i.test(token.text)) return token.text.toLowerCase() === 'true';
89
+ throw new VisibilityQueryError(`expected a value, got ${JSON.stringify(token.text)}`);
90
+ };
91
+ const primary = (): Expr => {
92
+ if (peek()?.kind === 'lparen') {
93
+ pos += 1;
94
+ const inner = or();
95
+ expect('rparen');
96
+ return inner;
97
+ }
98
+ if (keyword('NOT')) return { kind: 'not', expr: primary() };
99
+ const field = expect('ident').text;
100
+ if (keyword('IN')) {
101
+ expect('lparen');
102
+ const values = [literal()];
103
+ while (peek()?.kind === 'comma') { pos += 1; values.push(literal()); }
104
+ expect('rparen');
105
+ return { kind: 'in', field, values, negate: false };
106
+ }
107
+ if (keyword('NOT')) {
108
+ if (keyword('IN')) {
109
+ expect('lparen');
110
+ const values = [literal()];
111
+ while (peek()?.kind === 'comma') { pos += 1; values.push(literal()); }
112
+ expect('rparen');
113
+ return { kind: 'in', field, values, negate: true };
114
+ }
115
+ throw new VisibilityQueryError(`expected IN after NOT in ${field} NOT …`);
116
+ }
117
+ if (keyword('BETWEEN')) {
118
+ const low = literal();
119
+ if (!keyword('AND')) throw new VisibilityQueryError('expected AND in BETWEEN');
120
+ return { kind: 'between', field, low, high: literal() };
121
+ }
122
+ if (keyword('STARTS_WITH')) {
123
+ const prefix = literal();
124
+ return { kind: 'starts', field, prefix: String(prefix) };
125
+ }
126
+ if (keyword('IS')) {
127
+ const negate = keyword('NOT');
128
+ if (!keyword('NULL')) throw new VisibilityQueryError('expected NULL after IS');
129
+ return { kind: 'null', field, negate };
130
+ }
131
+ const op = expect('op').text;
132
+ return { kind: 'cmp', field, op, value: literal() };
133
+ };
134
+ const and = (): Expr => {
135
+ let left = primary();
136
+ while (keyword('AND')) left = { kind: 'and', left, right: primary() };
137
+ return left;
138
+ };
139
+ const or = (): Expr => {
140
+ let left = and();
141
+ while (keyword('OR')) left = { kind: 'or', left, right: and() };
142
+ return left;
143
+ };
144
+ let where: Expr | null = null;
145
+ if (tokens.length > 0 && !(tokens[0]!.kind === 'ident' && tokens[0]!.text.toUpperCase() === 'ORDER')) where = or();
146
+ const orderBy: Array<{ field: string; desc: boolean }> = [];
147
+ if (keyword('ORDER')) {
148
+ if (!keyword('BY')) throw new VisibilityQueryError('expected BY after ORDER');
149
+ do {
150
+ const field = expect('ident').text;
151
+ const desc = keyword('DESC');
152
+ if (!desc) keyword('ASC');
153
+ orderBy.push({ field, desc });
154
+ if (peek()?.kind !== 'comma') break;
155
+ pos += 1;
156
+ } while (pos < tokens.length);
157
+ }
158
+ if (keyword('GROUP')) throw new VisibilityQueryError('GROUP BY is not supported by this server');
159
+ if (pos !== tokens.length) throw new VisibilityQueryError(`unexpected ${JSON.stringify(tokens[pos]!.text)} in query`);
160
+ return { where, orderBy };
161
+ }
162
+
163
+ function compare(a: VisibilityValue | undefined, b: VisibilityValue): number | null {
164
+ if (a === undefined || a === null || b === null) return null;
165
+ if (typeof a === 'number' || typeof b === 'number') {
166
+ // a Datetime attribute is held as epoch milliseconds; its literal is an RFC 3339 string
167
+ const asNumber = (value: VisibilityValue): number => typeof value === 'number' ? value
168
+ : Number.isNaN(Number(value)) ? Date.parse(String(value)) : Number(value);
169
+ const x = asNumber(a);
170
+ const y = asNumber(b);
171
+ return Number.isNaN(x) || Number.isNaN(y) ? null : x - y;
172
+ }
173
+ const x = String(a);
174
+ const y = String(b);
175
+ return x < y ? -1 : x > y ? 1 : 0;
176
+ }
177
+
178
+ function evaluate(expr: Expr, row: VisibilityRow, fieldOf: (name: string) => string): boolean {
179
+ switch (expr.kind) {
180
+ case 'and': return evaluate(expr.left, row, fieldOf) && evaluate(expr.right, row, fieldOf);
181
+ case 'or': return evaluate(expr.left, row, fieldOf) || evaluate(expr.right, row, fieldOf);
182
+ case 'not': return !evaluate(expr.expr, row, fieldOf);
183
+ case 'null': {
184
+ const value = row[fieldOf(expr.field)];
185
+ const absent = value === undefined || value === null;
186
+ return expr.negate ? !absent : absent;
187
+ }
188
+ case 'starts': {
189
+ const value = row[fieldOf(expr.field)];
190
+ return typeof value === 'string' && value.startsWith(expr.prefix);
191
+ }
192
+ case 'in': {
193
+ const value = row[fieldOf(expr.field)];
194
+ const hit = expr.values.some((candidate) => (Array.isArray(value) ? value.includes(String(candidate)) : compare(value, candidate) === 0));
195
+ return expr.negate ? !hit : hit;
196
+ }
197
+ case 'between': {
198
+ const value = row[fieldOf(expr.field)];
199
+ const low = compare(value, expr.low);
200
+ const high = compare(value, expr.high);
201
+ return low !== null && high !== null && low >= 0 && high <= 0;
202
+ }
203
+ case 'cmp': {
204
+ const value = row[fieldOf(expr.field)];
205
+ if (Array.isArray(value)) {
206
+ // a keyword list matches `=` when it holds the value (Temporal's KeywordList semantics)
207
+ if (expr.op === '=') return value.includes(String(expr.value));
208
+ if (expr.op === '!=') return !value.includes(String(expr.value));
209
+ return false;
210
+ }
211
+ const order = compare(value, expr.value);
212
+ if (order === null) return expr.op === '!=' && value !== undefined && value !== null;
213
+ switch (expr.op) {
214
+ case '=': return order === 0;
215
+ case '!=': return order !== 0;
216
+ case '>': return order > 0;
217
+ case '>=': return order >= 0;
218
+ case '<': return order < 0;
219
+ case '<=': return order <= 0;
220
+ default: return false;
221
+ }
222
+ }
223
+ }
224
+ }
225
+
226
+ /** Does `row` satisfy the query's filter? `fieldOf` maps a name as written to the row's key (it
227
+ * throws INVALID_ARGUMENT's error for a name the namespace does not define). */
228
+ export function matchesVisibilityQuery(parsed: ParsedQuery, row: VisibilityRow, fieldOf: (name: string) => string): boolean {
229
+ return parsed.where === null || evaluate(parsed.where, row, fieldOf);
230
+ }
231
+
232
+ /** Every field name a query mentions, so the caller can refuse an undefined one before evaluating. */
233
+ export function visibilityFields(parsed: ParsedQuery): string[] {
234
+ const out = new Set<string>();
235
+ const walk = (expr: Expr | null): void => {
236
+ if (!expr) return;
237
+ if ('field' in expr) { out.add(expr.field); return; }
238
+ if (expr.kind === 'not') { walk(expr.expr); return; }
239
+ walk(expr.left);
240
+ walk(expr.right);
241
+ };
242
+ walk(parsed.where);
243
+ for (const order of parsed.orderBy) out.add(order.field);
244
+ return [...out];
245
+ }