@korajs/core 0.3.2 → 0.5.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/dist/{chunk-KQYXWUFV.js → chunk-H4FXU5OP.js} +66 -20
- package/dist/chunk-H4FXU5OP.js.map +1 -0
- package/dist/events-BeIEDJBW.d.cts +887 -0
- package/dist/events-BeIEDJBW.d.ts +887 -0
- package/dist/index.cjs +1410 -72
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +564 -98
- package/dist/index.d.ts +564 -98
- package/dist/index.js +1287 -25
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +56 -17
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +2 -2
- package/dist/internal.d.ts +2 -2
- package/dist/internal.js +1 -1
- package/package.json +2 -1
- package/dist/chunk-KQYXWUFV.js.map +0 -1
- package/dist/events-7Fhdjxd2.d.cts +0 -327
- package/dist/events-7Fhdjxd2.d.ts +0 -327
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ClockDriftError,
|
|
3
3
|
HybridLogicalClock,
|
|
4
|
+
KORA_ERROR_FIX_SUGGESTIONS,
|
|
4
5
|
KoraError,
|
|
5
6
|
MergeConflictError,
|
|
6
7
|
OperationError,
|
|
@@ -8,10 +9,11 @@ import {
|
|
|
8
9
|
StorageError,
|
|
9
10
|
SyncError,
|
|
10
11
|
createOperation,
|
|
12
|
+
getKoraErrorFix,
|
|
11
13
|
isValidOperation,
|
|
12
14
|
topologicalSort,
|
|
13
15
|
verifyOperationIntegrity
|
|
14
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-H4FXU5OP.js";
|
|
15
17
|
|
|
16
18
|
// src/types.ts
|
|
17
19
|
var MERGE_STRATEGIES = [
|
|
@@ -23,6 +25,50 @@ var MERGE_STRATEGIES = [
|
|
|
23
25
|
];
|
|
24
26
|
var CONNECTION_QUALITIES = ["excellent", "good", "fair", "poor", "offline"];
|
|
25
27
|
|
|
28
|
+
// src/causal/causal-tracker.ts
|
|
29
|
+
var CausalTracker = class {
|
|
30
|
+
lastOpIdByCollection = /* @__PURE__ */ new Map();
|
|
31
|
+
lastTransactionOpId = null;
|
|
32
|
+
/**
|
|
33
|
+
* Start a new transaction boundary. Clears in-transaction op ids.
|
|
34
|
+
*/
|
|
35
|
+
beginTransaction() {
|
|
36
|
+
this.lastTransactionOpId = null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Clear the transaction boundary without recording ops (after rollback).
|
|
40
|
+
*/
|
|
41
|
+
clearTransaction() {
|
|
42
|
+
this.lastTransactionOpId = null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Compute causal dependencies for the next operation in a collection.
|
|
46
|
+
* Uses direct parents only: collection head and the previous op in the open transaction.
|
|
47
|
+
*/
|
|
48
|
+
nextCausalDeps(collection, inTransaction) {
|
|
49
|
+
const deps = [];
|
|
50
|
+
const lastInCollection = this.lastOpIdByCollection.get(collection);
|
|
51
|
+
if (lastInCollection !== void 0) {
|
|
52
|
+
deps.push(lastInCollection);
|
|
53
|
+
}
|
|
54
|
+
if (inTransaction && this.lastTransactionOpId !== null) {
|
|
55
|
+
if (!deps.includes(this.lastTransactionOpId)) {
|
|
56
|
+
deps.push(this.lastTransactionOpId);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return deps;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Record an operation after it has been created and assigned an id.
|
|
63
|
+
*/
|
|
64
|
+
afterOperation(collection, operationId, inTransaction) {
|
|
65
|
+
this.lastOpIdByCollection.set(collection, operationId);
|
|
66
|
+
if (inTransaction) {
|
|
67
|
+
this.lastTransactionOpId = operationId;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
26
72
|
// src/identifiers/uuid-v7.ts
|
|
27
73
|
var defaultRandom = globalThis.crypto;
|
|
28
74
|
function generateUUIDv7(timestamp = Date.now(), randomSource = defaultRandom) {
|
|
@@ -59,10 +105,288 @@ function formatUUID(bytes) {
|
|
|
59
105
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
60
106
|
}
|
|
61
107
|
|
|
108
|
+
// src/operations/apply-result.ts
|
|
109
|
+
var APPLY_RESULTS = ["applied", "duplicate", "skipped", "rejected", "deferred"];
|
|
110
|
+
var APPLY_FAILURE_CODES = {
|
|
111
|
+
APPLY_FAILED: "APPLY_FAILED",
|
|
112
|
+
APPLY_SKIPPED: "APPLY_SKIPPED",
|
|
113
|
+
APPLY_REJECTED: "APPLY_REJECTED",
|
|
114
|
+
APPLY_DEFERRED: "APPLY_DEFERRED",
|
|
115
|
+
CLOCK_DRIFT: "CLOCK_DRIFT",
|
|
116
|
+
REFERENTIAL_INTEGRITY: "REFERENTIAL_INTEGRITY",
|
|
117
|
+
SCHEMA_MISMATCH: "SCHEMA_MISMATCH"
|
|
118
|
+
};
|
|
119
|
+
function isApplyFailure(result) {
|
|
120
|
+
return result === "skipped" || result === "rejected" || result === "deferred";
|
|
121
|
+
}
|
|
122
|
+
function defaultApplyFailureReason(result, overrides) {
|
|
123
|
+
const base = result === "deferred" ? {
|
|
124
|
+
code: APPLY_FAILURE_CODES.APPLY_DEFERRED,
|
|
125
|
+
message: "Operation apply was deferred.",
|
|
126
|
+
retriable: true
|
|
127
|
+
} : result === "rejected" ? {
|
|
128
|
+
code: APPLY_FAILURE_CODES.APPLY_REJECTED,
|
|
129
|
+
message: "Operation apply was rejected.",
|
|
130
|
+
retriable: false
|
|
131
|
+
} : {
|
|
132
|
+
code: APPLY_FAILURE_CODES.APPLY_SKIPPED,
|
|
133
|
+
message: "Operation apply was skipped.",
|
|
134
|
+
retriable: false
|
|
135
|
+
};
|
|
136
|
+
return { ...base, ...overrides };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/operations/atomic-ops.ts
|
|
140
|
+
var KORA_ATOMIC_OP = /* @__PURE__ */ Symbol.for("kora.atomic_op");
|
|
141
|
+
function isAtomicOp(value) {
|
|
142
|
+
return typeof value === "object" && value !== null && KORA_ATOMIC_OP in value && value[KORA_ATOMIC_OP] === true;
|
|
143
|
+
}
|
|
144
|
+
function resolveAtomicOp(currentValue, sentinel) {
|
|
145
|
+
switch (sentinel.type) {
|
|
146
|
+
case "increment": {
|
|
147
|
+
const current = typeof currentValue === "number" ? currentValue : 0;
|
|
148
|
+
const operand = sentinel.value;
|
|
149
|
+
return current + operand;
|
|
150
|
+
}
|
|
151
|
+
case "max": {
|
|
152
|
+
const current = typeof currentValue === "number" ? currentValue : Number.NEGATIVE_INFINITY;
|
|
153
|
+
return Math.max(current, sentinel.value);
|
|
154
|
+
}
|
|
155
|
+
case "min": {
|
|
156
|
+
const current = typeof currentValue === "number" ? currentValue : Number.POSITIVE_INFINITY;
|
|
157
|
+
return Math.min(current, sentinel.value);
|
|
158
|
+
}
|
|
159
|
+
case "append": {
|
|
160
|
+
const current = Array.isArray(currentValue) ? [...currentValue] : [];
|
|
161
|
+
current.push(sentinel.value);
|
|
162
|
+
return current;
|
|
163
|
+
}
|
|
164
|
+
case "remove": {
|
|
165
|
+
if (!Array.isArray(currentValue)) return [];
|
|
166
|
+
return currentValue.filter((item) => item !== sentinel.value);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function createSentinel(type, value) {
|
|
171
|
+
return Object.freeze({
|
|
172
|
+
[KORA_ATOMIC_OP]: true,
|
|
173
|
+
type,
|
|
174
|
+
value
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
var op = {
|
|
178
|
+
/**
|
|
179
|
+
* Increment a number field by the given amount.
|
|
180
|
+
* Concurrent increments compose: the sum of all deltas is applied to the base.
|
|
181
|
+
*
|
|
182
|
+
* @param n - The amount to increment (use negative values to decrement)
|
|
183
|
+
*/
|
|
184
|
+
increment(n) {
|
|
185
|
+
return createSentinel("increment", n);
|
|
186
|
+
},
|
|
187
|
+
/**
|
|
188
|
+
* Decrement a number field by the given amount.
|
|
189
|
+
* Syntactic sugar for `op.increment(-n)`.
|
|
190
|
+
*
|
|
191
|
+
* @param n - The amount to decrement
|
|
192
|
+
*/
|
|
193
|
+
decrement(n) {
|
|
194
|
+
return createSentinel("increment", -n);
|
|
195
|
+
},
|
|
196
|
+
/**
|
|
197
|
+
* Set the field to the maximum of the current value and the given value.
|
|
198
|
+
* Concurrent max operations take the maximum of all values.
|
|
199
|
+
*
|
|
200
|
+
* @param n - The value to compare against the current value
|
|
201
|
+
*/
|
|
202
|
+
max(n) {
|
|
203
|
+
return createSentinel("max", n);
|
|
204
|
+
},
|
|
205
|
+
/**
|
|
206
|
+
* Set the field to the minimum of the current value and the given value.
|
|
207
|
+
* Concurrent min operations take the minimum of all values.
|
|
208
|
+
*
|
|
209
|
+
* @param n - The value to compare against the current value
|
|
210
|
+
*/
|
|
211
|
+
min(n) {
|
|
212
|
+
return createSentinel("min", n);
|
|
213
|
+
},
|
|
214
|
+
/**
|
|
215
|
+
* Append an item to an array field.
|
|
216
|
+
* Concurrent appends include all appended items.
|
|
217
|
+
*
|
|
218
|
+
* @param item - The item to append to the array
|
|
219
|
+
*/
|
|
220
|
+
append(item) {
|
|
221
|
+
return createSentinel("append", item);
|
|
222
|
+
},
|
|
223
|
+
/**
|
|
224
|
+
* Remove an item from an array field (by value equality).
|
|
225
|
+
* Concurrent removes of the same item are idempotent.
|
|
226
|
+
*
|
|
227
|
+
* @param item - The item to remove from the array
|
|
228
|
+
*/
|
|
229
|
+
remove(item) {
|
|
230
|
+
return createSentinel("remove", item);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
function toAtomicOp(sentinel) {
|
|
234
|
+
return { type: sentinel.type, value: sentinel.value };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/scopes/sync-scope-bindings.ts
|
|
238
|
+
function hasSchemaSyncRules(schema) {
|
|
239
|
+
return schema.sync !== void 0 && Object.keys(schema.sync).length > 0;
|
|
240
|
+
}
|
|
241
|
+
function getCollectionScopeBindings(schema, collectionName) {
|
|
242
|
+
const syncRule = schema.sync?.[collectionName];
|
|
243
|
+
if (syncRule && Object.keys(syncRule.where).length > 0) {
|
|
244
|
+
return { ...syncRule.where };
|
|
245
|
+
}
|
|
246
|
+
const scopeFields = schema.collections[collectionName]?.scope ?? [];
|
|
247
|
+
if (scopeFields.length === 0) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
const bindings = {};
|
|
251
|
+
for (const field of scopeFields) {
|
|
252
|
+
bindings[field] = field;
|
|
253
|
+
}
|
|
254
|
+
return bindings;
|
|
255
|
+
}
|
|
256
|
+
function isCollectionSyncScoped(schema, collectionName) {
|
|
257
|
+
if (hasSchemaSyncRules(schema)) {
|
|
258
|
+
if (schema.sync?.[collectionName]) {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
const legacyScope = schema.collections[collectionName]?.scope ?? [];
|
|
262
|
+
return legacyScope.length > 0;
|
|
263
|
+
}
|
|
264
|
+
return getCollectionScopeBindings(schema, collectionName) !== null;
|
|
265
|
+
}
|
|
266
|
+
function collectSchemaScopeValueKeys(schema) {
|
|
267
|
+
const keys = /* @__PURE__ */ new Set();
|
|
268
|
+
for (const collection of Object.values(schema.collections)) {
|
|
269
|
+
for (const field of collection.scope) {
|
|
270
|
+
keys.add(field);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (schema.sync) {
|
|
274
|
+
for (const rule of Object.values(schema.sync)) {
|
|
275
|
+
for (const scopeKey of Object.values(rule.where)) {
|
|
276
|
+
keys.add(scopeKey);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return [...keys];
|
|
281
|
+
}
|
|
282
|
+
function normalizeSyncRuleWhere(collectionName, where) {
|
|
283
|
+
const normalized = {};
|
|
284
|
+
for (const [field, scopeKey] of Object.entries(where)) {
|
|
285
|
+
if (scopeKey === true) {
|
|
286
|
+
normalized[field] = field;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (typeof scopeKey === "string" && scopeKey.length > 0) {
|
|
290
|
+
normalized[field] = scopeKey;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
throw new Error(
|
|
294
|
+
`Invalid sync rule for collection "${collectionName}": where.${field} must be true or a non-empty scope key string`
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
return normalized;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// src/state-machine/state-machine.ts
|
|
301
|
+
function validateTransition(constraint, fromValue, toValue) {
|
|
302
|
+
const from = String(fromValue);
|
|
303
|
+
const to = String(toValue);
|
|
304
|
+
const allowedTargets = constraint.transitions[from] ?? [];
|
|
305
|
+
return {
|
|
306
|
+
valid: allowedTargets.includes(to),
|
|
307
|
+
from,
|
|
308
|
+
to,
|
|
309
|
+
field: constraint.field,
|
|
310
|
+
collection: constraint.collection,
|
|
311
|
+
allowedTargets
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
function buildStateMachineConstraints(schema) {
|
|
315
|
+
const constraints = [];
|
|
316
|
+
for (const [collectionName, collection] of Object.entries(schema.collections)) {
|
|
317
|
+
for (const [fieldName, descriptor] of Object.entries(collection.fields)) {
|
|
318
|
+
if (descriptor.kind === "enum" && descriptor.transitions !== null) {
|
|
319
|
+
constraints.push({
|
|
320
|
+
field: fieldName,
|
|
321
|
+
collection: collectionName,
|
|
322
|
+
transitions: descriptor.transitions
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return constraints;
|
|
328
|
+
}
|
|
329
|
+
function getTransitionMap(schema, collection, field) {
|
|
330
|
+
const collectionDef = schema.collections[collection];
|
|
331
|
+
if (!collectionDef) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
const fieldDef = collectionDef.fields[field];
|
|
335
|
+
if (!fieldDef || fieldDef.kind !== "enum") {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
return fieldDef.transitions;
|
|
339
|
+
}
|
|
340
|
+
function validateStateMachineDefinition(collectionName, sm, fields) {
|
|
341
|
+
const fieldDef = fields[sm.field];
|
|
342
|
+
if (!fieldDef) {
|
|
343
|
+
throw new SchemaValidationError(
|
|
344
|
+
`State machine field "${sm.field}" does not exist in collection "${collectionName}". Available fields: ${Object.keys(fields).join(", ")}`,
|
|
345
|
+
{ collection: collectionName, field: sm.field }
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
if (fieldDef.kind !== "enum") {
|
|
349
|
+
throw new SchemaValidationError(
|
|
350
|
+
`State machine field "${sm.field}" in collection "${collectionName}" must be an enum field, but got "${fieldDef.kind}"`,
|
|
351
|
+
{ collection: collectionName, field: sm.field, kind: fieldDef.kind }
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
const enumValues = fieldDef.enumValues;
|
|
355
|
+
if (!enumValues || enumValues.length === 0) {
|
|
356
|
+
throw new SchemaValidationError(
|
|
357
|
+
`State machine field "${sm.field}" in collection "${collectionName}" has no enum values defined`,
|
|
358
|
+
{ collection: collectionName, field: sm.field }
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
const validValues = new Set(enumValues);
|
|
362
|
+
for (const [state, targets] of Object.entries(sm.transitions)) {
|
|
363
|
+
if (!validValues.has(state)) {
|
|
364
|
+
throw new SchemaValidationError(
|
|
365
|
+
`State machine transition source "${state}" is not a valid enum value for field "${sm.field}" in collection "${collectionName}". Valid values: ${[...validValues].join(", ")}`,
|
|
366
|
+
{ collection: collectionName, field: sm.field, state }
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
for (const target of targets) {
|
|
370
|
+
if (!validValues.has(target)) {
|
|
371
|
+
throw new SchemaValidationError(
|
|
372
|
+
`State machine transition target "${target}" is not a valid enum value for field "${sm.field}" in collection "${collectionName}". Valid values: ${[...validValues].join(", ")}`,
|
|
373
|
+
{ collection: collectionName, field: sm.field, state, target }
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (sm.onInvalidTransition !== "reject" && sm.onInvalidTransition !== "last-valid-state") {
|
|
379
|
+
throw new SchemaValidationError(
|
|
380
|
+
`State machine onInvalidTransition must be "reject" or "last-valid-state", got "${sm.onInvalidTransition}" in collection "${collectionName}"`,
|
|
381
|
+
{ collection: collectionName, onInvalidTransition: sm.onInvalidTransition }
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
62
386
|
// src/schema/define.ts
|
|
63
387
|
var COLLECTION_NAME_RE = /^[a-z][a-z0-9_]*$/;
|
|
64
388
|
var FIELD_NAME_RE = /^[a-z][a-zA-Z0-9_]*$/;
|
|
65
|
-
var RESERVED_FIELDS = /* @__PURE__ */ new Set(["id", "_created_at", "_updated_at", "_deleted"]);
|
|
389
|
+
var RESERVED_FIELDS = /* @__PURE__ */ new Set(["id", "_created_at", "_updated_at", "_version", "_deleted"]);
|
|
66
390
|
function defineSchema(input) {
|
|
67
391
|
validateVersion(input.version);
|
|
68
392
|
const collections = {};
|
|
@@ -80,7 +404,40 @@ function defineSchema(input) {
|
|
|
80
404
|
relations[name] = { ...relationInput };
|
|
81
405
|
}
|
|
82
406
|
}
|
|
83
|
-
|
|
407
|
+
const migrations = {};
|
|
408
|
+
if (input.migrations) {
|
|
409
|
+
for (const [versionStr, migration] of Object.entries(input.migrations)) {
|
|
410
|
+
const version = Number(versionStr);
|
|
411
|
+
if (!Number.isInteger(version) || version < 2) {
|
|
412
|
+
throw new SchemaValidationError(
|
|
413
|
+
`Migration key "${versionStr}" is invalid. Must be an integer >= 2 (version 1 is the initial schema).`,
|
|
414
|
+
{ version: versionStr }
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
if (version > input.version) {
|
|
418
|
+
throw new SchemaValidationError(
|
|
419
|
+
`Migration version ${version} exceeds schema version ${input.version}. Migrations must target versions <= schema version.`,
|
|
420
|
+
{ migrationVersion: version, schemaVersion: input.version }
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
if (!migration.steps || migration.steps.length === 0) {
|
|
424
|
+
throw new SchemaValidationError(
|
|
425
|
+
`Migration for version ${version} has no steps. Use migrate() to define at least one step.`,
|
|
426
|
+
{ version }
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
migrations[version] = migration;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
const sync = buildSyncRules(input.sync, collections);
|
|
433
|
+
applySyncRulesToCollectionScope(collections, sync);
|
|
434
|
+
return {
|
|
435
|
+
version: input.version,
|
|
436
|
+
collections,
|
|
437
|
+
relations,
|
|
438
|
+
migrations,
|
|
439
|
+
...sync ? { sync } : {}
|
|
440
|
+
};
|
|
84
441
|
}
|
|
85
442
|
function validateVersion(version) {
|
|
86
443
|
if (typeof version !== "number" || !Number.isInteger(version) || version < 1) {
|
|
@@ -142,7 +499,81 @@ function buildCollection(name, input) {
|
|
|
142
499
|
resolvers[fieldName] = resolver;
|
|
143
500
|
}
|
|
144
501
|
}
|
|
145
|
-
|
|
502
|
+
const scope = [];
|
|
503
|
+
if (input.scope) {
|
|
504
|
+
for (const scopeField of input.scope) {
|
|
505
|
+
if (!(scopeField in fields)) {
|
|
506
|
+
throw new SchemaValidationError(
|
|
507
|
+
`Scope field "${scopeField}" does not exist in collection "${name}". Available fields: ${Object.keys(fields).join(", ")}`,
|
|
508
|
+
{ collection: name, field: scopeField }
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
scope.push(scopeField);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
let stateMachine;
|
|
515
|
+
if (input.stateMachine) {
|
|
516
|
+
validateStateMachineDefinition(name, input.stateMachine, fields);
|
|
517
|
+
stateMachine = { ...input.stateMachine };
|
|
518
|
+
}
|
|
519
|
+
return { fields, indexes, constraints, resolvers, scope, stateMachine };
|
|
520
|
+
}
|
|
521
|
+
function buildSyncRules(syncInput, collections) {
|
|
522
|
+
if (!syncInput) {
|
|
523
|
+
return void 0;
|
|
524
|
+
}
|
|
525
|
+
const sync = {};
|
|
526
|
+
for (const [collectionName, ruleInput] of Object.entries(syncInput)) {
|
|
527
|
+
if (!(collectionName in collections)) {
|
|
528
|
+
throw new SchemaValidationError(
|
|
529
|
+
`Sync rule references collection "${collectionName}" which does not exist. Available collections: ${Object.keys(collections).join(", ")}`,
|
|
530
|
+
{ collection: collectionName }
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
if (!ruleInput.where || Object.keys(ruleInput.where).length === 0) {
|
|
534
|
+
throw new SchemaValidationError(
|
|
535
|
+
`Sync rule for collection "${collectionName}" must declare at least one where binding`,
|
|
536
|
+
{ collection: collectionName }
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
const collection = collections[collectionName];
|
|
540
|
+
if (!collection) {
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
for (const fieldName of Object.keys(ruleInput.where)) {
|
|
544
|
+
if (!(fieldName in collection.fields)) {
|
|
545
|
+
throw new SchemaValidationError(
|
|
546
|
+
`Sync rule where field "${fieldName}" does not exist in collection "${collectionName}". Available fields: ${Object.keys(collection.fields).join(", ")}`,
|
|
547
|
+
{ collection: collectionName, field: fieldName }
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
try {
|
|
552
|
+
sync[collectionName] = { where: normalizeSyncRuleWhere(collectionName, ruleInput.where) };
|
|
553
|
+
} catch (error) {
|
|
554
|
+
throw new SchemaValidationError(
|
|
555
|
+
error instanceof Error ? error.message : "Invalid sync rule",
|
|
556
|
+
{ collection: collectionName }
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return sync;
|
|
561
|
+
}
|
|
562
|
+
function applySyncRulesToCollectionScope(collections, sync) {
|
|
563
|
+
if (!sync) {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
for (const [collectionName, rule] of Object.entries(sync)) {
|
|
567
|
+
const collection = collections[collectionName];
|
|
568
|
+
if (!collection) {
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
for (const fieldName of Object.keys(rule.where)) {
|
|
572
|
+
if (!collection.scope.includes(fieldName)) {
|
|
573
|
+
collection.scope.push(fieldName);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
146
577
|
}
|
|
147
578
|
function validateFieldName(collection, fieldName) {
|
|
148
579
|
if (RESERVED_FIELDS.has(fieldName)) {
|
|
@@ -231,6 +662,7 @@ function generateSQL(collectionName, collection, relations) {
|
|
|
231
662
|
}
|
|
232
663
|
columns.push("_created_at INTEGER NOT NULL");
|
|
233
664
|
columns.push("_updated_at INTEGER NOT NULL");
|
|
665
|
+
columns.push("_version TEXT NOT NULL DEFAULT ''");
|
|
234
666
|
columns.push("_deleted INTEGER NOT NULL DEFAULT 0");
|
|
235
667
|
statements.push(`CREATE TABLE IF NOT EXISTS ${collectionName} (
|
|
236
668
|
${columns.join(",\n ")}
|
|
@@ -240,6 +672,10 @@ function generateSQL(collectionName, collection, relations) {
|
|
|
240
672
|
statements.push(`--kora:safe-alter
|
|
241
673
|
ALTER TABLE ${collectionName} ADD COLUMN ${colDef}`);
|
|
242
674
|
}
|
|
675
|
+
statements.push(
|
|
676
|
+
`--kora:safe-alter
|
|
677
|
+
ALTER TABLE ${collectionName} ADD COLUMN _version TEXT NOT NULL DEFAULT ''`
|
|
678
|
+
);
|
|
243
679
|
for (const indexField of collection.indexes) {
|
|
244
680
|
statements.push(
|
|
245
681
|
`CREATE INDEX IF NOT EXISTS idx_${collectionName}_${indexField} ON ${collectionName} (${indexField})`
|
|
@@ -276,6 +712,21 @@ function generateFullDDL(schema) {
|
|
|
276
712
|
statements.push(
|
|
277
713
|
"CREATE TABLE IF NOT EXISTS _kora_version_vector (\n node_id TEXT PRIMARY KEY NOT NULL,\n sequence_number INTEGER NOT NULL\n)"
|
|
278
714
|
);
|
|
715
|
+
statements.push(
|
|
716
|
+
"CREATE TABLE IF NOT EXISTS _kora_sequences (\n name TEXT NOT NULL,\n scope TEXT NOT NULL DEFAULT '',\n node_id TEXT NOT NULL,\n counter INTEGER NOT NULL DEFAULT 0,\n PRIMARY KEY (name, scope, node_id)\n)"
|
|
717
|
+
);
|
|
718
|
+
statements.push(
|
|
719
|
+
"CREATE TABLE IF NOT EXISTS _kora_sync_queue (\n id TEXT PRIMARY KEY NOT NULL,\n payload TEXT NOT NULL\n)"
|
|
720
|
+
);
|
|
721
|
+
statements.push(
|
|
722
|
+
"CREATE TABLE IF NOT EXISTS _kora_audit_traces (\n id TEXT PRIMARY KEY NOT NULL,\n recorded_at INTEGER NOT NULL,\n event_type TEXT NOT NULL,\n collection TEXT NOT NULL,\n record_id TEXT NOT NULL,\n field TEXT NOT NULL,\n strategy TEXT NOT NULL,\n tier INTEGER NOT NULL,\n constraint_name TEXT,\n trace_json TEXT NOT NULL\n)"
|
|
723
|
+
);
|
|
724
|
+
statements.push(
|
|
725
|
+
"CREATE INDEX IF NOT EXISTS idx_kora_audit_traces_recorded_at ON _kora_audit_traces (recorded_at)"
|
|
726
|
+
);
|
|
727
|
+
statements.push(
|
|
728
|
+
"CREATE INDEX IF NOT EXISTS idx_kora_audit_traces_collection ON _kora_audit_traces (collection)"
|
|
729
|
+
);
|
|
279
730
|
for (const [name, collection] of Object.entries(schema.collections)) {
|
|
280
731
|
statements.push(...generateSQL(name, collection, schema.relations));
|
|
281
732
|
}
|
|
@@ -329,23 +780,47 @@ var FieldBuilder = class _FieldBuilder {
|
|
|
329
780
|
_required;
|
|
330
781
|
_defaultValue;
|
|
331
782
|
_auto;
|
|
332
|
-
|
|
783
|
+
_mergeStrategy;
|
|
784
|
+
constructor(kind, required = true, defaultValue = void 0, auto = false, mergeStrategy = null) {
|
|
333
785
|
this._kind = kind;
|
|
334
786
|
this._required = required;
|
|
335
787
|
this._defaultValue = defaultValue;
|
|
336
788
|
this._auto = auto;
|
|
789
|
+
this._mergeStrategy = mergeStrategy;
|
|
337
790
|
}
|
|
338
791
|
/** Mark this field as optional (not required on insert) */
|
|
339
792
|
optional() {
|
|
340
|
-
return new _FieldBuilder(this._kind, false, this._defaultValue, this._auto);
|
|
793
|
+
return new _FieldBuilder(this._kind, false, this._defaultValue, this._auto, this._mergeStrategy);
|
|
341
794
|
}
|
|
342
795
|
/** Set a default value for this field. Implicitly makes the field optional. */
|
|
343
796
|
default(value) {
|
|
344
|
-
return new _FieldBuilder(this._kind, false, value, this._auto);
|
|
797
|
+
return new _FieldBuilder(this._kind, false, value, this._auto, this._mergeStrategy);
|
|
345
798
|
}
|
|
346
799
|
/** Mark this field as auto-populated (e.g., createdAt timestamps). Developers cannot set auto fields. */
|
|
347
800
|
auto() {
|
|
348
|
-
return new _FieldBuilder(this._kind, false, void 0, true);
|
|
801
|
+
return new _FieldBuilder(this._kind, false, void 0, true, this._mergeStrategy);
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Declare a merge strategy for this field.
|
|
805
|
+
* Controls how concurrent modifications are resolved during sync.
|
|
806
|
+
*
|
|
807
|
+
* @param strategy - The merge strategy to use:
|
|
808
|
+
* - `'lww'`: Last-write-wins (default for scalar fields)
|
|
809
|
+
* - `'counter'`: Sum of deltas from base (for numbers)
|
|
810
|
+
* - `'max'`: Keep the maximum value (for numbers/timestamps)
|
|
811
|
+
* - `'min'`: Keep the minimum value (for numbers/timestamps)
|
|
812
|
+
* - `'union'`: Set-union merge (default for arrays)
|
|
813
|
+
* - `'append-only'`: Concatenate additions (for arrays)
|
|
814
|
+
* - `'server-authoritative'`: Always prefer the remote/server value
|
|
815
|
+
*/
|
|
816
|
+
merge(strategy) {
|
|
817
|
+
return new _FieldBuilder(
|
|
818
|
+
this._kind,
|
|
819
|
+
this._required,
|
|
820
|
+
this._defaultValue,
|
|
821
|
+
this._auto,
|
|
822
|
+
strategy
|
|
823
|
+
);
|
|
349
824
|
}
|
|
350
825
|
/** @internal Build the final FieldDescriptor. Used by defineSchema(). */
|
|
351
826
|
_build() {
|
|
@@ -355,24 +830,102 @@ var FieldBuilder = class _FieldBuilder {
|
|
|
355
830
|
defaultValue: this._defaultValue,
|
|
356
831
|
auto: this._auto,
|
|
357
832
|
enumValues: null,
|
|
358
|
-
itemKind: null
|
|
833
|
+
itemKind: null,
|
|
834
|
+
mergeStrategy: this._mergeStrategy,
|
|
835
|
+
transitions: null
|
|
359
836
|
};
|
|
360
837
|
}
|
|
361
838
|
};
|
|
362
839
|
var EnumFieldBuilder = class _EnumFieldBuilder extends FieldBuilder {
|
|
363
840
|
_enumValues;
|
|
364
|
-
|
|
365
|
-
|
|
841
|
+
_transitions;
|
|
842
|
+
constructor(values, required = true, defaultValue = void 0, auto = false, mergeStrategy = null, transitions = null) {
|
|
843
|
+
super("enum", required, defaultValue, auto, mergeStrategy);
|
|
366
844
|
this._enumValues = values;
|
|
845
|
+
this._transitions = transitions;
|
|
367
846
|
}
|
|
368
847
|
optional() {
|
|
369
|
-
return new _EnumFieldBuilder(
|
|
848
|
+
return new _EnumFieldBuilder(
|
|
849
|
+
this._enumValues,
|
|
850
|
+
false,
|
|
851
|
+
this._defaultValue,
|
|
852
|
+
this._auto,
|
|
853
|
+
this._mergeStrategy,
|
|
854
|
+
this._transitions
|
|
855
|
+
);
|
|
370
856
|
}
|
|
371
857
|
default(value) {
|
|
372
|
-
return new _EnumFieldBuilder(
|
|
858
|
+
return new _EnumFieldBuilder(
|
|
859
|
+
this._enumValues,
|
|
860
|
+
false,
|
|
861
|
+
value,
|
|
862
|
+
this._auto,
|
|
863
|
+
this._mergeStrategy,
|
|
864
|
+
this._transitions
|
|
865
|
+
);
|
|
373
866
|
}
|
|
374
867
|
auto() {
|
|
375
|
-
return new _EnumFieldBuilder(
|
|
868
|
+
return new _EnumFieldBuilder(
|
|
869
|
+
this._enumValues,
|
|
870
|
+
false,
|
|
871
|
+
void 0,
|
|
872
|
+
true,
|
|
873
|
+
this._mergeStrategy,
|
|
874
|
+
this._transitions
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
merge(strategy) {
|
|
878
|
+
return new _EnumFieldBuilder(
|
|
879
|
+
this._enumValues,
|
|
880
|
+
this._required,
|
|
881
|
+
this._defaultValue,
|
|
882
|
+
this._auto,
|
|
883
|
+
strategy,
|
|
884
|
+
this._transitions
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Declare allowed state transitions for this enum field.
|
|
889
|
+
* Enables state machine validation during mutations and merges.
|
|
890
|
+
*
|
|
891
|
+
* @param map - Map of state to allowed next states
|
|
892
|
+
*
|
|
893
|
+
* @example
|
|
894
|
+
* ```typescript
|
|
895
|
+
* t.enum(['draft', 'pending', 'confirmed', 'cancelled']).transitions({
|
|
896
|
+
* draft: ['pending', 'cancelled'],
|
|
897
|
+
* pending: ['confirmed', 'cancelled'],
|
|
898
|
+
* confirmed: [],
|
|
899
|
+
* cancelled: [],
|
|
900
|
+
* })
|
|
901
|
+
* ```
|
|
902
|
+
*/
|
|
903
|
+
transitions(map) {
|
|
904
|
+
const validValues = new Set(this._enumValues);
|
|
905
|
+
for (const [state, targets] of Object.entries(map)) {
|
|
906
|
+
if (!validValues.has(state)) {
|
|
907
|
+
throw new SchemaValidationError(
|
|
908
|
+
`Invalid source state "${state}" in transition map. Valid values: ${[...validValues].join(", ")}`,
|
|
909
|
+
{ state, validValues: [...validValues] }
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
for (const target of targets) {
|
|
913
|
+
if (!validValues.has(target)) {
|
|
914
|
+
throw new SchemaValidationError(
|
|
915
|
+
`Invalid target state "${target}" in transition from "${state}". Valid values: ${[...validValues].join(", ")}`,
|
|
916
|
+
{ state, target, validValues: [...validValues] }
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return new _EnumFieldBuilder(
|
|
922
|
+
this._enumValues,
|
|
923
|
+
this._required,
|
|
924
|
+
this._defaultValue,
|
|
925
|
+
this._auto,
|
|
926
|
+
this._mergeStrategy,
|
|
927
|
+
map
|
|
928
|
+
);
|
|
376
929
|
}
|
|
377
930
|
_build() {
|
|
378
931
|
return {
|
|
@@ -381,14 +934,16 @@ var EnumFieldBuilder = class _EnumFieldBuilder extends FieldBuilder {
|
|
|
381
934
|
defaultValue: this._defaultValue,
|
|
382
935
|
auto: this._auto,
|
|
383
936
|
enumValues: this._enumValues,
|
|
384
|
-
itemKind: null
|
|
937
|
+
itemKind: null,
|
|
938
|
+
mergeStrategy: this._mergeStrategy,
|
|
939
|
+
transitions: this._transitions
|
|
385
940
|
};
|
|
386
941
|
}
|
|
387
942
|
};
|
|
388
943
|
var ArrayFieldBuilder = class _ArrayFieldBuilder extends FieldBuilder {
|
|
389
944
|
_itemKind;
|
|
390
|
-
constructor(itemBuilder, required = true, defaultValue = void 0, auto = false) {
|
|
391
|
-
super("array", required, defaultValue, auto);
|
|
945
|
+
constructor(itemBuilder, required = true, defaultValue = void 0, auto = false, mergeStrategy = null) {
|
|
946
|
+
super("array", required, defaultValue, auto, mergeStrategy);
|
|
392
947
|
this._itemKind = itemBuilder._build().kind;
|
|
393
948
|
}
|
|
394
949
|
optional() {
|
|
@@ -396,14 +951,36 @@ var ArrayFieldBuilder = class _ArrayFieldBuilder extends FieldBuilder {
|
|
|
396
951
|
new FieldBuilder(this._itemKind),
|
|
397
952
|
false,
|
|
398
953
|
this._defaultValue,
|
|
399
|
-
this._auto
|
|
954
|
+
this._auto,
|
|
955
|
+
this._mergeStrategy
|
|
400
956
|
);
|
|
401
957
|
}
|
|
402
958
|
default(value) {
|
|
403
|
-
return new _ArrayFieldBuilder(
|
|
959
|
+
return new _ArrayFieldBuilder(
|
|
960
|
+
new FieldBuilder(this._itemKind),
|
|
961
|
+
false,
|
|
962
|
+
value,
|
|
963
|
+
this._auto,
|
|
964
|
+
this._mergeStrategy
|
|
965
|
+
);
|
|
404
966
|
}
|
|
405
967
|
auto() {
|
|
406
|
-
return new _ArrayFieldBuilder(
|
|
968
|
+
return new _ArrayFieldBuilder(
|
|
969
|
+
new FieldBuilder(this._itemKind),
|
|
970
|
+
false,
|
|
971
|
+
void 0,
|
|
972
|
+
true,
|
|
973
|
+
this._mergeStrategy
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
merge(strategy) {
|
|
977
|
+
return new _ArrayFieldBuilder(
|
|
978
|
+
new FieldBuilder(this._itemKind),
|
|
979
|
+
this._required,
|
|
980
|
+
this._defaultValue,
|
|
981
|
+
this._auto,
|
|
982
|
+
strategy
|
|
983
|
+
);
|
|
407
984
|
}
|
|
408
985
|
_build() {
|
|
409
986
|
return {
|
|
@@ -412,7 +989,9 @@ var ArrayFieldBuilder = class _ArrayFieldBuilder extends FieldBuilder {
|
|
|
412
989
|
defaultValue: this._defaultValue,
|
|
413
990
|
auto: this._auto,
|
|
414
991
|
enumValues: null,
|
|
415
|
-
itemKind: this._itemKind
|
|
992
|
+
itemKind: this._itemKind,
|
|
993
|
+
mergeStrategy: this._mergeStrategy,
|
|
994
|
+
transitions: null
|
|
416
995
|
};
|
|
417
996
|
}
|
|
418
997
|
};
|
|
@@ -466,10 +1045,14 @@ function validateRecord(collection, collectionDef, data, operationType) {
|
|
|
466
1045
|
}
|
|
467
1046
|
if (operationType === "update") {
|
|
468
1047
|
if (hasValue) {
|
|
469
|
-
if (value
|
|
1048
|
+
if (isAtomicOp(value)) {
|
|
1049
|
+
result[fieldName] = value;
|
|
1050
|
+
} else if (value !== void 0 && value !== null) {
|
|
470
1051
|
validateFieldValue(collection, fieldName, descriptor, value);
|
|
1052
|
+
result[fieldName] = value;
|
|
1053
|
+
} else {
|
|
1054
|
+
result[fieldName] = value;
|
|
471
1055
|
}
|
|
472
|
-
result[fieldName] = value;
|
|
473
1056
|
}
|
|
474
1057
|
continue;
|
|
475
1058
|
}
|
|
@@ -657,12 +1240,13 @@ function vectorsEqual(a, b) {
|
|
|
657
1240
|
}
|
|
658
1241
|
return true;
|
|
659
1242
|
}
|
|
660
|
-
function computeDelta(localVector, remoteVector, operationLog) {
|
|
1243
|
+
async function computeDelta(localVector, remoteVector, operationLog) {
|
|
661
1244
|
const missing = [];
|
|
662
1245
|
for (const [nodeId, localSeq] of localVector) {
|
|
663
1246
|
const remoteSeq = remoteVector.get(nodeId) ?? 0;
|
|
664
1247
|
if (localSeq > remoteSeq) {
|
|
665
|
-
|
|
1248
|
+
const ops = await operationLog.getRange(nodeId, remoteSeq + 1, localSeq);
|
|
1249
|
+
missing.push(...ops);
|
|
666
1250
|
}
|
|
667
1251
|
}
|
|
668
1252
|
return topologicalSort(missing);
|
|
@@ -675,37 +1259,715 @@ function deserializeVector(s) {
|
|
|
675
1259
|
const entries = JSON.parse(s);
|
|
676
1260
|
return new Map(entries);
|
|
677
1261
|
}
|
|
1262
|
+
|
|
1263
|
+
// src/scopes/build-scope-map.ts
|
|
1264
|
+
function buildScopeMap(schema, scopeValues) {
|
|
1265
|
+
const result = {};
|
|
1266
|
+
const partialSync = hasSchemaSyncRules(schema);
|
|
1267
|
+
for (const collName of Object.keys(schema.collections)) {
|
|
1268
|
+
if (partialSync && !isCollectionSyncScoped(schema, collName)) {
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
1271
|
+
const bindings = getCollectionScopeBindings(schema, collName);
|
|
1272
|
+
if (bindings) {
|
|
1273
|
+
const collScope = {};
|
|
1274
|
+
for (const [field, scopeKey] of Object.entries(bindings)) {
|
|
1275
|
+
if (scopeKey in scopeValues) {
|
|
1276
|
+
collScope[field] = scopeValues[scopeKey];
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
result[collName] = collScope;
|
|
1280
|
+
} else {
|
|
1281
|
+
result[collName] = {};
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
return result;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// src/scopes/extract-scope-from-claims.ts
|
|
1288
|
+
function collectSchemaScopeFields(schema) {
|
|
1289
|
+
return collectSchemaScopeValueKeys(schema);
|
|
1290
|
+
}
|
|
1291
|
+
function extractScopeValuesFromClaims(schema, claims) {
|
|
1292
|
+
const scopeFields = collectSchemaScopeValueKeys(schema);
|
|
1293
|
+
if (scopeFields.length === 0) {
|
|
1294
|
+
return {};
|
|
1295
|
+
}
|
|
1296
|
+
const nestedScope = claims.scope;
|
|
1297
|
+
const scopeObject = typeof nestedScope === "object" && nestedScope !== null && !Array.isArray(nestedScope) ? nestedScope : {};
|
|
1298
|
+
const result = {};
|
|
1299
|
+
for (const field of scopeFields) {
|
|
1300
|
+
if (field in claims) {
|
|
1301
|
+
result[field] = claims[field];
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1304
|
+
if (field in scopeObject) {
|
|
1305
|
+
result[field] = scopeObject[field];
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1308
|
+
if (field === "userId" && typeof claims.sub === "string") {
|
|
1309
|
+
result.userId = claims.sub;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
return result;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// src/sequences/sequence-format.ts
|
|
1316
|
+
function formatSequenceValue(template, counter, nodeId, now) {
|
|
1317
|
+
const date = now ?? /* @__PURE__ */ new Date();
|
|
1318
|
+
const yyyy = String(date.getUTCFullYear());
|
|
1319
|
+
const mm = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
1320
|
+
const dd = String(date.getUTCDate()).padStart(2, "0");
|
|
1321
|
+
const dateStr = `${yyyy}${mm}${dd}`;
|
|
1322
|
+
return template.replace(/\{([^}]+)\}/g, (_match, token) => {
|
|
1323
|
+
if (token === "date") return dateStr;
|
|
1324
|
+
if (token === "node4") return nodeId.slice(0, 4);
|
|
1325
|
+
if (token === "node8") return nodeId.slice(0, 8);
|
|
1326
|
+
if (token === "seq") return String(counter).padStart(4, "0");
|
|
1327
|
+
if (token.startsWith("seq:")) {
|
|
1328
|
+
const width = Number.parseInt(token.slice(4), 10);
|
|
1329
|
+
if (Number.isNaN(width) || width < 1) {
|
|
1330
|
+
return String(counter).padStart(4, "0");
|
|
1331
|
+
}
|
|
1332
|
+
return String(counter).padStart(width, "0");
|
|
1333
|
+
}
|
|
1334
|
+
return `{${token}}`;
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
function defaultSequenceFormat(name) {
|
|
1338
|
+
return `${name}-{seq:4}`;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// src/migration/apply-operation-transforms.ts
|
|
1342
|
+
var MAX_TRANSFORM_STEPS = 32;
|
|
1343
|
+
function applyOperationTransforms(operation, targetSchemaVersion, transforms) {
|
|
1344
|
+
let current = operation;
|
|
1345
|
+
for (let step = 0; step < MAX_TRANSFORM_STEPS; step++) {
|
|
1346
|
+
if (current.schemaVersion === targetSchemaVersion) {
|
|
1347
|
+
return current;
|
|
1348
|
+
}
|
|
1349
|
+
const transform = transforms.find(
|
|
1350
|
+
(candidate) => candidate.fromVersion === current.schemaVersion
|
|
1351
|
+
);
|
|
1352
|
+
if (!transform) {
|
|
1353
|
+
return null;
|
|
1354
|
+
}
|
|
1355
|
+
const next = transform.transform(current);
|
|
1356
|
+
if (next === null) {
|
|
1357
|
+
return null;
|
|
1358
|
+
}
|
|
1359
|
+
current = next;
|
|
1360
|
+
}
|
|
1361
|
+
return null;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// src/migrations/migration-builder.ts
|
|
1365
|
+
var RollbackBuilder = class {
|
|
1366
|
+
_steps = [];
|
|
1367
|
+
/**
|
|
1368
|
+
* Add a field during rollback (to reverse a removeField).
|
|
1369
|
+
*/
|
|
1370
|
+
addField(collection, field, builder) {
|
|
1371
|
+
this._steps.push({ type: "addField", collection, field, descriptor: builder._build() });
|
|
1372
|
+
return this;
|
|
1373
|
+
}
|
|
1374
|
+
/**
|
|
1375
|
+
* Remove a field during rollback (to reverse an addField).
|
|
1376
|
+
*/
|
|
1377
|
+
removeField(collection, field) {
|
|
1378
|
+
this._steps.push({ type: "removeField", collection, field });
|
|
1379
|
+
return this;
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Rename a field during rollback (to reverse a renameField).
|
|
1383
|
+
*/
|
|
1384
|
+
renameField(collection, from, to) {
|
|
1385
|
+
this._steps.push({ type: "renameField", collection, from, to });
|
|
1386
|
+
return this;
|
|
1387
|
+
}
|
|
1388
|
+
/**
|
|
1389
|
+
* Add an index during rollback (to reverse a removeIndex).
|
|
1390
|
+
*/
|
|
1391
|
+
addIndex(collection, field) {
|
|
1392
|
+
this._steps.push({ type: "addIndex", collection, field });
|
|
1393
|
+
return this;
|
|
1394
|
+
}
|
|
1395
|
+
/**
|
|
1396
|
+
* Remove an index during rollback (to reverse an addIndex).
|
|
1397
|
+
*/
|
|
1398
|
+
removeIndex(collection, field) {
|
|
1399
|
+
this._steps.push({ type: "removeIndex", collection, field });
|
|
1400
|
+
return this;
|
|
1401
|
+
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Run a backfill during rollback (to reverse a previous backfill).
|
|
1404
|
+
*/
|
|
1405
|
+
backfill(collection, transform) {
|
|
1406
|
+
this._steps.push({ type: "backfill", collection, transform });
|
|
1407
|
+
return this;
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Get the accumulated rollback steps.
|
|
1411
|
+
*/
|
|
1412
|
+
_getSteps() {
|
|
1413
|
+
return [...this._steps];
|
|
1414
|
+
}
|
|
1415
|
+
};
|
|
1416
|
+
var MigrationBuilder = class _MigrationBuilder {
|
|
1417
|
+
steps;
|
|
1418
|
+
rollbackSteps;
|
|
1419
|
+
safelyReversible;
|
|
1420
|
+
constructor(steps = [], rollbackSteps, safelyReversible) {
|
|
1421
|
+
this.steps = steps;
|
|
1422
|
+
this.rollbackSteps = rollbackSteps;
|
|
1423
|
+
this.safelyReversible = safelyReversible ?? this._computeSafelyReversible(steps, rollbackSteps);
|
|
1424
|
+
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Add a new field to a collection.
|
|
1427
|
+
* The field builder provides the type and default value.
|
|
1428
|
+
*/
|
|
1429
|
+
addField(collection, field, builder) {
|
|
1430
|
+
return new _MigrationBuilder([
|
|
1431
|
+
...this.steps,
|
|
1432
|
+
{ type: "addField", collection, field, descriptor: builder._build() }
|
|
1433
|
+
]);
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* Remove a field from a collection.
|
|
1437
|
+
* Optionally accepts a field builder to preserve the descriptor for rollback.
|
|
1438
|
+
* Without the descriptor, rollback of this step requires a custom `.down()`.
|
|
1439
|
+
*
|
|
1440
|
+
* @param collection - The collection name
|
|
1441
|
+
* @param field - The field name to remove
|
|
1442
|
+
* @param builder - Optional field builder preserving the type info for rollback
|
|
1443
|
+
*/
|
|
1444
|
+
removeField(collection, field, builder) {
|
|
1445
|
+
const step = builder ? { type: "removeField", collection, field, descriptor: builder._build() } : { type: "removeField", collection, field };
|
|
1446
|
+
return new _MigrationBuilder([...this.steps, step]);
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Rename a field in a collection.
|
|
1450
|
+
* Implemented as ALTER TABLE RENAME COLUMN (SQLite 3.25+).
|
|
1451
|
+
*/
|
|
1452
|
+
renameField(collection, from, to) {
|
|
1453
|
+
return new _MigrationBuilder([...this.steps, { type: "renameField", collection, from, to }]);
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Add an index on a field.
|
|
1457
|
+
*/
|
|
1458
|
+
addIndex(collection, field) {
|
|
1459
|
+
return new _MigrationBuilder([...this.steps, { type: "addIndex", collection, field }]);
|
|
1460
|
+
}
|
|
1461
|
+
/**
|
|
1462
|
+
* Remove an index on a field.
|
|
1463
|
+
*/
|
|
1464
|
+
removeIndex(collection, field) {
|
|
1465
|
+
return new _MigrationBuilder([...this.steps, { type: "removeIndex", collection, field }]);
|
|
1466
|
+
}
|
|
1467
|
+
/**
|
|
1468
|
+
* Backfill records in a collection using a transform function.
|
|
1469
|
+
* The transform receives each record and returns the fields to update.
|
|
1470
|
+
* Runs after structural changes (addField, renameField, etc.).
|
|
1471
|
+
*
|
|
1472
|
+
* @param collection - The collection name
|
|
1473
|
+
* @param transform - Forward transform function
|
|
1474
|
+
* @param reverseTransform - Optional reverse transform for rollback support
|
|
1475
|
+
*/
|
|
1476
|
+
backfill(collection, transform, reverseTransform) {
|
|
1477
|
+
return new _MigrationBuilder([
|
|
1478
|
+
...this.steps,
|
|
1479
|
+
{ type: "backfill", collection, transform, reverseTransform }
|
|
1480
|
+
]);
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* Define explicit rollback steps for this migration.
|
|
1484
|
+
* If not called, inverse steps are auto-generated from forward steps.
|
|
1485
|
+
*
|
|
1486
|
+
* @param fn - Function that receives a RollbackBuilder to define rollback steps
|
|
1487
|
+
* @returns A new MigrationBuilder with the rollback steps attached
|
|
1488
|
+
*
|
|
1489
|
+
* @example
|
|
1490
|
+
* ```typescript
|
|
1491
|
+
* migrate()
|
|
1492
|
+
* .addField('todos', 'priority', t.enum(['low', 'medium', 'high']).default('medium'))
|
|
1493
|
+
* .addIndex('todos', 'priority')
|
|
1494
|
+
* .down((rollback) => {
|
|
1495
|
+
* rollback
|
|
1496
|
+
* .removeIndex('todos', 'priority')
|
|
1497
|
+
* .removeField('todos', 'priority')
|
|
1498
|
+
* })
|
|
1499
|
+
* ```
|
|
1500
|
+
*/
|
|
1501
|
+
down(fn) {
|
|
1502
|
+
const rollbackBuilder = new RollbackBuilder();
|
|
1503
|
+
fn(rollbackBuilder);
|
|
1504
|
+
const rbSteps = rollbackBuilder._getSteps();
|
|
1505
|
+
return new _MigrationBuilder(this.steps, rbSteps, true);
|
|
1506
|
+
}
|
|
1507
|
+
/**
|
|
1508
|
+
* Determine if a migration is safely reversible based on its steps.
|
|
1509
|
+
* A migration is not safely reversible if:
|
|
1510
|
+
* - It has a backfill step without a reverseTransform and no explicit rollback
|
|
1511
|
+
* - It has a removeField without a stored descriptor and no explicit rollback
|
|
1512
|
+
*/
|
|
1513
|
+
_computeSafelyReversible(steps, rollbackSteps) {
|
|
1514
|
+
if (rollbackSteps !== void 0) {
|
|
1515
|
+
return true;
|
|
1516
|
+
}
|
|
1517
|
+
for (const step of steps) {
|
|
1518
|
+
if (step.type === "backfill" && !step.reverseTransform) {
|
|
1519
|
+
return false;
|
|
1520
|
+
}
|
|
1521
|
+
if (step.type === "removeField" && !step.descriptor) {
|
|
1522
|
+
return false;
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
return true;
|
|
1526
|
+
}
|
|
1527
|
+
};
|
|
1528
|
+
function migrate() {
|
|
1529
|
+
return new MigrationBuilder();
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// src/migrations/migration-rollback.ts
|
|
1533
|
+
var MigrationRollbackError = class extends KoraError {
|
|
1534
|
+
constructor(step) {
|
|
1535
|
+
super(
|
|
1536
|
+
`Cannot auto-generate rollback for "${step.type}" step on collection "${step.collection}". Provide an explicit .down() definition for this migration.`,
|
|
1537
|
+
"MIGRATION_ROLLBACK",
|
|
1538
|
+
{ stepType: step.type, collection: step.collection }
|
|
1539
|
+
);
|
|
1540
|
+
this.name = "MigrationRollbackError";
|
|
1541
|
+
}
|
|
1542
|
+
};
|
|
1543
|
+
function canAutoRollback(step) {
|
|
1544
|
+
switch (step.type) {
|
|
1545
|
+
case "addField":
|
|
1546
|
+
case "addIndex":
|
|
1547
|
+
case "removeIndex":
|
|
1548
|
+
case "renameField":
|
|
1549
|
+
return true;
|
|
1550
|
+
case "removeField":
|
|
1551
|
+
case "backfill":
|
|
1552
|
+
return false;
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
function generateRollbackSteps(forwardSteps) {
|
|
1556
|
+
const rollbackSteps = [];
|
|
1557
|
+
for (let i = forwardSteps.length - 1; i >= 0; i--) {
|
|
1558
|
+
const step = forwardSteps[i];
|
|
1559
|
+
if (step === void 0) continue;
|
|
1560
|
+
const rollback = generateSingleRollbackStep(step);
|
|
1561
|
+
rollbackSteps.push(rollback);
|
|
1562
|
+
}
|
|
1563
|
+
return rollbackSteps;
|
|
1564
|
+
}
|
|
1565
|
+
function generateSingleRollbackStep(step) {
|
|
1566
|
+
switch (step.type) {
|
|
1567
|
+
case "addField":
|
|
1568
|
+
return { type: "removeField", collection: step.collection, field: step.field };
|
|
1569
|
+
case "removeField":
|
|
1570
|
+
if (step.descriptor) {
|
|
1571
|
+
return {
|
|
1572
|
+
type: "addField",
|
|
1573
|
+
collection: step.collection,
|
|
1574
|
+
field: step.field,
|
|
1575
|
+
descriptor: step.descriptor
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
throw new MigrationRollbackError(step);
|
|
1579
|
+
case "renameField":
|
|
1580
|
+
return { type: "renameField", collection: step.collection, from: step.to, to: step.from };
|
|
1581
|
+
case "addIndex":
|
|
1582
|
+
return { type: "removeIndex", collection: step.collection, field: step.field };
|
|
1583
|
+
case "removeIndex":
|
|
1584
|
+
return { type: "addIndex", collection: step.collection, field: step.field };
|
|
1585
|
+
case "backfill":
|
|
1586
|
+
if (step.reverseTransform) {
|
|
1587
|
+
return {
|
|
1588
|
+
type: "backfill",
|
|
1589
|
+
collection: step.collection,
|
|
1590
|
+
transform: step.reverseTransform
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
throw new MigrationRollbackError(step);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
function createReversibleMigration(upSteps, downSteps, fromVersion, toVersion) {
|
|
1597
|
+
const down = downSteps !== null ? [...downSteps] : generateRollbackSteps(upSteps);
|
|
1598
|
+
return {
|
|
1599
|
+
up: [...upSteps],
|
|
1600
|
+
down,
|
|
1601
|
+
fromVersion,
|
|
1602
|
+
toVersion
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
// src/migrations/migration-sql.ts
|
|
1607
|
+
function migrationStepsToSQL(steps) {
|
|
1608
|
+
const statements = [];
|
|
1609
|
+
for (const step of steps) {
|
|
1610
|
+
switch (step.type) {
|
|
1611
|
+
case "addField":
|
|
1612
|
+
statements.push(addFieldSQL(step.collection, step.field, step.descriptor));
|
|
1613
|
+
break;
|
|
1614
|
+
case "removeField":
|
|
1615
|
+
statements.push(`ALTER TABLE ${step.collection} DROP COLUMN ${step.field}`);
|
|
1616
|
+
break;
|
|
1617
|
+
case "renameField":
|
|
1618
|
+
statements.push(`ALTER TABLE ${step.collection} RENAME COLUMN ${step.from} TO ${step.to}`);
|
|
1619
|
+
break;
|
|
1620
|
+
case "addIndex":
|
|
1621
|
+
statements.push(
|
|
1622
|
+
`CREATE INDEX IF NOT EXISTS idx_${step.collection}_${step.field} ON ${step.collection} (${step.field})`
|
|
1623
|
+
);
|
|
1624
|
+
break;
|
|
1625
|
+
case "removeIndex":
|
|
1626
|
+
statements.push(`DROP INDEX IF EXISTS idx_${step.collection}_${step.field}`);
|
|
1627
|
+
break;
|
|
1628
|
+
case "backfill":
|
|
1629
|
+
break;
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
return statements;
|
|
1633
|
+
}
|
|
1634
|
+
function rollbackStepsToSQL(migration) {
|
|
1635
|
+
const rollbackSteps = migration.rollbackSteps ?? generateRollbackSteps(migration.steps);
|
|
1636
|
+
return migrationStepsToSQL(rollbackSteps);
|
|
1637
|
+
}
|
|
1638
|
+
function addFieldSQL(collection, field, descriptor) {
|
|
1639
|
+
const sqlType = mapFieldType2(descriptor);
|
|
1640
|
+
const parts = [`ALTER TABLE ${collection} ADD COLUMN ${field}`, sqlType];
|
|
1641
|
+
if (descriptor.defaultValue !== void 0) {
|
|
1642
|
+
parts.push(`DEFAULT ${sqlDefault2(descriptor.defaultValue)}`);
|
|
1643
|
+
}
|
|
1644
|
+
if (descriptor.kind === "enum" && descriptor.enumValues) {
|
|
1645
|
+
const values = descriptor.enumValues.map((v) => `'${v}'`).join(", ");
|
|
1646
|
+
parts.push(`CHECK (${field} IN (${values}))`);
|
|
1647
|
+
}
|
|
1648
|
+
return parts.join(" ");
|
|
1649
|
+
}
|
|
1650
|
+
function mapFieldType2(descriptor) {
|
|
1651
|
+
switch (descriptor.kind) {
|
|
1652
|
+
case "string":
|
|
1653
|
+
return "TEXT";
|
|
1654
|
+
case "number":
|
|
1655
|
+
return "REAL";
|
|
1656
|
+
case "boolean":
|
|
1657
|
+
return "INTEGER";
|
|
1658
|
+
case "enum":
|
|
1659
|
+
return "TEXT";
|
|
1660
|
+
case "timestamp":
|
|
1661
|
+
return "INTEGER";
|
|
1662
|
+
case "array":
|
|
1663
|
+
return "TEXT";
|
|
1664
|
+
case "richtext":
|
|
1665
|
+
return "BLOB";
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
function sqlDefault2(value) {
|
|
1669
|
+
if (value === null) return "NULL";
|
|
1670
|
+
if (typeof value === "string") return `'${value}'`;
|
|
1671
|
+
if (typeof value === "number") return String(value);
|
|
1672
|
+
if (typeof value === "boolean") return value ? "1" : "0";
|
|
1673
|
+
return `'${JSON.stringify(value)}'`;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
// src/codegen/proto-generator.ts
|
|
1677
|
+
var SCALAR_TYPE_MAP = {
|
|
1678
|
+
string: "string",
|
|
1679
|
+
number: "double",
|
|
1680
|
+
boolean: "bool",
|
|
1681
|
+
timestamp: "int64",
|
|
1682
|
+
richtext: "bytes"
|
|
1683
|
+
};
|
|
1684
|
+
var ARRAY_ITEM_TYPE_MAP = {
|
|
1685
|
+
string: "string",
|
|
1686
|
+
number: "double",
|
|
1687
|
+
boolean: "bool",
|
|
1688
|
+
timestamp: "int64"
|
|
1689
|
+
};
|
|
1690
|
+
function toPascalCase(name) {
|
|
1691
|
+
return name.split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
1692
|
+
}
|
|
1693
|
+
function toSnakeCase(name) {
|
|
1694
|
+
return name.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
1695
|
+
}
|
|
1696
|
+
function resolveProtoType(fieldName, descriptor, messageName) {
|
|
1697
|
+
if (descriptor.kind === "enum") {
|
|
1698
|
+
return `${messageName}${toPascalCase(fieldName)}`;
|
|
1699
|
+
}
|
|
1700
|
+
if (descriptor.kind === "array") {
|
|
1701
|
+
const itemType = descriptor.itemKind ? ARRAY_ITEM_TYPE_MAP[descriptor.itemKind] ?? "string" : "string";
|
|
1702
|
+
return itemType;
|
|
1703
|
+
}
|
|
1704
|
+
return SCALAR_TYPE_MAP[descriptor.kind];
|
|
1705
|
+
}
|
|
1706
|
+
function generateEnumBlock(enumTypeName, enumValues, indent) {
|
|
1707
|
+
const lines = [];
|
|
1708
|
+
lines.push(`${indent}enum ${enumTypeName} {`);
|
|
1709
|
+
lines.push(`${indent} ${enumTypeName.toUpperCase()}_UNSPECIFIED = 0;`);
|
|
1710
|
+
for (let i = 0; i < enumValues.length; i++) {
|
|
1711
|
+
const value = enumValues[i];
|
|
1712
|
+
if (value === void 0) continue;
|
|
1713
|
+
lines.push(`${indent} ${enumTypeName.toUpperCase()}_${value.toUpperCase()} = ${i + 1};`);
|
|
1714
|
+
}
|
|
1715
|
+
lines.push(`${indent}}`);
|
|
1716
|
+
return lines.join("\n");
|
|
1717
|
+
}
|
|
1718
|
+
function generateCollectionMessage(collectionName, collection, typeMap) {
|
|
1719
|
+
const messageName = `${toPascalCase(collectionName)}Record`;
|
|
1720
|
+
const lines = [];
|
|
1721
|
+
const nestedEnums = [];
|
|
1722
|
+
lines.push(`message ${messageName} {`);
|
|
1723
|
+
lines.push(" string id = 1;");
|
|
1724
|
+
typeMap.set(`${collectionName}.id`, "string");
|
|
1725
|
+
let fieldNumber = 2;
|
|
1726
|
+
const entries = Object.entries(collection.fields);
|
|
1727
|
+
for (const [fieldName, descriptor] of entries) {
|
|
1728
|
+
const protoFieldName = toSnakeCase(fieldName);
|
|
1729
|
+
const protoType = resolveProtoType(fieldName, descriptor, messageName);
|
|
1730
|
+
const mapKey = `${collectionName}.${fieldName}`;
|
|
1731
|
+
if (descriptor.kind === "enum" && descriptor.enumValues) {
|
|
1732
|
+
const enumTypeName = `${messageName}${toPascalCase(fieldName)}`;
|
|
1733
|
+
nestedEnums.push(generateEnumBlock(enumTypeName, descriptor.enumValues, " "));
|
|
1734
|
+
typeMap.set(mapKey, enumTypeName);
|
|
1735
|
+
lines.push(` ${enumTypeName} ${protoFieldName} = ${fieldNumber};`);
|
|
1736
|
+
} else if (descriptor.kind === "array") {
|
|
1737
|
+
typeMap.set(mapKey, `repeated ${protoType}`);
|
|
1738
|
+
lines.push(` repeated ${protoType} ${protoFieldName} = ${fieldNumber};`);
|
|
1739
|
+
} else {
|
|
1740
|
+
typeMap.set(mapKey, protoType);
|
|
1741
|
+
lines.push(` ${protoType} ${protoFieldName} = ${fieldNumber};`);
|
|
1742
|
+
}
|
|
1743
|
+
fieldNumber++;
|
|
1744
|
+
}
|
|
1745
|
+
if (nestedEnums.length > 0) {
|
|
1746
|
+
lines.push("");
|
|
1747
|
+
for (const enumBlock of nestedEnums) {
|
|
1748
|
+
lines.push(enumBlock);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
lines.push("}");
|
|
1752
|
+
return lines.join("\n");
|
|
1753
|
+
}
|
|
1754
|
+
var KORA_OPERATION_MESSAGE = `message KoraOperation {
|
|
1755
|
+
string id = 1;
|
|
1756
|
+
string node_id = 2;
|
|
1757
|
+
string type = 3;
|
|
1758
|
+
string collection = 4;
|
|
1759
|
+
string record_id = 5;
|
|
1760
|
+
bytes data = 6;
|
|
1761
|
+
bytes previous_data = 7;
|
|
1762
|
+
int64 wall_time = 8;
|
|
1763
|
+
int32 logical = 9;
|
|
1764
|
+
string timestamp_node_id = 10;
|
|
1765
|
+
int64 sequence_number = 11;
|
|
1766
|
+
repeated string causal_deps = 12;
|
|
1767
|
+
int32 schema_version = 13;
|
|
1768
|
+
}`;
|
|
1769
|
+
var OPERATION_BATCH_MESSAGE = `message OperationBatch {
|
|
1770
|
+
repeated KoraOperation operations = 1;
|
|
1771
|
+
bool is_final = 2;
|
|
1772
|
+
}`;
|
|
1773
|
+
var HANDSHAKE_MESSAGE = `message HandshakeMessage {
|
|
1774
|
+
map<string, int64> version_vector = 1;
|
|
1775
|
+
int32 schema_version = 2;
|
|
1776
|
+
string node_id = 3;
|
|
1777
|
+
}`;
|
|
1778
|
+
var HANDSHAKE_RESPONSE_MESSAGE = `message HandshakeResponse {
|
|
1779
|
+
map<string, int64> version_vector = 1;
|
|
1780
|
+
int32 schema_version = 2;
|
|
1781
|
+
}`;
|
|
1782
|
+
var ACKNOWLEDGMENT_MESSAGE = `message Acknowledgment {
|
|
1783
|
+
int64 sequence_number = 1;
|
|
1784
|
+
string node_id = 2;
|
|
1785
|
+
}`;
|
|
1786
|
+
function buildJsonDescriptor(schema) {
|
|
1787
|
+
const nested = {};
|
|
1788
|
+
for (const [collectionName, collection] of Object.entries(schema.collections)) {
|
|
1789
|
+
const messageName = `${toPascalCase(collectionName)}Record`;
|
|
1790
|
+
const fields = {
|
|
1791
|
+
id: { type: "string", id: 1 }
|
|
1792
|
+
};
|
|
1793
|
+
const nestedTypes = {};
|
|
1794
|
+
let fieldNumber = 2;
|
|
1795
|
+
for (const [fieldName, descriptor] of Object.entries(collection.fields)) {
|
|
1796
|
+
const protoFieldName = toSnakeCase(fieldName);
|
|
1797
|
+
const fieldDef = { id: fieldNumber };
|
|
1798
|
+
if (descriptor.kind === "enum" && descriptor.enumValues) {
|
|
1799
|
+
const enumTypeName = `${messageName}${toPascalCase(fieldName)}`;
|
|
1800
|
+
fieldDef.type = enumTypeName;
|
|
1801
|
+
const enumValuesObj = {
|
|
1802
|
+
[`${enumTypeName.toUpperCase()}_UNSPECIFIED`]: 0
|
|
1803
|
+
};
|
|
1804
|
+
for (let i = 0; i < descriptor.enumValues.length; i++) {
|
|
1805
|
+
const val = descriptor.enumValues[i];
|
|
1806
|
+
if (val === void 0) continue;
|
|
1807
|
+
enumValuesObj[`${enumTypeName.toUpperCase()}_${val.toUpperCase()}`] = i + 1;
|
|
1808
|
+
}
|
|
1809
|
+
nestedTypes[enumTypeName] = { values: enumValuesObj };
|
|
1810
|
+
} else if (descriptor.kind === "array") {
|
|
1811
|
+
const itemType = descriptor.itemKind ? ARRAY_ITEM_TYPE_MAP[descriptor.itemKind] ?? "string" : "string";
|
|
1812
|
+
fieldDef.type = itemType;
|
|
1813
|
+
fieldDef.rule = "repeated";
|
|
1814
|
+
} else {
|
|
1815
|
+
fieldDef.type = SCALAR_TYPE_MAP[descriptor.kind];
|
|
1816
|
+
}
|
|
1817
|
+
fields[protoFieldName] = fieldDef;
|
|
1818
|
+
fieldNumber++;
|
|
1819
|
+
}
|
|
1820
|
+
const messageDescriptor = { fields };
|
|
1821
|
+
if (Object.keys(nestedTypes).length > 0) {
|
|
1822
|
+
messageDescriptor.nested = nestedTypes;
|
|
1823
|
+
}
|
|
1824
|
+
nested[messageName] = messageDescriptor;
|
|
1825
|
+
}
|
|
1826
|
+
nested.KoraOperation = {
|
|
1827
|
+
fields: {
|
|
1828
|
+
id: { type: "string", id: 1 },
|
|
1829
|
+
node_id: { type: "string", id: 2 },
|
|
1830
|
+
type: { type: "string", id: 3 },
|
|
1831
|
+
collection: { type: "string", id: 4 },
|
|
1832
|
+
record_id: { type: "string", id: 5 },
|
|
1833
|
+
data: { type: "bytes", id: 6 },
|
|
1834
|
+
previous_data: { type: "bytes", id: 7 },
|
|
1835
|
+
wall_time: { type: "int64", id: 8 },
|
|
1836
|
+
logical: { type: "int32", id: 9 },
|
|
1837
|
+
timestamp_node_id: { type: "string", id: 10 },
|
|
1838
|
+
sequence_number: { type: "int64", id: 11 },
|
|
1839
|
+
causal_deps: { type: "string", id: 12, rule: "repeated" },
|
|
1840
|
+
schema_version: { type: "int32", id: 13 }
|
|
1841
|
+
}
|
|
1842
|
+
};
|
|
1843
|
+
nested.OperationBatch = {
|
|
1844
|
+
fields: {
|
|
1845
|
+
operations: { type: "KoraOperation", id: 1, rule: "repeated" },
|
|
1846
|
+
is_final: { type: "bool", id: 2 }
|
|
1847
|
+
}
|
|
1848
|
+
};
|
|
1849
|
+
nested.HandshakeMessage = {
|
|
1850
|
+
fields: {
|
|
1851
|
+
version_vector: { keyType: "string", type: "int64", id: 1 },
|
|
1852
|
+
schema_version: { type: "int32", id: 2 },
|
|
1853
|
+
node_id: { type: "string", id: 3 }
|
|
1854
|
+
}
|
|
1855
|
+
};
|
|
1856
|
+
nested.HandshakeResponse = {
|
|
1857
|
+
fields: {
|
|
1858
|
+
version_vector: { keyType: "string", type: "int64", id: 1 },
|
|
1859
|
+
schema_version: { type: "int32", id: 2 }
|
|
1860
|
+
}
|
|
1861
|
+
};
|
|
1862
|
+
nested.Acknowledgment = {
|
|
1863
|
+
fields: {
|
|
1864
|
+
sequence_number: { type: "int64", id: 1 },
|
|
1865
|
+
node_id: { type: "string", id: 2 }
|
|
1866
|
+
}
|
|
1867
|
+
};
|
|
1868
|
+
return {
|
|
1869
|
+
nested: {
|
|
1870
|
+
kora: { nested }
|
|
1871
|
+
}
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
function generateProtoDefinitions(schema) {
|
|
1875
|
+
const typeMap = /* @__PURE__ */ new Map();
|
|
1876
|
+
const sections = [];
|
|
1877
|
+
sections.push('syntax = "proto3";');
|
|
1878
|
+
sections.push("");
|
|
1879
|
+
sections.push("package kora;");
|
|
1880
|
+
const collectionEntries = Object.entries(schema.collections);
|
|
1881
|
+
if (collectionEntries.length > 0) {
|
|
1882
|
+
sections.push("");
|
|
1883
|
+
sections.push("// Collection record messages");
|
|
1884
|
+
for (const [collectionName, collection] of collectionEntries) {
|
|
1885
|
+
sections.push("");
|
|
1886
|
+
sections.push(generateCollectionMessage(collectionName, collection, typeMap));
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
sections.push("");
|
|
1890
|
+
sections.push("// Sync protocol messages");
|
|
1891
|
+
sections.push("");
|
|
1892
|
+
sections.push(KORA_OPERATION_MESSAGE);
|
|
1893
|
+
sections.push("");
|
|
1894
|
+
sections.push(OPERATION_BATCH_MESSAGE);
|
|
1895
|
+
sections.push("");
|
|
1896
|
+
sections.push(HANDSHAKE_MESSAGE);
|
|
1897
|
+
sections.push("");
|
|
1898
|
+
sections.push(HANDSHAKE_RESPONSE_MESSAGE);
|
|
1899
|
+
sections.push("");
|
|
1900
|
+
sections.push(ACKNOWLEDGMENT_MESSAGE);
|
|
1901
|
+
const proto = `${sections.join("\n")}
|
|
1902
|
+
`;
|
|
1903
|
+
const jsonDescriptor = buildJsonDescriptor(schema);
|
|
1904
|
+
return { proto, typeMap, jsonDescriptor };
|
|
1905
|
+
}
|
|
678
1906
|
export {
|
|
1907
|
+
APPLY_FAILURE_CODES,
|
|
1908
|
+
APPLY_RESULTS,
|
|
679
1909
|
ArrayFieldBuilder,
|
|
680
1910
|
CONNECTION_QUALITIES,
|
|
1911
|
+
CausalTracker,
|
|
681
1912
|
ClockDriftError,
|
|
682
1913
|
EnumFieldBuilder,
|
|
683
1914
|
FieldBuilder,
|
|
684
1915
|
HybridLogicalClock,
|
|
1916
|
+
KORA_ERROR_FIX_SUGGESTIONS,
|
|
685
1917
|
KoraError,
|
|
686
1918
|
MERGE_STRATEGIES,
|
|
687
1919
|
MergeConflictError,
|
|
1920
|
+
MigrationBuilder,
|
|
1921
|
+
MigrationRollbackError,
|
|
688
1922
|
OperationError,
|
|
1923
|
+
RollbackBuilder,
|
|
689
1924
|
SchemaValidationError,
|
|
690
1925
|
StorageError,
|
|
691
1926
|
SyncError,
|
|
692
1927
|
advanceVector,
|
|
1928
|
+
applyOperationTransforms,
|
|
1929
|
+
buildScopeMap,
|
|
1930
|
+
buildStateMachineConstraints,
|
|
1931
|
+
canAutoRollback,
|
|
1932
|
+
collectSchemaScopeFields,
|
|
1933
|
+
collectSchemaScopeValueKeys,
|
|
693
1934
|
computeDelta,
|
|
694
1935
|
createOperation,
|
|
1936
|
+
createReversibleMigration,
|
|
695
1937
|
createVersionVector,
|
|
1938
|
+
defaultApplyFailureReason,
|
|
1939
|
+
defaultSequenceFormat,
|
|
696
1940
|
defineSchema,
|
|
697
1941
|
deserializeVector,
|
|
698
1942
|
dominates,
|
|
1943
|
+
extractScopeValuesFromClaims,
|
|
699
1944
|
extractTimestamp,
|
|
1945
|
+
formatSequenceValue,
|
|
700
1946
|
generateFullDDL,
|
|
1947
|
+
generateProtoDefinitions,
|
|
1948
|
+
generateRollbackSteps,
|
|
701
1949
|
generateSQL,
|
|
702
1950
|
generateUUIDv7,
|
|
1951
|
+
getCollectionScopeBindings,
|
|
1952
|
+
getKoraErrorFix,
|
|
1953
|
+
getTransitionMap,
|
|
1954
|
+
hasSchemaSyncRules,
|
|
1955
|
+
isApplyFailure,
|
|
1956
|
+
isAtomicOp,
|
|
1957
|
+
isCollectionSyncScoped,
|
|
703
1958
|
isValidOperation,
|
|
704
1959
|
isValidUUIDv7,
|
|
705
1960
|
mergeVectors,
|
|
1961
|
+
migrate,
|
|
1962
|
+
migrationStepsToSQL,
|
|
1963
|
+
op,
|
|
1964
|
+
resolveAtomicOp,
|
|
1965
|
+
rollbackStepsToSQL,
|
|
706
1966
|
serializeVector,
|
|
707
1967
|
t,
|
|
1968
|
+
toAtomicOp,
|
|
708
1969
|
validateRecord,
|
|
1970
|
+
validateTransition,
|
|
709
1971
|
vectorsEqual,
|
|
710
1972
|
verifyOperationIntegrity
|
|
711
1973
|
};
|