@korajs/core 0.3.1 → 0.4.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
@@ -11,7 +11,7 @@ import {
11
11
  isValidOperation,
12
12
  topologicalSort,
13
13
  verifyOperationIntegrity
14
- } from "./chunk-KQYXWUFV.js";
14
+ } from "./chunk-CFP5YFXC.js";
15
15
 
16
16
  // src/types.ts
17
17
  var MERGE_STRATEGIES = [
@@ -59,6 +59,190 @@ function formatUUID(bytes) {
59
59
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
60
60
  }
61
61
 
62
+ // src/operations/atomic-ops.ts
63
+ var KORA_ATOMIC_OP = /* @__PURE__ */ Symbol.for("kora.atomic_op");
64
+ function isAtomicOp(value) {
65
+ return typeof value === "object" && value !== null && KORA_ATOMIC_OP in value && value[KORA_ATOMIC_OP] === true;
66
+ }
67
+ function resolveAtomicOp(currentValue, sentinel) {
68
+ switch (sentinel.type) {
69
+ case "increment": {
70
+ const current = typeof currentValue === "number" ? currentValue : 0;
71
+ const operand = sentinel.value;
72
+ return current + operand;
73
+ }
74
+ case "max": {
75
+ const current = typeof currentValue === "number" ? currentValue : Number.NEGATIVE_INFINITY;
76
+ return Math.max(current, sentinel.value);
77
+ }
78
+ case "min": {
79
+ const current = typeof currentValue === "number" ? currentValue : Number.POSITIVE_INFINITY;
80
+ return Math.min(current, sentinel.value);
81
+ }
82
+ case "append": {
83
+ const current = Array.isArray(currentValue) ? [...currentValue] : [];
84
+ current.push(sentinel.value);
85
+ return current;
86
+ }
87
+ case "remove": {
88
+ if (!Array.isArray(currentValue)) return [];
89
+ return currentValue.filter((item) => item !== sentinel.value);
90
+ }
91
+ }
92
+ }
93
+ function createSentinel(type, value) {
94
+ return Object.freeze({
95
+ [KORA_ATOMIC_OP]: true,
96
+ type,
97
+ value
98
+ });
99
+ }
100
+ var op = {
101
+ /**
102
+ * Increment a number field by the given amount.
103
+ * Concurrent increments compose: the sum of all deltas is applied to the base.
104
+ *
105
+ * @param n - The amount to increment (use negative values to decrement)
106
+ */
107
+ increment(n) {
108
+ return createSentinel("increment", n);
109
+ },
110
+ /**
111
+ * Decrement a number field by the given amount.
112
+ * Syntactic sugar for `op.increment(-n)`.
113
+ *
114
+ * @param n - The amount to decrement
115
+ */
116
+ decrement(n) {
117
+ return createSentinel("increment", -n);
118
+ },
119
+ /**
120
+ * Set the field to the maximum of the current value and the given value.
121
+ * Concurrent max operations take the maximum of all values.
122
+ *
123
+ * @param n - The value to compare against the current value
124
+ */
125
+ max(n) {
126
+ return createSentinel("max", n);
127
+ },
128
+ /**
129
+ * Set the field to the minimum of the current value and the given value.
130
+ * Concurrent min operations take the minimum of all values.
131
+ *
132
+ * @param n - The value to compare against the current value
133
+ */
134
+ min(n) {
135
+ return createSentinel("min", n);
136
+ },
137
+ /**
138
+ * Append an item to an array field.
139
+ * Concurrent appends include all appended items.
140
+ *
141
+ * @param item - The item to append to the array
142
+ */
143
+ append(item) {
144
+ return createSentinel("append", item);
145
+ },
146
+ /**
147
+ * Remove an item from an array field (by value equality).
148
+ * Concurrent removes of the same item are idempotent.
149
+ *
150
+ * @param item - The item to remove from the array
151
+ */
152
+ remove(item) {
153
+ return createSentinel("remove", item);
154
+ }
155
+ };
156
+ function toAtomicOp(sentinel) {
157
+ return { type: sentinel.type, value: sentinel.value };
158
+ }
159
+
160
+ // src/state-machine/state-machine.ts
161
+ function validateTransition(constraint, fromValue, toValue) {
162
+ const from = String(fromValue);
163
+ const to = String(toValue);
164
+ const allowedTargets = constraint.transitions[from] ?? [];
165
+ return {
166
+ valid: allowedTargets.includes(to),
167
+ from,
168
+ to,
169
+ field: constraint.field,
170
+ collection: constraint.collection,
171
+ allowedTargets
172
+ };
173
+ }
174
+ function buildStateMachineConstraints(schema) {
175
+ const constraints = [];
176
+ for (const [collectionName, collection] of Object.entries(schema.collections)) {
177
+ for (const [fieldName, descriptor] of Object.entries(collection.fields)) {
178
+ if (descriptor.kind === "enum" && descriptor.transitions !== null) {
179
+ constraints.push({
180
+ field: fieldName,
181
+ collection: collectionName,
182
+ transitions: descriptor.transitions
183
+ });
184
+ }
185
+ }
186
+ }
187
+ return constraints;
188
+ }
189
+ function getTransitionMap(schema, collection, field) {
190
+ const collectionDef = schema.collections[collection];
191
+ if (!collectionDef) {
192
+ return null;
193
+ }
194
+ const fieldDef = collectionDef.fields[field];
195
+ if (!fieldDef || fieldDef.kind !== "enum") {
196
+ return null;
197
+ }
198
+ return fieldDef.transitions;
199
+ }
200
+ function validateStateMachineDefinition(collectionName, sm, fields) {
201
+ const fieldDef = fields[sm.field];
202
+ if (!fieldDef) {
203
+ throw new SchemaValidationError(
204
+ `State machine field "${sm.field}" does not exist in collection "${collectionName}". Available fields: ${Object.keys(fields).join(", ")}`,
205
+ { collection: collectionName, field: sm.field }
206
+ );
207
+ }
208
+ if (fieldDef.kind !== "enum") {
209
+ throw new SchemaValidationError(
210
+ `State machine field "${sm.field}" in collection "${collectionName}" must be an enum field, but got "${fieldDef.kind}"`,
211
+ { collection: collectionName, field: sm.field, kind: fieldDef.kind }
212
+ );
213
+ }
214
+ const enumValues = fieldDef.enumValues;
215
+ if (!enumValues || enumValues.length === 0) {
216
+ throw new SchemaValidationError(
217
+ `State machine field "${sm.field}" in collection "${collectionName}" has no enum values defined`,
218
+ { collection: collectionName, field: sm.field }
219
+ );
220
+ }
221
+ const validValues = new Set(enumValues);
222
+ for (const [state, targets] of Object.entries(sm.transitions)) {
223
+ if (!validValues.has(state)) {
224
+ throw new SchemaValidationError(
225
+ `State machine transition source "${state}" is not a valid enum value for field "${sm.field}" in collection "${collectionName}". Valid values: ${[...validValues].join(", ")}`,
226
+ { collection: collectionName, field: sm.field, state }
227
+ );
228
+ }
229
+ for (const target of targets) {
230
+ if (!validValues.has(target)) {
231
+ throw new SchemaValidationError(
232
+ `State machine transition target "${target}" is not a valid enum value for field "${sm.field}" in collection "${collectionName}". Valid values: ${[...validValues].join(", ")}`,
233
+ { collection: collectionName, field: sm.field, state, target }
234
+ );
235
+ }
236
+ }
237
+ }
238
+ if (sm.onInvalidTransition !== "reject" && sm.onInvalidTransition !== "last-valid-state") {
239
+ throw new SchemaValidationError(
240
+ `State machine onInvalidTransition must be "reject" or "last-valid-state", got "${sm.onInvalidTransition}" in collection "${collectionName}"`,
241
+ { collection: collectionName, onInvalidTransition: sm.onInvalidTransition }
242
+ );
243
+ }
244
+ }
245
+
62
246
  // src/schema/define.ts
63
247
  var COLLECTION_NAME_RE = /^[a-z][a-z0-9_]*$/;
64
248
  var FIELD_NAME_RE = /^[a-z][a-zA-Z0-9_]*$/;
@@ -80,7 +264,32 @@ function defineSchema(input) {
80
264
  relations[name] = { ...relationInput };
81
265
  }
82
266
  }
