@strata-sync/client 0.1.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/AGENTS.md +24 -0
- package/README.md +63 -0
- package/package.json +32 -0
- package/src/client.ts +1051 -0
- package/src/identity-map.ts +294 -0
- package/src/index.ts +33 -0
- package/src/outbox-manager.ts +399 -0
- package/src/query.ts +224 -0
- package/src/sync-orchestrator.ts +1041 -0
- package/src/types.ts +425 -0
- package/tests/rebase-integration.test.ts +920 -0
- package/tests/reverse-linear-sync-engine.test.ts +701 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +31 -0
package/src/query.ts
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type { IdentityMap } from "./identity-map";
|
|
2
|
+
import type { QueryOptions, QueryResult } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Executes a query against an identity map
|
|
6
|
+
*/
|
|
7
|
+
export function executeQuery<T extends Record<string, unknown>>(
|
|
8
|
+
map: IdentityMap<T>,
|
|
9
|
+
options: QueryOptions<T> = {}
|
|
10
|
+
): QueryResult<T> {
|
|
11
|
+
let results = map.values();
|
|
12
|
+
|
|
13
|
+
// Filter by predicate
|
|
14
|
+
if (options.where) {
|
|
15
|
+
results = results.filter(options.where);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Filter archived unless explicitly included
|
|
19
|
+
if (!options.includeArchived) {
|
|
20
|
+
results = results.filter((item) => !item.archivedAt);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Get total count before pagination
|
|
24
|
+
const totalCount = results.length;
|
|
25
|
+
|
|
26
|
+
// Sort results
|
|
27
|
+
if (options.orderBy) {
|
|
28
|
+
results = results.sort(options.orderBy);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Apply offset
|
|
32
|
+
if (options.offset && options.offset > 0) {
|
|
33
|
+
results = results.slice(options.offset);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Apply limit
|
|
37
|
+
let hasMore = false;
|
|
38
|
+
if (options.limit && options.limit > 0) {
|
|
39
|
+
hasMore = results.length > options.limit;
|
|
40
|
+
results = results.slice(0, options.limit);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
data: results,
|
|
45
|
+
hasMore,
|
|
46
|
+
totalCount,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Sorts by a field name
|
|
52
|
+
*/
|
|
53
|
+
export function sortBy<T>(
|
|
54
|
+
field: keyof T,
|
|
55
|
+
direction: "asc" | "desc" = "asc"
|
|
56
|
+
): (a: T, b: T) => number {
|
|
57
|
+
return (a: T, b: T) => {
|
|
58
|
+
const aVal = a[field];
|
|
59
|
+
const bVal = b[field];
|
|
60
|
+
|
|
61
|
+
if (aVal === bVal) {
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
if (aVal === null || aVal === undefined) {
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
if (bVal === null || bVal === undefined) {
|
|
68
|
+
return -1;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let result: number;
|
|
72
|
+
if (typeof aVal === "string" && typeof bVal === "string") {
|
|
73
|
+
result = aVal.localeCompare(bVal);
|
|
74
|
+
} else if (typeof aVal === "number" && typeof bVal === "number") {
|
|
75
|
+
result = aVal - bVal;
|
|
76
|
+
} else if (aVal instanceof Date && bVal instanceof Date) {
|
|
77
|
+
result = aVal.getTime() - bVal.getTime();
|
|
78
|
+
} else {
|
|
79
|
+
result = String(aVal).localeCompare(String(bVal));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return direction === "desc" ? -result : result;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Combines multiple sort functions
|
|
88
|
+
*/
|
|
89
|
+
export function combineSorts<T>(
|
|
90
|
+
...sorts: Array<(a: T, b: T) => number>
|
|
91
|
+
): (a: T, b: T) => number {
|
|
92
|
+
return (a: T, b: T) => {
|
|
93
|
+
for (const sort of sorts) {
|
|
94
|
+
const result = sort(a, b);
|
|
95
|
+
if (result !== 0) {
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return 0;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Creates a filter for equality
|
|
105
|
+
*/
|
|
106
|
+
export function eq<T, K extends keyof T>(
|
|
107
|
+
field: K,
|
|
108
|
+
value: T[K]
|
|
109
|
+
): (item: T) => boolean {
|
|
110
|
+
return (item: T) => item[field] === value;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Creates a filter for not equal
|
|
115
|
+
*/
|
|
116
|
+
export function neq<T, K extends keyof T>(
|
|
117
|
+
field: K,
|
|
118
|
+
value: T[K]
|
|
119
|
+
): (item: T) => boolean {
|
|
120
|
+
return (item: T) => item[field] !== value;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Creates a filter for greater than
|
|
125
|
+
*/
|
|
126
|
+
export function gt<T, K extends keyof T>(
|
|
127
|
+
field: K,
|
|
128
|
+
value: T[K]
|
|
129
|
+
): (item: T) => boolean {
|
|
130
|
+
return (item: T) => {
|
|
131
|
+
const itemVal = item[field];
|
|
132
|
+
if (typeof itemVal === "number" && typeof value === "number") {
|
|
133
|
+
return itemVal > value;
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Creates a filter for less than
|
|
141
|
+
*/
|
|
142
|
+
export function lt<T, K extends keyof T>(
|
|
143
|
+
field: K,
|
|
144
|
+
value: T[K]
|
|
145
|
+
): (item: T) => boolean {
|
|
146
|
+
return (item: T) => {
|
|
147
|
+
const itemVal = item[field];
|
|
148
|
+
if (typeof itemVal === "number" && typeof value === "number") {
|
|
149
|
+
return itemVal < value;
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Creates a filter for inclusion in a list
|
|
157
|
+
*/
|
|
158
|
+
export function isIn<T, K extends keyof T>(
|
|
159
|
+
field: K,
|
|
160
|
+
values: T[K][]
|
|
161
|
+
): (item: T) => boolean {
|
|
162
|
+
const valueSet = new Set(values);
|
|
163
|
+
return (item: T) => valueSet.has(item[field]);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Creates a filter for substring matching
|
|
168
|
+
*/
|
|
169
|
+
export function contains<T>(
|
|
170
|
+
field: keyof T,
|
|
171
|
+
substring: string,
|
|
172
|
+
caseSensitive = false
|
|
173
|
+
): (item: T) => boolean {
|
|
174
|
+
const searchStr = caseSensitive ? substring : substring.toLowerCase();
|
|
175
|
+
return (item: T) => {
|
|
176
|
+
const value = item[field];
|
|
177
|
+
if (typeof value !== "string") {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
const compareVal = caseSensitive ? value : value.toLowerCase();
|
|
181
|
+
return compareVal.includes(searchStr);
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Creates a filter for regex matching
|
|
187
|
+
*/
|
|
188
|
+
export function matches<T>(
|
|
189
|
+
field: keyof T,
|
|
190
|
+
pattern: RegExp
|
|
191
|
+
): (item: T) => boolean {
|
|
192
|
+
return (item: T) => {
|
|
193
|
+
const value = item[field];
|
|
194
|
+
if (typeof value !== "string") {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
return pattern.test(value);
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Combines multiple filters with AND logic
|
|
203
|
+
*/
|
|
204
|
+
export function and<T>(
|
|
205
|
+
...filters: Array<(item: T) => boolean>
|
|
206
|
+
): (item: T) => boolean {
|
|
207
|
+
return (item: T) => filters.every((f) => f(item));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Combines multiple filters with OR logic
|
|
212
|
+
*/
|
|
213
|
+
export function or<T>(
|
|
214
|
+
...filters: Array<(item: T) => boolean>
|
|
215
|
+
): (item: T) => boolean {
|
|
216
|
+
return (item: T) => filters.some((f) => f(item));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Negates a filter
|
|
221
|
+
*/
|
|
222
|
+
export function not<T>(filter: (item: T) => boolean): (item: T) => boolean {
|
|
223
|
+
return (item: T) => !filter(item);
|
|
224
|
+
}
|