@korajs/server 1.0.0-beta.2 → 1.0.0-beta.4
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 +173 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +88 -1
- package/dist/index.d.ts +88 -1
- package/dist/index.js +180 -33
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.d.cts
CHANGED
|
@@ -5,6 +5,32 @@ import { S as ServerTransport, a as ServerMessageHandler, b as ServerCloseHandle
|
|
|
5
5
|
import { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
|
|
6
6
|
import { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3';
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Predicate evaluation for conditional route mutations.
|
|
10
|
+
*
|
|
11
|
+
* A predicate is a small, total comparison language evaluated against the current
|
|
12
|
+
* materialized state of a single record at admission time. It is deliberately not
|
|
13
|
+
* a general query language: it exists so a server route can express an atomic
|
|
14
|
+
* admission guard (for example "accept this response only while the form is still
|
|
15
|
+
* below its cap") without pulling a query DSL into the write path.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Comparison operators applied to one field. All operators present on a field
|
|
19
|
+
* must hold for the field to match. Numeric operators (`$lt`, `$lte`, `$gt`,
|
|
20
|
+
* `$gte`) require the record value to be a number; a non-numeric value fails.
|
|
21
|
+
*/
|
|
22
|
+
interface RoutePredicateOperators {
|
|
23
|
+
$eq?: unknown;
|
|
24
|
+
$ne?: unknown;
|
|
25
|
+
$lt?: number;
|
|
26
|
+
$lte?: number;
|
|
27
|
+
$gt?: number;
|
|
28
|
+
$gte?: number;
|
|
29
|
+
$in?: readonly unknown[];
|
|
30
|
+
}
|
|
31
|
+
/** A predicate: every listed field must satisfy all of its operators. */
|
|
32
|
+
type RoutePredicate = Record<string, RoutePredicateOperators>;
|
|
33
|
+
|
|
8
34
|
/**
|
|
9
35
|
* Per-collection scope map from auth context.
|
|
10
36
|
*/
|
|
@@ -463,6 +489,61 @@ type RouteApplyResult = {
|
|
|
463
489
|
message: string;
|
|
464
490
|
retriable: boolean;
|
|
465
491
|
};
|
|
492
|
+
/**
|
|
493
|
+
* A conditional, multi-collection admission gate applied atomically per instance.
|
|
494
|
+
*
|
|
495
|
+
* The gate reads the current state of the target record (`collection` + `id`),
|
|
496
|
+
* checks the `if` predicate against it, and only then applies the `update` to the
|
|
497
|
+
* target plus every mutation in `also` as one set. If the predicate fails it
|
|
498
|
+
* applies nothing and returns the structured `reject`. `idempotencyKey` names a
|
|
499
|
+
* record whose prior existence proves the set already committed, so a retry
|
|
500
|
+
* returns the earlier outcome instead of applying the set (and its non-idempotent
|
|
501
|
+
* counter increments) again.
|
|
502
|
+
*
|
|
503
|
+
* Atomicity note: within one server instance the check and the writes do not
|
|
504
|
+
* interleave with other route mutations. Cross-instance, race-free admission and
|
|
505
|
+
* all-or-nothing across a mid-set crash require the store-level conditional
|
|
506
|
+
* transaction (a Postgres `WHERE ... < cap` commit), which is the next increment.
|
|
507
|
+
*/
|
|
508
|
+
interface RouteConditionalApply {
|
|
509
|
+
/** Collection of the record the predicate reads and the `update` targets. */
|
|
510
|
+
collection: string;
|
|
511
|
+
/** Id of the target record. */
|
|
512
|
+
id: string;
|
|
513
|
+
/** Admission predicate evaluated against the target's current state. */
|
|
514
|
+
if?: RoutePredicate;
|
|
515
|
+
/** Partial update applied to the target record when the predicate holds. */
|
|
516
|
+
update?: Record<string, unknown> | null;
|
|
517
|
+
/** Additional mutations committed atomically with the target update. */
|
|
518
|
+
also?: RouteMutation[];
|
|
519
|
+
/** Structured rejection returned when the predicate does not hold. */
|
|
520
|
+
reject?: {
|
|
521
|
+
code: string;
|
|
522
|
+
message: string;
|
|
523
|
+
};
|
|
524
|
+
/**
|
|
525
|
+
* Names a record whose existence means this set already committed. When it is
|
|
526
|
+
* present the set is skipped and the prior outcome returned. Use a stable,
|
|
527
|
+
* client-generated id (for example the accepted record's own id).
|
|
528
|
+
*/
|
|
529
|
+
idempotencyKey?: {
|
|
530
|
+
collection: string;
|
|
531
|
+
id: string;
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
/** Result of {@link ProductionHttpRouteContext.applyConditional}. */
|
|
535
|
+
type RouteConditionalResult = {
|
|
536
|
+
ok: true;
|
|
537
|
+
/** True when the set was already committed and this call was a no-op. */
|
|
538
|
+
idempotent: boolean;
|
|
539
|
+
operations: Operation[];
|
|
540
|
+
records: (MaterializedRecord | null)[];
|
|
541
|
+
} | {
|
|
542
|
+
ok: false;
|
|
543
|
+
code: string;
|
|
544
|
+
message: string;
|
|
545
|
+
retriable: boolean;
|
|
546
|
+
};
|
|
466
547
|
/**
|
|
467
548
|
* Scoped, validated data-plane access handed to custom HTTP route handlers.
|
|
468
549
|
*
|
|
@@ -477,6 +558,12 @@ interface ProductionHttpRouteContext {
|
|
|
477
558
|
* resulting operation(s) to connected clients.
|
|
478
559
|
*/
|
|
479
560
|
apply(mutation: RouteMutation, options?: RouteScopeOptions): Promise<RouteApplyResult>;
|
|
561
|
+
/**
|
|
562
|
+
* Conditionally apply a multi-collection mutation set as one admission gate:
|
|
563
|
+
* read the target, check the predicate, and commit the update plus every `also`
|
|
564
|
+
* mutation only if it holds, with at-most-once semantics via `idempotencyKey`.
|
|
565
|
+
*/
|
|
566
|
+
applyConditional(spec: RouteConditionalApply, options?: RouteScopeOptions): Promise<RouteConditionalResult>;
|
|
480
567
|
/** Read materialized records from a collection, optionally scoped. */
|
|
481
568
|
query(collection: string, options?: CollectionQueryOptions & RouteScopeOptions): Promise<MaterializedRecord[]>;
|
|
482
569
|
/** Read a single materialized record by id, optionally scoped. */
|
|
@@ -1562,4 +1649,4 @@ declare class AwarenessRelay {
|
|
|
1562
1649
|
*/
|
|
1563
1650
|
declare function createKoraServer(config: KoraSyncServerConfig): KoraSyncServer;
|
|
1564
1651
|
|
|
1565
|
-
export { type AuthContext, type AuthProvider, AwarenessRelay, type AwarenessRelayCallback, type ClientMetrics, ClientSession, type ClientSessionOptions, type CollectionQueryOptions, type HttpPollResponse, HttpServerTransport, type HttpSyncRequest, type HttpSyncResponse, KoraAuthProvider, type KoraAuthProviderOptions, KoraSyncServer, type KoraSyncServerConfig, type LogEntry, type LogLevel, type Logger, type MaterializedRecord, MemoryServerStore, MixedAuthProvider, type MixedAuthProviderOptions, NoAuthProvider, type OperationDecision, type OperationRejection, type OperationValidationContext, type OperationValidator, PostgresServerStore, type ProductionHttpRoute, type ProductionHttpRouteContext, type ProductionHttpRouteRequest, type ProductionHttpRouteResponse, type ProductionServer, type ProductionServerConfig, RETRIABLE_REJECTION_CODES, type RelayCallback, type RouteApplyResult, type RouteMutation, type RouteScopeOptions, ServerCloseHandler, ServerErrorHandler, ServerMessageHandler, ServerMetricsCollector, type ServerMetricsSnapshot, type ServerStatus, type ServerStore, ServerTransport, type SessionState, SqliteServerStore, TokenAuthProvider, type TokenAuthProviderOptions, type WsServerConstructor, type WsServerLike, WsServerTransport, type WsServerTransportOptions, type WsWebSocket, createDefaultLogger, createJsonLogger, createKoraServer, createPostgresServerStore, createPrettyLogger, createProductionServer, createSilentLogger, createSqliteServerStore, isRetriableRejection };
|
|
1652
|
+
export { type AuthContext, type AuthProvider, AwarenessRelay, type AwarenessRelayCallback, type ClientMetrics, ClientSession, type ClientSessionOptions, type CollectionQueryOptions, type HttpPollResponse, HttpServerTransport, type HttpSyncRequest, type HttpSyncResponse, KoraAuthProvider, type KoraAuthProviderOptions, KoraSyncServer, type KoraSyncServerConfig, type LogEntry, type LogLevel, type Logger, type MaterializedRecord, MemoryServerStore, MixedAuthProvider, type MixedAuthProviderOptions, NoAuthProvider, type OperationDecision, type OperationRejection, type OperationValidationContext, type OperationValidator, PostgresServerStore, type ProductionHttpRoute, type ProductionHttpRouteContext, type ProductionHttpRouteRequest, type ProductionHttpRouteResponse, type ProductionServer, type ProductionServerConfig, RETRIABLE_REJECTION_CODES, type RelayCallback, type RouteApplyResult, type RouteConditionalApply, type RouteConditionalResult, type RouteMutation, type RoutePredicate, type RoutePredicateOperators, type RouteScopeOptions, ServerCloseHandler, ServerErrorHandler, ServerMessageHandler, ServerMetricsCollector, type ServerMetricsSnapshot, type ServerStatus, type ServerStore, ServerTransport, type SessionState, SqliteServerStore, TokenAuthProvider, type TokenAuthProviderOptions, type WsServerConstructor, type WsServerLike, WsServerTransport, type WsServerTransportOptions, type WsWebSocket, createDefaultLogger, createJsonLogger, createKoraServer, createPostgresServerStore, createPrettyLogger, createProductionServer, createSilentLogger, createSqliteServerStore, isRetriableRejection };
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,32 @@ import { S as ServerTransport, a as ServerMessageHandler, b as ServerCloseHandle
|
|
|
5
5
|
import { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
|
|
6
6
|
import { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3';
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Predicate evaluation for conditional route mutations.
|
|
10
|
+
*
|
|
11
|
+
* A predicate is a small, total comparison language evaluated against the current
|
|
12
|
+
* materialized state of a single record at admission time. It is deliberately not
|
|
13
|
+
* a general query language: it exists so a server route can express an atomic
|
|
14
|
+
* admission guard (for example "accept this response only while the form is still
|
|
15
|
+
* below its cap") without pulling a query DSL into the write path.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Comparison operators applied to one field. All operators present on a field
|
|
19
|
+
* must hold for the field to match. Numeric operators (`$lt`, `$lte`, `$gt`,
|
|
20
|
+
* `$gte`) require the record value to be a number; a non-numeric value fails.
|
|
21
|
+
*/
|
|
22
|
+
interface RoutePredicateOperators {
|
|
23
|
+
$eq?: unknown;
|
|
24
|
+
$ne?: unknown;
|
|
25
|
+
$lt?: number;
|
|
26
|
+
$lte?: number;
|
|
27
|
+
$gt?: number;
|
|
28
|
+
$gte?: number;
|
|
29
|
+
$in?: readonly unknown[];
|
|
30
|
+
}
|
|
31
|
+
/** A predicate: every listed field must satisfy all of its operators. */
|
|
32
|
+
type RoutePredicate = Record<string, RoutePredicateOperators>;
|
|
33
|
+
|
|
8
34
|
/**
|
|
9
35
|
* Per-collection scope map from auth context.
|
|
10
36
|
*/
|
|
@@ -463,6 +489,61 @@ type RouteApplyResult = {
|
|
|
463
489
|
message: string;
|
|
464
490
|
retriable: boolean;
|
|
465
491
|
};
|
|
492
|
+
/**
|
|
493
|
+
* A conditional, multi-collection admission gate applied atomically per instance.
|
|
494
|
+
*
|
|
495
|
+
* The gate reads the current state of the target record (`collection` + `id`),
|
|
496
|
+
* checks the `if` predicate against it, and only then applies the `update` to the
|
|
497
|
+
* target plus every mutation in `also` as one set. If the predicate fails it
|
|
498
|
+
* applies nothing and returns the structured `reject`. `idempotencyKey` names a
|
|
499
|
+
* record whose prior existence proves the set already committed, so a retry
|
|
500
|
+
* returns the earlier outcome instead of applying the set (and its non-idempotent
|
|
501
|
+
* counter increments) again.
|
|
502
|
+
*
|
|
503
|
+
* Atomicity note: within one server instance the check and the writes do not
|
|
504
|
+
* interleave with other route mutations. Cross-instance, race-free admission and
|
|
505
|
+
* all-or-nothing across a mid-set crash require the store-level conditional
|
|
506
|
+
* transaction (a Postgres `WHERE ... < cap` commit), which is the next increment.
|
|
507
|
+
*/
|
|
508
|
+
interface RouteConditionalApply {
|
|
509
|
+
/** Collection of the record the predicate reads and the `update` targets. */
|
|
510
|
+
collection: string;
|
|
511
|
+
/** Id of the target record. */
|
|
512
|
+
id: string;
|
|
513
|
+
/** Admission predicate evaluated against the target's current state. */
|
|
514
|
+
if?: RoutePredicate;
|
|
515
|
+
/** Partial update applied to the target record when the predicate holds. */
|
|
516
|
+
update?: Record<string, unknown> | null;
|
|
517
|
+
/** Additional mutations committed atomically with the target update. */
|
|
518
|
+
also?: RouteMutation[];
|
|
519
|
+
/** Structured rejection returned when the predicate does not hold. */
|
|
520
|
+
reject?: {
|
|
521
|
+
code: string;
|
|
522
|
+
message: string;
|
|
523
|
+
};
|
|
524
|
+
/**
|
|
525
|
+
* Names a record whose existence means this set already committed. When it is
|
|
526
|
+
* present the set is skipped and the prior outcome returned. Use a stable,
|
|
527
|
+
* client-generated id (for example the accepted record's own id).
|
|
528
|
+
*/
|
|
529
|
+
idempotencyKey?: {
|
|
530
|
+
collection: string;
|
|
531
|
+
id: string;
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
/** Result of {@link ProductionHttpRouteContext.applyConditional}. */
|
|
535
|
+
type RouteConditionalResult = {
|
|
536
|
+
ok: true;
|
|
537
|
+
/** True when the set was already committed and this call was a no-op. */
|
|
538
|
+
idempotent: boolean;
|
|
539
|
+
operations: Operation[];
|
|
540
|
+
records: (MaterializedRecord | null)[];
|
|
541
|
+
} | {
|
|
542
|
+
ok: false;
|
|
543
|
+
code: string;
|
|
544
|
+
message: string;
|
|
545
|
+
retriable: boolean;
|
|
546
|
+
};
|
|
466
547
|
/**
|
|
467
548
|
* Scoped, validated data-plane access handed to custom HTTP route handlers.
|
|
468
549
|
*
|
|
@@ -477,6 +558,12 @@ interface ProductionHttpRouteContext {
|
|
|
477
558
|
* resulting operation(s) to connected clients.
|
|
478
559
|
*/
|
|
479
560
|
apply(mutation: RouteMutation, options?: RouteScopeOptions): Promise<RouteApplyResult>;
|
|
561
|
+
/**
|
|
562
|
+
* Conditionally apply a multi-collection mutation set as one admission gate:
|
|
563
|
+
* read the target, check the predicate, and commit the update plus every `also`
|
|
564
|
+
* mutation only if it holds, with at-most-once semantics via `idempotencyKey`.
|
|
565
|
+
*/
|
|
566
|
+
applyConditional(spec: RouteConditionalApply, options?: RouteScopeOptions): Promise<RouteConditionalResult>;
|
|
480
567
|
/** Read materialized records from a collection, optionally scoped. */
|
|
481
568
|
query(collection: string, options?: CollectionQueryOptions & RouteScopeOptions): Promise<MaterializedRecord[]>;
|
|
482
569
|
/** Read a single materialized record by id, optionally scoped. */
|
|
@@ -1562,4 +1649,4 @@ declare class AwarenessRelay {
|
|
|
1562
1649
|
*/
|
|
1563
1650
|
declare function createKoraServer(config: KoraSyncServerConfig): KoraSyncServer;
|
|
1564
1651
|
|
|
1565
|
-
export { type AuthContext, type AuthProvider, AwarenessRelay, type AwarenessRelayCallback, type ClientMetrics, ClientSession, type ClientSessionOptions, type CollectionQueryOptions, type HttpPollResponse, HttpServerTransport, type HttpSyncRequest, type HttpSyncResponse, KoraAuthProvider, type KoraAuthProviderOptions, KoraSyncServer, type KoraSyncServerConfig, type LogEntry, type LogLevel, type Logger, type MaterializedRecord, MemoryServerStore, MixedAuthProvider, type MixedAuthProviderOptions, NoAuthProvider, type OperationDecision, type OperationRejection, type OperationValidationContext, type OperationValidator, PostgresServerStore, type ProductionHttpRoute, type ProductionHttpRouteContext, type ProductionHttpRouteRequest, type ProductionHttpRouteResponse, type ProductionServer, type ProductionServerConfig, RETRIABLE_REJECTION_CODES, type RelayCallback, type RouteApplyResult, type RouteMutation, type RouteScopeOptions, ServerCloseHandler, ServerErrorHandler, ServerMessageHandler, ServerMetricsCollector, type ServerMetricsSnapshot, type ServerStatus, type ServerStore, ServerTransport, type SessionState, SqliteServerStore, TokenAuthProvider, type TokenAuthProviderOptions, type WsServerConstructor, type WsServerLike, WsServerTransport, type WsServerTransportOptions, type WsWebSocket, createDefaultLogger, createJsonLogger, createKoraServer, createPostgresServerStore, createPrettyLogger, createProductionServer, createSilentLogger, createSqliteServerStore, isRetriableRejection };
|
|
1652
|
+
export { type AuthContext, type AuthProvider, AwarenessRelay, type AwarenessRelayCallback, type ClientMetrics, ClientSession, type ClientSessionOptions, type CollectionQueryOptions, type HttpPollResponse, HttpServerTransport, type HttpSyncRequest, type HttpSyncResponse, KoraAuthProvider, type KoraAuthProviderOptions, KoraSyncServer, type KoraSyncServerConfig, type LogEntry, type LogLevel, type Logger, type MaterializedRecord, MemoryServerStore, MixedAuthProvider, type MixedAuthProviderOptions, NoAuthProvider, type OperationDecision, type OperationRejection, type OperationValidationContext, type OperationValidator, PostgresServerStore, type ProductionHttpRoute, type ProductionHttpRouteContext, type ProductionHttpRouteRequest, type ProductionHttpRouteResponse, type ProductionServer, type ProductionServerConfig, RETRIABLE_REJECTION_CODES, type RelayCallback, type RouteApplyResult, type RouteConditionalApply, type RouteConditionalResult, type RouteMutation, type RoutePredicate, type RoutePredicateOperators, type RouteScopeOptions, ServerCloseHandler, ServerErrorHandler, ServerMessageHandler, ServerMetricsCollector, type ServerMetricsSnapshot, type ServerStatus, type ServerStore, ServerTransport, type SessionState, SqliteServerStore, TokenAuthProvider, type TokenAuthProviderOptions, type WsServerConstructor, type WsServerLike, WsServerTransport, type WsServerTransportOptions, type WsWebSocket, createDefaultLogger, createJsonLogger, createKoraServer, createPostgresServerStore, createPrettyLogger, createProductionServer, createSilentLogger, createSqliteServerStore, isRetriableRejection };
|
package/dist/index.js
CHANGED
|
@@ -3068,7 +3068,64 @@ var YjsDocRelay = class {
|
|
|
3068
3068
|
};
|
|
3069
3069
|
|
|
3070
3070
|
// src/server/route-context.ts
|
|
3071
|
-
import {
|
|
3071
|
+
import {
|
|
3072
|
+
HybridLogicalClock as HybridLogicalClock2,
|
|
3073
|
+
createOperation as createOperation2,
|
|
3074
|
+
generateUUIDv7 as generateUUIDv76,
|
|
3075
|
+
isAtomicOp,
|
|
3076
|
+
resolveAtomicOp,
|
|
3077
|
+
toAtomicOp
|
|
3078
|
+
} from "@korajs/core";
|
|
3079
|
+
|
|
3080
|
+
// src/apply/route-predicate.ts
|
|
3081
|
+
function evaluateField(actual, operators) {
|
|
3082
|
+
if ("$eq" in operators && !Object.is(actual, operators.$eq)) {
|
|
3083
|
+
return false;
|
|
3084
|
+
}
|
|
3085
|
+
if ("$ne" in operators && Object.is(actual, operators.$ne)) {
|
|
3086
|
+
return false;
|
|
3087
|
+
}
|
|
3088
|
+
if (operators.$in !== void 0 && !operators.$in.some((candidate) => Object.is(actual, candidate))) {
|
|
3089
|
+
return false;
|
|
3090
|
+
}
|
|
3091
|
+
const numericChecks = [
|
|
3092
|
+
["$lt", (a, b) => a < b],
|
|
3093
|
+
["$lte", (a, b) => a <= b],
|
|
3094
|
+
["$gt", (a, b) => a > b],
|
|
3095
|
+
["$gte", (a, b) => a >= b]
|
|
3096
|
+
];
|
|
3097
|
+
for (const [operator, compare] of numericChecks) {
|
|
3098
|
+
const bound = operators[operator];
|
|
3099
|
+
if (bound === void 0) {
|
|
3100
|
+
continue;
|
|
3101
|
+
}
|
|
3102
|
+
if (typeof actual !== "number" || typeof bound !== "number") {
|
|
3103
|
+
return false;
|
|
3104
|
+
}
|
|
3105
|
+
if (!compare(actual, bound)) {
|
|
3106
|
+
return false;
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
return true;
|
|
3110
|
+
}
|
|
3111
|
+
function evaluateRoutePredicate(record, predicate) {
|
|
3112
|
+
const fields = Object.keys(predicate);
|
|
3113
|
+
if (fields.length === 0) {
|
|
3114
|
+
return true;
|
|
3115
|
+
}
|
|
3116
|
+
if (record === null) {
|
|
3117
|
+
return false;
|
|
3118
|
+
}
|
|
3119
|
+
for (const field of fields) {
|
|
3120
|
+
const operators = predicate[field];
|
|
3121
|
+
if (operators && !evaluateField(record[field], operators)) {
|
|
3122
|
+
return false;
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
return true;
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
// src/server/route-context.ts
|
|
3072
3129
|
function recordMatchesScope(collection, record, scope) {
|
|
3073
3130
|
const collectionScope = scope[collection];
|
|
3074
3131
|
if (!collectionScope) {
|
|
@@ -3119,7 +3176,18 @@ async function buildRouteOperation(store, mutation) {
|
|
|
3119
3176
|
}
|
|
3120
3177
|
const current = await store.findRecord(mutation.collection, mutation.recordId);
|
|
3121
3178
|
if (mutation.type === "update") {
|
|
3122
|
-
const
|
|
3179
|
+
const rawData = mutation.data ?? {};
|
|
3180
|
+
const data = {};
|
|
3181
|
+
const atomicOps = {};
|
|
3182
|
+
for (const key of Object.keys(rawData)) {
|
|
3183
|
+
const value = rawData[key];
|
|
3184
|
+
if (isAtomicOp(value)) {
|
|
3185
|
+
data[key] = resolveAtomicOp(current ? current[key] : void 0, value);
|
|
3186
|
+
atomicOps[key] = toAtomicOp(value);
|
|
3187
|
+
} else {
|
|
3188
|
+
data[key] = value;
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3123
3191
|
return createOperation2(
|
|
3124
3192
|
{
|
|
3125
3193
|
nodeId,
|
|
@@ -3127,10 +3195,11 @@ async function buildRouteOperation(store, mutation) {
|
|
|
3127
3195
|
collection: mutation.collection,
|
|
3128
3196
|
recordId: mutation.recordId,
|
|
3129
3197
|
data,
|
|
3130
|
-
previousData: current ? pickFields(current, Object.keys(
|
|
3198
|
+
previousData: current ? pickFields(current, Object.keys(rawData)) : {},
|
|
3131
3199
|
sequenceNumber,
|
|
3132
3200
|
causalDeps: [],
|
|
3133
|
-
schemaVersion
|
|
3201
|
+
schemaVersion,
|
|
3202
|
+
...Object.keys(atomicOps).length > 0 ? { atomicOps } : {}
|
|
3134
3203
|
},
|
|
3135
3204
|
clock
|
|
3136
3205
|
);
|
|
@@ -3168,45 +3237,123 @@ function createRouteContext(server, store) {
|
|
|
3168
3237
|
);
|
|
3169
3238
|
return run;
|
|
3170
3239
|
}
|
|
3240
|
+
async function prepareMutation(mutation, scope) {
|
|
3241
|
+
let op;
|
|
3242
|
+
try {
|
|
3243
|
+
op = await buildRouteOperation(store, mutation);
|
|
3244
|
+
} catch (error) {
|
|
3245
|
+
if (error instanceof RouteMutationError) {
|
|
3246
|
+
return {
|
|
3247
|
+
ok: false,
|
|
3248
|
+
rejection: {
|
|
3249
|
+
ok: false,
|
|
3250
|
+
code: error.code,
|
|
3251
|
+
message: error.message,
|
|
3252
|
+
retriable: isRetriableRejection(error.code)
|
|
3253
|
+
}
|
|
3254
|
+
};
|
|
3255
|
+
}
|
|
3256
|
+
throw error;
|
|
3257
|
+
}
|
|
3258
|
+
if (scope && !operationMatchesScopes(op, scope)) {
|
|
3259
|
+
return {
|
|
3260
|
+
ok: false,
|
|
3261
|
+
rejection: {
|
|
3262
|
+
ok: false,
|
|
3263
|
+
code: "SCOPE_VIOLATION",
|
|
3264
|
+
message: `Mutation on "${mutation.collection}" is outside the provided scope.`,
|
|
3265
|
+
retriable: isRetriableRejection("SCOPE_VIOLATION")
|
|
3266
|
+
}
|
|
3267
|
+
};
|
|
3268
|
+
}
|
|
3269
|
+
return { ok: true, op };
|
|
3270
|
+
}
|
|
3271
|
+
async function commitOperation(op, collection) {
|
|
3272
|
+
const result = await server.applyLocalOperation(op);
|
|
3273
|
+
if (result.result !== "applied") {
|
|
3274
|
+
const code = result.rejection?.code ?? "NOT_APPLIED";
|
|
3275
|
+
return {
|
|
3276
|
+
ok: false,
|
|
3277
|
+
code,
|
|
3278
|
+
message: result.rejection?.message ?? `Operation on "${collection}" was not applied (${result.result}).`,
|
|
3279
|
+
// Prefer the pipeline's own classification when present; fall back
|
|
3280
|
+
// to classifying the code so this stays correct if new codes appear.
|
|
3281
|
+
retriable: result.rejection?.retriable ?? isRetriableRejection(code)
|
|
3282
|
+
};
|
|
3283
|
+
}
|
|
3284
|
+
const record = await store.findRecord(collection, op.recordId);
|
|
3285
|
+
return { ok: true, operation: op, record };
|
|
3286
|
+
}
|
|
3287
|
+
async function rawApply(mutation, scope) {
|
|
3288
|
+
const prepared = await prepareMutation(mutation, scope);
|
|
3289
|
+
if (!prepared.ok) {
|
|
3290
|
+
return prepared.rejection;
|
|
3291
|
+
}
|
|
3292
|
+
return commitOperation(prepared.op, mutation.collection);
|
|
3293
|
+
}
|
|
3171
3294
|
return {
|
|
3172
3295
|
apply(mutation, options) {
|
|
3296
|
+
return serialize(() => rawApply(mutation, options?.scope));
|
|
3297
|
+
},
|
|
3298
|
+
applyConditional(spec, options) {
|
|
3173
3299
|
return serialize(async () => {
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3300
|
+
if (spec.idempotencyKey) {
|
|
3301
|
+
const existing = await store.findRecord(
|
|
3302
|
+
spec.idempotencyKey.collection,
|
|
3303
|
+
spec.idempotencyKey.id
|
|
3304
|
+
);
|
|
3305
|
+
if (existing) {
|
|
3306
|
+
return { ok: true, idempotent: true, operations: [], records: [existing] };
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
if (spec.if && Object.keys(spec.if).length > 0) {
|
|
3310
|
+
const target = await store.findRecord(spec.collection, spec.id);
|
|
3311
|
+
if (!evaluateRoutePredicate(target, spec.if)) {
|
|
3312
|
+
const code = spec.reject?.code ?? "CONDITION_NOT_MET";
|
|
3179
3313
|
return {
|
|
3180
3314
|
ok: false,
|
|
3181
|
-
code
|
|
3182
|
-
message:
|
|
3183
|
-
retriable:
|
|
3315
|
+
code,
|
|
3316
|
+
message: spec.reject?.message ?? `Admission condition not met on "${spec.collection}" record "${spec.id}".`,
|
|
3317
|
+
retriable: false
|
|
3184
3318
|
};
|
|
3185
3319
|
}
|
|
3186
|
-
throw error;
|
|
3187
3320
|
}
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3321
|
+
const mutations = [];
|
|
3322
|
+
if (spec.update !== void 0 && spec.update !== null) {
|
|
3323
|
+
mutations.push({
|
|
3324
|
+
collection: spec.collection,
|
|
3325
|
+
type: "update",
|
|
3326
|
+
recordId: spec.id,
|
|
3327
|
+
data: spec.update
|
|
3328
|
+
});
|
|
3195
3329
|
}
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3330
|
+
if (spec.also) {
|
|
3331
|
+
mutations.push(...spec.also);
|
|
3332
|
+
}
|
|
3333
|
+
const prepared = [];
|
|
3334
|
+
for (const mutation of mutations) {
|
|
3335
|
+
const p = await prepareMutation(mutation, options?.scope);
|
|
3336
|
+
if (!p.ok) {
|
|
3337
|
+
return p.rejection;
|
|
3338
|
+
}
|
|
3339
|
+
prepared.push({ op: p.op, collection: mutation.collection });
|
|
3340
|
+
}
|
|
3341
|
+
const operations2 = [];
|
|
3342
|
+
const records = [];
|
|
3343
|
+
for (const { op, collection } of prepared) {
|
|
3344
|
+
const result = await commitOperation(op, collection);
|
|
3345
|
+
if (!result.ok) {
|
|
3346
|
+
return {
|
|
3347
|
+
ok: false,
|
|
3348
|
+
code: result.code,
|
|
3349
|
+
message: result.message,
|
|
3350
|
+
retriable: result.retriable
|
|
3351
|
+
};
|
|
3352
|
+
}
|
|
3353
|
+
operations2.push(result.operation);
|
|
3354
|
+
records.push(result.record);
|
|
3207
3355
|
}
|
|
3208
|
-
|
|
3209
|
-
return { ok: true, operation: op, record };
|
|
3356
|
+
return { ok: true, idempotent: false, operations: operations2, records };
|
|
3210
3357
|
});
|
|
3211
3358
|
},
|
|
3212
3359
|
async query(collection, options) {
|