@syncular/server 0.15.21 → 0.15.23
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/README.md +39 -0
- package/dist/d1-storage.d.ts +2 -1
- package/dist/d1-storage.js +51 -1
- package/dist/events.d.ts +10 -0
- package/dist/handler.js +21 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/postgres-storage.d.ts +2 -1
- package/dist/postgres-storage.js +29 -1
- package/dist/push.d.ts +13 -0
- package/dist/push.js +54 -14
- package/dist/relational-rows.d.ts +11 -1
- package/dist/relational-rows.js +29 -0
- package/dist/schema.js +3 -0
- package/dist/seed.d.ts +27 -2
- package/dist/seed.js +71 -7
- package/dist/sqlite-dialect.js +12 -0
- package/dist/sqlite-storage.d.ts +2 -1
- package/dist/sqlite-storage.js +17 -1
- package/dist/storage-errors.d.ts +11 -0
- package/dist/storage-errors.js +19 -0
- package/dist/storage-query.d.ts +6 -0
- package/dist/storage-query.js +30 -0
- package/dist/storage.d.ts +36 -1
- package/package.json +2 -2
- package/src/d1-storage.ts +81 -0
- package/src/events.ts +10 -0
- package/src/handler.ts +21 -5
- package/src/index.ts +4 -0
- package/src/postgres-storage.ts +61 -0
- package/src/push.ts +91 -17
- package/src/relational-rows.ts +45 -1
- package/src/schema.ts +5 -0
- package/src/seed.ts +95 -10
- package/src/sqlite-dialect.ts +14 -0
- package/src/sqlite-storage.ts +38 -0
- package/src/storage-errors.ts +36 -0
- package/src/storage-query.ts +48 -0
- package/src/storage.ts +41 -1
package/src/storage-errors.ts
CHANGED
|
@@ -13,6 +13,42 @@ export class StorageConstraintError extends Error {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/** Stable, privacy-safe failures for trusted server storage queries. */
|
|
17
|
+
export type StorageQueryErrorCode =
|
|
18
|
+
| 'sync.storage.scan_requires_scope'
|
|
19
|
+
| 'sync.storage.index_not_found'
|
|
20
|
+
| 'sync.storage.index_not_materialized'
|
|
21
|
+
| 'sync.storage.index_value_count_mismatch'
|
|
22
|
+
| 'sync.storage.invalid_limit';
|
|
23
|
+
|
|
24
|
+
const STORAGE_QUERY_MESSAGES: Readonly<Record<StorageQueryErrorCode, string>> =
|
|
25
|
+
{
|
|
26
|
+
'sync.storage.scan_requires_scope':
|
|
27
|
+
'scope-indexed row scans require at least one scope variable',
|
|
28
|
+
'sync.storage.index_not_found':
|
|
29
|
+
'trusted row lookup requires a declared relational index',
|
|
30
|
+
'sync.storage.index_not_materialized':
|
|
31
|
+
'trusted row lookup requires a materialized relational table',
|
|
32
|
+
'sync.storage.index_value_count_mismatch':
|
|
33
|
+
'trusted row lookup requires one exact value per index column',
|
|
34
|
+
'sync.storage.invalid_limit':
|
|
35
|
+
'trusted row lookup limit must be an integer from 1 through 1,000',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Host-only query error. Messages never include identifiers, values, SQL,
|
|
40
|
+
* paths, or row data; callers branch on `code`, never message text.
|
|
41
|
+
*/
|
|
42
|
+
export class StorageQueryError extends Error {
|
|
43
|
+
override readonly name = 'StorageQueryError';
|
|
44
|
+
readonly code: StorageQueryErrorCode;
|
|
45
|
+
|
|
46
|
+
constructor(code: StorageQueryErrorCode) {
|
|
47
|
+
super(STORAGE_QUERY_MESSAGES[code]);
|
|
48
|
+
this.code = code;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
16
52
|
interface DriverError {
|
|
17
53
|
readonly code?: unknown;
|
|
18
54
|
readonly errno?: unknown;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ScopeMap } from '@syncular/core';
|
|
2
|
+
import type { CompiledTable, IndexSchema } from './schema';
|
|
3
|
+
import type { IndexRowScanQuery, RowScanQuery } from './storage';
|
|
4
|
+
import { StorageQueryError } from './storage-errors';
|
|
5
|
+
|
|
6
|
+
/** Fail loudly instead of making an unsupported unscoped scan look empty. */
|
|
7
|
+
export function assertScopeIndexedScan(query: RowScanQuery): void {
|
|
8
|
+
const scopeFilter = (
|
|
9
|
+
query as RowScanQuery & { readonly scopeFilter?: ScopeMap | null }
|
|
10
|
+
).scopeFilter;
|
|
11
|
+
if (
|
|
12
|
+
scopeFilter === undefined ||
|
|
13
|
+
scopeFilter === null ||
|
|
14
|
+
Object.keys(scopeFilter).length === 0
|
|
15
|
+
) {
|
|
16
|
+
throw new StorageQueryError('sync.storage.scan_requires_scope');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Validate and resolve one exact trusted-host relational index lookup. */
|
|
21
|
+
export function resolveIndexRowScan(
|
|
22
|
+
table: CompiledTable,
|
|
23
|
+
query: IndexRowScanQuery,
|
|
24
|
+
): IndexSchema {
|
|
25
|
+
if (
|
|
26
|
+
!Number.isInteger(query.limit) ||
|
|
27
|
+
query.limit < 1 ||
|
|
28
|
+
query.limit > 1_000
|
|
29
|
+
) {
|
|
30
|
+
throw new StorageQueryError('sync.storage.invalid_limit');
|
|
31
|
+
}
|
|
32
|
+
if (!table.materialize) {
|
|
33
|
+
throw new StorageQueryError('sync.storage.index_not_materialized');
|
|
34
|
+
}
|
|
35
|
+
const index = table.indexes.find(
|
|
36
|
+
(candidate) => candidate.name === query.index,
|
|
37
|
+
);
|
|
38
|
+
if (index === undefined) {
|
|
39
|
+
throw new StorageQueryError('sync.storage.index_not_found');
|
|
40
|
+
}
|
|
41
|
+
if (
|
|
42
|
+
!Array.isArray(query.values) ||
|
|
43
|
+
query.values.length !== index.columns.length
|
|
44
|
+
) {
|
|
45
|
+
throw new StorageQueryError('sync.storage.index_value_count_mismatch');
|
|
46
|
+
}
|
|
47
|
+
return index;
|
|
48
|
+
}
|
package/src/storage.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* The interface is async throughout so a Postgres implementation slots in
|
|
17
17
|
* without touching the core. All methods are partition-local (§2.1).
|
|
18
18
|
*/
|
|
19
|
-
import type { PushOperationResult, ScopeMap } from '@syncular/core';
|
|
19
|
+
import type { PushOperationResult, RowValue, ScopeMap } from '@syncular/core';
|
|
20
20
|
import type { CompiledSchema } from './schema';
|
|
21
21
|
|
|
22
22
|
/** The current stored state of a synced row. */
|
|
@@ -70,6 +70,10 @@ export interface StoredPushResult {
|
|
|
70
70
|
readonly status: 'applied' | 'rejected';
|
|
71
71
|
/** Present iff `status` is `applied`. */
|
|
72
72
|
readonly commitSeq?: number;
|
|
73
|
+
/** Host clock when this terminal idempotency outcome was first recorded. */
|
|
74
|
+
readonly recordedAtMs?: number;
|
|
75
|
+
/** Privacy-safe identity used to distinguish this stored outcome from a race. */
|
|
76
|
+
readonly cacheIdentity?: string;
|
|
73
77
|
readonly results: readonly PushOperationResult[];
|
|
74
78
|
}
|
|
75
79
|
|
|
@@ -113,6 +117,26 @@ export interface RowScanQuery {
|
|
|
113
117
|
readonly limit: number;
|
|
114
118
|
}
|
|
115
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Exact server-host lookup through one declared relational index.
|
|
122
|
+
*
|
|
123
|
+
* This is deliberately NOT a Syncular scope or client query. It is available
|
|
124
|
+
* only to trusted server code that already owns a `ServerStorage` or
|
|
125
|
+
* `StorageTransaction` capability. Every declared index column must have one
|
|
126
|
+
* exact value, so adapters can keep the lookup bounded and deterministic.
|
|
127
|
+
*/
|
|
128
|
+
export interface IndexRowScanQuery {
|
|
129
|
+
readonly table: string;
|
|
130
|
+
/** `TableSchema.indexes[].name`; never exposed as a client subscription. */
|
|
131
|
+
readonly index: string;
|
|
132
|
+
/** Exact values in the index declaration's column order. */
|
|
133
|
+
readonly values: readonly RowValue[];
|
|
134
|
+
/** Resume after this rowId (exclusive); `null` = start of the match set. */
|
|
135
|
+
readonly afterRowId?: string | null;
|
|
136
|
+
/** Integer from 1 through 1,000. */
|
|
137
|
+
readonly limit: number;
|
|
138
|
+
}
|
|
139
|
+
|
|
116
140
|
export interface ClientCursorInfo {
|
|
117
141
|
readonly clientId: string;
|
|
118
142
|
readonly cursor: number;
|
|
@@ -172,6 +196,12 @@ export interface StorageTransaction {
|
|
|
172
196
|
* semantics. A custom backend may omit it until `commitValidator` is used.
|
|
173
197
|
*/
|
|
174
198
|
scanRows?(query: RowScanQuery): Promise<StoredRow[]>;
|
|
199
|
+
/**
|
|
200
|
+
* Optional additive capability for trusted authoritative commands. In-tree
|
|
201
|
+
* SQLite/PostgreSQL/D1 adapters implement it with transaction-local
|
|
202
|
+
* read-your-own-writes semantics. It is not reachable from SSP2 requests.
|
|
203
|
+
*/
|
|
204
|
+
scanRowsByIndex?(query: IndexRowScanQuery): Promise<StoredRow[]>;
|
|
175
205
|
/**
|
|
176
206
|
* Serialize candidate-state validation for this partition before any row
|
|
177
207
|
* read/write. Required at runtime when `commitValidator` is configured.
|
|
@@ -279,6 +309,16 @@ export interface ServerStorage {
|
|
|
279
309
|
/** Scope-filtered snapshot scan, ordered by rowId (bootstrap paging). */
|
|
280
310
|
scanRows(partition: string, query: RowScanQuery): Promise<StoredRow[]>;
|
|
281
311
|
|
|
312
|
+
/**
|
|
313
|
+
* Optional trusted-host exact lookup through a declared relational index.
|
|
314
|
+
* This capability is outside client scope/subscription authorization and
|
|
315
|
+
* MUST NOT be re-exported as a client-controlled endpoint.
|
|
316
|
+
*/
|
|
317
|
+
scanRowsByIndex?(
|
|
318
|
+
partition: string,
|
|
319
|
+
query: IndexRowScanQuery,
|
|
320
|
+
): Promise<StoredRow[]>;
|
|
321
|
+
|
|
282
322
|
getClientRecord(
|
|
283
323
|
partition: string,
|
|
284
324
|
clientId: string,
|