@korajs/core 0.3.2 → 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/{chunk-KQYXWUFV.js → chunk-CFP5YFXC.js} +26 -16
- package/dist/chunk-CFP5YFXC.js.map +1 -0
- package/dist/events-BMulupSB.d.cts +844 -0
- package/dist/events-BMulupSB.d.ts +844 -0
- package/dist/index.cjs +1050 -65
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +385 -69
- package/dist/index.d.ts +385 -69
- package/dist/index.js +977 -22
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +19 -13
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +2 -2
- package/dist/internal.d.ts +2 -2
- package/dist/internal.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-KQYXWUFV.js.map +0 -1
- package/dist/events-7Fhdjxd2.d.cts +0 -327
- package/dist/events-7Fhdjxd2.d.ts +0 -327
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
isValidOperation,
|
|
12
12
|
topologicalSort,
|
|
13
13
|
verifyOperationIntegrity
|
|
14
|
-
} from "./chunk-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
365
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
};
|