@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 CHANGED
@@ -3277,6 +3277,57 @@ var YjsDocRelay = class {
3277
3277
  // src/server/route-context.ts
3278
3278
  init_cjs_shims();
3279
3279
  var import_core12 = require("@korajs/core");
3280
+
3281
+ // src/apply/route-predicate.ts
3282
+ init_cjs_shims();
3283
+ function evaluateField(actual, operators) {
3284
+ if ("$eq" in operators && !Object.is(actual, operators.$eq)) {
3285
+ return false;
3286
+ }
3287
+ if ("$ne" in operators && Object.is(actual, operators.$ne)) {
3288
+ return false;
3289
+ }
3290
+ if (operators.$in !== void 0 && !operators.$in.some((candidate) => Object.is(actual, candidate))) {
3291
+ return false;
3292
+ }
3293
+ const numericChecks = [
3294
+ ["$lt", (a, b) => a < b],
3295
+ ["$lte", (a, b) => a <= b],
3296
+ ["$gt", (a, b) => a > b],
3297
+ ["$gte", (a, b) => a >= b]
3298
+ ];
3299
+ for (const [operator, compare] of numericChecks) {
3300
+ const bound = operators[operator];
3301
+ if (bound === void 0) {
3302
+ continue;
3303
+ }
3304
+ if (typeof actual !== "number" || typeof bound !== "number") {
3305
+ return false;
3306
+ }
3307
+ if (!compare(actual, bound)) {
3308
+ return false;
3309
+ }
3310
+ }
3311
+ return true;
3312
+ }
3313
+ function evaluateRoutePredicate(record, predicate) {
3314
+ const fields = Object.keys(predicate);
3315
+ if (fields.length === 0) {
3316
+ return true;
3317
+ }
3318
+ if (record === null) {
3319
+ return false;
3320
+ }
3321
+ for (const field of fields) {
3322
+ const operators = predicate[field];
3323
+ if (operators && !evaluateField(record[field], operators)) {
3324
+ return false;
3325
+ }
3326
+ }
3327
+ return true;
3328
+ }
3329
+
3330
+ // src/server/route-context.ts
3280
3331
  function recordMatchesScope(collection, record, scope) {
3281
3332
  const collectionScope = scope[collection];
3282
3333
  if (!collectionScope) {
@@ -3327,7 +3378,18 @@ async function buildRouteOperation(store, mutation) {
3327
3378
  }
3328
3379
  const current = await store.findRecord(mutation.collection, mutation.recordId);
3329
3380
  if (mutation.type === "update") {
3330
- const data = mutation.data ?? {};
3381
+ const rawData = mutation.data ?? {};
3382
+ const data = {};
3383
+ const atomicOps = {};
3384
+ for (const key of Object.keys(rawData)) {
3385
+ const value = rawData[key];
3386
+ if ((0, import_core12.isAtomicOp)(value)) {
3387
+ data[key] = (0, import_core12.resolveAtomicOp)(current ? current[key] : void 0, value);
3388
+ atomicOps[key] = (0, import_core12.toAtomicOp)(value);
3389
+ } else {
3390
+ data[key] = value;
3391
+ }
3392
+ }
3331
3393
  return (0, import_core12.createOperation)(
3332
3394
  {
3333
3395
  nodeId,
@@ -3335,10 +3397,11 @@ async function buildRouteOperation(store, mutation) {
3335
3397
  collection: mutation.collection,
3336
3398
  recordId: mutation.recordId,
3337
3399
  data,
3338
- previousData: current ? pickFields(current, Object.keys(data)) : {},
3400
+ previousData: current ? pickFields(current, Object.keys(rawData)) : {},
3339
3401
  sequenceNumber,
3340
3402
  causalDeps: [],
3341
- schemaVersion
3403
+ schemaVersion,
3404
+ ...Object.keys(atomicOps).length > 0 ? { atomicOps } : {}
3342
3405
  },
3343
3406
  clock
3344
3407
  );
@@ -3376,45 +3439,123 @@ function createRouteContext(server, store) {
3376
3439
  );
3377
3440
  return run;
3378
3441
  }
3442
+ async function prepareMutation(mutation, scope) {
3443
+ let op;
3444
+ try {
3445
+ op = await buildRouteOperation(store, mutation);
3446
+ } catch (error) {
3447
+ if (error instanceof RouteMutationError) {
3448
+ return {
3449
+ ok: false,
3450
+ rejection: {
3451
+ ok: false,
3452
+ code: error.code,
3453
+ message: error.message,
3454
+ retriable: isRetriableRejection(error.code)
3455
+ }
3456
+ };
3457
+ }
3458
+ throw error;
3459
+ }
3460
+ if (scope && !operationMatchesScopes(op, scope)) {
3461
+ return {
3462
+ ok: false,
3463
+ rejection: {
3464
+ ok: false,
3465
+ code: "SCOPE_VIOLATION",
3466
+ message: `Mutation on "${mutation.collection}" is outside the provided scope.`,
3467
+ retriable: isRetriableRejection("SCOPE_VIOLATION")
3468
+ }
3469
+ };
3470
+ }
3471
+ return { ok: true, op };
3472
+ }
3473
+ async function commitOperation(op, collection) {
3474
+ const result = await server.applyLocalOperation(op);
3475
+ if (result.result !== "applied") {
3476
+ const code = result.rejection?.code ?? "NOT_APPLIED";
3477
+ return {
3478
+ ok: false,
3479
+ code,
3480
+ message: result.rejection?.message ?? `Operation on "${collection}" was not applied (${result.result}).`,
3481
+ // Prefer the pipeline's own classification when present; fall back
3482
+ // to classifying the code so this stays correct if new codes appear.
3483
+ retriable: result.rejection?.retriable ?? isRetriableRejection(code)
3484
+ };
3485
+ }
3486
+ const record = await store.findRecord(collection, op.recordId);
3487
+ return { ok: true, operation: op, record };
3488
+ }
3489
+ async function rawApply(mutation, scope) {
3490
+ const prepared = await prepareMutation(mutation, scope);
3491
+ if (!prepared.ok) {
3492
+ return prepared.rejection;
3493
+ }
3494
+ return commitOperation(prepared.op, mutation.collection);
3495
+ }
3379
3496
  return {
3380
3497
  apply(mutation, options) {
3498
+ return serialize(() => rawApply(mutation, options?.scope));
3499
+ },
3500
+ applyConditional(spec, options) {
3381
3501
  return serialize(async () => {
3382
- let op;
3383
- try {
3384
- op = await buildRouteOperation(store, mutation);
3385
- } catch (error) {
3386
- if (error instanceof RouteMutationError) {
3502
+ if (spec.idempotencyKey) {
3503
+ const existing = await store.findRecord(
3504
+ spec.idempotencyKey.collection,
3505
+ spec.idempotencyKey.id
3506
+ );
3507
+ if (existing) {
3508
+ return { ok: true, idempotent: true, operations: [], records: [existing] };
3509
+ }
3510
+ }
3511
+ if (spec.if && Object.keys(spec.if).length > 0) {
3512
+ const target = await store.findRecord(spec.collection, spec.id);
3513
+ if (!evaluateRoutePredicate(target, spec.if)) {
3514
+ const code = spec.reject?.code ?? "CONDITION_NOT_MET";
3387
3515
  return {
3388
3516
  ok: false,
3389
- code: error.code,
3390
- message: error.message,
3391
- retriable: isRetriableRejection(error.code)
3517
+ code,
3518
+ message: spec.reject?.message ?? `Admission condition not met on "${spec.collection}" record "${spec.id}".`,
3519
+ retriable: false
3392
3520
  };
3393
3521
  }
3394
- throw error;
3395
3522
  }
3396
- if (options?.scope && !operationMatchesScopes(op, options.scope)) {
3397
- return {
3398
- ok: false,
3399
- code: "SCOPE_VIOLATION",
3400
- message: `Mutation on "${mutation.collection}" is outside the provided scope.`,
3401
- retriable: isRetriableRejection("SCOPE_VIOLATION")
3402
- };
3523
+ const mutations = [];
3524
+ if (spec.update !== void 0 && spec.update !== null) {
3525
+ mutations.push({
3526
+ collection: spec.collection,
3527
+ type: "update",
3528
+ recordId: spec.id,
3529
+ data: spec.update
3530
+ });
3403
3531
  }
3404
- const result = await server.applyLocalOperation(op);
3405
- if (result.result !== "applied") {
3406
- const code = result.rejection?.code ?? "NOT_APPLIED";
3407
- return {
3408
- ok: false,
3409
- code,
3410
- message: result.rejection?.message ?? `Operation on "${mutation.collection}" was not applied (${result.result}).`,
3411
- // Prefer the pipeline's own classification when present; fall back
3412
- // to classifying the code so this stays correct if new codes appear.
3413
- retriable: result.rejection?.retriable ?? isRetriableRejection(code)
3414
- };
3532
+ if (spec.also) {
3533
+ mutations.push(...spec.also);
3534
+ }
3535
+ const prepared = [];
3536
+ for (const mutation of mutations) {
3537
+ const p = await prepareMutation(mutation, options?.scope);
3538
+ if (!p.ok) {
3539
+ return p.rejection;
3540
+ }
3541
+ prepared.push({ op: p.op, collection: mutation.collection });
3542
+ }
3543
+ const operations2 = [];
3544
+ const records = [];
3545
+ for (const { op, collection } of prepared) {
3546
+ const result = await commitOperation(op, collection);
3547
+ if (!result.ok) {
3548
+ return {
3549
+ ok: false,
3550
+ code: result.code,
3551
+ message: result.message,
3552
+ retriable: result.retriable
3553
+ };
3554
+ }
3555
+ operations2.push(result.operation);
3556
+ records.push(result.record);
3415
3557
  }
3416
- const record = await store.findRecord(mutation.collection, op.recordId);
3417
- return { ok: true, operation: op, record };
3558
+ return { ok: true, idempotent: false, operations: operations2, records };
3418
3559
  });
3419
3560
  },
3420
3561
  async query(collection, options) {