zero-server 0.0.1__py3-none-any.whl
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.
- zero_server/__init__.py +36 -0
- zero_server/codegen.py +133 -0
- zero_server/integrations/__init__.py +1 -0
- zero_server/integrations/starlette.py +115 -0
- zero_server/mutators.py +87 -0
- zero_server/protocol/__init__.py +1 -0
- zero_server/protocol/application_error.py +36 -0
- zero_server/protocol/ast.py +342 -0
- zero_server/protocol/custom_queries.py +51 -0
- zero_server/protocol/error.py +119 -0
- zero_server/protocol/mutate_server.py +33 -0
- zero_server/protocol/mutation.py +127 -0
- zero_server/protocol/mutation_id.py +12 -0
- zero_server/protocol/push.py +25 -0
- zero_server/protocol/query_server.py +29 -0
- zero_server/protocol/wire.py +88 -0
- zero_server/py.typed +0 -0
- zero_server/query/__init__.py +1 -0
- zero_server/query/builder.py +338 -0
- zero_server/query/expression.py +167 -0
- zero_server/query/registry.py +143 -0
- zero_server/schema.py +142 -0
- zero_server/server/__init__.py +1 -0
- zero_server/server/adapters/__init__.py +1 -0
- zero_server/server/adapters/asyncpg.py +43 -0
- zero_server/server/adapters/psycopg.py +55 -0
- zero_server/server/adapters/sqlalchemy.py +49 -0
- zero_server/server/crud.py +134 -0
- zero_server/server/pg_query_executor.py +70 -0
- zero_server/server/pg_types.py +69 -0
- zero_server/server/process_mutations.py +412 -0
- zero_server/server/process_queries.py +109 -0
- zero_server/server/push_processor.py +49 -0
- zero_server/server/server_schema.py +165 -0
- zero_server/server/transaction.py +70 -0
- zero_server/server/zql_database.py +129 -0
- zero_server/shared/__init__.py +1 -0
- zero_server/shared/asserts.py +19 -0
- zero_server/shared/error.py +97 -0
- zero_server/shared/js.py +121 -0
- zero_server/shared/json.py +27 -0
- zero_server/shared/registry.py +64 -0
- zero_server/z2s/__init__.py +1 -0
- zero_server/z2s/compiler.py +375 -0
- zero_server/z2s/sql.py +130 -0
- zero_server-0.0.1.dist-info/METADATA +134 -0
- zero_server-0.0.1.dist-info/RECORD +50 -0
- zero_server-0.0.1.dist-info/WHEEL +4 -0
- zero_server-0.0.1.dist-info/entry_points.txt +3 -0
- zero_server-0.0.1.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""The ZQL AST: what a query endpoint returns for each query zero-cache asks it to transform.
|
|
2
|
+
|
|
3
|
+
Ported from rocicorp/mono `packages/zero-protocol/src/ast.ts`, `data.ts` and `primary-key.ts` at
|
|
4
|
+
zero/v1.10.0 (905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp.
|
|
5
|
+
|
|
6
|
+
The AST carries server names: `map_ast` maps a client-named AST through the schema's name mapper before
|
|
7
|
+
it goes on the wire, as `handleQueryRequest` does upstream.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Callable, Sequence
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Annotated, Literal, Protocol
|
|
15
|
+
|
|
16
|
+
from pydantic import Field, StrictBool, StrictStr
|
|
17
|
+
|
|
18
|
+
from zero_server.protocol.wire import Number, StrictWireModel
|
|
19
|
+
from zero_server.shared.asserts import AssertionFailedError, check
|
|
20
|
+
from zero_server.shared.js import js_sort, js_string
|
|
21
|
+
from zero_server.shared.json import JSONValue
|
|
22
|
+
|
|
23
|
+
SUBQ_PREFIX = "zsubq_"
|
|
24
|
+
|
|
25
|
+
type System = Literal["permissions", "client", "test"]
|
|
26
|
+
type Direction = Literal["asc", "desc"]
|
|
27
|
+
type OrderPart = tuple[StrictStr, Direction]
|
|
28
|
+
type Ordering = tuple[OrderPart, ...]
|
|
29
|
+
|
|
30
|
+
type EqualityOp = Literal["=", "!=", "IS", "IS NOT"]
|
|
31
|
+
type OrderOp = Literal["<", ">", "<=", ">="]
|
|
32
|
+
type LikeOp = Literal["LIKE", "NOT LIKE", "ILIKE", "NOT ILIKE"]
|
|
33
|
+
type InOp = Literal["IN", "NOT IN"]
|
|
34
|
+
type SimpleOperator = Literal[
|
|
35
|
+
"=", "!=", "IS", "IS NOT", "<", ">", "<=", ">=", "LIKE", "NOT LIKE", "ILIKE", "NOT ILIKE", "IN", "NOT IN"
|
|
36
|
+
]
|
|
37
|
+
type CorrelatedSubqueryConditionOperator = Literal["EXISTS", "NOT EXISTS"]
|
|
38
|
+
|
|
39
|
+
type Scalar = StrictStr | Number | StrictBool
|
|
40
|
+
type LiteralValue = Scalar | tuple[Scalar, ...] | None
|
|
41
|
+
|
|
42
|
+
type Value = JSONValue
|
|
43
|
+
"""A row value. Upstream also allows `undefined`, which it treats as `null`."""
|
|
44
|
+
type Row = dict[str, Value]
|
|
45
|
+
|
|
46
|
+
type PrimaryKey = tuple[StrictStr, ...]
|
|
47
|
+
type CompoundKey = Annotated[tuple[StrictStr, ...], Field(min_length=1)]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class LiteralReference(StrictWireModel):
|
|
51
|
+
nullable = frozenset({"value"})
|
|
52
|
+
|
|
53
|
+
type: Literal["literal"] = "literal"
|
|
54
|
+
value: LiteralValue
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ColumnReference(StrictWireModel):
|
|
58
|
+
type: Literal["column"] = "column"
|
|
59
|
+
name: StrictStr
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Parameter(StrictWireModel):
|
|
63
|
+
"""A value resolved at runtime: the caller's auth data, or a row before a mutation touched it."""
|
|
64
|
+
|
|
65
|
+
type: Literal["static"] = "static"
|
|
66
|
+
anchor: Literal["authData", "preMutationRow"]
|
|
67
|
+
field: StrictStr | tuple[StrictStr, ...]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
type ValuePosition = Annotated[LiteralReference | ColumnReference | Parameter, Field(discriminator="type")]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class SimpleCondition(StrictWireModel):
|
|
74
|
+
type: Literal["simple"] = "simple"
|
|
75
|
+
op: SimpleOperator
|
|
76
|
+
left: ValuePosition
|
|
77
|
+
right: Annotated[Parameter | LiteralReference, Field(discriminator="type")]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class Conjunction(StrictWireModel):
|
|
81
|
+
type: Literal["and"] = "and"
|
|
82
|
+
conditions: tuple[Condition, ...]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class Disjunction(StrictWireModel):
|
|
86
|
+
type: Literal["or"] = "or"
|
|
87
|
+
conditions: tuple[Condition, ...]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class CorrelatedSubqueryCondition(StrictWireModel):
|
|
91
|
+
type: Literal["correlatedSubquery"] = "correlatedSubquery"
|
|
92
|
+
related: CorrelatedSubquery
|
|
93
|
+
op: CorrelatedSubqueryConditionOperator
|
|
94
|
+
flip: StrictBool | None = None
|
|
95
|
+
scalar: StrictBool | None = None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
type Condition = Annotated[
|
|
99
|
+
SimpleCondition | Conjunction | Disjunction | CorrelatedSubqueryCondition, Field(discriminator="type")
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class Correlation(StrictWireModel):
|
|
104
|
+
parent_field: CompoundKey = Field(alias="parentField")
|
|
105
|
+
child_field: CompoundKey = Field(alias="childField")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class CorrelatedSubquery(StrictWireModel):
|
|
109
|
+
correlation: Correlation
|
|
110
|
+
subquery: AST
|
|
111
|
+
system: System | None = None
|
|
112
|
+
# A hop the client never sees, such as the junction table of a many-to-many relationship.
|
|
113
|
+
hidden: StrictBool | None = None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class Bound(StrictWireModel):
|
|
117
|
+
row: Row
|
|
118
|
+
exclusive: StrictBool
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class AST(StrictWireModel):
|
|
122
|
+
schema_: StrictStr | None = Field(default=None, alias="schema")
|
|
123
|
+
table: StrictStr
|
|
124
|
+
# Set on every subquery, to tell apart two subqueries over the same table.
|
|
125
|
+
alias: StrictStr | None = None
|
|
126
|
+
where: Condition | None = None
|
|
127
|
+
related: tuple[CorrelatedSubquery, ...] | None = None
|
|
128
|
+
limit: Number | None = None
|
|
129
|
+
order_by: Ordering | None = Field(default=None, alias="orderBy")
|
|
130
|
+
start: Bound | None = None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
for _model in (Conjunction, Disjunction, CorrelatedSubqueryCondition, CorrelatedSubquery, AST):
|
|
134
|
+
_model.model_rebuild()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class NameMapper(Protocol):
|
|
138
|
+
def table_name(self, table: str, /) -> str: ...
|
|
139
|
+
|
|
140
|
+
def column_name(self, table: str, column: str, /) -> str: ...
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@dataclass(frozen=True)
|
|
144
|
+
class _Transform:
|
|
145
|
+
table_name: Callable[[str], str]
|
|
146
|
+
column_name: Callable[[str, str], str]
|
|
147
|
+
related: Callable[[list[CorrelatedSubquery]], Sequence[CorrelatedSubquery]]
|
|
148
|
+
where: Callable[[Condition], Condition | None]
|
|
149
|
+
conditions: Callable[[list[Condition]], Sequence[Condition]]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _key(transform: _Transform, table: str, key: Sequence[str]) -> tuple[str, ...]:
|
|
153
|
+
mapped = tuple(transform.column_name(table, column) for column in key)
|
|
154
|
+
check(len(mapped) >= 1, "Expected non-empty array for compound key")
|
|
155
|
+
return mapped
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _transform_ast(ast: AST, transform: _Transform) -> AST:
|
|
159
|
+
def col(column: str) -> str:
|
|
160
|
+
return transform.column_name(ast.table, column)
|
|
161
|
+
|
|
162
|
+
where = transform.where(ast.where) if ast.where is not None else None
|
|
163
|
+
related = (
|
|
164
|
+
tuple(
|
|
165
|
+
transform.related(
|
|
166
|
+
[
|
|
167
|
+
CorrelatedSubquery(
|
|
168
|
+
correlation=Correlation(
|
|
169
|
+
parent_field=_key(transform, ast.table, r.correlation.parent_field),
|
|
170
|
+
child_field=_key(transform, r.subquery.table, r.correlation.child_field),
|
|
171
|
+
),
|
|
172
|
+
hidden=r.hidden,
|
|
173
|
+
subquery=_transform_ast(r.subquery, transform),
|
|
174
|
+
system=r.system,
|
|
175
|
+
)
|
|
176
|
+
for r in ast.related
|
|
177
|
+
]
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
if ast.related is not None
|
|
181
|
+
else None
|
|
182
|
+
)
|
|
183
|
+
return AST(
|
|
184
|
+
schema=ast.schema_,
|
|
185
|
+
table=transform.table_name(ast.table),
|
|
186
|
+
alias=ast.alias,
|
|
187
|
+
where=_transform_where(where, ast.table, transform) if where is not None else None,
|
|
188
|
+
related=related,
|
|
189
|
+
start=(
|
|
190
|
+
Bound(row={col(name): value for name, value in ast.start.row.items()}, exclusive=ast.start.exclusive)
|
|
191
|
+
if ast.start is not None
|
|
192
|
+
else None
|
|
193
|
+
),
|
|
194
|
+
limit=ast.limit,
|
|
195
|
+
orderBy=tuple((col(name), direction) for name, direction in ast.order_by) if ast.order_by is not None else None,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _transform_where(where: Condition, table: str, transform: _Transform) -> Condition:
|
|
200
|
+
match where:
|
|
201
|
+
case SimpleCondition(left=ColumnReference(name=name)):
|
|
202
|
+
return where.model_copy(update={"left": ColumnReference(name=transform.column_name(table, name))})
|
|
203
|
+
case SimpleCondition():
|
|
204
|
+
return where
|
|
205
|
+
case CorrelatedSubqueryCondition(related=related):
|
|
206
|
+
mapped = related.model_copy(
|
|
207
|
+
update={
|
|
208
|
+
"correlation": Correlation(
|
|
209
|
+
parent_field=_key(transform, table, related.correlation.parent_field),
|
|
210
|
+
child_field=_key(transform, related.subquery.table, related.correlation.child_field),
|
|
211
|
+
),
|
|
212
|
+
"subquery": _transform_ast(related.subquery, transform),
|
|
213
|
+
}
|
|
214
|
+
)
|
|
215
|
+
return where.model_copy(update={"related": mapped})
|
|
216
|
+
case _:
|
|
217
|
+
conditions = tuple(transform.conditions([_transform_where(c, table, transform) for c in where.conditions]))
|
|
218
|
+
return type(where)(conditions=conditions)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def map_ast(ast: AST, mapper: NameMapper) -> AST:
|
|
222
|
+
"""Rename every table and column in `ast` through `mapper`, client names to server names upstream."""
|
|
223
|
+
return _transform_ast(ast, _mapping(mapper))
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def map_condition(condition: Condition, table: str, mapper: NameMapper) -> Condition:
|
|
227
|
+
return _transform_where(condition, table, _mapping(mapper))
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _mapping(mapper: NameMapper) -> _Transform:
|
|
231
|
+
return _Transform(
|
|
232
|
+
table_name=mapper.table_name,
|
|
233
|
+
column_name=mapper.column_name,
|
|
234
|
+
related=lambda r: r,
|
|
235
|
+
where=lambda w: w,
|
|
236
|
+
conditions=lambda c: c,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def normalize_ast(ast: AST) -> AST:
|
|
241
|
+
"""The canonical form zero-cache hashes: flattened conditions, sorted conditions and subqueries."""
|
|
242
|
+
return _transform_ast(
|
|
243
|
+
ast,
|
|
244
|
+
_Transform(
|
|
245
|
+
table_name=lambda t: t,
|
|
246
|
+
column_name=lambda _t, c: c,
|
|
247
|
+
related=lambda r: js_sort(r, _cmp_related),
|
|
248
|
+
where=_flattened,
|
|
249
|
+
conditions=lambda c: js_sort(c, _cmp_condition),
|
|
250
|
+
),
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _utf8(value: str) -> bytes:
|
|
255
|
+
return value.encode("utf-8", "surrogatepass")
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _cmp(a: object, b: object) -> int:
|
|
259
|
+
return (a > b) - (a < b) # ty: ignore[unsupported-operator]
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _cmp_utf8(a: str, b: str) -> int:
|
|
263
|
+
return _cmp(_utf8(a), _utf8(b))
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _cmp_value_position(
|
|
267
|
+
a: LiteralReference | ColumnReference | Parameter, b: LiteralReference | ColumnReference | Parameter
|
|
268
|
+
) -> int:
|
|
269
|
+
if a.type != b.type:
|
|
270
|
+
return _cmp_utf8(a.type, b.type)
|
|
271
|
+
match a, b:
|
|
272
|
+
case LiteralReference(), LiteralReference():
|
|
273
|
+
return _cmp_utf8(js_string(a.value), js_string(b.value))
|
|
274
|
+
case ColumnReference(), ColumnReference():
|
|
275
|
+
return _cmp_utf8(a.name, b.name)
|
|
276
|
+
case _:
|
|
277
|
+
raise ValueError("Static parameters should be resolved before normalization")
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _cmp_related(a: CorrelatedSubquery, b: CorrelatedSubquery) -> int:
|
|
281
|
+
return _cmp_utf8(_must(a.subquery.alias), _must(b.subquery.alias))
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _must(alias: str | None) -> str:
|
|
285
|
+
if alias is None:
|
|
286
|
+
raise AssertionFailedError("Unexpected undefined value")
|
|
287
|
+
return alias
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _cmp_optional_bool(a: bool | None, b: bool | None) -> int:
|
|
291
|
+
def rank(v: bool | None) -> int:
|
|
292
|
+
return 0 if v is None else 2 if v else 1
|
|
293
|
+
|
|
294
|
+
return rank(a) - rank(b)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _cmp_condition(a: Condition, b: Condition) -> int:
|
|
298
|
+
if isinstance(a, SimpleCondition):
|
|
299
|
+
if not isinstance(b, SimpleCondition):
|
|
300
|
+
return -1
|
|
301
|
+
return _cmp_value_position(a.left, b.left) or _cmp_utf8(a.op, b.op) or _cmp_value_position(a.right, b.right)
|
|
302
|
+
if isinstance(b, SimpleCondition):
|
|
303
|
+
return 1
|
|
304
|
+
if isinstance(a, CorrelatedSubqueryCondition):
|
|
305
|
+
if not isinstance(b, CorrelatedSubqueryCondition):
|
|
306
|
+
return -1
|
|
307
|
+
return (
|
|
308
|
+
_cmp_related(a.related, b.related)
|
|
309
|
+
or _cmp_utf8(a.op, b.op)
|
|
310
|
+
or _cmp_optional_bool(a.flip, b.flip)
|
|
311
|
+
or _cmp_optional_bool(a.scalar, b.scalar)
|
|
312
|
+
)
|
|
313
|
+
if isinstance(b, CorrelatedSubqueryCondition):
|
|
314
|
+
# Upstream returns -1 here too, so a subquery and a compound condition each sort first.
|
|
315
|
+
return -1
|
|
316
|
+
by_type = _cmp_utf8(a.type, b.type)
|
|
317
|
+
if by_type:
|
|
318
|
+
return by_type
|
|
319
|
+
for left, right in zip(a.conditions, b.conditions, strict=False):
|
|
320
|
+
by_condition = _cmp_condition(left, right)
|
|
321
|
+
if by_condition:
|
|
322
|
+
return by_condition
|
|
323
|
+
return len(a.conditions) - len(b.conditions)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _flattened(condition: Condition) -> Condition | None:
|
|
327
|
+
if isinstance(condition, SimpleCondition | CorrelatedSubqueryCondition):
|
|
328
|
+
return condition
|
|
329
|
+
flat: list[Condition | None] = []
|
|
330
|
+
for c in condition.conditions:
|
|
331
|
+
if isinstance(c, Conjunction | Disjunction) and c.type == condition.type:
|
|
332
|
+
flat.extend(_flattened(inner) for inner in c.conditions)
|
|
333
|
+
else:
|
|
334
|
+
flat.append(_flattened(c))
|
|
335
|
+
conditions = [c for c in flat if c is not None]
|
|
336
|
+
match len(conditions):
|
|
337
|
+
case 0:
|
|
338
|
+
return None
|
|
339
|
+
case 1:
|
|
340
|
+
return conditions[0]
|
|
341
|
+
case _:
|
|
342
|
+
return type(condition)(conditions=tuple(conditions))
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""What zero-cache POSTs to a query endpoint, and what each query becomes.
|
|
2
|
+
|
|
3
|
+
Ported from rocicorp/mono `packages/zero-protocol/src/custom-queries.ts` at zero/v1.10.0
|
|
4
|
+
(905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
from pydantic import StrictStr
|
|
10
|
+
|
|
11
|
+
from zero_server.protocol.ast import AST
|
|
12
|
+
from zero_server.protocol.wire import StrictWireModel, WireModel
|
|
13
|
+
from zero_server.shared.json import JSONValue
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TransformRequestQuery(StrictWireModel):
|
|
17
|
+
"""Parsed in upstream's default (strict) mode, so an undeclared key fails the whole request."""
|
|
18
|
+
|
|
19
|
+
id: StrictStr
|
|
20
|
+
name: StrictStr
|
|
21
|
+
args: list[JSONValue]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
type TransformRequestBody = list[TransformRequestQuery]
|
|
25
|
+
type TransformRequestMessage = tuple[Literal["transform"], TransformRequestBody]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TransformedQuery(WireModel):
|
|
29
|
+
id: StrictStr
|
|
30
|
+
name: StrictStr
|
|
31
|
+
ast: AST
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AppErroredQuery(WireModel):
|
|
35
|
+
error: Literal["app"] = "app"
|
|
36
|
+
id: StrictStr
|
|
37
|
+
name: StrictStr
|
|
38
|
+
message: StrictStr | None = None
|
|
39
|
+
details: JSONValue | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ParseErroredQuery(WireModel):
|
|
43
|
+
error: Literal["parse"] = "parse"
|
|
44
|
+
id: StrictStr
|
|
45
|
+
name: StrictStr
|
|
46
|
+
message: StrictStr
|
|
47
|
+
details: JSONValue | None = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
type ErroredQuery = AppErroredQuery | ParseErroredQuery
|
|
51
|
+
type TransformResponseBody = list[TransformedQuery | ErroredQuery]
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Error kinds, origins and reasons, and the bodies of a failed push or transform.
|
|
2
|
+
|
|
3
|
+
Ported from rocicorp/mono `packages/zero-protocol/src/error.ts`, `error-kind-enum.ts`,
|
|
4
|
+
`error-origin-enum.ts` and `error-reason-enum.ts` at zero/v1.10.0
|
|
5
|
+
(905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from enum import StrEnum
|
|
9
|
+
from typing import Literal
|
|
10
|
+
|
|
11
|
+
from pydantic import Field, StrictStr
|
|
12
|
+
|
|
13
|
+
from zero_server.protocol.mutation_id import MutationID
|
|
14
|
+
from zero_server.protocol.wire import Number, WireModel
|
|
15
|
+
from zero_server.shared.json import JSONValue
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ErrorKind(StrEnum):
|
|
19
|
+
AUTH_INVALIDATED = "AuthInvalidated"
|
|
20
|
+
CLIENT_NOT_FOUND = "ClientNotFound"
|
|
21
|
+
INVALID_CONNECTION_REQUEST = "InvalidConnectionRequest"
|
|
22
|
+
INVALID_CONNECTION_REQUEST_BASE_COOKIE = "InvalidConnectionRequestBaseCookie"
|
|
23
|
+
INVALID_CONNECTION_REQUEST_LAST_MUTATION_ID = "InvalidConnectionRequestLastMutationID"
|
|
24
|
+
INVALID_CONNECTION_REQUEST_CLIENT_DELETED = "InvalidConnectionRequestClientDeleted"
|
|
25
|
+
INVALID_MESSAGE = "InvalidMessage"
|
|
26
|
+
INVALID_PUSH = "InvalidPush"
|
|
27
|
+
PUSH_FAILED = "PushFailed"
|
|
28
|
+
MUTATION_FAILED = "MutationFailed"
|
|
29
|
+
MUTATION_RATE_LIMITED = "MutationRateLimited"
|
|
30
|
+
REBALANCE = "Rebalance"
|
|
31
|
+
REHOME = "Rehome"
|
|
32
|
+
TRANSFORM_FAILED = "TransformFailed"
|
|
33
|
+
UNAUTHORIZED = "Unauthorized"
|
|
34
|
+
VERSION_NOT_SUPPORTED = "VersionNotSupported"
|
|
35
|
+
SCHEMA_VERSION_NOT_SUPPORTED = "SchemaVersionNotSupported"
|
|
36
|
+
SERVER_OVERLOADED = "ServerOverloaded"
|
|
37
|
+
INTERNAL = "Internal"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ErrorOrigin(StrEnum):
|
|
41
|
+
CLIENT = "client"
|
|
42
|
+
SERVER = "server"
|
|
43
|
+
ZERO_CACHE = "zeroCache"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ErrorReason(StrEnum):
|
|
47
|
+
DATABASE = "database"
|
|
48
|
+
PARSE = "parse"
|
|
49
|
+
OUT_OF_ORDER_MUTATION = "oooMutation"
|
|
50
|
+
UNSUPPORTED_PUSH_VERSION = "unsupportedPushVersion"
|
|
51
|
+
INTERNAL = "internal"
|
|
52
|
+
HTTP = "http"
|
|
53
|
+
TIMEOUT = "timeout"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
type ServerPushFailedReason = Literal[
|
|
57
|
+
ErrorReason.DATABASE,
|
|
58
|
+
ErrorReason.PARSE,
|
|
59
|
+
ErrorReason.OUT_OF_ORDER_MUTATION,
|
|
60
|
+
ErrorReason.UNSUPPORTED_PUSH_VERSION,
|
|
61
|
+
ErrorReason.INTERNAL,
|
|
62
|
+
]
|
|
63
|
+
type ServerTransformFailedReason = Literal[ErrorReason.DATABASE, ErrorReason.PARSE, ErrorReason.INTERNAL]
|
|
64
|
+
type ZeroCacheFailedReason = Literal[ErrorReason.TIMEOUT, ErrorReason.PARSE, ErrorReason.INTERNAL]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class _PushFailedBase(WireModel):
|
|
68
|
+
kind: Literal[ErrorKind.PUSH_FAILED] = ErrorKind.PUSH_FAILED
|
|
69
|
+
details: JSONValue | None = None
|
|
70
|
+
# The mutations that went unprocessed, which can be a tail of the request's.
|
|
71
|
+
mutation_ids: list[MutationID] = Field(alias="mutationIDs")
|
|
72
|
+
message: StrictStr
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ServerPushFailed(_PushFailedBase):
|
|
76
|
+
origin: Literal[ErrorOrigin.SERVER] = ErrorOrigin.SERVER
|
|
77
|
+
reason: ServerPushFailedReason
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ZeroCachePushHTTPFailed(_PushFailedBase):
|
|
81
|
+
origin: Literal[ErrorOrigin.ZERO_CACHE]
|
|
82
|
+
reason: Literal[ErrorReason.HTTP]
|
|
83
|
+
status: Number
|
|
84
|
+
body_preview: StrictStr | None = Field(default=None, alias="bodyPreview")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ZeroCachePushFailed(_PushFailedBase):
|
|
88
|
+
origin: Literal[ErrorOrigin.ZERO_CACHE]
|
|
89
|
+
reason: ZeroCacheFailedReason
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
type PushFailedBody = ServerPushFailed | ZeroCachePushHTTPFailed | ZeroCachePushFailed
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class _TransformFailedBase(WireModel):
|
|
96
|
+
kind: Literal[ErrorKind.TRANSFORM_FAILED] = ErrorKind.TRANSFORM_FAILED
|
|
97
|
+
details: JSONValue | None = None
|
|
98
|
+
query_ids: list[StrictStr] = Field(alias="queryIDs")
|
|
99
|
+
message: StrictStr
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class ServerTransformFailed(_TransformFailedBase):
|
|
103
|
+
origin: Literal[ErrorOrigin.SERVER] = ErrorOrigin.SERVER
|
|
104
|
+
reason: ServerTransformFailedReason
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ZeroCacheTransformHTTPFailed(_TransformFailedBase):
|
|
108
|
+
origin: Literal[ErrorOrigin.ZERO_CACHE]
|
|
109
|
+
reason: Literal[ErrorReason.HTTP]
|
|
110
|
+
status: Number
|
|
111
|
+
body_preview: StrictStr | None = Field(default=None, alias="bodyPreview")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ZeroCacheTransformFailed(_TransformFailedBase):
|
|
115
|
+
origin: Literal[ErrorOrigin.ZERO_CACHE]
|
|
116
|
+
reason: ZeroCacheFailedReason
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
type TransformFailedBody = ServerTransformFailed | ZeroCacheTransformHTTPFailed | ZeroCacheTransformFailed
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""The mutate endpoint's response and query string.
|
|
2
|
+
|
|
3
|
+
Ported from rocicorp/mono `packages/zero-protocol/src/mutate-server.ts` at zero/v1.10.0
|
|
4
|
+
(905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
from pydantic import Field, StrictStr
|
|
10
|
+
|
|
11
|
+
from zero_server.protocol.error import PushFailedBody
|
|
12
|
+
from zero_server.protocol.mutation import MutationResponse
|
|
13
|
+
from zero_server.protocol.wire import WireModel
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MutateSuccess(WireModel):
|
|
17
|
+
"""`user_id` is reported to zero-cache only when the caller set it, `None` included."""
|
|
18
|
+
|
|
19
|
+
nullable = frozenset({"user_id"})
|
|
20
|
+
|
|
21
|
+
kind: Literal["MutateResponse"] = "MutateResponse"
|
|
22
|
+
user_id: StrictStr | None = Field(default=None, alias="userID")
|
|
23
|
+
mutations: list[MutationResponse]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
type MutateResponse = MutateSuccess | PushFailedBody
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MutateParams(WireModel):
|
|
30
|
+
"""The query string zero-cache appends: the upstream schema holding its bookkeeping, and the app."""
|
|
31
|
+
|
|
32
|
+
schema_: StrictStr = Field(alias="schema")
|
|
33
|
+
app_id: StrictStr = Field(alias="appID")
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Mutations as a push carries them, and the result recorded for each.
|
|
2
|
+
|
|
3
|
+
Ported from rocicorp/mono `packages/zero-protocol/src/mutation.ts` and `mutation-type-enum.ts` at
|
|
4
|
+
zero/v1.10.0 (905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Annotated, Literal
|
|
8
|
+
|
|
9
|
+
from pydantic import Field, StrictBool, StrictStr
|
|
10
|
+
|
|
11
|
+
from zero_server.protocol.ast import PrimaryKey, Row
|
|
12
|
+
from zero_server.protocol.mutation_id import MutationID
|
|
13
|
+
from zero_server.protocol.wire import Number, StrictWireModel, WireModel
|
|
14
|
+
from zero_server.shared.json import JSONValue
|
|
15
|
+
|
|
16
|
+
CRUD_MUTATION_NAME = "_zero_crud"
|
|
17
|
+
|
|
18
|
+
# zero-cache sends this to delete results its clients have acknowledged. It is not a client mutation.
|
|
19
|
+
CLEANUP_RESULTS_MUTATION_NAME = "_zero_cleanupResults"
|
|
20
|
+
|
|
21
|
+
type NonEmptyKey = Annotated[PrimaryKey, Field(min_length=1)]
|
|
22
|
+
type PrimaryKeyValue = StrictStr | Number | StrictBool
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CleanupResultsLegacyArg(StrictWireModel):
|
|
26
|
+
"""The oldest form, with no `type`. Treated as `single`."""
|
|
27
|
+
|
|
28
|
+
client_group_id: StrictStr = Field(alias="clientGroupID")
|
|
29
|
+
client_id: StrictStr = Field(alias="clientID")
|
|
30
|
+
up_to_mutation_id: Number = Field(alias="upToMutationID")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CleanupResultsSingleArg(StrictWireModel):
|
|
34
|
+
type: Literal["single"]
|
|
35
|
+
client_group_id: StrictStr = Field(alias="clientGroupID")
|
|
36
|
+
client_id: StrictStr = Field(alias="clientID")
|
|
37
|
+
up_to_mutation_id: Number = Field(alias="upToMutationID")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class CleanupResultsBulkArg(StrictWireModel):
|
|
41
|
+
type: Literal["bulk"]
|
|
42
|
+
client_group_id: StrictStr = Field(alias="clientGroupID")
|
|
43
|
+
client_ids: Annotated[list[StrictStr], Field(min_length=1)] = Field(alias="clientIDs")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
type CleanupResultsArg = CleanupResultsLegacyArg | CleanupResultsSingleArg | CleanupResultsBulkArg
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class InsertOp(WireModel):
|
|
50
|
+
op: Literal["insert"]
|
|
51
|
+
table_name: StrictStr = Field(alias="tableName")
|
|
52
|
+
primary_key: NonEmptyKey = Field(alias="primaryKey")
|
|
53
|
+
value: Row
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class UpsertOp(WireModel):
|
|
57
|
+
op: Literal["upsert"]
|
|
58
|
+
table_name: StrictStr = Field(alias="tableName")
|
|
59
|
+
primary_key: NonEmptyKey = Field(alias="primaryKey")
|
|
60
|
+
value: Row
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class UpdateOp(WireModel):
|
|
64
|
+
op: Literal["update"]
|
|
65
|
+
table_name: StrictStr = Field(alias="tableName")
|
|
66
|
+
primary_key: NonEmptyKey = Field(alias="primaryKey")
|
|
67
|
+
value: Row
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class DeleteOp(WireModel):
|
|
71
|
+
op: Literal["delete"]
|
|
72
|
+
table_name: StrictStr = Field(alias="tableName")
|
|
73
|
+
primary_key: NonEmptyKey = Field(alias="primaryKey")
|
|
74
|
+
value: dict[str, PrimaryKeyValue]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
type CRUDOp = Annotated[InsertOp | UpsertOp | UpdateOp | DeleteOp, Field(discriminator="op")]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class CRUDMutationArg(WireModel):
|
|
81
|
+
ops: list[CRUDOp]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class CRUDMutation(WireModel):
|
|
85
|
+
type: Literal["crud"]
|
|
86
|
+
id: Number
|
|
87
|
+
client_id: StrictStr = Field(alias="clientID")
|
|
88
|
+
name: Literal["_zero_crud"]
|
|
89
|
+
args: tuple[CRUDMutationArg]
|
|
90
|
+
timestamp: Number
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class CustomMutation(WireModel):
|
|
94
|
+
type: Literal["custom"]
|
|
95
|
+
id: Number
|
|
96
|
+
client_id: StrictStr = Field(alias="clientID")
|
|
97
|
+
name: StrictStr
|
|
98
|
+
args: list[JSONValue]
|
|
99
|
+
timestamp: Number
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
type Mutation = Annotated[CRUDMutation | CustomMutation, Field(discriminator="type")]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class MutationOk(WireModel):
|
|
106
|
+
# Whatever the mutator returned, if anything.
|
|
107
|
+
data: JSONValue | None = None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class MutationAppError(WireModel):
|
|
111
|
+
error: Literal["app"] = "app"
|
|
112
|
+
message: StrictStr | None = None
|
|
113
|
+
details: JSONValue | None = None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class MutationZeroError(WireModel):
|
|
117
|
+
# `oooMutation` is reported as a failed push now; the literal stays for older peers.
|
|
118
|
+
error: Literal["oooMutation", "alreadyProcessed"]
|
|
119
|
+
details: JSONValue | None = None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
type MutationResult = MutationAppError | MutationZeroError | MutationOk
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class MutationResponse(WireModel):
|
|
126
|
+
id: MutationID
|
|
127
|
+
result: MutationResult
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Ported from rocicorp/mono `packages/zero-protocol/src/mutation-id.ts` at zero/v1.10.0
|
|
2
|
+
(905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pydantic import Field, StrictStr
|
|
6
|
+
|
|
7
|
+
from zero_server.protocol.wire import Number, WireModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MutationID(WireModel):
|
|
11
|
+
id: Number
|
|
12
|
+
client_id: StrictStr = Field(alias="clientID")
|