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.
Files changed (50) hide show
  1. zero_server/__init__.py +36 -0
  2. zero_server/codegen.py +133 -0
  3. zero_server/integrations/__init__.py +1 -0
  4. zero_server/integrations/starlette.py +115 -0
  5. zero_server/mutators.py +87 -0
  6. zero_server/protocol/__init__.py +1 -0
  7. zero_server/protocol/application_error.py +36 -0
  8. zero_server/protocol/ast.py +342 -0
  9. zero_server/protocol/custom_queries.py +51 -0
  10. zero_server/protocol/error.py +119 -0
  11. zero_server/protocol/mutate_server.py +33 -0
  12. zero_server/protocol/mutation.py +127 -0
  13. zero_server/protocol/mutation_id.py +12 -0
  14. zero_server/protocol/push.py +25 -0
  15. zero_server/protocol/query_server.py +29 -0
  16. zero_server/protocol/wire.py +88 -0
  17. zero_server/py.typed +0 -0
  18. zero_server/query/__init__.py +1 -0
  19. zero_server/query/builder.py +338 -0
  20. zero_server/query/expression.py +167 -0
  21. zero_server/query/registry.py +143 -0
  22. zero_server/schema.py +142 -0
  23. zero_server/server/__init__.py +1 -0
  24. zero_server/server/adapters/__init__.py +1 -0
  25. zero_server/server/adapters/asyncpg.py +43 -0
  26. zero_server/server/adapters/psycopg.py +55 -0
  27. zero_server/server/adapters/sqlalchemy.py +49 -0
  28. zero_server/server/crud.py +134 -0
  29. zero_server/server/pg_query_executor.py +70 -0
  30. zero_server/server/pg_types.py +69 -0
  31. zero_server/server/process_mutations.py +412 -0
  32. zero_server/server/process_queries.py +109 -0
  33. zero_server/server/push_processor.py +49 -0
  34. zero_server/server/server_schema.py +165 -0
  35. zero_server/server/transaction.py +70 -0
  36. zero_server/server/zql_database.py +129 -0
  37. zero_server/shared/__init__.py +1 -0
  38. zero_server/shared/asserts.py +19 -0
  39. zero_server/shared/error.py +97 -0
  40. zero_server/shared/js.py +121 -0
  41. zero_server/shared/json.py +27 -0
  42. zero_server/shared/registry.py +64 -0
  43. zero_server/z2s/__init__.py +1 -0
  44. zero_server/z2s/compiler.py +375 -0
  45. zero_server/z2s/sql.py +130 -0
  46. zero_server-0.0.1.dist-info/METADATA +134 -0
  47. zero_server-0.0.1.dist-info/RECORD +50 -0
  48. zero_server-0.0.1.dist-info/WHEEL +4 -0
  49. zero_server-0.0.1.dist-info/entry_points.txt +3 -0
  50. 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")