@korajs/core 1.0.0-beta.1 → 1.0.0-beta.11

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.d.cts CHANGED
@@ -1,7 +1,7 @@
1
- import { R as RandomSource, B as BlobRef, C as CollectionDefinition, A as AtomicOpType, a as AtomicOp, F as FieldBuilder, b as CustomResolver, M as MigrationDefinition, S as SchemaDefinition, c as RelationDefinition, O as OperationType, E as EnumFieldBuilder, J as JsonFieldBuilder, d as ArrayFieldBuilder, e as FieldKind, f as Operation, V as VersionVector, g as MigrationStep, h as StateMachineConstraint, T as TransitionMap, i as TransitionValidationResult } from './events-CqDkr1LN.cjs';
2
- export { j as CONNECTION_QUALITIES, k as ConnectionQuality, l as Constraint, m as FieldDescriptor, n as FieldMergeStrategy, H as HLCTimestamp, K as KoraEvent, o as KoraEventByType, p as KoraEventEmitter, q as KoraEventListener, r as KoraEventType, s as MERGE_STRATEGIES, t as MergeStrategy, u as MergeTrace, v as MigrationBuilder, w as OnDeleteAction, x as OperationInput, y as RelationType, z as RollbackBuilder, D as SecretMode, G as SequenceConfig, I as StateMachineDefinition, L as SyncDiagnosticsSnapshot, N as SyncRuleDefinition, P as TimeSource, Q as migrate, U as t } from './events-CqDkr1LN.cjs';
3
- export { H as HybridLogicalClock, M as MAX_LOGICAL, c as createOperation, i as isValidOperation, v as verifyOperationIntegrity } from './operation-CFoY-ZmB.cjs';
4
- export { S as ScopeMap, b as buildScopeMap } from './build-scope-map-DtTr9C7A.cjs';
1
+ import { R as RandomSource, B as BlobRef, C as CollectionDefinition, A as AtomicOpType, a as AtomicOp, F as FieldBuilder, b as CustomResolver, M as MigrationDefinition, S as SchemaDefinition, c as RelationDefinition, O as OperationType, E as EnumFieldBuilder, J as JsonFieldBuilder, d as ArrayFieldBuilder, e as FieldKind, f as Operation, V as VersionVector, g as MigrationStep, h as StateMachineConstraint, T as TransitionMap, i as TransitionValidationResult } from './events-BPbYUbrE.cjs';
2
+ export { j as CONNECTION_QUALITIES, k as ConnectionQuality, l as Constraint, m as FieldDescriptor, n as FieldMergeStrategy, H as HLCTimestamp, K as KoraEvent, o as KoraEventByType, p as KoraEventEmitter, q as KoraEventListener, r as KoraEventType, s as MERGE_STRATEGIES, t as MergeStrategy, u as MergeTrace, v as MigrationBuilder, w as OnDeleteAction, x as OperationInput, y as RelationType, z as RollbackBuilder, D as SecretMode, G as SequenceConfig, I as StateMachineDefinition, L as SyncDiagnosticsSnapshot, N as SyncRuleDefinition, P as TimeSource, Q as migrate, U as t } from './events-BPbYUbrE.cjs';
3
+ export { H as HybridLogicalClock, M as MAX_LOGICAL, c as createOperation, i as isValidOperation, v as verifyOperationIntegrity } from './operation-DbynlVcp.cjs';
4
+ export { S as ScopeMap, b as buildScopeMap } from './build-scope-map--0H8l6Tw.cjs';
5
5
 
6
6
  /**
7
7
  * Base error class for all Kora errors.
@@ -359,6 +359,20 @@ interface AtomicOpSentinel {
359
359
  * @returns true if the value is an AtomicOpSentinel
360
360
  */
361
361
  declare function isAtomicOp(value: unknown): value is AtomicOpSentinel;
362
+ /**
363
+ * Apply a plain atomic op (`{ type, value }`) to a current value, producing the
364
+ * concrete result. This is the single source of truth for atomic-op semantics:
365
+ * {@link resolveAtomicOp} (sentinel form, used on the write path) delegates here,
366
+ * and the server's materialization replays stored atomic ops through it, so the
367
+ * server's materialized view composes concurrent atomic writes identically to the
368
+ * client. Folding this over a field's writes in HLC order yields, for a same-type
369
+ * atomic chain, exactly the composed intent (sum of increments, max of maxes, ...).
370
+ *
371
+ * @param currentValue - The current value of the field
372
+ * @param atomicOp - The atomic op to apply (type + operand)
373
+ * @returns The resulting concrete value
374
+ */
375
+ declare function applyAtomicOp(currentValue: unknown, atomicOp: AtomicOp): unknown;
362
376
  /**
363
377
  * Resolve an atomic op sentinel against a current value to produce a concrete result.
364
378
  *
@@ -437,6 +451,40 @@ declare const op: {
437
451
  */
438
452
  declare function toAtomicOp(sentinel: AtomicOpSentinel): AtomicOp;
439
453
 
454
+ /** One operation's contribution to a record's materialized state. */
455
+ interface ReplayOperation {
456
+ type: string;
457
+ data: Record<string, unknown> | null;
458
+ /**
459
+ * Atomic-op intents carried by this operation, per field. When present, a field
460
+ * is composed (rather than overwritten) if the previous writer on that field was
461
+ * an atomic op of the same type. Absent means every field resolves by
462
+ * last-write-wins.
463
+ */
464
+ atomicOps?: Record<string, AtomicOp> | null;
465
+ }
466
+ /**
467
+ * Replay a list of operations (MUST be pre-sorted in HLC total order —
468
+ * wallTime, then logical, then nodeId) to produce the current state of a single
469
+ * record, composing atomic-op intents.
470
+ *
471
+ * Per field, an atomic op composes onto the running value only when the field's
472
+ * previous writer was an atomic op of the SAME type (a same-type atomic chain:
473
+ * concurrent increments sum, maxes take the max, appends accumulate); any other
474
+ * write, including the first atomic write after a plain set, resolves by
475
+ * last-write-wins on the resolved value. Because operations are folded in HLC order,
476
+ * the current op always wins that last-write-wins comparison, so the result is
477
+ * deterministic and identical on every replica that folds the same operation set.
478
+ *
479
+ * This is the single definition of atomic materialization shared by the client
480
+ * (`@korajs/store` apply pipeline) and the server (`@korajs/server` materialization),
481
+ * so both converge to the same value for any number of concurrent atomic writers.
482
+ *
483
+ * Returns the record field data (without `id`) or null if the record was deleted
484
+ * or never inserted.
485
+ */
486
+ declare function replayOperationsForRecord(ops: ReplayOperation[]): Record<string, unknown> | null;
487
+
440
488
  /**
441
489
  * Canonical JSON-safe encoding of binary bytes stored inside `op.data` /
442
490
  * `op.previousData` (currently richtext field values). A raw Uint8Array
@@ -605,6 +653,38 @@ declare function generateSQL(collectionName: string, collection: CollectionDefin
605
653
  */
