@stamprally/server 0.18.0 → 0.20.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.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as _stamprally_core from '@stamprally/core';
2
- import { UserRallyState, AdminRallyConfig } from '@stamprally/core';
3
- export { UserRallyState } from '@stamprally/core';
2
+ import { UserRallyState, AdminRallyConfig, SyncProgressResponse } from '@stamprally/core';
3
+ export { SyncOperationResult, SyncProgressResponse, UserRallyState } from '@stamprally/core';
4
4
 
5
5
  /** Authentication already verified by the host application's middleware. */
6
6
  interface TrustedAuthContext {
@@ -248,6 +248,7 @@ declare function validateSyncRequest(value: unknown): RequestValidationResult<{
248
248
  readonly rallyId: string;
249
249
  readonly userId?: string;
250
250
  readonly anonymousSessionId?: string;
251
+ readonly operations?: SyncProgressRequest["operations"];
251
252
  }>;
252
253
 
253
254
  type ErrorResponse = {
@@ -275,11 +276,7 @@ declare class StampRallyServer {
275
276
  checkIn(request: CheckInRequest, authContext?: TrustedAuthContext): Promise<CheckInResponse>;
276
277
  claimReward(request: ClaimRewardRequest, authContext?: TrustedAuthContext): Promise<ClaimResponse>;
277
278
  sync(rallyId: string, identity: string | TrustedAuthContext): Promise<UserRallyState>;
278
- syncProgress(request: {
279
- readonly rallyId: string;
280
- readonly userId?: string;
281
- readonly anonymousSessionId?: string;
282
- }, authContext: TrustedAuthContext): Promise<UserRallyState>;
279
+ syncProgress(request: SyncProgressRequest, authContext: TrustedAuthContext): Promise<SyncProgressResponse>;
283
280
  }
284
281
 
285
282
  /**
@@ -319,6 +316,19 @@ interface ClaimRewardRequest {
319
316
  readonly staffId?: string;
320
317
  readonly now?: string | number;
321
318
  }
319
+ type SyncProgressOperation = {
320
+ readonly kind: "checkIn";
321
+ readonly request: CheckInRequest;
322
+ } | {
323
+ readonly kind: "claimReward";
324
+ readonly request: ClaimRewardRequest;
325
+ };
326
+ interface SyncProgressRequest {
327
+ readonly rallyId: string;
328
+ readonly userId?: string;
329
+ readonly anonymousSessionId?: string;
330
+ readonly operations?: ReadonlyArray<SyncProgressOperation>;
331
+ }
322
332
  type CheckInResponse = {
323
333
  readonly ok: true;
324
334
  readonly state: UserRallyState;
@@ -338,4 +348,4 @@ interface ServerOptions {
338
348
  readonly anonymousPolicy?: "session_scoped" | "reject" | "shared_global_opt_in_insecure";
339
349
  }
340
350
 
341
- export { type AuditLog, type AuthenticationContext, type CheckInRequest, type CheckInResponse, type CheckInTransactionMutation, type CheckInTransactionParams, type ClaimRewardMutationContext, type ClaimRewardRequest, type ClaimRewardTransactionMutation, type ClaimRewardTransactionParams, InMemoryServerPersistenceAdapter, type InMemoryServerPersistenceOptions, type RedisMultiExecutor, type RequestValidationError, RequestValidationException, type RequestValidationResult, type ServerOptions, type ServerPersistenceAdapter, type SqlCheckInStore, type SqlClaimRewardStore, type SqlTransactionDatabase, StampRallyServer, type SyncOperationStatus, type TrustedAuthContext, type UserClaimRecord, assertValidCheckInParams, assertValidClaimParams, assertValidSyncParams, executeCheckInTransaction, executeClaimRewardTransaction, executeRedisTransaction, runPersistenceAdapterComplianceTests, validateCheckInRequest, validateClaimRewardRequest, validateSyncRequest };
351
+ export { type AuditLog, type AuthenticationContext, type CheckInRequest, type CheckInResponse, type CheckInTransactionMutation, type CheckInTransactionParams, type ClaimRewardMutationContext, type ClaimRewardRequest, type ClaimRewardTransactionMutation, type ClaimRewardTransactionParams, InMemoryServerPersistenceAdapter, type InMemoryServerPersistenceOptions, type RedisMultiExecutor, type RequestValidationError, RequestValidationException, type RequestValidationResult, type ServerOptions, type ServerPersistenceAdapter, type SqlCheckInStore, type SqlClaimRewardStore, type SqlTransactionDatabase, StampRallyServer, type SyncOperationStatus, type SyncProgressOperation, type SyncProgressRequest, type TrustedAuthContext, type UserClaimRecord, assertValidCheckInParams, assertValidClaimParams, assertValidSyncParams, executeCheckInTransaction, executeClaimRewardTransaction, executeRedisTransaction, runPersistenceAdapterComplianceTests, validateCheckInRequest, validateClaimRewardRequest, validateSyncRequest };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as _stamprally_core from '@stamprally/core';
2
- import { UserRallyState, AdminRallyConfig } from '@stamprally/core';
3
- export { UserRallyState } from '@stamprally/core';
2
+ import { UserRallyState, AdminRallyConfig, SyncProgressResponse } from '@stamprally/core';
3
+ export { SyncOperationResult, SyncProgressResponse, UserRallyState } from '@stamprally/core';
4
4
 
5
5
  /** Authentication already verified by the host application's middleware. */
6
6
  interface TrustedAuthContext {
@@ -248,6 +248,7 @@ declare function validateSyncRequest(value: unknown): RequestValidationResult<{
248
248
  readonly rallyId: string;
249
249
  readonly userId?: string;
250
250
  readonly anonymousSessionId?: string;
251
+ readonly operations?: SyncProgressRequest["operations"];
251
252
  }>;
252
253
 
253
254
  type ErrorResponse = {
@@ -275,11 +276,7 @@ declare class StampRallyServer {
275
276
  checkIn(request: CheckInRequest, authContext?: TrustedAuthContext): Promise<CheckInResponse>;
276
277
  claimReward(request: ClaimRewardRequest, authContext?: TrustedAuthContext): Promise<ClaimResponse>;
277
278
  sync(rallyId: string, identity: string | TrustedAuthContext): Promise<UserRallyState>;
278
- syncProgress(request: {
279
- readonly rallyId: string;
280
- readonly userId?: string;
281
- readonly anonymousSessionId?: string;
282
- }, authContext: TrustedAuthContext): Promise<UserRallyState>;
279
+ syncProgress(request: SyncProgressRequest, authContext: TrustedAuthContext): Promise<SyncProgressResponse>;
283
280
  }
284
281
 
285
282
  /**
@@ -319,6 +316,19 @@ interface ClaimRewardRequest {
319
316
  readonly staffId?: string;
320
317
  readonly now?: string | number;
321
318
  }
319
+ type SyncProgressOperation = {
320
+ readonly kind: "checkIn";
321
+ readonly request: CheckInRequest;
322
+ } | {
323
+ readonly kind: "claimReward";
324
+ readonly request: ClaimRewardRequest;
325
+ };
326
+ interface SyncProgressRequest {
327
+ readonly rallyId: string;
328
+ readonly userId?: string;
329
+ readonly anonymousSessionId?: string;
330
+ readonly operations?: ReadonlyArray<SyncProgressOperation>;
331
+ }
322
332
  type CheckInResponse = {
323
333
  readonly ok: true;
324
334
  readonly state: UserRallyState;
@@ -338,4 +348,4 @@ interface ServerOptions {
338
348
  readonly anonymousPolicy?: "session_scoped" | "reject" | "shared_global_opt_in_insecure";
339
349
  }
340
350
 
341
- export { type AuditLog, type AuthenticationContext, type CheckInRequest, type CheckInResponse, type CheckInTransactionMutation, type CheckInTransactionParams, type ClaimRewardMutationContext, type ClaimRewardRequest, type ClaimRewardTransactionMutation, type ClaimRewardTransactionParams, InMemoryServerPersistenceAdapter, type InMemoryServerPersistenceOptions, type RedisMultiExecutor, type RequestValidationError, RequestValidationException, type RequestValidationResult, type ServerOptions, type ServerPersistenceAdapter, type SqlCheckInStore, type SqlClaimRewardStore, type SqlTransactionDatabase, StampRallyServer, type SyncOperationStatus, type TrustedAuthContext, type UserClaimRecord, assertValidCheckInParams, assertValidClaimParams, assertValidSyncParams, executeCheckInTransaction, executeClaimRewardTransaction, executeRedisTransaction, runPersistenceAdapterComplianceTests, validateCheckInRequest, validateClaimRewardRequest, validateSyncRequest };
351
+ export { type AuditLog, type AuthenticationContext, type CheckInRequest, type CheckInResponse, type CheckInTransactionMutation, type CheckInTransactionParams, type ClaimRewardMutationContext, type ClaimRewardRequest, type ClaimRewardTransactionMutation, type ClaimRewardTransactionParams, InMemoryServerPersistenceAdapter, type InMemoryServerPersistenceOptions, type RedisMultiExecutor, type RequestValidationError, RequestValidationException, type RequestValidationResult, type ServerOptions, type ServerPersistenceAdapter, type SqlCheckInStore, type SqlClaimRewardStore, type SqlTransactionDatabase, StampRallyServer, type SyncOperationStatus, type SyncProgressOperation, type SyncProgressRequest, type TrustedAuthContext, type UserClaimRecord, assertValidCheckInParams, assertValidClaimParams, assertValidSyncParams, executeCheckInTransaction, executeClaimRewardTransaction, executeRedisTransaction, runPersistenceAdapterComplianceTests, validateCheckInRequest, validateClaimRewardRequest, validateSyncRequest };
package/dist/index.js CHANGED
@@ -2,9 +2,13 @@ import { reconcileRewardStates, consumeReward, evaluateConditionDetailed } from
2
2
 
3
3
  // src/examples/transaction.ts
4
4
  async function executeClaimRewardTransaction(database, store, params2, mutation2) {
5
+ if (params2.secondaryStockKey !== void 0 && store.writeSecondaryStock === void 0)
6
+ return { success: false, error: "SECONDARY_STOCK_UNSUPPORTED" };
5
7
  try {
6
8
  return await database.transaction(async (transaction) => {
7
9
  const current = await store.readContext(transaction, params2);
10
+ if (params2.secondaryStockKey !== void 0 && current.secondaryStock === void 0)
11
+ return { success: false, error: "SECONDARY_STOCK_UNSUPPORTED" };
8
12
  const secondaryStock = current.secondaryStock ?? null;
9
13
  const rewardStock2 = params2.stockKey === "__shared__" ? secondaryStock : current.stock;
10
14
  const next = mutation2({
@@ -596,6 +600,40 @@ function validateSyncRequest(value) {
596
600
  return errors({ path: "$", message: "Expected an object.", code: "INVALID_TYPE" });
597
601
  if (value.userId !== void 0 && !nonEmpty(value.userId))
598
602
  return errors({ path: "userId", message: "userId must be non-empty.", code: "INVALID_TYPE" });
603
+ if (value.operations !== void 0) {
604
+ if (!Array.isArray(value.operations))
605
+ return errors({
606
+ path: "operations",
607
+ message: "operations must be an array.",
608
+ code: "INVALID_TYPE"
609
+ });
610
+ const operationErrors = [];
611
+ value.operations.forEach((operation, index) => {
612
+ const path = `operations[${index}]`;
613
+ if (!record(operation)) {
614
+ operationErrors.push({
615
+ path,
616
+ message: "Expected an operation object.",
617
+ code: "INVALID_TYPE"
618
+ });
619
+ return;
620
+ }
621
+ if (operation.kind !== "checkIn" && operation.kind !== "claimReward") {
622
+ operationErrors.push({
623
+ path: `${path}.kind`,
624
+ message: "Unknown sync operation.",
625
+ code: "INVALID_ENUM"
626
+ });
627
+ return;
628
+ }
629
+ const result = operation.kind === "checkIn" ? validateCheckInRequest(operation.request) : validateClaimRewardRequest(operation.request);
630
+ if (!result.success)
631
+ operationErrors.push(
632
+ ...result.errors.map((error) => ({ ...error, path: `${path}.request.${error.path}` }))
633
+ );
634
+ });
635
+ if (operationErrors.length > 0) return errors(...operationErrors);
636
+ }
599
637
  return {
600
638
  success: true,
601
639
  data: value
@@ -623,6 +661,15 @@ function timestampMillis(timestamp) {
623
661
  const value = Date.parse(timestamp);
624
662
  return Number.isFinite(value) ? value : Date.now();
625
663
  }
664
+ function operationTimestamp(operation) {
665
+ const value = operation.request.now;
666
+ if (typeof value === "number" && Number.isFinite(value)) return value;
667
+ if (typeof value === "string") {
668
+ const parsed = Date.parse(value);
669
+ if (Number.isFinite(parsed)) return parsed;
670
+ }
671
+ return Number.MAX_SAFE_INTEGER;
672
+ }
626
673
  function validationResponse(errors2) {
627
674
  return json(
628
675
  {
@@ -710,6 +757,38 @@ function operationStatus(result) {
710
757
  if (result.ok) return "ACCEPTED";
711
758
  return result.code === "CONFLICT" || result.code === "PERSISTENCE_FAILED" ? "RETRYABLE_ERROR" : "REJECTED_PERMANENT";
712
759
  }
760
+ function syncOperationId(operation, userId) {
761
+ return `${operation.kind}:${operation.request.rallyId}:${userId}:${operation.request.idempotencyKey}`;
762
+ }
763
+ function syncOperationResult(operation, userId, result) {
764
+ const operationId = syncOperationId(operation, userId);
765
+ const resourceId = operation.kind === "checkIn" ? operation.request.spotId : operation.request.rewardId;
766
+ const action = operation.kind === "checkIn" ? "CHECK_IN" : "CLAIM_REWARD";
767
+ const status = operationStatus(result);
768
+ if (status === "ACCEPTED") {
769
+ return {
770
+ operationId,
771
+ status,
772
+ resourceId,
773
+ action,
774
+ appliedAt: timestampMillis(result.ok ? result.state.updatedAt : "")
775
+ };
776
+ }
777
+ if (status === "RETRYABLE_ERROR")
778
+ return {
779
+ operationId,
780
+ status: "FAILED_RETRYABLE",
781
+ resourceId,
782
+ error: result.ok ? "The operation can be retried." : result.message
783
+ };
784
+ return {
785
+ operationId,
786
+ status,
787
+ resourceId,
788
+ errorCode: result.ok ? "REJECTED_PERMANENT" : result.code,
789
+ reason: result.ok ? "The operation was rejected." : result.message
790
+ };
791
+ }
713
792
  function withDirectIdentity(request, authContext) {
714
793
  if (authContext !== void 0) {
715
794
  if (authContext.authenticatedUserId.trim() === "")
@@ -842,15 +921,17 @@ var StampRallyServer = class {
842
921
  ...sessionId === null ? {} : { isAnonymous: true, sessionId }
843
922
  };
844
923
  try {
924
+ const progress = await this.syncProgress(
925
+ {
926
+ rallyId: body.data.rallyId,
927
+ ...body.data.operations === void 0 ? {} : { operations: body.data.operations },
928
+ ...sessionId === null ? {} : { anonymousSessionId: sessionId }
929
+ },
930
+ authContext
931
+ );
845
932
  return json({
846
933
  ok: true,
847
- state: await this.syncProgress(
848
- {
849
- rallyId: body.data.rallyId,
850
- ...sessionId === null ? {} : { anonymousSessionId: sessionId }
851
- },
852
- authContext
853
- )
934
+ ...progress
854
935
  });
855
936
  } catch (error) {
856
937
  if (error instanceof RequestValidationException) return validationResponse(error.errors);
@@ -1187,7 +1268,39 @@ var StampRallyServer = class {
1187
1268
  async syncProgress(request, authContext) {
1188
1269
  const directRequest = withDirectIdentity(request, authContext);
1189
1270
  assertValidSyncParams(directRequest, this.#config);
1190
- return this.sync(directRequest.rallyId, authContext);
1271
+ const results = [];
1272
+ const rejectedCheckIns = /* @__PURE__ */ new Set();
1273
+ const operations = [...request.operations ?? []].map((operation, index) => ({ operation, index })).sort(
1274
+ (left, right) => operationTimestamp(left.operation) - operationTimestamp(right.operation) || left.index - right.index
1275
+ ).map(({ operation }) => operation);
1276
+ for (const operation of operations) {
1277
+ const resourceId = operation.kind === "checkIn" ? operation.request.spotId : operation.request.rewardId;
1278
+ if (operation.kind === "checkIn") {
1279
+ const spot = this.#config.spots.find((candidate) => candidate.id === resourceId);
1280
+ if (spot?.prerequisites?.some((prerequisite) => rejectedCheckIns.has(prerequisite))) {
1281
+ results.push({
1282
+ operationId: syncOperationId(operation, directRequest.userId),
1283
+ status: "REJECTED_PERMANENT",
1284
+ resourceId,
1285
+ errorCode: "REJECTED_PREREQUISITE_FAILED",
1286
+ reason: "A prerequisite operation was rejected by the server."
1287
+ });
1288
+ rejectedCheckIns.add(resourceId);
1289
+ continue;
1290
+ }
1291
+ }
1292
+ const result = operation.kind === "checkIn" ? await this.checkIn(operation.request, authContext) : await this.claimReward(operation.request, authContext);
1293
+ const operationResult = syncOperationResult(operation, directRequest.userId, result);
1294
+ results.push(operationResult);
1295
+ if (operation.kind === "checkIn" && operationResult.status === "REJECTED_PERMANENT")
1296
+ rejectedCheckIns.add(resourceId);
1297
+ }
1298
+ const currentState = await this.sync(directRequest.rallyId, authContext);
1299
+ return {
1300
+ results,
1301
+ currentState,
1302
+ syncTimestamp: timestampMillis(now(this.#options))
1303
+ };
1191
1304
  }
1192
1305
  async #attachInventory(state2) {
1193
1306
  const rewardRemaining = {};