@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/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-KQYXWUFV.js";
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
- return { version: input.version, collections, relations };
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
- return { fields, indexes, constraints, resolvers };
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
- constructor(kind, required = true, defaultValue = void 0, auto = false) {
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
- constructor(values, required = true, defaultValue = void 0, auto = false) {
365
- super("enum", required, defaultValue, auto);
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(this._enumValues, false, this._defaultValue, this._auto);
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(this._enumValues, false, value, this._auto);
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(this._enumValues, false, void 0, true);
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(new FieldBuilder(this._itemKind), false, value, this._auto);
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(new FieldBuilder(this._itemKind), false, void 0, true);
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 !== void 0 && value !== null) {
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
- missing.push(...operationLog.getRange(nodeId, remoteSeq + 1, localSeq));
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
  };