@korajs/merge 0.4.0 → 0.6.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.cjs +16 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +135 -123
- package/dist/index.d.ts +135 -123
- package/dist/index.js +16 -7
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,126 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { OnDeleteAction, MergeTrace, SchemaDefinition, Operation, Constraint, CollectionDefinition, HLCTimestamp, AtomicOp, FieldMergeStrategy, FieldDescriptor, CustomResolver } from '@korajs/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Describes an incoming relation to a target collection.
|
|
5
|
+
* Used by the delete-side referential integrity checker to know which
|
|
6
|
+
* source collections reference the collection being deleted from.
|
|
7
|
+
*/
|
|
8
|
+
interface MergeIncomingRelation {
|
|
9
|
+
/** Name of the relation in the schema (e.g., 'todoBelongsToProject') */
|
|
10
|
+
relationName: string;
|
|
11
|
+
/** The collection that holds the foreign key (source of the relation) */
|
|
12
|
+
sourceCollection: string;
|
|
13
|
+
/** The foreign key field in the source collection */
|
|
14
|
+
foreignKeyField: string;
|
|
15
|
+
/** What to do when the referenced record is deleted */
|
|
16
|
+
onDelete: OnDeleteAction;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Pluggable context for querying the database during referential integrity checks.
|
|
20
|
+
* The store layer provides the implementation; the merge package only depends on this interface.
|
|
21
|
+
*/
|
|
22
|
+
interface ReferentialMergeContext {
|
|
23
|
+
/** Query records in a collection matching a where clause */
|
|
24
|
+
queryRecords(collection: string, where: Record<string, unknown>): Promise<Record<string, unknown>[]>;
|
|
25
|
+
/** Check if a record exists in a collection */
|
|
26
|
+
recordExists(collection: string, recordId: string): Promise<boolean>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Result of a referential integrity check on a delete operation.
|
|
30
|
+
*/
|
|
31
|
+
interface ReferentialCheckResult {
|
|
32
|
+
/** Whether the delete operation is allowed to proceed */
|
|
33
|
+
allowed: boolean;
|
|
34
|
+
/** Side-effect operations generated by cascade/set-null policies */
|
|
35
|
+
sideEffectOps: SideEffectOp[];
|
|
36
|
+
/** MergeTraces for every decision made (feeds DevTools) */
|
|
37
|
+
traces: MergeTrace[];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A side-effect operation generated during referential integrity enforcement.
|
|
41
|
+
* These are not full Operations; they describe mutations the caller must apply.
|
|
42
|
+
*/
|
|
43
|
+
interface SideEffectOp {
|
|
44
|
+
/** The type of mutation to apply */
|
|
45
|
+
type: 'delete' | 'update';
|
|
46
|
+
/** The collection containing the affected record */
|
|
47
|
+
collection: string;
|
|
48
|
+
/** The ID of the affected record */
|
|
49
|
+
recordId: string;
|
|
50
|
+
/** For updates: the new field values. null for deletes. */
|
|
51
|
+
data: Record<string, unknown> | null;
|
|
52
|
+
/** For updates: the previous field values. null for deletes. */
|
|
53
|
+
previousData: Record<string, unknown> | null;
|
|
54
|
+
/** The onDelete policy that produced this side effect */
|
|
55
|
+
policy: OnDeleteAction;
|
|
56
|
+
/** The relation that triggered this side effect */
|
|
57
|
+
relationName: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Result of resolving a concurrent delete-vs-insert conflict.
|
|
61
|
+
*/
|
|
62
|
+
interface DeleteVsInsertResolution {
|
|
63
|
+
/** Whether to block the delete or allow it */
|
|
64
|
+
action: 'block-delete' | 'allow-delete';
|
|
65
|
+
/** Any side-effect operations (for cascade/set-null after allowing delete) */
|
|
66
|
+
sideEffects: SideEffectOp[];
|
|
67
|
+
/** Trace of the resolution decision, or null if no trace needed */
|
|
68
|
+
trace: MergeTrace | null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Builds a lookup map from target collection to all incoming relations.
|
|
72
|
+
*
|
|
73
|
+
* This pre-computes which collections reference a given collection via foreign keys,
|
|
74
|
+
* so that delete operations can efficiently find all relations that need enforcement.
|
|
75
|
+
*
|
|
76
|
+
* @param schema - The full schema definition with relations
|
|
77
|
+
* @returns Map from target collection name to array of incoming relations, sorted by relation name for determinism
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```typescript
|
|
81
|
+
* const lookup = buildMergeRelationLookup(schema)
|
|
82
|
+
* // lookup.get('projects') => [{ relationName: 'todoBelongsToProject', sourceCollection: 'todos', ... }]
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
declare function buildMergeRelationLookup(schema: SchemaDefinition): Map<string, MergeIncomingRelation[]>;
|
|
86
|
+
/**
|
|
87
|
+
* Checks referential integrity when a delete operation is being applied.
|
|
88
|
+
*
|
|
89
|
+
* For each relation that targets the deleted record's collection, queries
|
|
90
|
+
* for referencing records and applies the relation's onDelete policy:
|
|
91
|
+
* - `restrict`: Blocks the delete if any references exist
|
|
92
|
+
* - `cascade`: Allows the delete and generates cascaded delete SideEffectOps
|
|
93
|
+
* - `set-null`: Allows the delete and generates update SideEffectOps to null out FKs
|
|
94
|
+
* - `no-action`: Allows the delete with no side effects (dangling references permitted)
|
|
95
|
+
*
|
|
96
|
+
* Processing is deterministic: relations are processed in sorted order by name,
|
|
97
|
+
* and referencing records within each relation are processed in sorted order by ID.
|
|
98
|
+
*
|
|
99
|
+
* @param deleteOp - The delete operation being evaluated
|
|
100
|
+
* @param schema - The full schema definition
|
|
101
|
+
* @param ctx - Database lookup context
|
|
102
|
+
* @param relationLookup - Pre-built relation lookup (optional; will be built if not provided)
|
|
103
|
+
* @returns Whether the delete is allowed, any side-effect ops, and traces for DevTools
|
|
104
|
+
*/
|
|
105
|
+
declare function checkReferentialIntegrityOnDelete(deleteOp: Operation, schema: SchemaDefinition, ctx: ReferentialMergeContext, relationLookup?: Map<string, MergeIncomingRelation[]>): Promise<ReferentialCheckResult>;
|
|
106
|
+
/**
|
|
107
|
+
* Resolves a concurrent delete-vs-insert conflict on a referenced record.
|
|
108
|
+
*
|
|
109
|
+
* When one node deletes a record while another concurrently inserts a record
|
|
110
|
+
* that references the deleted record, we need to decide what wins.
|
|
111
|
+
*
|
|
112
|
+
* Resolution strategy depends on the relation's onDelete policy:
|
|
113
|
+
* - `restrict`: Block the delete (the insert wins — data integrity preserved)
|
|
114
|
+
* - `cascade`: Allow the delete and cascade-delete the newly inserted record
|
|
115
|
+
* - `set-null`: Allow the delete and null out the FK on the newly inserted record
|
|
116
|
+
* - `no-action`: Allow the delete (dangling reference is acceptable)
|
|
117
|
+
*
|
|
118
|
+
* @param deleteOp - The delete operation on the referenced record
|
|
119
|
+
* @param insertOp - The concurrent insert operation that references the deleted record
|
|
120
|
+
* @param relation - The relation definition connecting the two collections
|
|
121
|
+
* @returns The resolution action, side effects, and trace
|
|
122
|
+
*/
|
|
123
|
+
declare function resolveDeleteVsInsertConflict(deleteOp: Operation, insertOp: Operation, relation: MergeIncomingRelation): DeleteVsInsertResolution;
|
|
2
124
|
|
|
3
125
|
/**
|
|
4
126
|
* Input to the merge engine when two concurrent operations conflict.
|
|
@@ -23,6 +145,12 @@ interface MergeResult {
|
|
|
23
145
|
traces: MergeTrace[];
|
|
24
146
|
/** Which operation's values dominate overall, or 'merged' if mixed */
|
|
25
147
|
appliedOperation: 'local' | 'remote' | 'merged';
|
|
148
|
+
/**
|
|
149
|
+
* Side-effect operations generated during constraint resolution
|
|
150
|
+
* (e.g., cascade deletes, set-null updates from referential integrity).
|
|
151
|
+
* These must be applied by the caller after the merge.
|
|
152
|
+
*/
|
|
153
|
+
sideEffects: SideEffectOp[];
|
|
26
154
|
}
|
|
27
155
|
/**
|
|
28
156
|
* Output of a single field-level merge decision.
|
|
@@ -249,6 +377,12 @@ interface ConstraintResolution {
|
|
|
249
377
|
resolvedRecord: Record<string, unknown>;
|
|
250
378
|
/** Trace of the resolution decision for DevTools */
|
|
251
379
|
trace: MergeTrace;
|
|
380
|
+
/**
|
|
381
|
+
* Side-effect operations generated during resolution
|
|
382
|
+
* (e.g., referring record updates for referential integrity).
|
|
383
|
+
* Empty unless the constraint resolver generates cascading actions.
|
|
384
|
+
*/
|
|
385
|
+
sideEffects?: SideEffectOp[];
|
|
252
386
|
}
|
|
253
387
|
/**
|
|
254
388
|
* Resolves a constraint violation by applying the constraint's onConflict strategy.
|
|
@@ -269,128 +403,6 @@ interface ConstraintResolution {
|
|
|
269
403
|
*/
|
|
270
404
|
declare function resolveConstraintViolation(violation: ConstraintViolation, mergedRecord: Record<string, unknown>, localOp: Operation, remoteOp: Operation, baseState: Record<string, unknown>): ConstraintResolution;
|
|
271
405
|
|
|
272
|
-
/**
|
|
273
|
-
* Describes an incoming relation to a target collection.
|
|
274
|
-
* Used by the delete-side referential integrity checker to know which
|
|
275
|
-
* source collections reference the collection being deleted from.
|
|
276
|
-
*/
|
|
277
|
-
interface MergeIncomingRelation {
|
|
278
|
-
/** Name of the relation in the schema (e.g., 'todoBelongsToProject') */
|
|
279
|
-
relationName: string;
|
|
280
|
-
/** The collection that holds the foreign key (source of the relation) */
|
|
281
|
-
sourceCollection: string;
|
|
282
|
-
/** The foreign key field in the source collection */
|
|
283
|
-
foreignKeyField: string;
|
|
284
|
-
/** What to do when the referenced record is deleted */
|
|
285
|
-
onDelete: OnDeleteAction;
|
|
286
|
-
}
|
|
287
|
-
/**
|
|
288
|
-
* Pluggable context for querying the database during referential integrity checks.
|
|
289
|
-
* The store layer provides the implementation; the merge package only depends on this interface.
|
|
290
|
-
*/
|
|
291
|
-
interface ReferentialMergeContext {
|
|
292
|
-
/** Query records in a collection matching a where clause */
|
|
293
|
-
queryRecords(collection: string, where: Record<string, unknown>): Promise<Record<string, unknown>[]>;
|
|
294
|
-
/** Check if a record exists in a collection */
|
|
295
|
-
recordExists(collection: string, recordId: string): Promise<boolean>;
|
|
296
|
-
}
|
|
297
|
-
/**
|
|
298
|
-
* Result of a referential integrity check on a delete operation.
|
|
299
|
-
*/
|
|
300
|
-
interface ReferentialCheckResult {
|
|
301
|
-
/** Whether the delete operation is allowed to proceed */
|
|
302
|
-
allowed: boolean;
|
|
303
|
-
/** Side-effect operations generated by cascade/set-null policies */
|
|
304
|
-
sideEffectOps: SideEffectOp[];
|
|
305
|
-
/** MergeTraces for every decision made (feeds DevTools) */
|
|
306
|
-
traces: MergeTrace[];
|
|
307
|
-
}
|
|
308
|
-
/**
|
|
309
|
-
* A side-effect operation generated during referential integrity enforcement.
|
|
310
|
-
* These are not full Operations; they describe mutations the caller must apply.
|
|
311
|
-
*/
|
|
312
|
-
interface SideEffectOp {
|
|
313
|
-
/** The type of mutation to apply */
|
|
314
|
-
type: 'delete' | 'update';
|
|
315
|
-
/** The collection containing the affected record */
|
|
316
|
-
collection: string;
|
|
317
|
-
/** The ID of the affected record */
|
|
318
|
-
recordId: string;
|
|
319
|
-
/** For updates: the new field values. null for deletes. */
|
|
320
|
-
data: Record<string, unknown> | null;
|
|
321
|
-
/** For updates: the previous field values. null for deletes. */
|
|
322
|
-
previousData: Record<string, unknown> | null;
|
|
323
|
-
/** The onDelete policy that produced this side effect */
|
|
324
|
-
policy: OnDeleteAction;
|
|
325
|
-
/** The relation that triggered this side effect */
|
|
326
|
-
relationName: string;
|
|
327
|
-
}
|
|
328
|
-
/**
|
|
329
|
-
* Result of resolving a concurrent delete-vs-insert conflict.
|
|
330
|
-
*/
|
|
331
|
-
interface DeleteVsInsertResolution {
|
|
332
|
-
/** Whether to block the delete or allow it */
|
|
333
|
-
action: 'block-delete' | 'allow-delete';
|
|
334
|
-
/** Any side-effect operations (for cascade/set-null after allowing delete) */
|
|
335
|
-
sideEffects: SideEffectOp[];
|
|
336
|
-
/** Trace of the resolution decision, or null if no trace needed */
|
|
337
|
-
trace: MergeTrace | null;
|
|
338
|
-
}
|
|
339
|
-
/**
|
|
340
|
-
* Builds a lookup map from target collection to all incoming relations.
|
|
341
|
-
*
|
|
342
|
-
* This pre-computes which collections reference a given collection via foreign keys,
|
|
343
|
-
* so that delete operations can efficiently find all relations that need enforcement.
|
|
344
|
-
*
|
|
345
|
-
* @param schema - The full schema definition with relations
|
|
346
|
-
* @returns Map from target collection name to array of incoming relations, sorted by relation name for determinism
|
|
347
|
-
*
|
|
348
|
-
* @example
|
|
349
|
-
* ```typescript
|
|
350
|
-
* const lookup = buildMergeRelationLookup(schema)
|
|
351
|
-
* // lookup.get('projects') => [{ relationName: 'todoBelongsToProject', sourceCollection: 'todos', ... }]
|
|
352
|
-
* ```
|
|
353
|
-
*/
|
|
354
|
-
declare function buildMergeRelationLookup(schema: SchemaDefinition): Map<string, MergeIncomingRelation[]>;
|
|
355
|
-
/**
|
|
356
|
-
* Checks referential integrity when a delete operation is being applied.
|
|
357
|
-
*
|
|
358
|
-
* For each relation that targets the deleted record's collection, queries
|
|
359
|
-
* for referencing records and applies the relation's onDelete policy:
|
|
360
|
-
* - `restrict`: Blocks the delete if any references exist
|
|
361
|
-
* - `cascade`: Allows the delete and generates cascaded delete SideEffectOps
|
|
362
|
-
* - `set-null`: Allows the delete and generates update SideEffectOps to null out FKs
|
|
363
|
-
* - `no-action`: Allows the delete with no side effects (dangling references permitted)
|
|
364
|
-
*
|
|
365
|
-
* Processing is deterministic: relations are processed in sorted order by name,
|
|
366
|
-
* and referencing records within each relation are processed in sorted order by ID.
|
|
367
|
-
*
|
|
368
|
-
* @param deleteOp - The delete operation being evaluated
|
|
369
|
-
* @param schema - The full schema definition
|
|
370
|
-
* @param ctx - Database lookup context
|
|
371
|
-
* @param relationLookup - Pre-built relation lookup (optional; will be built if not provided)
|
|
372
|
-
* @returns Whether the delete is allowed, any side-effect ops, and traces for DevTools
|
|
373
|
-
*/
|
|
374
|
-
declare function checkReferentialIntegrityOnDelete(deleteOp: Operation, schema: SchemaDefinition, ctx: ReferentialMergeContext, relationLookup?: Map<string, MergeIncomingRelation[]>): Promise<ReferentialCheckResult>;
|
|
375
|
-
/**
|
|
376
|
-
* Resolves a concurrent delete-vs-insert conflict on a referenced record.
|
|
377
|
-
*
|
|
378
|
-
* When one node deletes a record while another concurrently inserts a record
|
|
379
|
-
* that references the deleted record, we need to decide what wins.
|
|
380
|
-
*
|
|
381
|
-
* Resolution strategy depends on the relation's onDelete policy:
|
|
382
|
-
* - `restrict`: Block the delete (the insert wins — data integrity preserved)
|
|
383
|
-
* - `cascade`: Allow the delete and cascade-delete the newly inserted record
|
|
384
|
-
* - `set-null`: Allow the delete and null out the FK on the newly inserted record
|
|
385
|
-
* - `no-action`: Allow the delete (dangling reference is acceptable)
|
|
386
|
-
*
|
|
387
|
-
* @param deleteOp - The delete operation on the referenced record
|
|
388
|
-
* @param insertOp - The concurrent insert operation that references the deleted record
|
|
389
|
-
* @param relation - The relation definition connecting the two collections
|
|
390
|
-
* @returns The resolution action, side effects, and trace
|
|
391
|
-
*/
|
|
392
|
-
declare function resolveDeleteVsInsertConflict(deleteOp: Operation, insertOp: Operation, relation: MergeIncomingRelation): DeleteVsInsertResolution;
|
|
393
|
-
|
|
394
406
|
/**
|
|
395
407
|
* Three-tier merge engine for resolving concurrent operations.
|
|
396
408
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,126 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { OnDeleteAction, MergeTrace, SchemaDefinition, Operation, Constraint, CollectionDefinition, HLCTimestamp, AtomicOp, FieldMergeStrategy, FieldDescriptor, CustomResolver } from '@korajs/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Describes an incoming relation to a target collection.
|
|
5
|
+
* Used by the delete-side referential integrity checker to know which
|
|
6
|
+
* source collections reference the collection being deleted from.
|
|
7
|
+
*/
|
|
8
|
+
interface MergeIncomingRelation {
|
|
9
|
+
/** Name of the relation in the schema (e.g., 'todoBelongsToProject') */
|
|
10
|
+
relationName: string;
|
|
11
|
+
/** The collection that holds the foreign key (source of the relation) */
|
|
12
|
+
sourceCollection: string;
|
|
13
|
+
/** The foreign key field in the source collection */
|
|
14
|
+
foreignKeyField: string;
|
|
15
|
+
/** What to do when the referenced record is deleted */
|
|
16
|
+
onDelete: OnDeleteAction;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Pluggable context for querying the database during referential integrity checks.
|
|
20
|
+
* The store layer provides the implementation; the merge package only depends on this interface.
|
|
21
|
+
*/
|
|
22
|
+
interface ReferentialMergeContext {
|
|
23
|
+
/** Query records in a collection matching a where clause */
|
|
24
|
+
queryRecords(collection: string, where: Record<string, unknown>): Promise<Record<string, unknown>[]>;
|
|
25
|
+
/** Check if a record exists in a collection */
|
|
26
|
+
recordExists(collection: string, recordId: string): Promise<boolean>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Result of a referential integrity check on a delete operation.
|
|
30
|
+
*/
|
|
31
|
+
interface ReferentialCheckResult {
|
|
32
|
+
/** Whether the delete operation is allowed to proceed */
|
|
33
|
+
allowed: boolean;
|
|
34
|
+
/** Side-effect operations generated by cascade/set-null policies */
|
|
35
|
+
sideEffectOps: SideEffectOp[];
|
|
36
|
+
/** MergeTraces for every decision made (feeds DevTools) */
|
|
37
|
+
traces: MergeTrace[];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A side-effect operation generated during referential integrity enforcement.
|
|
41
|
+
* These are not full Operations; they describe mutations the caller must apply.
|
|
42
|
+
*/
|
|
43
|
+
interface SideEffectOp {
|
|
44
|
+
/** The type of mutation to apply */
|
|
45
|
+
type: 'delete' | 'update';
|
|
46
|
+
/** The collection containing the affected record */
|
|
47
|
+
collection: string;
|
|
48
|
+
/** The ID of the affected record */
|
|
49
|
+
recordId: string;
|
|
50
|
+
/** For updates: the new field values. null for deletes. */
|
|
51
|
+
data: Record<string, unknown> | null;
|
|
52
|
+
/** For updates: the previous field values. null for deletes. */
|
|
53
|
+
previousData: Record<string, unknown> | null;
|
|
54
|
+
/** The onDelete policy that produced this side effect */
|
|
55
|
+
policy: OnDeleteAction;
|
|
56
|
+
/** The relation that triggered this side effect */
|
|
57
|
+
relationName: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Result of resolving a concurrent delete-vs-insert conflict.
|
|
61
|
+
*/
|
|
62
|
+
interface DeleteVsInsertResolution {
|
|
63
|
+
/** Whether to block the delete or allow it */
|
|
64
|
+
action: 'block-delete' | 'allow-delete';
|
|
65
|
+
/** Any side-effect operations (for cascade/set-null after allowing delete) */
|
|
66
|
+
sideEffects: SideEffectOp[];
|
|
67
|
+
/** Trace of the resolution decision, or null if no trace needed */
|
|
68
|
+
trace: MergeTrace | null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Builds a lookup map from target collection to all incoming relations.
|
|
72
|
+
*
|
|
73
|
+
* This pre-computes which collections reference a given collection via foreign keys,
|
|
74
|
+
* so that delete operations can efficiently find all relations that need enforcement.
|
|
75
|
+
*
|
|
76
|
+
* @param schema - The full schema definition with relations
|
|
77
|
+
* @returns Map from target collection name to array of incoming relations, sorted by relation name for determinism
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```typescript
|
|
81
|
+
* const lookup = buildMergeRelationLookup(schema)
|
|
82
|
+
* // lookup.get('projects') => [{ relationName: 'todoBelongsToProject', sourceCollection: 'todos', ... }]
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
declare function buildMergeRelationLookup(schema: SchemaDefinition): Map<string, MergeIncomingRelation[]>;
|
|
86
|
+
/**
|
|
87
|
+
* Checks referential integrity when a delete operation is being applied.
|
|
88
|
+
*
|
|
89
|
+
* For each relation that targets the deleted record's collection, queries
|
|
90
|
+
* for referencing records and applies the relation's onDelete policy:
|
|
91
|
+
* - `restrict`: Blocks the delete if any references exist
|
|
92
|
+
* - `cascade`: Allows the delete and generates cascaded delete SideEffectOps
|
|
93
|
+
* - `set-null`: Allows the delete and generates update SideEffectOps to null out FKs
|
|
94
|
+
* - `no-action`: Allows the delete with no side effects (dangling references permitted)
|
|
95
|
+
*
|
|
96
|
+
* Processing is deterministic: relations are processed in sorted order by name,
|
|
97
|
+
* and referencing records within each relation are processed in sorted order by ID.
|
|
98
|
+
*
|
|
99
|
+
* @param deleteOp - The delete operation being evaluated
|
|
100
|
+
* @param schema - The full schema definition
|
|
101
|
+
* @param ctx - Database lookup context
|
|
102
|
+
* @param relationLookup - Pre-built relation lookup (optional; will be built if not provided)
|
|
103
|
+
* @returns Whether the delete is allowed, any side-effect ops, and traces for DevTools
|
|
104
|
+
*/
|
|
105
|
+
declare function checkReferentialIntegrityOnDelete(deleteOp: Operation, schema: SchemaDefinition, ctx: ReferentialMergeContext, relationLookup?: Map<string, MergeIncomingRelation[]>): Promise<ReferentialCheckResult>;
|
|
106
|
+
/**
|
|
107
|
+
* Resolves a concurrent delete-vs-insert conflict on a referenced record.
|
|
108
|
+
*
|
|
109
|
+
* When one node deletes a record while another concurrently inserts a record
|
|
110
|
+
* that references the deleted record, we need to decide what wins.
|
|
111
|
+
*
|
|
112
|
+
* Resolution strategy depends on the relation's onDelete policy:
|
|
113
|
+
* - `restrict`: Block the delete (the insert wins — data integrity preserved)
|
|
114
|
+
* - `cascade`: Allow the delete and cascade-delete the newly inserted record
|
|
115
|
+
* - `set-null`: Allow the delete and null out the FK on the newly inserted record
|
|
116
|
+
* - `no-action`: Allow the delete (dangling reference is acceptable)
|
|
117
|
+
*
|
|
118
|
+
* @param deleteOp - The delete operation on the referenced record
|
|
119
|
+
* @param insertOp - The concurrent insert operation that references the deleted record
|
|
120
|
+
* @param relation - The relation definition connecting the two collections
|
|
121
|
+
* @returns The resolution action, side effects, and trace
|
|
122
|
+
*/
|
|
123
|
+
declare function resolveDeleteVsInsertConflict(deleteOp: Operation, insertOp: Operation, relation: MergeIncomingRelation): DeleteVsInsertResolution;
|
|
2
124
|
|
|
3
125
|
/**
|
|
4
126
|
* Input to the merge engine when two concurrent operations conflict.
|
|
@@ -23,6 +145,12 @@ interface MergeResult {
|
|
|
23
145
|
traces: MergeTrace[];
|
|
24
146
|
/** Which operation's values dominate overall, or 'merged' if mixed */
|
|
25
147
|
appliedOperation: 'local' | 'remote' | 'merged';
|
|
148
|
+
/**
|
|
149
|
+
* Side-effect operations generated during constraint resolution
|
|
150
|
+
* (e.g., cascade deletes, set-null updates from referential integrity).
|
|
151
|
+
* These must be applied by the caller after the merge.
|
|
152
|
+
*/
|
|
153
|
+
sideEffects: SideEffectOp[];
|
|
26
154
|
}
|
|
27
155
|
/**
|
|
28
156
|
* Output of a single field-level merge decision.
|
|
@@ -249,6 +377,12 @@ interface ConstraintResolution {
|
|
|
249
377
|
resolvedRecord: Record<string, unknown>;
|
|
250
378
|
/** Trace of the resolution decision for DevTools */
|
|
251
379
|
trace: MergeTrace;
|
|
380
|
+
/**
|
|
381
|
+
* Side-effect operations generated during resolution
|
|
382
|
+
* (e.g., referring record updates for referential integrity).
|
|
383
|
+
* Empty unless the constraint resolver generates cascading actions.
|
|
384
|
+
*/
|
|
385
|
+
sideEffects?: SideEffectOp[];
|
|
252
386
|
}
|
|
253
387
|
/**
|
|
254
388
|
* Resolves a constraint violation by applying the constraint's onConflict strategy.
|
|
@@ -269,128 +403,6 @@ interface ConstraintResolution {
|
|
|
269
403
|
*/
|
|
270
404
|
declare function resolveConstraintViolation(violation: ConstraintViolation, mergedRecord: Record<string, unknown>, localOp: Operation, remoteOp: Operation, baseState: Record<string, unknown>): ConstraintResolution;
|
|
271
405
|
|
|
272
|
-
/**
|
|
273
|
-
* Describes an incoming relation to a target collection.
|
|
274
|
-
* Used by the delete-side referential integrity checker to know which
|
|
275
|
-
* source collections reference the collection being deleted from.
|
|
276
|
-
*/
|
|
277
|
-
interface MergeIncomingRelation {
|
|
278
|
-
/** Name of the relation in the schema (e.g., 'todoBelongsToProject') */
|
|
279
|
-
relationName: string;
|
|
280
|
-
/** The collection that holds the foreign key (source of the relation) */
|
|
281
|
-
sourceCollection: string;
|
|
282
|
-
/** The foreign key field in the source collection */
|
|
283
|
-
foreignKeyField: string;
|
|
284
|
-
/** What to do when the referenced record is deleted */
|
|
285
|
-
onDelete: OnDeleteAction;
|
|
286
|
-
}
|
|
287
|
-
/**
|
|
288
|
-
* Pluggable context for querying the database during referential integrity checks.
|
|
289
|
-
* The store layer provides the implementation; the merge package only depends on this interface.
|
|
290
|
-
*/
|
|
291
|
-
interface ReferentialMergeContext {
|
|
292
|
-
/** Query records in a collection matching a where clause */
|
|
293
|
-
queryRecords(collection: string, where: Record<string, unknown>): Promise<Record<string, unknown>[]>;
|
|
294
|
-
/** Check if a record exists in a collection */
|
|
295
|
-
recordExists(collection: string, recordId: string): Promise<boolean>;
|
|
296
|
-
}
|
|
297
|
-
/**
|
|
298
|
-
* Result of a referential integrity check on a delete operation.
|
|
299
|
-
*/
|
|
300
|
-
interface ReferentialCheckResult {
|
|
301
|
-
/** Whether the delete operation is allowed to proceed */
|
|
302
|
-
allowed: boolean;
|
|
303
|
-
/** Side-effect operations generated by cascade/set-null policies */
|
|
304
|
-
sideEffectOps: SideEffectOp[];
|
|
305
|
-
/** MergeTraces for every decision made (feeds DevTools) */
|
|
306
|
-
traces: MergeTrace[];
|
|
307
|
-
}
|
|
308
|
-
/**
|
|
309
|
-
* A side-effect operation generated during referential integrity enforcement.
|
|
310
|
-
* These are not full Operations; they describe mutations the caller must apply.
|
|
311
|
-
*/
|
|
312
|
-
interface SideEffectOp {
|
|
313
|
-
/** The type of mutation to apply */
|
|
314
|
-
type: 'delete' | 'update';
|
|
315
|
-
/** The collection containing the affected record */
|
|
316
|
-
collection: string;
|
|
317
|
-
/** The ID of the affected record */
|
|
318
|
-
recordId: string;
|
|
319
|
-
/** For updates: the new field values. null for deletes. */
|
|
320
|
-
data: Record<string, unknown> | null;
|
|
321
|
-
/** For updates: the previous field values. null for deletes. */
|
|
322
|
-
previousData: Record<string, unknown> | null;
|
|
323
|
-
/** The onDelete policy that produced this side effect */
|
|
324
|
-
policy: OnDeleteAction;
|
|
325
|
-
/** The relation that triggered this side effect */
|
|
326
|
-
relationName: string;
|
|
327
|
-
}
|
|
328
|
-
/**
|
|
329
|
-
* Result of resolving a concurrent delete-vs-insert conflict.
|
|
330
|
-
*/
|
|
331
|
-
interface DeleteVsInsertResolution {
|
|
332
|
-
/** Whether to block the delete or allow it */
|
|
333
|
-
action: 'block-delete' | 'allow-delete';
|
|
334
|
-
/** Any side-effect operations (for cascade/set-null after allowing delete) */
|
|
335
|
-
sideEffects: SideEffectOp[];
|
|
336
|
-
/** Trace of the resolution decision, or null if no trace needed */
|
|
337
|
-
trace: MergeTrace | null;
|
|
338
|
-
}
|
|
339
|
-
/**
|
|
340
|
-
* Builds a lookup map from target collection to all incoming relations.
|
|
341
|
-
*
|
|
342
|
-
* This pre-computes which collections reference a given collection via foreign keys,
|
|
343
|
-
* so that delete operations can efficiently find all relations that need enforcement.
|
|
344
|
-
*
|
|
345
|
-
* @param schema - The full schema definition with relations
|
|
346
|
-
* @returns Map from target collection name to array of incoming relations, sorted by relation name for determinism
|
|
347
|
-
*
|
|
348
|
-
* @example
|
|
349
|
-
* ```typescript
|
|
350
|
-
* const lookup = buildMergeRelationLookup(schema)
|
|
351
|
-
* // lookup.get('projects') => [{ relationName: 'todoBelongsToProject', sourceCollection: 'todos', ... }]
|
|
352
|
-
* ```
|
|
353
|
-
*/
|
|
354
|
-
declare function buildMergeRelationLookup(schema: SchemaDefinition): Map<string, MergeIncomingRelation[]>;
|
|
355
|
-
/**
|
|
356
|
-
* Checks referential integrity when a delete operation is being applied.
|
|
357
|
-
*
|
|
358
|
-
* For each relation that targets the deleted record's collection, queries
|
|
359
|
-
* for referencing records and applies the relation's onDelete policy:
|
|
360
|
-
* - `restrict`: Blocks the delete if any references exist
|
|
361
|
-
* - `cascade`: Allows the delete and generates cascaded delete SideEffectOps
|
|
362
|
-
* - `set-null`: Allows the delete and generates update SideEffectOps to null out FKs
|
|
363
|
-
* - `no-action`: Allows the delete with no side effects (dangling references permitted)
|
|
364
|
-
*
|
|
365
|
-
* Processing is deterministic: relations are processed in sorted order by name,
|
|
366
|
-
* and referencing records within each relation are processed in sorted order by ID.
|
|
367
|
-
*
|
|
368
|
-
* @param deleteOp - The delete operation being evaluated
|
|
369
|
-
* @param schema - The full schema definition
|
|
370
|
-
* @param ctx - Database lookup context
|
|
371
|
-
* @param relationLookup - Pre-built relation lookup (optional; will be built if not provided)
|
|
372
|
-
* @returns Whether the delete is allowed, any side-effect ops, and traces for DevTools
|
|
373
|
-
*/
|
|
374
|
-
declare function checkReferentialIntegrityOnDelete(deleteOp: Operation, schema: SchemaDefinition, ctx: ReferentialMergeContext, relationLookup?: Map<string, MergeIncomingRelation[]>): Promise<ReferentialCheckResult>;
|
|
375
|
-
/**
|
|
376
|
-
* Resolves a concurrent delete-vs-insert conflict on a referenced record.
|
|
377
|
-
*
|
|
378
|
-
* When one node deletes a record while another concurrently inserts a record
|
|
379
|
-
* that references the deleted record, we need to decide what wins.
|
|
380
|
-
*
|
|
381
|
-
* Resolution strategy depends on the relation's onDelete policy:
|
|
382
|
-
* - `restrict`: Block the delete (the insert wins — data integrity preserved)
|
|
383
|
-
* - `cascade`: Allow the delete and cascade-delete the newly inserted record
|
|
384
|
-
* - `set-null`: Allow the delete and null out the FK on the newly inserted record
|
|
385
|
-
* - `no-action`: Allow the delete (dangling reference is acceptable)
|
|
386
|
-
*
|
|
387
|
-
* @param deleteOp - The delete operation on the referenced record
|
|
388
|
-
* @param insertOp - The concurrent insert operation that references the deleted record
|
|
389
|
-
* @param relation - The relation definition connecting the two collections
|
|
390
|
-
* @returns The resolution action, side effects, and trace
|
|
391
|
-
*/
|
|
392
|
-
declare function resolveDeleteVsInsertConflict(deleteOp: Operation, insertOp: Operation, relation: MergeIncomingRelation): DeleteVsInsertResolution;
|
|
393
|
-
|
|
394
406
|
/**
|
|
395
407
|
* Three-tier merge engine for resolving concurrent operations.
|
|
396
408
|
*
|
package/dist/index.js
CHANGED
|
@@ -1172,7 +1172,8 @@ var MergeEngine = class {
|
|
|
1172
1172
|
return {
|
|
1173
1173
|
mergedData: {},
|
|
1174
1174
|
traces: [],
|
|
1175
|
-
appliedOperation: "merged"
|
|
1175
|
+
appliedOperation: "merged",
|
|
1176
|
+
sideEffects: []
|
|
1176
1177
|
};
|
|
1177
1178
|
}
|
|
1178
1179
|
if (input.local.type === "delete" || input.remote.type === "delete") {
|
|
@@ -1190,6 +1191,7 @@ var MergeEngine = class {
|
|
|
1190
1191
|
);
|
|
1191
1192
|
let mergedData = fieldResult.mergedData;
|
|
1192
1193
|
const allTraces = [...fieldResult.traces];
|
|
1194
|
+
const allSideEffects = [...fieldResult.sideEffects];
|
|
1193
1195
|
for (const violation of violations) {
|
|
1194
1196
|
const resolution = resolveConstraintViolation(
|
|
1195
1197
|
violation,
|
|
@@ -1200,11 +1202,15 @@ var MergeEngine = class {
|
|
|
1200
1202
|
);
|
|
1201
1203
|
mergedData = resolution.resolvedRecord;
|
|
1202
1204
|
allTraces.push(resolution.trace);
|
|
1205
|
+
if (resolution.sideEffects) {
|
|
1206
|
+
allSideEffects.push(...resolution.sideEffects);
|
|
1207
|
+
}
|
|
1203
1208
|
}
|
|
1204
1209
|
return {
|
|
1205
1210
|
mergedData,
|
|
1206
1211
|
traces: allTraces,
|
|
1207
|
-
appliedOperation: determineAppliedOperation(allTraces)
|
|
1212
|
+
appliedOperation: determineAppliedOperation(allTraces),
|
|
1213
|
+
sideEffects: allSideEffects
|
|
1208
1214
|
};
|
|
1209
1215
|
}
|
|
1210
1216
|
return fieldResult;
|
|
@@ -1252,7 +1258,8 @@ var MergeEngine = class {
|
|
|
1252
1258
|
return {
|
|
1253
1259
|
mergedData,
|
|
1254
1260
|
traces,
|
|
1255
|
-
appliedOperation: determineAppliedOperation(traces)
|
|
1261
|
+
appliedOperation: determineAppliedOperation(traces),
|
|
1262
|
+
sideEffects: []
|
|
1256
1263
|
};
|
|
1257
1264
|
}
|
|
1258
1265
|
/**
|
|
@@ -1264,21 +1271,23 @@ var MergeEngine = class {
|
|
|
1264
1271
|
const comparison = HybridLogicalClock6.compare(local.timestamp, remote.timestamp);
|
|
1265
1272
|
if (comparison >= 0) {
|
|
1266
1273
|
if (local.type === "delete") {
|
|
1267
|
-
return { mergedData: {}, traces: [], appliedOperation: "local" };
|
|
1274
|
+
return { mergedData: {}, traces: [], appliedOperation: "local", sideEffects: [] };
|
|
1268
1275
|
}
|
|
1269
1276
|
return {
|
|
1270
1277
|
mergedData: { ...input.baseState, ...local.data ?? {} },
|
|
1271
1278
|
traces: [],
|
|
1272
|
-
appliedOperation: "local"
|
|
1279
|
+
appliedOperation: "local",
|
|
1280
|
+
sideEffects: []
|
|
1273
1281
|
};
|
|
1274
1282
|
}
|
|
1275
1283
|
if (remote.type === "delete") {
|
|
1276
|
-
return { mergedData: {}, traces: [], appliedOperation: "remote" };
|
|
1284
|
+
return { mergedData: {}, traces: [], appliedOperation: "remote", sideEffects: [] };
|
|
1277
1285
|
}
|
|
1278
1286
|
return {
|
|
1279
1287
|
mergedData: { ...input.baseState, ...remote.data ?? {} },
|
|
1280
1288
|
traces: [],
|
|
1281
|
-
appliedOperation: "remote"
|
|
1289
|
+
appliedOperation: "remote",
|
|
1290
|
+
sideEffects: []
|
|
1282
1291
|
};
|
|
1283
1292
|
}
|
|
1284
1293
|
};
|