606
654
  declare function generateFullDDL(schema: SchemaDefinition): string[];
607
655
 
656
+ /**
657
+ * Quote a SQL identifier (table or column name) for safe interpolation into
658
+ * generated SQL.
659
+ *
660
+ * Uses standard double-quote quoting, which both SQLite and PostgreSQL honor,
661
+ * and doubles any embedded double-quote per the SQL standard. Quoting is what
662
+ * lets a valid JavaScript identifier — camelCase (`formResponses`), PascalCase
663
+ * (`UserProfiles`), or a word that happens to be a SQL keyword (`order`,
664
+ * `select`) — round-trip through generated DDL and queries without ever
665
+ * producing invalid or ambiguous SQL.
666
+ *
667
+ * Schema validation (see `validateCollectionName` / `validateFieldName`)
668
+ * already restricts identifiers to `[A-Za-z][A-Za-z0-9_]*`, so the escape here
669
+ * never actually fires in practice; it is defense in depth so this helper is
670
+ * correct for any input rather than only for pre-validated names.
671
+ *
672
+ * Consistency is the contract: every path that emits an identifier for a
673
+ * user-defined collection or field MUST go through this helper, because a
674
+ * quoted `CREATE TABLE "order"` paired with an unquoted `SELECT ... FROM order`
675
+ * would reintroduce the exact keyword-collision bug this guards against.
676
+ *
677
+ * @param name - The raw identifier (collection name, field/column name).
678
+ * @returns The identifier wrapped in double quotes, ready to splice into SQL.
679
+ *
680
+ * @example
681
+ * ```typescript
682
+ * quoteIdent('formResponses') // => '"formResponses"'
683
+ * quoteIdent('order') // => '"order"'
684
+ * ```
685
+ */
686
+ declare function quoteIdent(name: string): string;
687
+
608
688
  /**
609
689
  * Validates a record's data against a collection's field definitions.
610
690
  * Applies defaults, rejects auto fields, and type-checks each value.
@@ -854,7 +934,7 @@ declare function migrationStepsToSQL(steps: readonly MigrationStep[]): string[];
854
934
  *
855
935
  * const rollbackSQL = rollbackStepsToSQL(migration)
856
936
  * // ['DROP INDEX IF EXISTS idx_todos_priority',
857
- * // 'ALTER TABLE todos DROP COLUMN priority']
937
+ * // 'ALTER TABLE "todos" DROP COLUMN "priority"']
858
938
  * ```
859
939
  */
860
940
  declare function rollbackStepsToSQL(migration: MigrationDefinition): string[];
