@syncular/server 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/validate.ts CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  type RejectionDetails,
19
19
  type RowColumn,
20
20
  type RowValue,
21
+ type ScopeMap,
21
22
  } from '@syncular/core';
22
23
 
23
24
  /**
@@ -64,6 +65,63 @@ export interface ValidateOperation {
64
65
  readonly stored: ValidateRow | undefined;
65
66
  }
66
67
 
68
+ /**
69
+ * One authorized, decoded operation presented to the whole-commit validator.
70
+ * `row` is the final candidate row after scope stripping and CRDT merge;
71
+ * `stored` is the state observed immediately before this operation. Multiple
72
+ * operations targeting one row therefore retain their sequential evidence.
73
+ */
74
+ export interface ValidateCommitOperation extends ValidateOperation {
75
+ readonly opIndex: number;
76
+ readonly storedServerVersion?: number;
77
+ readonly nextServerVersion?: number;
78
+ }
79
+
80
+ /** One candidate-state row read from inside the still-open commit transaction. */
81
+ export interface CommitValidationRow {
82
+ readonly row: ValidateRow;
83
+ readonly serverVersion: number;
84
+ }
85
+
86
+ export interface CommitValidationScanInput {
87
+ readonly table: string;
88
+ /** Exact scope filter, using the same AND-across-keys semantics as sync. */
89
+ readonly scopeFilter: ScopeMap;
90
+ readonly afterRowId?: string | null;
91
+ /** Bounded per call by the server to 1..1,000; defaults to 100. */
92
+ readonly limit?: number;
93
+ }
94
+
95
+ /**
96
+ * Candidate-state reads bound to the same storage transaction as the commit.
97
+ * Reads observe every staged sibling operation and no uncommitted competing
98
+ * transaction when the storage's commit-validation lock contract is honored.
99
+ */
100
+ export interface CommitValidationReader {
101
+ getRow(
102
+ table: string,
103
+ rowId: string,
104
+ ): Promise<CommitValidationRow | undefined>;
105
+ scanRows(input: CommitValidationScanInput): Promise<CommitValidationRow[]>;
106
+ }
107
+
108
+ export interface ValidateCommitInput {
109
+ readonly clientId: string;
110
+ readonly clientCommitId: string;
111
+ readonly actorId: string;
112
+ readonly partition: string;
113
+ readonly operations: readonly ValidateCommitOperation[];
114
+ readonly read: CommitValidationReader;
115
+ }
116
+
117
+ /**
118
+ * Runs once after every operation passed protocol/scope/row validation and was
119
+ * staged, but before commit-log/idempotency append and transaction commit.
120
+ */
121
+ export type CommitValidator = (
122
+ input: ValidateCommitInput,
123
+ ) => void | Promise<void>;
124
+
67
125
  /** Ambient context a validator may consult (§6.7). */
68
126
  export interface ValidateContext {
69
127
  /** Host-authenticated actor (§1.1) performing the write. */
@@ -122,6 +180,27 @@ export class ValidationRejection extends Error {
122
180
  }
123
181
  }
124
182
 
183
+ /**
184
+ * A whole-commit rejection attributed to one operation for the existing
185
+ * per-operation PUSH_RESULT envelope. The validator may still describe
186
+ * multiple affected fields in `details.fieldPaths`.
187
+ */
188
+ export class CommitValidationRejection extends ValidationRejection {
189
+ constructor(
190
+ readonly opIndex: number,
191
+ code: string,
192
+ message?: string,
193
+ details?: RejectionDetails,
194
+ ) {
195
+ super(code, message, details);
196
+ if (!Number.isSafeInteger(opIndex) || opIndex < 0) {
197
+ throw new Error(
198
+ 'CommitValidationRejection opIndex must be a non-negative safe integer',
199
+ );
200
+ }
201
+ }
202
+ }
203
+
125
204
  /** Build the column-keyed row object a validator inspects (§6.7). */
126
205
  export function toValidateRow(
127
206
  columns: readonly RowColumn[],