83
- return { version: input.version, collections, relations };
267
+ const migrations = {};
268
+ if (input.migrations) {
269
+ for (const [versionStr, migration] of Object.entries(input.migrations)) {
270
+ const version = Number(versionStr);
271
+ if (!Number.isInteger(version) || version < 2) {
272
+ throw new SchemaValidationError(
273
+ `Migration key "${versionStr}" is invalid. Must be an integer >= 2 (version 1 is the initial schema).`,
274
+ { version: versionStr }
275
+ );
276
+ }
277
+ if (version > input.version) {
278
+ throw new SchemaValidationError(
279
+ `Migration version ${version} exceeds schema version ${input.version}. Migrations must target versions <= schema version.`,
280
+ { migrationVersion: version, schemaVersion: input.version }
281
+ );
282
+ }
283
+ if (!migration.steps || migration.steps.length === 0) {
284
+ throw new SchemaValidationError(
285
+ `Migration for version ${version} has no steps. Use migrate() to define at least one step.`,
286
+ { version }
287
+ );
288
+ }
289
+ migrations[version] = migration;
290
+ }
291
+ }
292
+ return { version: input.version, collections, relations, migrations };
84
293
  }
85
294
  function validateVersion(version) {
86
295
  if (typeof version !== "number" || !Number.isInteger(version) || version < 1) {
@@ -142,7 +351,24 @@ function buildCollection(name, input) {
142
351
  resolvers[fieldName] = resolver;
143
352
  }
144
353
  }
145
- return { fields, indexes, constraints, resolvers };
354
+ const scope = [];
355
+ if (input.scope) {
356
+ for (const scopeField of input.scope) {
357
+ if (!(scopeField in fields)) {
358
+ throw new SchemaValidationError(
359
+ `Scope field "${scopeField}" does not exist in collection "${name}". Available fields: ${Object.keys(fields).join(", ")}`,
360
+ { collection: name, field: scopeField }
361
+ );
362
+ }
363
+ scope.push(scopeField);
364
+ }
365
+ }
366
+ let stateMachine;
367
+ if (input.stateMachine) {
368
+ validateStateMachineDefinition(name, input.stateMachine, fields);
369
+ stateMachine = { ...input.stateMachine };
370
+ }
371
+ return { fields, indexes, constraints, resolvers, scope, stateMachine };
146
372
  }
