@syncular/core 0.0.1-60
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/dist/blobs.d.ts +137 -0
- package/dist/blobs.d.ts.map +1 -0
- package/dist/blobs.js +47 -0
- package/dist/blobs.js.map +1 -0
- package/dist/conflict.d.ts +22 -0
- package/dist/conflict.d.ts.map +1 -0
- package/dist/conflict.js +81 -0
- package/dist/conflict.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/kysely-serialize.d.ts +22 -0
- package/dist/kysely-serialize.d.ts.map +1 -0
- package/dist/kysely-serialize.js +147 -0
- package/dist/kysely-serialize.js.map +1 -0
- package/dist/logger.d.ts +46 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +48 -0
- package/dist/logger.js.map +1 -0
- package/dist/proxy/index.d.ts +5 -0
- package/dist/proxy/index.d.ts.map +1 -0
- package/dist/proxy/index.js +5 -0
- package/dist/proxy/index.js.map +1 -0
- package/dist/proxy/types.d.ts +54 -0
- package/dist/proxy/types.d.ts.map +1 -0
- package/dist/proxy/types.js +7 -0
- package/dist/proxy/types.js.map +1 -0
- package/dist/schemas/blobs.d.ts +76 -0
- package/dist/schemas/blobs.d.ts.map +1 -0
- package/dist/schemas/blobs.js +63 -0
- package/dist/schemas/blobs.js.map +1 -0
- package/dist/schemas/common.d.ts +28 -0
- package/dist/schemas/common.d.ts.map +1 -0
- package/dist/schemas/common.js +26 -0
- package/dist/schemas/common.js.map +1 -0
- package/dist/schemas/index.d.ts +7 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +7 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/sync.d.ts +391 -0
- package/dist/schemas/sync.d.ts.map +1 -0
- package/dist/schemas/sync.js +156 -0
- package/dist/schemas/sync.js.map +1 -0
- package/dist/scopes/index.d.ts +65 -0
- package/dist/scopes/index.d.ts.map +1 -0
- package/dist/scopes/index.js +67 -0
- package/dist/scopes/index.js.map +1 -0
- package/dist/transforms.d.ts +146 -0
- package/dist/transforms.d.ts.map +1 -0
- package/dist/transforms.js +155 -0
- package/dist/transforms.js.map +1 -0
- package/dist/types.d.ts +129 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +20 -0
- package/dist/types.js.map +1 -0
- package/package.json +56 -0
- package/src/__tests__/conflict.test.ts +325 -0
- package/src/blobs.ts +187 -0
- package/src/conflict.ts +92 -0
- package/src/index.ts +30 -0
- package/src/kysely-serialize.ts +214 -0
- package/src/logger.ts +80 -0
- package/src/proxy/index.ts +10 -0
- package/src/proxy/types.ts +57 -0
- package/src/schemas/blobs.ts +101 -0
- package/src/schemas/common.ts +45 -0
- package/src/schemas/index.ts +7 -0
- package/src/schemas/sync.ts +222 -0
- package/src/scopes/index.ts +122 -0
- package/src/transforms.ts +256 -0
- package/src/types.ts +158 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ColumnUpdateNode,
|
|
3
|
+
type KyselyPlugin,
|
|
4
|
+
OperationNodeTransformer,
|
|
5
|
+
type PluginTransformQueryArgs,
|
|
6
|
+
type PluginTransformResultArgs,
|
|
7
|
+
type PrimitiveValueListNode,
|
|
8
|
+
type QueryResult,
|
|
9
|
+
type RootOperationNode,
|
|
10
|
+
type UnknownRow,
|
|
11
|
+
type ValueNode,
|
|
12
|
+
} from 'kysely';
|
|
13
|
+
|
|
14
|
+
type Serializer = (parameter: unknown) => unknown;
|
|
15
|
+
type Deserializer = (parameter: unknown) => unknown;
|
|
16
|
+
|
|
17
|
+
const dateRegex = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?$/;
|
|
18
|
+
|
|
19
|
+
function isBufferLike(value: object): value is { buffer: unknown } {
|
|
20
|
+
return 'buffer' in value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function skipTransform(parameter: unknown): boolean {
|
|
24
|
+
if (
|
|
25
|
+
parameter === undefined ||
|
|
26
|
+
parameter === null ||
|
|
27
|
+
typeof parameter === 'bigint' ||
|
|
28
|
+
typeof parameter === 'number'
|
|
29
|
+
) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (typeof parameter === 'object') {
|
|
34
|
+
return isBufferLike(parameter);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function maybeJson(parameter: string): boolean {
|
|
41
|
+
return (
|
|
42
|
+
(parameter.startsWith('{') && parameter.endsWith('}')) ||
|
|
43
|
+
(parameter.startsWith('[') && parameter.endsWith(']'))
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const defaultSerializer: Serializer = (parameter) => {
|
|
48
|
+
if (skipTransform(parameter) || typeof parameter === 'string') {
|
|
49
|
+
return parameter;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (typeof parameter === 'boolean') {
|
|
53
|
+
return String(parameter);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (parameter instanceof Date) {
|
|
57
|
+
return parameter.toISOString();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
return JSON.stringify(parameter);
|
|
62
|
+
} catch {
|
|
63
|
+
return parameter;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const defaultDeserializer: Deserializer = (parameter) => {
|
|
68
|
+
if (skipTransform(parameter)) {
|
|
69
|
+
return parameter;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (typeof parameter !== 'string') {
|
|
73
|
+
return parameter;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (parameter === 'true') return true;
|
|
77
|
+
if (parameter === 'false') return false;
|
|
78
|
+
if (dateRegex.test(parameter)) return new Date(parameter);
|
|
79
|
+
|
|
80
|
+
if (maybeJson(parameter)) {
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(parameter);
|
|
83
|
+
} catch {
|
|
84
|
+
return parameter;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return parameter;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
class SerializeParametersTransformer extends OperationNodeTransformer {
|
|
92
|
+
readonly #serializer: Serializer;
|
|
93
|
+
|
|
94
|
+
constructor(serializer: Serializer) {
|
|
95
|
+
super();
|
|
96
|
+
this.#serializer = serializer;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
protected override transformPrimitiveValueList(
|
|
100
|
+
node: PrimitiveValueListNode
|
|
101
|
+
): PrimitiveValueListNode {
|
|
102
|
+
return {
|
|
103
|
+
...node,
|
|
104
|
+
values: node.values.map((v) => this.#serializer(v)),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
protected override transformColumnUpdate(
|
|
109
|
+
node: ColumnUpdateNode,
|
|
110
|
+
queryId?: { readonly queryId: string }
|
|
111
|
+
): ColumnUpdateNode {
|
|
112
|
+
const valueNode = node.value;
|
|
113
|
+
if (valueNode.kind !== 'ValueNode') {
|
|
114
|
+
return super.transformColumnUpdate(node, queryId);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const currentValue = (valueNode as ValueNode).value;
|
|
118
|
+
const serializedValue = this.#serializer(currentValue);
|
|
119
|
+
if (currentValue === serializedValue) {
|
|
120
|
+
return super.transformColumnUpdate(node, queryId);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const updatedValue: ValueNode = {
|
|
124
|
+
...(valueNode as ValueNode),
|
|
125
|
+
value: serializedValue,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return super.transformColumnUpdate(
|
|
129
|
+
{ ...node, value: updatedValue },
|
|
130
|
+
queryId
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
protected override transformValue(node: ValueNode): ValueNode {
|
|
135
|
+
return { ...node, value: this.#serializer(node.value) };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
class BaseSerializePlugin implements KyselyPlugin {
|
|
140
|
+
readonly #transformer: SerializeParametersTransformer;
|
|
141
|
+
readonly #deserializer: Deserializer;
|
|
142
|
+
readonly #skipNodeSet: Set<RootOperationNode['kind']> | null;
|
|
143
|
+
readonly #ctx: WeakSet<object> | null;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Base class for {@link SerializePlugin}, without default options.
|
|
147
|
+
*/
|
|
148
|
+
constructor(
|
|
149
|
+
serializer: Serializer,
|
|
150
|
+
deserializer: Deserializer,
|
|
151
|
+
skipNodeKind: Array<RootOperationNode['kind']>
|
|
152
|
+
) {
|
|
153
|
+
this.#transformer = new SerializeParametersTransformer(serializer);
|
|
154
|
+
this.#deserializer = deserializer;
|
|
155
|
+
if (skipNodeKind.length > 0) {
|
|
156
|
+
this.#skipNodeSet = new Set(skipNodeKind);
|
|
157
|
+
this.#ctx = new WeakSet<object>();
|
|
158
|
+
} else {
|
|
159
|
+
this.#skipNodeSet = null;
|
|
160
|
+
this.#ctx = null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
transformQuery({
|
|
165
|
+
node,
|
|
166
|
+
queryId,
|
|
167
|
+
}: PluginTransformQueryArgs): RootOperationNode {
|
|
168
|
+
if (this.#skipNodeSet?.has(node.kind)) {
|
|
169
|
+
this.#ctx?.add(queryId);
|
|
170
|
+
return node;
|
|
171
|
+
}
|
|
172
|
+
return this.#transformer.transformNode(node);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async transformResult({
|
|
176
|
+
result,
|
|
177
|
+
queryId,
|
|
178
|
+
}: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>> {
|
|
179
|
+
if (this.#ctx?.has(queryId)) {
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
return { ...result, rows: this.#parseRows(result.rows) };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#parseRows(rows: UnknownRow[]): UnknownRow[] {
|
|
186
|
+
const out: UnknownRow[] = [];
|
|
187
|
+
for (const row of rows) {
|
|
188
|
+
if (!row) continue;
|
|
189
|
+
const parsed: Record<string, unknown> = {};
|
|
190
|
+
for (const [key, value] of Object.entries(row)) {
|
|
191
|
+
parsed[key] = this.#deserializer(value);
|
|
192
|
+
}
|
|
193
|
+
out.push(parsed);
|
|
194
|
+
}
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
interface SerializePluginOptions {
|
|
200
|
+
serializer?: Serializer;
|
|
201
|
+
deserializer?: Deserializer;
|
|
202
|
+
skipNodeKind?: Array<RootOperationNode['kind']>;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export class SerializePlugin extends BaseSerializePlugin {
|
|
206
|
+
constructor(options: SerializePluginOptions = {}) {
|
|
207
|
+
const {
|
|
208
|
+
serializer = defaultSerializer,
|
|
209
|
+
deserializer = defaultDeserializer,
|
|
210
|
+
skipNodeKind = [],
|
|
211
|
+
} = options;
|
|
212
|
+
super(serializer, deserializer, skipNodeKind);
|
|
213
|
+
}
|
|
214
|
+
}
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/core - Structured logging utilities for sync operations
|
|
3
|
+
*
|
|
4
|
+
* Outputs JSON lines for easy parsing by log aggregation tools.
|
|
5
|
+
* Each log event includes a timestamp and event type.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sync log event structure
|
|
10
|
+
*/
|
|
11
|
+
interface SyncLogEvent {
|
|
12
|
+
/** Event type identifier */
|
|
13
|
+
event: string;
|
|
14
|
+
/** User ID (optional) */
|
|
15
|
+
userId?: string;
|
|
16
|
+
/** Operation duration in milliseconds (optional) */
|
|
17
|
+
durationMs?: number;
|
|
18
|
+
/** Number of rows affected (optional) */
|
|
19
|
+
rowCount?: number;
|
|
20
|
+
/** Whether a full reset was required (optional) */
|
|
21
|
+
resetRequired?: boolean;
|
|
22
|
+
/** Error message if operation failed (optional) */
|
|
23
|
+
error?: string;
|
|
24
|
+
/** Additional arbitrary properties */
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Logger function type - allows custom logging implementations
|
|
30
|
+
*/
|
|
31
|
+
type SyncLogger = (event: SyncLogEvent) => void;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Default logger that outputs JSON lines to console.
|
|
35
|
+
* Non-blocking - defers logging to avoid blocking the event loop.
|
|
36
|
+
*
|
|
37
|
+
* On server (Node.js), uses setImmediate.
|
|
38
|
+
* On client (browser), uses setTimeout(0).
|
|
39
|
+
*/
|
|
40
|
+
function createDefaultLogger(): SyncLogger {
|
|
41
|
+
// Detect environment
|
|
42
|
+
const isNode =
|
|
43
|
+
typeof globalThis !== 'undefined' &&
|
|
44
|
+
typeof globalThis.setImmediate === 'function';
|
|
45
|
+
|
|
46
|
+
const defer = isNode
|
|
47
|
+
? (fn: () => void) => globalThis.setImmediate(fn)
|
|
48
|
+
: (fn: () => void) => setTimeout(fn, 0);
|
|
49
|
+
|
|
50
|
+
return (event: SyncLogEvent) => {
|
|
51
|
+
defer(() => {
|
|
52
|
+
console.log(
|
|
53
|
+
JSON.stringify({
|
|
54
|
+
timestamp: new Date().toISOString(),
|
|
55
|
+
...event,
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Log a sync event using the default logger.
|
|
64
|
+
* For custom logging, create your own logger with createDefaultLogger pattern.
|
|
65
|
+
*/
|
|
66
|
+
export const logSyncEvent: SyncLogger = createDefaultLogger();
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Create a timer for measuring operation duration.
|
|
70
|
+
* Returns the elapsed time in milliseconds when called.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* const elapsed = createSyncTimer();
|
|
74
|
+
* await doSomeWork();
|
|
75
|
+
* logSyncEvent({ event: 'work_complete', durationMs: elapsed() });
|
|
76
|
+
*/
|
|
77
|
+
export function createSyncTimer(): () => number {
|
|
78
|
+
const start = performance.now();
|
|
79
|
+
return () => Math.round(performance.now() - start);
|
|
80
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/core - Proxy Protocol Types
|
|
3
|
+
*
|
|
4
|
+
* Shared protocol types between proxy client (Kysely dialect) and server (WebSocket handler).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Message sent from proxy client to server.
|
|
9
|
+
*/
|
|
10
|
+
export interface ProxyMessage {
|
|
11
|
+
/** Correlation ID for matching request/response */
|
|
12
|
+
id: string;
|
|
13
|
+
/** Message type */
|
|
14
|
+
type: 'query' | 'begin' | 'commit' | 'rollback';
|
|
15
|
+
/** SQL query (for 'query' type) */
|
|
16
|
+
sql?: string;
|
|
17
|
+
/** Query parameters (for 'query' type) */
|
|
18
|
+
parameters?: readonly unknown[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Response sent from server to proxy client.
|
|
23
|
+
*/
|
|
24
|
+
export interface ProxyResponse {
|
|
25
|
+
/** Correlation ID matching the request */
|
|
26
|
+
id: string;
|
|
27
|
+
/** Response type */
|
|
28
|
+
type: 'result' | 'error';
|
|
29
|
+
/** Query result rows (for SELECT queries) */
|
|
30
|
+
rows?: unknown[];
|
|
31
|
+
/** Number of affected rows (for mutations) */
|
|
32
|
+
rowCount?: number;
|
|
33
|
+
/** Error message (for 'error' type) */
|
|
34
|
+
error?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Handshake message sent when connection is established.
|
|
39
|
+
*/
|
|
40
|
+
export interface ProxyHandshake {
|
|
41
|
+
type: 'handshake';
|
|
42
|
+
/** Actor ID for oplog tracking */
|
|
43
|
+
actorId: string;
|
|
44
|
+
/** Client ID for oplog tracking */
|
|
45
|
+
clientId: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Handshake acknowledgement from server.
|
|
50
|
+
*/
|
|
51
|
+
export interface ProxyHandshakeAck {
|
|
52
|
+
type: 'handshake_ack';
|
|
53
|
+
/** Whether handshake was successful */
|
|
54
|
+
ok: boolean;
|
|
55
|
+
/** Error message if handshake failed */
|
|
56
|
+
error?: string;
|
|
57
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/core - Blob Zod schemas
|
|
3
|
+
*
|
|
4
|
+
* Runtime validation schemas for blob types.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Blob Reference Schema
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
export const BlobRefSchema = z.object({
|
|
14
|
+
hash: z.string().regex(/^sha256:[0-9a-f]{64}$/i, 'Invalid blob hash format'),
|
|
15
|
+
size: z.number().int().min(0),
|
|
16
|
+
mimeType: z.string().min(1),
|
|
17
|
+
encrypted: z.boolean().optional(),
|
|
18
|
+
keyId: z.string().optional(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export type BlobRef = z.infer<typeof BlobRefSchema>;
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Blob Metadata Schema
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
export const BlobMetadataSchema = z.object({
|
|
28
|
+
hash: z.string().regex(/^sha256:[0-9a-f]{64}$/i),
|
|
29
|
+
size: z.number().int().min(0),
|
|
30
|
+
mimeType: z.string().min(1),
|
|
31
|
+
createdAt: z.string(),
|
|
32
|
+
expiresAt: z.string().optional(),
|
|
33
|
+
uploadComplete: z.boolean(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export type BlobMetadata = z.infer<typeof BlobMetadataSchema>;
|
|
37
|
+
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// Upload Request/Response Schemas
|
|
40
|
+
// ============================================================================
|
|
41
|
+
|
|
42
|
+
export const BlobUploadInitRequestSchema = z.object({
|
|
43
|
+
hash: z.string().regex(/^sha256:[0-9a-f]{64}$/i, 'Invalid blob hash format'),
|
|
44
|
+
size: z.number().int().min(0),
|
|
45
|
+
mimeType: z.string().min(1),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export type BlobUploadInitRequest = z.infer<typeof BlobUploadInitRequestSchema>;
|
|
49
|
+
|
|
50
|
+
export const BlobUploadInitResponseSchema = z.object({
|
|
51
|
+
exists: z.boolean(),
|
|
52
|
+
uploadId: z.string().optional(),
|
|
53
|
+
uploadUrl: z.string().url().optional(),
|
|
54
|
+
uploadMethod: z.enum(['PUT', 'POST']).optional(),
|
|
55
|
+
uploadHeaders: z.record(z.string(), z.string()).optional(),
|
|
56
|
+
chunkSize: z.number().int().optional(),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export type BlobUploadInitResponse = z.infer<
|
|
60
|
+
typeof BlobUploadInitResponseSchema
|
|
61
|
+
>;
|
|
62
|
+
|
|
63
|
+
export const BlobUploadCompleteRequestSchema = z.object({
|
|
64
|
+
hash: z.string().regex(/^sha256:[0-9a-f]{64}$/i),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export type BlobUploadCompleteRequest = z.infer<
|
|
68
|
+
typeof BlobUploadCompleteRequestSchema
|
|
69
|
+
>;
|
|
70
|
+
|
|
71
|
+
export const BlobUploadCompleteResponseSchema = z.object({
|
|
72
|
+
ok: z.boolean(),
|
|
73
|
+
metadata: BlobMetadataSchema.optional(),
|
|
74
|
+
error: z.string().optional(),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export type BlobUploadCompleteResponse = z.infer<
|
|
78
|
+
typeof BlobUploadCompleteResponseSchema
|
|
79
|
+
>;
|
|
80
|
+
|
|
81
|
+
// ============================================================================
|
|
82
|
+
// Download URL Request/Response Schemas
|
|
83
|
+
// ============================================================================
|
|
84
|
+
|
|
85
|
+
export const BlobDownloadUrlRequestSchema = z.object({
|
|
86
|
+
hash: z.string().regex(/^sha256:[0-9a-f]{64}$/i),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
export type BlobDownloadUrlRequest = z.infer<
|
|
90
|
+
typeof BlobDownloadUrlRequestSchema
|
|
91
|
+
>;
|
|
92
|
+
|
|
93
|
+
export const BlobDownloadUrlResponseSchema = z.object({
|
|
94
|
+
url: z.string().url(),
|
|
95
|
+
expiresAt: z.string(),
|
|
96
|
+
metadata: BlobMetadataSchema,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
export type BlobDownloadUrlResponse = z.infer<
|
|
100
|
+
typeof BlobDownloadUrlResponseSchema
|
|
101
|
+
>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/core - Common Zod schemas
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Error Response Schemas
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
export const ErrorResponseSchema = z.object({
|
|
12
|
+
error: z.string(),
|
|
13
|
+
message: z.string().optional(),
|
|
14
|
+
code: z.string().optional(),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export type ErrorResponse = z.infer<typeof ErrorResponseSchema>;
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Pagination Schemas
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
export const PaginationQuerySchema = z.object({
|
|
24
|
+
limit: z.coerce.number().int().min(1).max(100).default(50),
|
|
25
|
+
offset: z.coerce.number().int().min(0).default(0),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export type PaginationQuery = z.infer<typeof PaginationQuerySchema>;
|
|
29
|
+
|
|
30
|
+
export const PaginatedResponseSchema = <T extends z.ZodTypeAny>(
|
|
31
|
+
itemSchema: T
|
|
32
|
+
) =>
|
|
33
|
+
z.object({
|
|
34
|
+
items: z.array(itemSchema),
|
|
35
|
+
total: z.number().int(),
|
|
36
|
+
offset: z.number().int(),
|
|
37
|
+
limit: z.number().int(),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export type PaginatedResponse<T> = {
|
|
41
|
+
items: T[];
|
|
42
|
+
total: number;
|
|
43
|
+
offset: number;
|
|
44
|
+
limit: number;
|
|
45
|
+
};
|