@@ -1045,4 +1125,4 @@ interface ProtoOutput {
1045
1125
  */
1046
1126
  declare function generateProtoDefinitions(schema: SchemaDefinition): ProtoOutput;
1047
1127
 
1048
- export { APPLY_FAILURE_CODES, APPLY_RESULTS, AppNotReadyError, type ApplyFailureReason, type ApplyResult, ArrayFieldBuilder, AtomicOp, type AtomicOpSentinel, AtomicOpType, BlobRef, type BlobRefMetadata, CausalTracker, ClockDriftError, CollectionDefinition, type CollectionInput, type ConstraintInput, CustomResolver, EnumFieldBuilder, FieldBuilder, FieldKind, type FieldKindToType, type InferFieldType, type InferInsertInput, type InferRecord, type InferUpdateInput, InvalidTimestampError, KORA_ERROR_FIX_SUGGESTIONS, type KoraBytesValue, KoraError, MergeConflictError, MigrationDefinition, MigrationRollbackError, MigrationStep, MissingSecretKeyError, Operation, OperationError, type OperationLog, type OperationTransform, OperationType, type ProtoOutput, RandomSource, RelationDefinition, type RelationInput, RemoteClockDriftError, type ReversibleMigration, SchemaDefinition, type SchemaInput, SchemaValidationError, type SecretKeyProvider, StateMachineConstraint, type StateMachineInput, StorageError, SyncError, TransitionMap, TransitionValidationResult, type TypedSchemaDefinition, VersionVector, advanceVector, applyOperationTransforms, base64ToBytes, buildStateMachineConstraints, bytesToBase64, canAutoRollback, collectSchemaScopeFields, collectSchemaScopeValueKeys, computeDelta, createBlobRef, createReversibleMigration, createVersionVector, decodeBytesFromOpData, decryptSecret, defaultApplyFailureReason, defaultSequenceFormat, defineSchema, deserializeVector, dominates, encodeBytesForOpData, encryptSecret, extractScopeValuesFromClaims, extractTimestamp, formatSequenceValue, generateFullDDL, generateProtoDefinitions, generateRollbackSteps, generateSQL, generateUUIDv7, getCollectionScopeBindings, getKoraErrorFix, getTransitionMap, hasSchemaSyncRules, hashBlob, hashSecret, isApplyFailure, isAtomicOp, isBlobRef, isCollectionSyncScoped, isKoraBytesValue, isLegacyNumericByteObject, isValidUUIDv7, mergeVectors, migrationStepsToSQL, op, resolveAtomicOp, revealSecret, rollbackStepsToSQL, serializeVector, toAtomicOp, transformSecretFieldsForWrite, validateRecord, validateTransition, vectorsEqual, verifySecret, verifySecretValue };
1128
+ export { APPLY_FAILURE_CODES, APPLY_RESULTS, AppNotReadyError, type ApplyFailureReason, type ApplyResult, ArrayFieldBuilder, AtomicOp, type AtomicOpSentinel, AtomicOpType, BlobRef, type BlobRefMetadata, CausalTracker, ClockDriftError, CollectionDefinition, type CollectionInput, type ConstraintInput, CustomResolver, EnumFieldBuilder, FieldBuilder, FieldKind, type FieldKindToType, type InferFieldType, type InferInsertInput, type InferRecord, type InferUpdateInput, InvalidTimestampError, KORA_ERROR_FIX_SUGGESTIONS, type KoraBytesValue, KoraError, MergeConflictError, MigrationDefinition, MigrationRollbackError, MigrationStep, MissingSecretKeyError, Operation, OperationError, type OperationLog, type OperationTransform, OperationType, type ProtoOutput, RandomSource, RelationDefinition, type RelationInput, RemoteClockDriftError, type ReplayOperation, type ReversibleMigration, SchemaDefinition, type SchemaInput, SchemaValidationError, type SecretKeyProvider, StateMachineConstraint, type StateMachineInput, StorageError, SyncError, TransitionMap, TransitionValidationResult, type TypedSchemaDefinition, VersionVector, advanceVector, applyAtomicOp, applyOperationTransforms, base64ToBytes, buildStateMachineConstraints, bytesToBase64, canAutoRollback, collectSchemaScopeFields, collectSchemaScopeValueKeys, computeDelta, createBlobRef, createReversibleMigration, createVersionVector, decodeBytesFromOpData, decryptSecret, defaultApplyFailureReason, defaultSequenceFormat, defineSchema, deserializeVector, dominates, encodeBytesForOpData, encryptSecret, extractScopeValuesFromClaims, extractTimestamp, formatSequenceValue, generateFullDDL, generateProtoDefinitions, generateRollbackSteps, generateSQL, generateUUIDv7, getCollectionScopeBindings, getKoraErrorFix, getTransitionMap, hasSchemaSyncRules, hashBlob, hashSecret, isApplyFailure, isAtomicOp, isBlobRef, isCollectionSyncScoped, isKoraBytesValue, isLegacyNumericByteObject, isValidUUIDv7, mergeVectors, migrationStepsToSQL, op, quoteIdent, replayOperationsForRecord, resolveAtomicOp, revealSecret, rollbackStepsToSQL, serializeVector, toAtomicOp, transformSecretFieldsForWrite, validateRecord, validateTransition, vectorsEqual, verifySecret, verifySecretValue };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { R as RandomSource, B as BlobRef, C as CollectionDefinition, A as AtomicOpType, a as AtomicOp, F as FieldBuilder, b as CustomResolver, M as MigrationDefinition, S as SchemaDefinition, c as RelationDefinition, O as OperationType, E as EnumFieldBuilder, J as JsonFieldBuilder, d as ArrayFieldBuilder, e as FieldKind, f as Operation, V as VersionVector, g as MigrationStep, h as StateMachineConstraint, T as TransitionMap, i as TransitionValidationResult } from './events-CqDkr1LN.js';
2
- export { j as CONNECTION_QUALITIES, k as ConnectionQuality, l as Constraint, m as FieldDescriptor, n as FieldMergeStrategy, H as HLCTimestamp, K as KoraEvent, o as KoraEventByType, p as KoraEventEmitter, q as KoraEventListener, r as KoraEventType, s as MERGE_STRATEGIES, t as MergeStrategy, u as MergeTrace, v as MigrationBuilder, w as OnDeleteAction, x as OperationInput, y as RelationType, z as RollbackBuilder, D as SecretMode, G as SequenceConfig, I as StateMachineDefinition, L as SyncDiagnosticsSnapshot, N as SyncRuleDefinition, P as TimeSource, Q as migrate, U as t } from './events-CqDkr1LN.js';
3
- export { H as HybridLogicalClock, M as MAX_LOGICAL, c as createOperation, i as isValidOperation, v as verifyOperationIntegrity } from './operation-BJtu18Fi.js';
4
- export { S as ScopeMap, b as buildScopeMap } from './build-scope-map-DvOHbYjn.js';
1
+ import { R as RandomSource, B as BlobRef, C as CollectionDefinition, A as AtomicOpType, a as AtomicOp, F as FieldBuilder, b as CustomResolver, M as MigrationDefinition, S as SchemaDefinition, c as RelationDefinition, O as OperationType, E as EnumFieldBuilder, J as JsonFieldBuilder, d as ArrayFieldBuilder, e as FieldKind, f as Operation, V as VersionVector, g as MigrationStep, h as StateMachineConstraint, T as TransitionMap, i as TransitionValidationResult } from './events-BPbYUbrE.js';
2
+ export { j as CONNECTION_QUALITIES, k as ConnectionQuality, l as Constraint, m as FieldDescriptor, n as FieldMergeStrategy, H as HLCTimestamp, K as KoraEvent, o as KoraEventByType, p as KoraEventEmitter, q as KoraEventListener, r as KoraEventType, s as MERGE_STRATEGIES, t as MergeStrategy, u as MergeTrace, v as MigrationBuilder, w as OnDeleteAction, x as OperationInput, y as RelationType, z as RollbackBuilder, D as SecretMode, G as SequenceConfig, I as StateMachineDefinition, L as SyncDiagnosticsSnapshot, N as SyncRuleDefinition, P as TimeSource, Q as migrate, U as t } from './events-BPbYUbrE.js';
3
+ export { H as HybridLogicalClock, M as MAX_LOGICAL, c as createOperation, i as isValidOperation, v as verifyOperationIntegrity } from './operation-Dt0AtT8u.js';
4
+ export { S as ScopeMap, b as buildScopeMap } from './build-scope-map-B3R_emUU.js';
5
5
 
6
6
  /**
7
7
  * Base error class for all Kora errors.
@@ -359,6 +359,20 @@ interface AtomicOpSentinel {
359
359
  * @returns true if the value is an AtomicOpSentinel
360
360
  */
361
361
  declare function isAtomicOp(value: unknown): value is AtomicOpSentinel;
362
+ /**
363
+ * Apply a plain atomic op (`{ type, value }`) to a current value, producing the
364
+ * concrete result. This is the single source of truth for atomic-op semantics:
365
+ * {@link resolveAtomicOp} (sentinel form, used on the write path) delegates here,
366
+ * and the server's materialization replays stored atomic ops through it, so the
367
+ * server's materialized view composes concurrent atomic writes identically to the
368
+ * client. Folding this over a field's writes in HLC order yields, for a same-type
369
+ * atomic chain, exactly the composed intent (sum of increments, max of maxes, ...).
370
+ *
371
+ * @param currentValue - The current value of the field
372
+ * @param atomicOp - The atomic op to apply (type + operand)
373
+ * @returns The resulting concrete value
374
+ */
375
+ declare function applyAtomicOp(currentValue: unknown, atomicOp: AtomicOp): unknown;
362
376
  /**
363
377
  * Resolve an atomic op sentinel against a current value to produce a concrete result.
364
378
  *
@@ -437,6 +451,40 @@ declare const op: {
437
451
  */
438
452
  declare function toAtomicOp(sentinel: AtomicOpSentinel): AtomicOp;
439
453
 
454
+ /** One operation's contribution to a record's materialized state. */
455
+ interface ReplayOperation {
456
+ type: string;
457
+ data: Record<string, unknown> | null;
458
+ /**
459
+ * Atomic-op intents carried by this operation, per field. When present, a field
460
+ * is composed (rather than overwritten) if the previous writer on that field was
461
+ * an atomic op of the same type. Absent means every field resolves by
462
+ * last-write-wins.
463
+ */
464
+ atomicOps?: Record<string, AtomicOp> | null;
465
+ }
466
+ /**
467
+ * Replay a list of operations (MUST be pre-sorted in HLC total order —
468
+ * wallTime, then logical, then nodeId) to produce the current state of a single
469
+ * record, composing atomic-op intents.
470
+ *
471
+ * Per field, an atomic op composes onto the running value only when the field's
472
+ * previous writer was an atomic op of the SAME type (a same-type atomic chain:
473
+ * concurrent increments sum, maxes take the max, appends accumulate); any other
474
+ * write, including the first atomic write after a plain set, resolves by
475
+ * last-write-wins on the resolved value. Because operations are folded in HLC order,
476
+ * the current op always wins that last-write-wins comparison, so the result is
477
+ * deterministic and identical on every replica that folds the same operation set.
478
+ *
479
+ * This is the single definition of atomic materialization shared by the client
480
+ * (`@korajs/store` apply pipeline) and the server (`@korajs/server` materialization),
481
+ * so both converge to the same value for any number of concurrent atomic writers.
482
+ *
483
+ * Returns the record field data (without `id`) or null if the record was deleted
484
+ * or never inserted.
485
+ */
486
+ declare function replayOperationsForRecord(ops: ReplayOperation[]): Record<string, unknown> | null;
487
+
440
488
  /**
441
489
  * Canonical JSON-safe encoding of binary bytes stored inside `op.data` /
442
490
  * `op.previousData` (currently richtext field values). A raw Uint8Array
@@ -605,6 +653,38 @@ declare function generateSQL(collectionName: string, collection: CollectionDefin
605
653
  */
606
654
  declare function generateFullDDL(schema: SchemaDefinition): string[];
607
655
 
656
+ /**
657
+ * Quote a SQL identifier (table or column name) for safe interpolation into
658
+ * generated SQL.
659
+ *
660
+ * Uses standard double-quote quoting, which both SQLite and PostgreSQL honor,
661
+ * and doubles any embedded double-quote per the SQL standard. Quoting is what
662
+ * lets a valid JavaScript identifier — camelCase (`formResponses`), PascalCase
663
+ * (`UserProfiles`), or a word that happens to be a SQL keyword (`order`,
664
+ * `select`) — round-trip through generated DDL and queries without ever
665
+ * producing invalid or ambiguous SQL.
666
+ *
667
+ * Schema validation (see `validateCollectionName` / `validateFieldName`)
668
+ * already restricts identifiers to `[A-Za-z][A-Za-z0-9_]*`, so the escape here
669
+ * never actually fires in practice; it is defense in depth so this helper is
670
+ * correct for any input rather than only for pre-validated names.
671
+ *
672
+ * Consistency is the contract: every path that emits an identifier for a
673
+ * user-defined collection or field MUST go through this helper, because a
674
+ * quoted `CREATE TABLE "order"` paired with an unquoted `SELECT ... FROM order`
675
+ * would reintroduce the exact keyword-collision bug this guards against.
676
+ *
677
+ * @param name - The raw identifier (collection name, field/column name).
678
+ * @returns The identifier wrapped in double quotes, ready to splice into SQL.
679
+ *
680
+ * @example
681
+ * ```typescript
682
+ * quoteIdent('formResponses') // => '"formResponses"'
683
+ * quoteIdent('order') // => '"order"'
684
+ * ```
685
+ */
686
+ declare function quoteIdent(name: string): string;
687
+
608
688
  /**
609
689
  * Validates a record's data against a collection's field definitions.
610
690
  * Applies defaults, rejects auto fields, and type-checks each value.
@@ -854,7 +934,7 @@ declare function migrationStepsToSQL(steps: readonly MigrationStep[]): string[];
854
934
  *
855
935
  * const rollbackSQL = rollbackStepsToSQL(migration)
856
936
  * // ['DROP INDEX IF EXISTS idx_todos_priority',
857
- * // 'ALTER TABLE todos DROP COLUMN priority']
937
+ * // 'ALTER TABLE "todos" DROP COLUMN "priority"']
858
938
  * ```
859
939
  */
860
940
  declare function rollbackStepsToSQL(migration: MigrationDefinition): string[];
@@ -1045,4 +1125,4 @@ interface ProtoOutput {
1045
1125
  */
1046
1126
  declare function generateProtoDefinitions(schema: SchemaDefinition): ProtoOutput;
1047
1127
 
1048
- export { APPLY_FAILURE_CODES, APPLY_RESULTS, AppNotReadyError, type ApplyFailureReason, type ApplyResult, ArrayFieldBuilder, AtomicOp, type AtomicOpSentinel, AtomicOpType, BlobRef, type BlobRefMetadata, CausalTracker, ClockDriftError, CollectionDefinition, type CollectionInput, type ConstraintInput, CustomResolver, EnumFieldBuilder, FieldBuilder, FieldKind, type FieldKindToType, type InferFieldType, type InferInsertInput, type InferRecord, type InferUpdateInput, InvalidTimestampError, KORA_ERROR_FIX_SUGGESTIONS, type KoraBytesValue, KoraError, MergeConflictError, MigrationDefinition, MigrationRollbackError, MigrationStep, MissingSecretKeyError, Operation, OperationError, type OperationLog, type OperationTransform, OperationType, type ProtoOutput, RandomSource, RelationDefinition, type RelationInput, RemoteClockDriftError, type ReversibleMigration, SchemaDefinition, type SchemaInput, SchemaValidationError, type SecretKeyProvider, StateMachineConstraint, type StateMachineInput, StorageError, SyncError, TransitionMap, TransitionValidationResult, type TypedSchemaDefinition, VersionVector, advanceVector, applyOperationTransforms, base64ToBytes, buildStateMachineConstraints, bytesToBase64, canAutoRollback, collectSchemaScopeFields, collectSchemaScopeValueKeys, computeDelta, createBlobRef, createReversibleMigration, createVersionVector, decodeBytesFromOpData, decryptSecret, defaultApplyFailureReason, defaultSequenceFormat, defineSchema, deserializeVector, dominates, encodeBytesForOpData, encryptSecret, extractScopeValuesFromClaims, extractTimestamp, formatSequenceValue, generateFullDDL, generateProtoDefinitions, generateRollbackSteps, generateSQL, generateUUIDv7, getCollectionScopeBindings, getKoraErrorFix, getTransitionMap, hasSchemaSyncRules, hashBlob, hashSecret, isApplyFailure, isAtomicOp, isBlobRef, isCollectionSyncScoped, isKoraBytesValue, isLegacyNumericByteObject, isValidUUIDv7, mergeVectors, migrationStepsToSQL, op, resolveAtomicOp, revealSecret, rollbackStepsToSQL, serializeVector, toAtomicOp, transformSecretFieldsForWrite, validateRecord, validateTransition, vectorsEqual, verifySecret, verifySecretValue };
1128
+ export { APPLY_FAILURE_CODES, APPLY_RESULTS, AppNotReadyError, type ApplyFailureReason, type ApplyResult, ArrayFieldBuilder, AtomicOp, type AtomicOpSentinel, AtomicOpType, BlobRef, type BlobRefMetadata, CausalTracker, ClockDriftError, CollectionDefinition, type CollectionInput, type ConstraintInput, CustomResolver, EnumFieldBuilder, FieldBuilder, FieldKind, type FieldKindToType, type InferFieldType, type InferInsertInput, type InferRecord, type InferUpdateInput, InvalidTimestampError, KORA_ERROR_FIX_SUGGESTIONS, type KoraBytesValue, KoraError, MergeConflictError, MigrationDefinition, MigrationRollbackError, MigrationStep, MissingSecretKeyError, Operation, OperationError, type OperationLog, type OperationTransform, OperationType, type ProtoOutput, RandomSource, RelationDefinition, type RelationInput, RemoteClockDriftError, type ReplayOperation, type ReversibleMigration, SchemaDefinition, type SchemaInput, SchemaValidationError, type SecretKeyProvider, StateMachineConstraint, type StateMachineInput, StorageError, SyncError, TransitionMap, TransitionValidationResult, type TypedSchemaDefinition, VersionVector, advanceVector, applyAtomicOp, applyOperationTransforms, base64ToBytes, buildStateMachineConstraints, bytesToBase64, canAutoRollback, collectSchemaScopeFields, collectSchemaScopeValueKeys, computeDelta, createBlobRef, createReversibleMigration, createVersionVector, decodeBytesFromOpData, decryptSecret, defaultApplyFailureReason, defaultSequenceFormat, defineSchema, deserializeVector, dominates, encodeBytesForOpData, encryptSecret, extractScopeValuesFromClaims, extractTimestamp, formatSequenceValue, generateFullDDL, generateProtoDefinitions, generateRollbackSteps, generateSQL, generateUUIDv7, getCollectionScopeBindings, getKoraErrorFix, getTransitionMap, hasSchemaSyncRules, hashBlob, hashSecret, isApplyFailure, isAtomicOp, isBlobRef, isCollectionSyncScoped, isKoraBytesValue, isLegacyNumericByteObject, isValidUUIDv7, mergeVectors, migrationStepsToSQL, op, quoteIdent, replayOperationsForRecord, resolveAtomicOp, revealSecret, rollbackStepsToSQL, serializeVector, toAtomicOp, transformSecretFieldsForWrite, validateRecord, validateTransition, vectorsEqual, verifySecret, verifySecretValue };
package/dist/index.js CHANGED
@@ -365,32 +365,36 @@ var KORA_ATOMIC_OP = /* @__PURE__ */ Symbol.for("kora.atomic_op");
365
365
  function isAtomicOp(value) {
366
366
  return typeof value === "object" && value !== null && KORA_ATOMIC_OP in value && value[KORA_ATOMIC_OP] === true;
367
367
  }
368
- function resolveAtomicOp(currentValue, sentinel) {
369
- switch (sentinel.type) {
368
+ function applyAtomicOp(currentValue, atomicOp) {
369
+ switch (atomicOp.type) {
370
370
  case "increment": {
371
371
  const current = typeof currentValue === "number" ? currentValue : 0;
372
- const operand = sentinel.value;
372
+ const operand = atomicOp.value;
373
373
  return current + operand;
374
374
  }
375
375
  case "max": {
376
376
  const current = typeof currentValue === "number" ? currentValue : Number.NEGATIVE_INFINITY;
377
- return Math.max(current, sentinel.value);
377
+ return Math.max(current, atomicOp.value);
378
378
  }
379
379
  case "min": {
380
380
  const current = typeof currentValue === "number" ? currentValue : Number.POSITIVE_INFINITY;
381
- return Math.min(current, sentinel.value);
381
+ return Math.min(current, atomicOp.value);
382
382
  }
383
383
  case "append": {
384
384
  const current = Array.isArray(currentValue) ? [...currentValue] : [];
385
- current.push(sentinel.value);
385
+ current.push(atomicOp.value);
386
386
  return current;
387
387
  }
388
388
  case "remove": {
389
389
  if (!Array.isArray(currentValue)) return [];
390
- return currentValue.filter((item) => item !== sentinel.value);
390
+ const target = JSON.stringify(atomicOp.value);
391
+ return currentValue.filter((item) => JSON.stringify(item) !== target);
391
392
  }
392
393
  }
393
394
  }
395
+ function resolveAtomicOp(currentValue, sentinel) {
396
+ return applyAtomicOp(currentValue, { type: sentinel.type, value: sentinel.value });
397
+ }
394
398
  function createSentinel(type, value) {
395
399
  return Object.freeze({
396
400
  [KORA_ATOMIC_OP]: true,
@@ -458,6 +462,46 @@ function toAtomicOp(sentinel) {
458
462
  return { type: sentinel.type, value: sentinel.value };
459
463
  }
460
464
 
465
+ // src/operations/replay-record.ts
466
+ function replayOperationsForRecord(ops) {
467
+ let record = null;
468
+ let deleted = false;
469
+ const lastWriterKind = {};
470
+ for (const op2 of ops) {
471
+ switch (op2.type) {
472
+ case "insert":
473
+ if (op2.data) {
474
+ record = { ...op2.data };
475
+ deleted = false;
476
+ for (const field of Object.keys(op2.data)) {
477
+ lastWriterKind[field] = "lww";
478
+ }
479
+ }
480
+ break;
481
+ case "update":
482
+ if (op2.data) {
483
+ const next = { ...record ?? {} };
484
+ for (const [field, value] of Object.entries(op2.data)) {
485
+ const atomicOp = op2.atomicOps?.[field];
486
+ if (atomicOp && lastWriterKind[field] === `atomic:${atomicOp.type}`) {
487
+ next[field] = applyAtomicOp(next[field], atomicOp);
488
+ } else {
489
+ next[field] = value;
490
+ }
491
+ lastWriterKind[field] = atomicOp ? `atomic:${atomicOp.type}` : "lww";
492
+ }
493
+ record = next;
494
+ deleted = false;
495
+ }
496
+ break;
497
+ case "delete":
498
+ deleted = true;
499
+ break;
500
+ }
501
+ }
502
+ return deleted ? null : record;
503
+ }
504
+
461
505
  // src/operations/op-data-binary.ts
462
506
  function isKoraBytesValue(value) {
463
507
  if (typeof value !== "object" || value === null) {
@@ -708,7 +752,7 @@ function validateStateMachineDefinition(collectionName, sm, fields) {
708
752
  }
709
753
 
710
754
  // src/schema/define.ts
711
- var COLLECTION_NAME_RE = /^[a-z][a-z0-9_]*$/;
755
+ var COLLECTION_NAME_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
712
756
  var FIELD_NAME_RE = /^[a-z][a-zA-Z0-9_]*$/;
713
757
  var RESERVED_FIELDS = /* @__PURE__ */ new Set(["id", "_created_at", "_updated_at", "_version", "_deleted"]);
714
758
  function defineSchema(input) {
@@ -773,7 +817,7 @@ function validateVersion(version) {
773
817
  function validateCollectionName(name) {
774
818
  if (!COLLECTION_NAME_RE.test(name)) {
775
819
  throw new SchemaValidationError(
776
- `Collection name "${name}" is invalid. Must be lowercase, start with a letter, and contain only letters, numbers, and underscores.`,
820
+ `Collection name "${name}" is invalid. Must start with a letter and contain only letters, numbers, and underscores.`,
777
821
  { collection: name }
778
822
  );
779
823
  }
@@ -965,6 +1009,11 @@ function validateRelation(name, relation, collections) {
965
1009
  }
966
1010
  }
967
1011
 
1012
+ // src/schema/quote-ident.ts
1013
+ function quoteIdent(name) {
1014
+ return `"${name.replace(/"/g, '""')}"`;
1015
+ }
1016
+
968
1017
  // src/schema/sql-gen.ts
969
1018
  function generateSQL(collectionName, collection, relations) {
970
1019
  const statements = [];
@@ -976,7 +1025,7 @@ function generateSQL(collectionName, collection, relations) {
976
1025
  if (relations) {
977
1026
  for (const rel of Object.values(relations)) {
978
1027
  if (rel.from === collectionName && rel.field === fieldName) {
979
- colDef += ` REFERENCES ${rel.to}(id)`;
1028
+ colDef += ` REFERENCES ${quoteIdent(rel.to)}(id)`;
980
1029
  fkFields.push(fieldName);
981
1030
  break;
982
1031
  }
@@ -989,36 +1038,40 @@ function generateSQL(collectionName, collection, relations) {
989
1038
  columns.push("_version TEXT NOT NULL DEFAULT ''");
990
1039
  columns.push("_field_versions TEXT NOT NULL DEFAULT '{}'");
991
1040
  columns.push("_deleted INTEGER NOT NULL DEFAULT 0");
992
- statements.push(`CREATE TABLE IF NOT EXISTS ${collectionName} (
1041
+ statements.push(
1042
+ `CREATE TABLE IF NOT EXISTS ${quoteIdent(collectionName)} (
993
1043
  ${columns.join(",\n ")}
994
- )`);
1044
+ )`
1045
+ );
995
1046
  for (const [fieldName, descriptor] of Object.entries(collection.fields)) {
996
1047
  const colDef = columnDefinition(fieldName, descriptor);
997
- statements.push(`--kora:safe-alter
998
- ALTER TABLE ${collectionName} ADD COLUMN ${colDef}`);
1048
+ statements.push(
1049
+ `--kora:safe-alter
1050
+ ALTER TABLE ${quoteIdent(collectionName)} ADD COLUMN ${colDef}`
1051
+ );
999
1052
  }
1000
1053
  statements.push(
1001
1054
  `--kora:safe-alter
1002
- ALTER TABLE ${collectionName} ADD COLUMN _version TEXT NOT NULL DEFAULT ''`
1055
+ ALTER TABLE ${quoteIdent(collectionName)} ADD COLUMN _version TEXT NOT NULL DEFAULT ''`
1003
1056
  );
1004
1057
  statements.push(
1005
1058
  `--kora:safe-alter
1006
- ALTER TABLE ${collectionName} ADD COLUMN _field_versions TEXT NOT NULL DEFAULT '{}'`
1059
+ ALTER TABLE ${quoteIdent(collectionName)} ADD COLUMN _field_versions TEXT NOT NULL DEFAULT '{}'`
1007
1060
  );
1008
1061
  for (const indexField of collection.indexes) {
1009
1062
  statements.push(
1010
- `CREATE INDEX IF NOT EXISTS idx_${collectionName}_${indexField} ON ${collectionName} (${indexField})`
1063
+ `CREATE INDEX IF NOT EXISTS idx_${collectionName}_${indexField} ON ${quoteIdent(collectionName)} (${quoteIdent(indexField)})`
1011
1064
  );
1012
1065
  }
1013
1066
  for (const fkField of fkFields) {
1014
1067
  if (!indexedFields.has(fkField)) {
1015
1068
  statements.push(
1016
- `CREATE INDEX IF NOT EXISTS idx_${collectionName}_${fkField} ON ${collectionName} (${fkField})`
1069
+ `CREATE INDEX IF NOT EXISTS idx_${collectionName}_${fkField} ON ${quoteIdent(collectionName)} (${quoteIdent(fkField)})`
1017
1070
  );
1018
1071
  }
1019
1072
  }
1020
1073
  statements.push(
1021
- `CREATE TABLE IF NOT EXISTS _kora_ops_${collectionName} (
1074
+ `CREATE TABLE IF NOT EXISTS ${quoteIdent(`_kora_ops_${collectionName}`)} (
1022
1075
  id TEXT PRIMARY KEY NOT NULL,
1023
1076
  node_id TEXT NOT NULL,
1024
1077
  type TEXT NOT NULL,
@@ -1032,7 +1085,7 @@ ALTER TABLE ${collectionName} ADD COLUMN _field_versions TEXT NOT NULL DEFAULT '
1032
1085
  )`
1033
1086
  );
1034
1087
  statements.push(
1035
- `CREATE INDEX IF NOT EXISTS idx_kora_ops_${collectionName}_record_id ON _kora_ops_${collectionName} (record_id)`
1088
+ `CREATE INDEX IF NOT EXISTS idx_kora_ops_${collectionName}_record_id ON ${quoteIdent(`_kora_ops_${collectionName}`)} (record_id)`
1036
1089
  );
1037
1090
  return statements;
1038
1091
  }
@@ -1050,6 +1103,9 @@ function generateFullDDL(schema) {
1050
1103
  statements.push(
1051
1104
  "CREATE TABLE IF NOT EXISTS _kora_sync_queue (\n id TEXT PRIMARY KEY NOT NULL,\n payload TEXT NOT NULL\n)"
1052
1105
  );
1106
+ statements.push(
1107
+ "CREATE TABLE IF NOT EXISTS _kora_sync_rejected (\n operation_id TEXT PRIMARY KEY NOT NULL,\n collection TEXT NOT NULL,\n record_id TEXT NOT NULL,\n code TEXT NOT NULL,\n message TEXT NOT NULL,\n retriable INTEGER NOT NULL,\n rejected_at INTEGER NOT NULL\n)"
1108
+ );
1053
1109
  statements.push(
1054
1110
  "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)"
1055
1111
  );
@@ -1066,7 +1122,7 @@ function generateFullDDL(schema) {
1066
1122
  }
1067
1123
  function columnDefinition(fieldName, descriptor) {
1068
1124
  const sqlType = mapFieldType(descriptor);
1069
- const parts = [fieldName, sqlType];
1125
+ const parts = [quoteIdent(fieldName), sqlType];
1070
1126
  if (descriptor.required && descriptor.defaultValue === void 0 && !descriptor.auto) {
1071
1127
  parts.push("NOT NULL");
1072
1128
  }
@@ -1075,7 +1131,7 @@ function columnDefinition(fieldName, descriptor) {
1075
1131
  }
1076
1132
  if (descriptor.kind === "enum" && descriptor.enumValues) {
1077
1133
  const values = descriptor.enumValues.map((v) => `'${v}'`).join(", ");
1078
- parts.push(`CHECK (${fieldName} IN (${values}))`);
1134
+ parts.push(`CHECK (${quoteIdent(fieldName)} IN (${values}))`);
1079
1135
  }
1080
1136
  return parts.join(" ");
1081
1137
  }
@@ -1717,10 +1773,17 @@ function validateFieldValue(collection, fieldName, descriptor, value) {
1717
1773
  break;
1718
1774
  }
1719
1775
  case "json": {
1720
- if (!isJsonSerializable(value)) {
1776
+ const nonJson = findNonJsonSerializable(value, fieldName);
1777
+ if (nonJson) {
1721
1778
  throw new SchemaValidationError(
1722
- `Field "${fieldName}" in collection "${collection}" must be JSON-serializable, got ${describeType(value)}`,
1723
- { collection, field: fieldName, expectedType: "json", receivedType: describeType(value) }
1779
+ `Field "${fieldName}" in collection "${collection}" must be JSON-serializable; found ${nonJson.receivedType} at "${nonJson.path}"`,
1780
+ {
1781
+ collection,
1782
+ field: fieldName,
1783
+ expectedType: "json",
1784
+ receivedType: nonJson.receivedType,
1785
+ path: nonJson.path
1786
+ }
1724
1787
  );
1725
1788
  }
1726
1789
  break;
@@ -1757,21 +1820,41 @@ function isPlainObject(value) {
1757
1820
  const proto = Object.getPrototypeOf(value);
1758
1821
  return proto === null || proto === Object.prototype;
1759
1822
  }
1760
- function isJsonSerializable(value) {
1823
+ function findNonJsonSerializable(value, path, seen = /* @__PURE__ */ new WeakSet()) {
1761
1824
  if (value === null) {
1762
- return true;
1825
+ return null;
1763
1826
  }
1764
1827
  const type = typeof value;
1765
1828
  if (type === "string" || type === "number" || type === "boolean") {
1766
- return Number.isFinite(value) || type !== "number";
1829
+ if (type === "number" && !Number.isFinite(value)) {
1830
+ return { path, receivedType: describeType(value) };
1831
+ }
1832
+ return null;
1833
+ }
1834
+ if (type === "undefined" || type === "function" || type === "symbol" || type === "bigint") {
1835
+ return { path, receivedType: describeType(value) };
1767
1836
  }
1768
1837
  if (Array.isArray(value)) {
1769
- return value.every(isJsonSerializable);
1838
+ for (let index = 0; index < value.length; index++) {
1839
+ const invalid = findNonJsonSerializable(value[index], `${path}[${index}]`, seen);
1840
+ if (invalid) return invalid;
1841
+ }
1842
+ return null;
1770
1843
  }
1771
1844
  if (type === "object") {
1772
- return Object.values(value).every(isJsonSerializable);
1845
+ const objectValue = value;
1846
+ if (seen.has(objectValue)) {
1847
+ return { path, receivedType: "circular object" };
1848
+ }
1849
+ seen.add(objectValue);
1850
+ for (const [key, nestedValue] of Object.entries(objectValue)) {
1851
+ const invalid = findNonJsonSerializable(nestedValue, `${path}.${key}`, seen);
1852
+ if (invalid) return invalid;
1853
+ }
1854
+ seen.delete(objectValue);
1855
+ return null;
1773
1856
  }
1774
- return false;
1857
+ return { path, receivedType: describeType(value) };
1775
1858
  }
1776
1859
  function describeType(value) {
1777
1860
  if (value === null) {
@@ -2212,14 +2295,18 @@ function migrationStepsToSQL(steps) {
2212
2295
  statements.push(addFieldSQL(step.collection, step.field, step.descriptor));
2213
2296
  break;
2214
2297
  case "removeField":
2215
- statements.push(`ALTER TABLE ${step.collection} DROP COLUMN ${step.field}`);
2298
+ statements.push(
2299
+ `ALTER TABLE ${quoteIdent(step.collection)} DROP COLUMN ${quoteIdent(step.field)}`
2300
+ );
2216
2301
  break;
2217
2302
  case "renameField":
2218
- statements.push(`ALTER TABLE ${step.collection} RENAME COLUMN ${step.from} TO ${step.to}`);
2303
+ statements.push(
2304
+ `ALTER TABLE ${quoteIdent(step.collection)} RENAME COLUMN ${quoteIdent(step.from)} TO ${quoteIdent(step.to)}`
2305
+ );
2219
2306
  break;
2220
2307
  case "addIndex":
2221
2308
  statements.push(
2222
- `CREATE INDEX IF NOT EXISTS idx_${step.collection}_${step.field} ON ${step.collection} (${step.field})`
2309
+ `CREATE INDEX IF NOT EXISTS idx_${step.collection}_${step.field} ON ${quoteIdent(step.collection)} (${quoteIdent(step.field)})`
2223
2310
  );
2224
2311
  break;
2225
2312
  case "removeIndex":
@@ -2237,13 +2324,13 @@ function rollbackStepsToSQL(migration) {
2237
2324
  }
2238
2325
  function addFieldSQL(collection, field, descriptor) {
2239
2326
  const sqlType = mapFieldType2(descriptor);
2240
- const parts = [`ALTER TABLE ${collection} ADD COLUMN ${field}`, sqlType];
2327
+ const parts = [`ALTER TABLE ${quoteIdent(collection)} ADD COLUMN ${quoteIdent(field)}`, sqlType];
2241
2328
  if (descriptor.defaultValue !== void 0) {
2242
2329
  parts.push(`DEFAULT ${sqlDefault2(descriptor.defaultValue)}`);
2243
2330
  }
2244
2331
  if (descriptor.kind === "enum" && descriptor.enumValues) {
2245
2332
  const values = descriptor.enumValues.map((v) => `'${v}'`).join(", ");
2246
- parts.push(`CHECK (${field} IN (${values}))`);
2333
+ parts.push(`CHECK (${quoteIdent(field)} IN (${values}))`);
2247
2334
  }
2248
2335
  return parts.join(" ");
2249
2336
  }
@@ -2545,6 +2632,7 @@ export {
2545
2632
  StorageError,
2546
2633
  SyncError,
2547
2634
  advanceVector,
2635
+ applyAtomicOp,
2548
2636
  applyOperationTransforms,
2549
2637
  base64ToBytes,
2550
2638
  buildScopeMap,
@@ -2593,6 +2681,8 @@ export {
2593
2681
  migrate,
2594
2682
  migrationStepsToSQL,
2595
2683
  op,
2684
+ quoteIdent,
2685
+ replayOperationsForRecord,
2596
2686
  resolveAtomicOp,
2597
2687
  revealSecret,
2598
2688
  rollbackStepsToSQL,