147
373
  function validateFieldName(collection, fieldName) {
148
374
  if (RESERVED_FIELDS.has(fieldName)) {
@@ -276,6 +502,9 @@ function generateFullDDL(schema) {
276
502
  statements.push(
277
503
  "CREATE TABLE IF NOT EXISTS _kora_version_vector (\n node_id TEXT PRIMARY KEY NOT NULL,\n sequence_number INTEGER NOT NULL\n)"
278
504
  );
505
+ statements.push(
506
+ "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)"
507
+ );
279
508
  for (const [name, collection] of Object.entries(schema.collections)) {
280
509
  statements.push(...generateSQL(name, collection, schema.relations));
281
510
  }
@@ -329,23 +558,47 @@ var FieldBuilder = class _FieldBuilder {
329
558
  _required;
330
559
  _defaultValue;
331
560
  _auto;
332
- constructor(kind, required = true, defaultValue = void 0, auto = false) {
561
+ _mergeStrategy;
562
+ constructor(kind, required = true, defaultValue = void 0, auto = false, mergeStrategy = null) {
333
563
  this._kind = kind;
334
564
  this._required = required;
335
565
  this._defaultValue = defaultValue;
336
566
  this._auto = auto;
567
+ this._mergeStrategy = mergeStrategy;
337
568
  }
338
569
  /** Mark this field as optional (not required on insert) */
339
570
  optional() {
340
- return new _FieldBuilder(this._kind, false, this._defaultValue, this._auto);
571
+ return new _FieldBuilder(this._kind, false, this._defaultValue, this._auto, this._mergeStrategy);
341
572
  }
342
573
  /** Set a default value for this field. Implicitly makes the field optional. */
343
574
  default(value) {
344
- return new _FieldBuilder(this._kind, false, value, this._auto);
575
+ return new _FieldBuilder(this._kind, false, value, this._auto, this._mergeStrategy);
345
576
  }
346
577
  /** Mark this field as auto-populated (e.g., createdAt timestamps). Developers cannot set auto fields. */
347
578
  auto() {
348
- return new _FieldBuilder(this._kind, false, void 0, true);
579
+ return new _FieldBuilder(this._kind, false, void 0, true, this._mergeStrategy);
580
+ }
581
+ /**
582
+ * Declare a merge strategy for this field.
583
+ * Controls how concurrent modifications are resolved during sync.
584
+ *
585
+ * @param strategy - The merge strategy to use:
586
+ * - `'lww'`: Last-write-wins (default for scalar fields)
587
+ * - `'counter'`: Sum of deltas from base (for numbers)
588
+ * - `'max'`: Keep the maximum value (for numbers/timestamps)
589
+ * - `'min'`: Keep the minimum value (for numbers/timestamps)
590
+ * - `'union'`: Set-union merge (default for arrays)
591
+ * - `'append-only'`: Concatenate additions (for arrays)
592
+ * - `'server-authoritative'`: Always prefer the remote/server value
593
+ */
594
+ merge(strategy) {
595
+ return new _FieldBuilder(
596
+ this._kind,
597
+ this._required,
598
+ this._defaultValue,
599
+ this._auto,
600
+ strategy
601
+ );
349
602
  }
350
603
  /** @internal Build the final FieldDescriptor. Used by defineSchema(). */
351
604
  _build() {
@@ -355,24 +608,88 @@ var FieldBuilder = class _FieldBuilder {
355
608
  defaultValue: this._defaultValue,
356
609
  auto: this._auto,
357
610
  enumValues: null,
358
- itemKind: null
611
+ itemKind: null,
612
+ mergeStrategy: this._mergeStrategy,
613
+ transitions: null
359
614
  };
360
615
  }
361
616
  };
362
617
  var EnumFieldBuilder = class _EnumFieldBuilder extends FieldBuilder {
363
618
  _enumValues;
364
- constructor(values, required = true, defaultValue = void 0, auto = false) {
365
- super("enum", required, defaultValue, auto);
619
+ _transitions;
620
+ constructor(values, required = true, defaultValue = void 0, auto = false, mergeStrategy = null, transitions = null) {
621
+ super("enum", required, defaultValue, auto, mergeStrategy);
366
622
  this._enumValues = values;
623
+ this._transitions = transitions;
367
624
  }
368
625
  optional() {
369
- return new _EnumFieldBuilder(this._enumValues, false, this._defaultValue, this._auto);
626
+ return new _EnumFieldBuilder(
627
+ this._enumValues,
628
+ false,
629
+ this._defaultValue,
630
+ this._auto,
631
+ this._mergeStrategy,
632
+ this._transitions
633
+ );
370
634
  }
371
635
  default(value) {
372
- return new _EnumFieldBuilder(this._enumValues, false, value, this._auto);
636
+ return new _EnumFieldBuilder(this._enumValues, false, value, this._auto, this._mergeStrategy, this._transitions);
373
637
  }
374
638
  auto() {
375
- return new _EnumFieldBuilder(this._enumValues, false, void 0, true);
639
+ return new _EnumFieldBuilder(this._enumValues, false, void 0, true, this._mergeStrategy, this._transitions);
640
+ }
641
+ merge(strategy) {
642
+ return new _EnumFieldBuilder(
643
+ this._enumValues,
644
+ this._required,
645
+ this._defaultValue,
646
+ this._auto,
647
+ strategy,
648
+ this._transitions
649
+ );
650
+ }
651
+ /**
652
+ * Declare allowed state transitions for this enum field.
653
+ * Enables state machine validation during mutations and merges.
654
+ *
655
+ * @param map - Map of state to allowed next states
656
+ *
657
+ * @example
658
+ * ```typescript
659
+ * t.enum(['draft', 'pending', 'confirmed', 'cancelled']).transitions({
660
+ * draft: ['pending', 'cancelled'],
661
+ * pending: ['confirmed', 'cancelled'],
662
+ * confirmed: [],
663
+ * cancelled: [],
664
+ * })
665
+ * ```
666
+ */
667
+ transitions(map) {
668
+ const validValues = new Set(this._enumValues);
669
+ for (const [state, targets] of Object.entries(map)) {
670
+ if (!validValues.has(state)) {
671
+ throw new SchemaValidationError(
672
+ `Invalid source state "${state}" in transition map. Valid values: ${[...validValues].join(", ")}`,
673
+ { state, validValues: [...validValues] }
674
+ );
675
+ }
676
+ for (const target of targets) {
677
+ if (!validValues.has(target)) {
678
+ throw new SchemaValidationError(
679
+ `Invalid target state "${target}" in transition from "${state}". Valid values: ${[...validValues].join(", ")}`,
680
+ { state, target, validValues: [...validValues] }
681
+ );
682
+ }
683
+ }
684
+ }
685
+ return new _EnumFieldBuilder(
686
+ this._enumValues,
687
+ this._required,
688
+ this._defaultValue,
689
+ this._auto,
690
+ this._mergeStrategy,
691
+ map
692
+ );
376
693
  }
377
694
  _build() {
378
695
  return {
@@ -381,14 +698,16 @@ var EnumFieldBuilder = class _EnumFieldBuilder extends FieldBuilder {
381
698
  defaultValue: this._defaultValue,
382
699
  auto: this._auto,
383
700
  enumValues: this._enumValues,
384
- itemKind: null
701
+ itemKind: null,
702
+ mergeStrategy: this._mergeStrategy,
703
+ transitions: this._transitions
385
704
  };
386
705
  }
387
706
  };
388
707
  var ArrayFieldBuilder = class _ArrayFieldBuilder extends FieldBuilder {
389
708
  _itemKind;
390
- constructor(itemBuilder, required = true, defaultValue = void 0, auto = false) {
391
- super("array", required, defaultValue, auto);
709
+ constructor(itemBuilder, required = true, defaultValue = void 0, auto = false, mergeStrategy = null) {
710
+ super("array", required, defaultValue, auto, mergeStrategy);
392
711
  this._itemKind = itemBuilder._build().kind;
393
712
  }
394
713
  optional() {
@@ -396,14 +715,36 @@ var ArrayFieldBuilder = class _ArrayFieldBuilder extends FieldBuilder {
396
715
  new FieldBuilder(this._itemKind),
397
716
  false,
398
717
  this._defaultValue,
399
- this._auto
718
+ this._auto,
719
+ this._mergeStrategy
400
720
  );
401
721
  }
402
722
  default(value) {
403
- return new _ArrayFieldBuilder(new FieldBuilder(this._itemKind), false, value, this._auto);
723
+ return new _ArrayFieldBuilder(
724
+ new FieldBuilder(this._itemKind),
725
+ false,
726
+ value,
727
+ this._auto,
728
+ this._mergeStrategy
729
+ );
404
730
  }
405
731
  auto() {
406
- return new _ArrayFieldBuilder(new FieldBuilder(this._itemKind), false, void 0, true);
732
+ return new _ArrayFieldBuilder(
733
+ new FieldBuilder(this._itemKind),
734
+ false,
735
+ void 0,
736
+ true,
737
+ this._mergeStrategy
738
+ );
739
+ }
740
+ merge(strategy) {
741
+ return new _ArrayFieldBuilder(
742
+ new FieldBuilder(this._itemKind),
743
+ this._required,
744
+ this._defaultValue,
745
+ this._auto,
746
+ strategy
747
+ );
407
748
  }
408
749
  _build() {
409
750
  return {
@@ -412,7 +753,9 @@ var ArrayFieldBuilder = class _ArrayFieldBuilder extends FieldBuilder {
412
753
  defaultValue: this._defaultValue,
413
754
  auto: this._auto,
414
755
  enumValues: null,
415
- itemKind: this._itemKind
756
+ itemKind: this._itemKind,
757
+ mergeStrategy: this._mergeStrategy,
758
+ transitions: null
416
759
  };
417
760
  }
418
761
  };
@@ -466,10 +809,14 @@ function validateRecord(collection, collectionDef, data, operationType) {
466
809
  }
467
810
  if (operationType === "update") {
468
811
  if (hasValue) {
469
- if (value !== void 0 && value !== null) {
812
+ if (isAtomicOp(value)) {
813
+ result[fieldName] = value;
814
+ } else if (value !== void 0 && value !== null) {
470
815
  validateFieldValue(collection, fieldName, descriptor, value);
816
+ result[fieldName] = value;
817
+ } else {
818
+ result[fieldName] = value;
471
819
  }
472
- result[fieldName] = value;
473
820
  }
474
821
  continue;
475
822
  }
@@ -675,6 +1022,594 @@ function deserializeVector(s) {
675
1022
  const entries = JSON.parse(s);
676
1023
  return new Map(entries);
677
1024
  }
1025
+
1026
+ // src/scopes/build-scope-map.ts
1027
+ function buildScopeMap(schema, scopeValues) {
1028
+ const result = {};
1029
+ for (const [collName, collDef] of Object.entries(schema.collections)) {
1030
+ if (collDef.scope.length > 0) {
1031
+ const collScope = {};
1032
+ for (const field of collDef.scope) {
1033
+ if (field in scopeValues) {
1034
+ collScope[field] = scopeValues[field];
1035
+ }
1036
+ }
1037
+ result[collName] = collScope;
1038
+ } else {
1039
+ result[collName] = {};
1040
+ }
1041
+ }
1042
+ return result;
1043
+ }
1044
+
1045
+ // src/sequences/sequence-format.ts
1046
+ function formatSequenceValue(template, counter, nodeId, now) {
1047
+ const date = now ?? /* @__PURE__ */ new Date();
1048
+ const yyyy = String(date.getUTCFullYear());
1049
+ const mm = String(date.getUTCMonth() + 1).padStart(2, "0");
1050
+ const dd = String(date.getUTCDate()).padStart(2, "0");
1051
+ const dateStr = `${yyyy}${mm}${dd}`;
1052
+ return template.replace(/\{([^}]+)\}/g, (_match, token) => {
1053
+ if (token === "date") return dateStr;
1054
+ if (token === "node4") return nodeId.slice(0, 4);
1055
+ if (token === "node8") return nodeId.slice(0, 8);
1056
+ if (token === "seq") return String(counter).padStart(4, "0");
1057
+ if (token.startsWith("seq:")) {
1058
+ const width = Number.parseInt(token.slice(4), 10);
1059
+ if (Number.isNaN(width) || width < 1) {
1060
+ return String(counter).padStart(4, "0");
1061
+ }
1062
+ return String(counter).padStart(width, "0");
1063
+ }
1064
+ return `{${token}}`;
1065
+ });
1066
+ }
1067
+ function defaultSequenceFormat(name) {
1068
+ return `${name}-{seq:4}`;
1069
+ }
1070
+
1071
+ // src/migrations/migration-builder.ts
1072
+ var RollbackBuilder = class {
1073
+ _steps = [];
1074
+ /**
1075
+ * Add a field during rollback (to reverse a removeField).
1076
+ */
1077
+ addField(collection, field, builder) {
1078
+ this._steps.push({ type: "addField", collection, field, descriptor: builder._build() });
1079
+ return this;
1080
+ }
1081
+ /**
1082
+ * Remove a field during rollback (to reverse an addField).
1083
+ */
1084
+ removeField(collection, field) {
1085
+ this._steps.push({ type: "removeField", collection, field });
1086
+ return this;
1087
+ }
1088
+ /**
1089
+ * Rename a field during rollback (to reverse a renameField).
1090
+ */
1091
+ renameField(collection, from, to) {
1092
+ this._steps.push({ type: "renameField", collection, from, to });
1093
+ return this;
1094
+ }
1095
+ /**
1096
+ * Add an index during rollback (to reverse a removeIndex).
1097
+ */
1098
+ addIndex(collection, field) {
1099
+ this._steps.push({ type: "addIndex", collection, field });
1100
+ return this;
1101
+ }
1102
+ /**
1103
+ * Remove an index during rollback (to reverse an addIndex).
1104
+ */
1105
+ removeIndex(collection, field) {
1106
+ this._steps.push({ type: "removeIndex", collection, field });
1107
+ return this;
1108
+ }
1109
+ /**
1110
+ * Run a backfill during rollback (to reverse a previous backfill).
1111
+ */
1112
+ backfill(collection, transform) {
1113
+ this._steps.push({ type: "backfill", collection, transform });
1114
+ return this;
1115
+ }
1116
+ /**
1117
+ * Get the accumulated rollback steps.
1118
+ */
1119
+ _getSteps() {
1120
+ return [...this._steps];
1121
+ }
1122
+ };
1123
+ var MigrationBuilder = class _MigrationBuilder {
1124
+ steps;
1125
+ rollbackSteps;
1126
+ safelyReversible;
1127
+ constructor(steps = [], rollbackSteps, safelyReversible) {
1128
+ this.steps = steps;
1129
+ this.rollbackSteps = rollbackSteps;
1130
+ this.safelyReversible = safelyReversible ?? this._computeSafelyReversible(steps, rollbackSteps);
1131
+ }
1132
+ /**
1133
+ * Add a new field to a collection.
1134
+ * The field builder provides the type and default value.
1135
+ */
1136
+ addField(collection, field, builder) {
1137
+ return new _MigrationBuilder([
1138
+ ...this.steps,
1139
+ { type: "addField", collection, field, descriptor: builder._build() }
1140
+ ]);
1141
+ }
1142
+ /**
1143
+ * Remove a field from a collection.
1144
+ * Optionally accepts a field builder to preserve the descriptor for rollback.
1145
+ * Without the descriptor, rollback of this step requires a custom `.down()`.
1146
+ *
1147
+ * @param collection - The collection name
1148
+ * @param field - The field name to remove
1149
+ * @param builder - Optional field builder preserving the type info for rollback
1150
+ */
1151
+ removeField(collection, field, builder) {
1152
+ const step = builder ? { type: "removeField", collection, field, descriptor: builder._build() } : { type: "removeField", collection, field };
1153
+ return new _MigrationBuilder([...this.steps, step]);
1154
+ }
1155
+ /**
1156
+ * Rename a field in a collection.
1157
+ * Implemented as ALTER TABLE RENAME COLUMN (SQLite 3.25+).
1158
+ */
1159
+ renameField(collection, from, to) {
1160
+ return new _MigrationBuilder([...this.steps, { type: "renameField", collection, from, to }]);
1161
+ }
1162
+ /**
1163
+ * Add an index on a field.
1164
+ */
1165
+ addIndex(collection, field) {
1166
+ return new _MigrationBuilder([...this.steps, { type: "addIndex", collection, field }]);
1167
+ }
1168
+ /**
1169
+ * Remove an index on a field.
1170
+ */
1171
+ removeIndex(collection, field) {
1172
+ return new _MigrationBuilder([...this.steps, { type: "removeIndex", collection, field }]);
1173
+ }
1174
+ /**
1175
+ * Backfill records in a collection using a transform function.
1176
+ * The transform receives each record and returns the fields to update.
1177
+ * Runs after structural changes (addField, renameField, etc.).
1178
+ *
1179
+ * @param collection - The collection name
1180
+ * @param transform - Forward transform function
1181
+ * @param reverseTransform - Optional reverse transform for rollback support
1182
+ */
1183
+ backfill(collection, transform, reverseTransform) {
1184
+ return new _MigrationBuilder([
1185
+ ...this.steps,
1186
+ { type: "backfill", collection, transform, reverseTransform }
1187
+ ]);
1188
+ }
1189
+ /**
1190
+ * Define explicit rollback steps for this migration.
1191
+ * If not called, inverse steps are auto-generated from forward steps.
1192
+ *
1193
+ * @param fn - Function that receives a RollbackBuilder to define rollback steps
1194
+ * @returns A new MigrationBuilder with the rollback steps attached
1195
+ *
1196
+ * @example
1197
+ * ```typescript
1198
+ * migrate()
1199
+ * .addField('todos', 'priority', t.enum(['low', 'medium', 'high']).default('medium'))
1200
+ * .addIndex('todos', 'priority')
1201
+ * .down((rollback) => {
1202
+ * rollback
1203
+ * .removeIndex('todos', 'priority')
1204
+ * .removeField('todos', 'priority')
1205
+ * })
1206
+ * ```
1207
+ */
1208
+ down(fn) {
1209
+ const rollbackBuilder = new RollbackBuilder();
1210
+ fn(rollbackBuilder);
1211
+ const rbSteps = rollbackBuilder._getSteps();
1212
+ return new _MigrationBuilder(this.steps, rbSteps, true);
1213
+ }
1214
+ /**
1215
+ * Determine if a migration is safely reversible based on its steps.
1216
+ * A migration is not safely reversible if:
1217
+ * - It has a backfill step without a reverseTransform and no explicit rollback
1218
+ * - It has a removeField without a stored descriptor and no explicit rollback
1219
+ */
1220
+ _computeSafelyReversible(steps, rollbackSteps) {
1221
+ if (rollbackSteps !== void 0) {
1222
+ return true;
1223
+ }
1224
+ for (const step of steps) {
1225
+ if (step.type === "backfill" && !step.reverseTransform) {
1226
+ return false;
1227
+ }
1228
+ if (step.type === "removeField" && !step.descriptor) {
1229
+ return false;
1230
+ }
1231
+ }
1232
+ return true;
1233
+ }
1234
+ };
1235
+ function migrate() {
1236
+ return new MigrationBuilder();
1237
+ }
1238
+
1239
+ // src/migrations/migration-rollback.ts
1240
+ var MigrationRollbackError = class extends KoraError {
1241
+ constructor(step) {
1242
+ super(
1243
+ `Cannot auto-generate rollback for "${step.type}" step on collection "${step.collection}". Provide an explicit .down() definition for this migration.`,
1244
+ "MIGRATION_ROLLBACK",
1245
+ { stepType: step.type, collection: step.collection }
1246
+ );
1247
+ this.name = "MigrationRollbackError";
1248
+ }
1249
+ };
1250
+ function canAutoRollback(step) {
1251
+ switch (step.type) {
1252
+ case "addField":
1253
+ case "addIndex":
1254
+ case "removeIndex":
1255
+ case "renameField":
1256
+ return true;
1257
+ case "removeField":
1258
+ case "backfill":
1259
+ return false;
1260
+ }
1261
+ }
1262
+ function generateRollbackSteps(forwardSteps) {
1263
+ const rollbackSteps = [];
1264
+ for (let i = forwardSteps.length - 1; i >= 0; i--) {
1265
+ const step = forwardSteps[i];
1266
+ if (step === void 0) continue;
1267
+ const rollback = generateSingleRollbackStep(step);
1268
+ rollbackSteps.push(rollback);
1269
+ }
1270
+ return rollbackSteps;
1271
+ }
1272
+ function generateSingleRollbackStep(step) {
1273
+ switch (step.type) {
1274
+ case "addField":
1275
+ return { type: "removeField", collection: step.collection, field: step.field };
1276
+ case "removeField":
1277
+ if (step.descriptor) {
1278
+ return {
1279
+ type: "addField",
1280
+ collection: step.collection,
1281
+ field: step.field,
1282
+ descriptor: step.descriptor
1283
+ };
1284
+ }
1285
+ throw new MigrationRollbackError(step);
1286
+ case "renameField":
1287
+ return { type: "renameField", collection: step.collection, from: step.to, to: step.from };
1288
+ case "addIndex":
1289
+ return { type: "removeIndex", collection: step.collection, field: step.field };
1290
+ case "removeIndex":
1291
+ return { type: "addIndex", collection: step.collection, field: step.field };
1292
+ case "backfill":
1293
+ if (step.reverseTransform) {
1294
+ return {
1295
+ type: "backfill",
1296
+ collection: step.collection,
1297
+ transform: step.reverseTransform
1298
+ };
1299
+ }
1300
+ throw new MigrationRollbackError(step);
1301
+ }
1302
+ }
1303
+ function createReversibleMigration(upSteps, downSteps, fromVersion, toVersion) {
1304
+ const down = downSteps !== null ? [...downSteps] : generateRollbackSteps(upSteps);
1305
+ return {
1306
+ up: [...upSteps],
1307
+ down,
1308
+ fromVersion,
1309
+ toVersion
1310
+ };
1311
+ }
1312
+
1313
+ // src/migrations/migration-sql.ts
1314
+ function migrationStepsToSQL(steps) {
1315
+ const statements = [];
1316
+ for (const step of steps) {
1317
+ switch (step.type) {
1318
+ case "addField":
1319
+ statements.push(addFieldSQL(step.collection, step.field, step.descriptor));
1320
+ break;
1321
+ case "removeField":
1322
+ statements.push(`ALTER TABLE ${step.collection} DROP COLUMN ${step.field}`);
1323
+ break;
1324
+ case "renameField":
1325
+ statements.push(`ALTER TABLE ${step.collection} RENAME COLUMN ${step.from} TO ${step.to}`);
1326
+ break;
1327
+ case "addIndex":
1328
+ statements.push(
1329
+ `CREATE INDEX IF NOT EXISTS idx_${step.collection}_${step.field} ON ${step.collection} (${step.field})`
1330
+ );
1331
+ break;
1332
+ case "removeIndex":
1333
+ statements.push(`DROP INDEX IF EXISTS idx_${step.collection}_${step.field}`);
1334
+ break;
1335
+ case "backfill":
1336
+ break;
1337
+ }
1338
+ }
1339
+ return statements;
1340
+ }
1341
+ function rollbackStepsToSQL(migration) {
1342
+ const rollbackSteps = migration.rollbackSteps ?? generateRollbackSteps(migration.steps);
1343
+ return migrationStepsToSQL(rollbackSteps);
1344
+ }
1345
+ function addFieldSQL(collection, field, descriptor) {
1346
+ const sqlType = mapFieldType2(descriptor);
1347
+ const parts = [`ALTER TABLE ${collection} ADD COLUMN ${field}`, sqlType];
1348
+ if (descriptor.defaultValue !== void 0) {
1349
+ parts.push(`DEFAULT ${sqlDefault2(descriptor.defaultValue)}`);
1350
+ }
1351
+ if (descriptor.kind === "enum" && descriptor.enumValues) {
1352
+ const values = descriptor.enumValues.map((v) => `'${v}'`).join(", ");
1353
+ parts.push(`CHECK (${field} IN (${values}))`);
1354
+ }
1355
+ return parts.join(" ");
1356
+ }
1357
+ function mapFieldType2(descriptor) {
1358
+ switch (descriptor.kind) {
1359
+ case "string":
1360
+ return "TEXT";
1361
+ case "number":
1362
+ return "REAL";
1363
+ case "boolean":
1364
+ return "INTEGER";
1365
+ case "enum":
1366
+ return "TEXT";
1367
+ case "timestamp":
1368
+ return "INTEGER";
1369
+ case "array":
1370
+ return "TEXT";
1371
+ case "richtext":
1372
+ return "BLOB";
1373
+ }
1374
+ }
1375
+ function sqlDefault2(value) {
1376
+ if (value === null) return "NULL";
1377
+ if (typeof value === "string") return `'${value}'`;
1378
+ if (typeof value === "number") return String(value);
1379
+ if (typeof value === "boolean") return value ? "1" : "0";
1380
+ return `'${JSON.stringify(value)}'`;
1381
+ }
1382
+
1383
+ // src/codegen/proto-generator.ts
1384
+ var SCALAR_TYPE_MAP = {
1385
+ string: "string",
1386
+ number: "double",
1387
+ boolean: "bool",
1388
+ timestamp: "int64",
1389
+ richtext: "bytes"
1390
+ };
1391
+ var ARRAY_ITEM_TYPE_MAP = {
1392
+ string: "string",
1393
+ number: "double",
1394
+ boolean: "bool",
1395
+ timestamp: "int64"
1396
+ };
1397
+ function toPascalCase(name) {
1398
+ return name.split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
1399
+ }
1400
+ function toSnakeCase(name) {
1401
+ return name.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
1402
+ }
1403
+ function resolveProtoType(fieldName, descriptor, messageName) {
1404
+ if (descriptor.kind === "enum") {
1405
+ return `${messageName}${toPascalCase(fieldName)}`;
1406
+ }
1407
+ if (descriptor.kind === "array") {
1408
+ const itemType = descriptor.itemKind ? ARRAY_ITEM_TYPE_MAP[descriptor.itemKind] ?? "string" : "string";
1409
+ return itemType;
1410
+ }
1411
+ return SCALAR_TYPE_MAP[descriptor.kind];
1412
+ }
1413
+ function generateEnumBlock(enumTypeName, enumValues, indent) {
1414
+ const lines = [];
1415
+ lines.push(`${indent}enum ${enumTypeName} {`);
1416
+ lines.push(`${indent} ${enumTypeName.toUpperCase()}_UNSPECIFIED = 0;`);
1417
+ for (let i = 0; i < enumValues.length; i++) {
1418
+ const value = enumValues[i];
1419
+ if (value === void 0) continue;
1420
+ lines.push(`${indent} ${enumTypeName.toUpperCase()}_${value.toUpperCase()} = ${i + 1};`);
1421
+ }
1422
+ lines.push(`${indent}}`);
1423
+ return lines.join("\n");
1424
+ }
1425
+ function generateCollectionMessage(collectionName, collection, typeMap) {
1426
+ const messageName = `${toPascalCase(collectionName)}Record`;
1427
+ const lines = [];
1428
+ const nestedEnums = [];
1429
+ lines.push(`message ${messageName} {`);
1430
+ lines.push(" string id = 1;");
1431
+ typeMap.set(`${collectionName}.id`, "string");
1432
+ let fieldNumber = 2;
1433
+ const entries = Object.entries(collection.fields);
1434
+ for (const [fieldName, descriptor] of entries) {
1435
+ const protoFieldName = toSnakeCase(fieldName);
1436
+ const protoType = resolveProtoType(fieldName, descriptor, messageName);
1437
+ const mapKey = `${collectionName}.${fieldName}`;
1438
+ if (descriptor.kind === "enum" && descriptor.enumValues) {
1439
+ const enumTypeName = `${messageName}${toPascalCase(fieldName)}`;
1440
+ nestedEnums.push(generateEnumBlock(enumTypeName, descriptor.enumValues, " "));
1441
+ typeMap.set(mapKey, enumTypeName);
1442
+ lines.push(` ${enumTypeName} ${protoFieldName} = ${fieldNumber};`);
1443
+ } else if (descriptor.kind === "array") {
1444
+ typeMap.set(mapKey, `repeated ${protoType}`);
1445
+ lines.push(` repeated ${protoType} ${protoFieldName} = ${fieldNumber};`);
1446
+ } else {
1447
+ typeMap.set(mapKey, protoType);
1448
+ lines.push(` ${protoType} ${protoFieldName} = ${fieldNumber};`);
1449
+ }
1450
+ fieldNumber++;
1451
+ }
1452
+ if (nestedEnums.length > 0) {
1453
+ lines.push("");
1454
+ for (const enumBlock of nestedEnums) {
1455
+ lines.push(enumBlock);
1456
+ }
1457
+ }
1458
+ lines.push("}");
1459
+ return lines.join("\n");
1460
+ }
1461
+ var KORA_OPERATION_MESSAGE = `message KoraOperation {
1462
+ string id = 1;
1463
+ string node_id = 2;
1464
+ string type = 3;
1465
+ string collection = 4;
1466
+ string record_id = 5;
1467
+ bytes data = 6;
1468
+ bytes previous_data = 7;
1469
+ int64 wall_time = 8;
1470
+ int32 logical = 9;
1471
+ string timestamp_node_id = 10;
1472
+ int64 sequence_number = 11;
1473
+ repeated string causal_deps = 12;
1474
+ int32 schema_version = 13;
1475
+ }`;
1476
+ var OPERATION_BATCH_MESSAGE = `message OperationBatch {
1477
+ repeated KoraOperation operations = 1;
1478
+ bool is_final = 2;
1479
+ }`;
1480
+ var HANDSHAKE_MESSAGE = `message HandshakeMessage {
1481
+ map<string, int64> version_vector = 1;
1482
+ int32 schema_version = 2;
1483
+ string node_id = 3;
1484
+ }`;
1485
+ var HANDSHAKE_RESPONSE_MESSAGE = `message HandshakeResponse {
1486
+ map<string, int64> version_vector = 1;
1487
+ int32 schema_version = 2;
1488
+ }`;
1489
+ var ACKNOWLEDGMENT_MESSAGE = `message Acknowledgment {
1490
+ int64 sequence_number = 1;
1491
+ string node_id = 2;
1492
+ }`;
1493
+ function buildJsonDescriptor(schema) {
1494
+ const nested = {};
1495
+ for (const [collectionName, collection] of Object.entries(schema.collections)) {
1496
+ const messageName = `${toPascalCase(collectionName)}Record`;
1497
+ const fields = {
1498
+ id: { type: "string", id: 1 }
1499
+ };
1500
+ const nestedTypes = {};
1501
+ let fieldNumber = 2;
1502
+ for (const [fieldName, descriptor] of Object.entries(collection.fields)) {
1503
+ const protoFieldName = toSnakeCase(fieldName);
1504
+ const fieldDef = { id: fieldNumber };
1505
+ if (descriptor.kind === "enum" && descriptor.enumValues) {
1506
+ const enumTypeName = `${messageName}${toPascalCase(fieldName)}`;
1507
+ fieldDef.type = enumTypeName;
1508
+ const enumValuesObj = {
1509
+ [`${enumTypeName.toUpperCase()}_UNSPECIFIED`]: 0
1510
+ };
1511
+ for (let i = 0; i < descriptor.enumValues.length; i++) {
1512
+ const val = descriptor.enumValues[i];
1513
+ if (val === void 0) continue;
1514
+ enumValuesObj[`${enumTypeName.toUpperCase()}_${val.toUpperCase()}`] = i + 1;
1515
+ }
1516
+ nestedTypes[enumTypeName] = { values: enumValuesObj };
1517
+ } else if (descriptor.kind === "array") {
1518
+ const itemType = descriptor.itemKind ? ARRAY_ITEM_TYPE_MAP[descriptor.itemKind] ?? "string" : "string";
1519
+ fieldDef.type = itemType;
1520
+ fieldDef.rule = "repeated";
1521
+ } else {
1522
+ fieldDef.type = SCALAR_TYPE_MAP[descriptor.kind];
1523
+ }
1524
+ fields[protoFieldName] = fieldDef;
1525
+ fieldNumber++;
1526
+ }
1527
+ const messageDescriptor = { fields };
1528
+ if (Object.keys(nestedTypes).length > 0) {
1529
+ messageDescriptor.nested = nestedTypes;
1530
+ }
1531
+ nested[messageName] = messageDescriptor;
1532
+ }
1533
+ nested.KoraOperation = {
1534
+ fields: {
1535
+ id: { type: "string", id: 1 },
1536
+ node_id: { type: "string", id: 2 },
1537
+ type: { type: "string", id: 3 },
1538
+ collection: { type: "string", id: 4 },
1539
+ record_id: { type: "string", id: 5 },
1540
+ data: { type: "bytes", id: 6 },
1541
+ previous_data: { type: "bytes", id: 7 },
1542
+ wall_time: { type: "int64", id: 8 },
1543
+ logical: { type: "int32", id: 9 },
1544
+ timestamp_node_id: { type: "string", id: 10 },
1545
+ sequence_number: { type: "int64", id: 11 },
1546
+ causal_deps: { type: "string", id: 12, rule: "repeated" },
1547
+ schema_version: { type: "int32", id: 13 }
1548
+ }
1549
+ };
1550
+ nested.OperationBatch = {
1551
+ fields: {
1552
+ operations: { type: "KoraOperation", id: 1, rule: "repeated" },
1553
+ is_final: { type: "bool", id: 2 }
1554
+ }
1555
+ };
1556
+ nested.HandshakeMessage = {
1557
+ fields: {
1558
+ version_vector: { keyType: "string", type: "int64", id: 1 },
1559
+ schema_version: { type: "int32", id: 2 },
1560
+ node_id: { type: "string", id: 3 }
1561
+ }
1562
+ };
1563
+ nested.HandshakeResponse = {
1564
+ fields: {
1565
+ version_vector: { keyType: "string", type: "int64", id: 1 },
1566
+ schema_version: { type: "int32", id: 2 }
1567
+ }
1568
+ };
1569
+ nested.Acknowledgment = {
1570
+ fields: {
1571
+ sequence_number: { type: "int64", id: 1 },
1572
+ node_id: { type: "string", id: 2 }
1573
+ }
1574
+ };
1575
+ return {
1576
+ nested: {
1577
+ kora: { nested }
1578
+ }
1579
+ };
1580
+ }
1581
+ function generateProtoDefinitions(schema) {
1582
+ const typeMap = /* @__PURE__ */ new Map();
1583
+ const sections = [];
1584
+ sections.push('syntax = "proto3";');
1585
+ sections.push("");
1586
+ sections.push("package kora;");
1587
+ const collectionEntries = Object.entries(schema.collections);
1588
+ if (collectionEntries.length > 0) {
1589
+ sections.push("");
1590
+ sections.push("// Collection record messages");
1591
+ for (const [collectionName, collection] of collectionEntries) {
1592
+ sections.push("");
1593
+ sections.push(generateCollectionMessage(collectionName, collection, typeMap));
1594
+ }
1595
+ }
1596
+ sections.push("");
1597
+ sections.push("// Sync protocol messages");
1598
+ sections.push("");
1599
+ sections.push(KORA_OPERATION_MESSAGE);
1600
+ sections.push("");
1601
+ sections.push(OPERATION_BATCH_MESSAGE);
1602
+ sections.push("");
1603
+ sections.push(HANDSHAKE_MESSAGE);
1604
+ sections.push("");
1605
+ sections.push(HANDSHAKE_RESPONSE_MESSAGE);
1606
+ sections.push("");
1607
+ sections.push(ACKNOWLEDGMENT_MESSAGE);
1608
+ const proto = `${sections.join("\n")}
1609
+ `;
1610
+ const jsonDescriptor = buildJsonDescriptor(schema);
1611
+ return { proto, typeMap, jsonDescriptor };
1612
+ }
678
1613
  export {
679
1614
  ArrayFieldBuilder,
680
1615
  CONNECTION_QUALITIES,
@@ -685,27 +1620,47 @@ export {
685
1620
  KoraError,
686
1621
  MERGE_STRATEGIES,
687
1622
  MergeConflictError,
1623
+ MigrationBuilder,
1624
+ MigrationRollbackError,
688
1625
  OperationError,
1626
+ RollbackBuilder,
689
1627
  SchemaValidationError,
690
1628
  StorageError,
691
1629
  SyncError,
692
1630
  advanceVector,
1631
+ buildScopeMap,
1632
+ buildStateMachineConstraints,
1633
+ canAutoRollback,
693
1634
  computeDelta,
694
1635
  createOperation,
1636
+ createReversibleMigration,
695
1637
  createVersionVector,
1638
+ defaultSequenceFormat,
696
1639
  defineSchema,
697
1640
  deserializeVector,
698
1641
  dominates,
699
1642
  extractTimestamp,
1643
+ formatSequenceValue,
700
1644
  generateFullDDL,
1645
+ generateProtoDefinitions,
1646
+ generateRollbackSteps,
701
1647
  generateSQL,
702
1648
  generateUUIDv7,
1649
+ getTransitionMap,
1650
+ isAtomicOp,
703
1651
  isValidOperation,
704
1652
  isValidUUIDv7,
705
1653
  mergeVectors,
1654
+ migrate,
1655
+ migrationStepsToSQL,
1656
+ op,
1657
+ resolveAtomicOp,
1658
+ rollbackStepsToSQL,
706
1659
  serializeVector,
707
1660
  t,
1661
+ toAtomicOp,
708
1662
  validateRecord,
1663
+ validateTransition,
709
1664
  vectorsEqual,
710
1665
  verifyOperationIntegrity
711
1666
  };