@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/README.md +26 -1
- package/dist/index.cjs +121 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -8
- package/dist/index.d.ts +18 -8
- package/dist/index.js +121 -8
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
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
|
-
|
|
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 = {};
|