3y3anaaSDKsreviews-sdk

@duabalabs/reviews-sdk

Framework-agnostic client for the centralized 3y3anaa reviews platform — the shared review/rating store for the Duaba ecosystem. Reviews target a subject namespaced by (sourceApp, subjectType, externalId); reviewers are external identities (no Parse account needed). Your app authenticates with a per-app key.

⚠️

Call this server-side. The app key (X-App-Key) is secret and must never reach a browser bundle.

Install

npm i @duabalabs/reviews-sdk
# or
pnpm add @duabalabs/reviews-sdk

Ships CJS + ESM with bundled .d.ts types. No runtime dependencies — it uses the global fetch.

createReviewsClient(config)

import { createReviewsClient } from '@duabalabs/reviews-sdk';
 
const reviews = createReviewsClient({
  serverUrl: process.env.THREEYANAA_API_URL!,         // https://api.3y3anaa.com/parse
  appId: process.env.THREEYANAA_PARSE_APP_ID!,         // X-Parse-Application-Id
  javascriptKey: process.env.THREEYANAA_PARSE_JS_KEY!, // X-Parse-Javascript-Key (required; not secret)
  appKey: process.env.THREEYANAA_APP_KEY!,            // X-App-Key (secret, per-app scoping)
});

Config

FieldTypeRequiredNotes
serverUrlstringParse server base URL (trailing slash trimmed).
appIdstringSent as X-Parse-Application-Id.
appKeystringSent as X-App-Key. Secret — per-app scoping/auth.
javascriptKeystring⚠️Sent as X-Parse-Javascript-Key. Required when the server has a JS key configured (it does). Not secret.
fetchtypeof fetchCustom fetch impl (Node < 18 / tracing wrappers). Defaults to global fetch.

If no fetch is available and none is supplied, the constructor throws a ReviewsApiError.

Methods

The client returns an object with the following methods. Each POSTs to a Parse cloud function and unwraps the { result } envelope.

submitReview(input)

Create or edit a review. One review exists per (app, subject, author) — calling again with the same subject + author edits the existing one.

const { review, aggregate } = await reviews.submitReview({
  subjectType: 'product',
  externalId: 'prod_123',
  subjectDisplayName: 'Kente Tote Bag',     // recorded on the subject (first time)
  subjectImageUrl: 'https://cdn.example/…',
  subjectUrl: 'https://sellub.com/p/prod_123',
  author: { externalUserId: 'user_42', displayName: 'Ama', role: 'buyer' },
  rating: 5,                                 // 1–5
  title: 'Lovely bag',
  body: 'Beautiful craftsmanship, fast delivery.',
  verified: true,                            // e.g. verified purchase
});
// → { review: Review, aggregate: ReviewAggregate }

SubmitReviewInput: subjectType, externalId, author, rating (required); title?, body?, verified?, subjectDisplayName?, subjectImageUrl?, subjectUrl?, subjectMeta? (optional). Maps to submitAppReview.

Server-side, a review whose computed qualityScore is < 35 is set to under_review rather than published. Inspect review.status after submitting.

getReviews(input)

Paginated reviews for a subject, scoped to your app, with the aggregate.

const { items, total, hasMore, aggregate } = await reviews.getReviews({
  subjectType: 'product',
  externalId: 'prod_123',
  limit: 20,          // ≤ 100, default 20
  skip: 0,
  sort: 'newest',     // 'newest' | 'helpful' | 'highest' | 'lowest'
});

Maps to getAppReviews.

getAggregate(input)

The aggregate for a single subject.

const agg = await reviews.getAggregate({ subjectType: 'product', externalId: 'prod_123' });
// → { subjectType, externalId, avgRating, reviewCount, ratingBreakdown }

Maps to getAppReviewAggregate.

getAggregates(subjects)

Batch aggregates for product grids / cards (≤ 200 per call).

const aggs = await reviews.getAggregates([
  { subjectType: 'product', externalId: 'prod_123' },
  { subjectType: 'product', externalId: 'prod_456' },
]);
// → Array<{ subjectType, externalId, avgRating, reviewCount, ratingBreakdown }>

Maps to getAppReviewAggregates (the SDK unwraps { aggregates }).

markHelpful(input)

One helpful vote per external voter.

const { helpfulCount, alreadyVoted } = await reviews.markHelpful({
  reviewId: 'abc123',
  voterExternalUserId: 'user_99',
});

Maps to markAppReviewHelpful.

respondToReview(input)

The subject owner’s public response to a review.

const { review } = await reviews.respondToReview({
  reviewId: 'abc123',
  responderExternalUserId: 'seller_7',
  responderDisplayName: 'Accra Crafts',
  text: 'Thanks! We’ve refunded the delivery fee.',
});

Maps to respondToAppReview.

getReviewsByAuthor(input)

An author’s own reviews (“my reviews”), scoped to your app.

const { items, total, hasMore } = await reviews.getReviewsByAuthor({
  authorExternalUserId: 'user_42',
  limit: 20,
  skip: 0,
});

Maps to getAppReviewsByAuthor.

Types

type SubjectType =
  | 'artisan' | 'business' | 'client'
  | 'product' | 'shop' | 'supplier' | 'courier'
  | (string & {});   // open union — any string accepted, these are canonical
 
type ReviewerRole =
  | 'artisan' | 'business' | 'client'
  | 'buyer' | 'seller' | 'courier'
  | (string & {});
 
interface ReviewAuthor {
  externalUserId: string;
  displayName?: string;
  role?: ReviewerRole;
}
 
interface RatingBreakdown { 1: number; 2: number; 3: number; 4: number; 5: number }
 
interface ReviewAggregate {
  avgRating: number;
  reviewCount: number;
  ratingBreakdown: RatingBreakdown;
}
 
interface Review {
  id: string;
  subjectType: SubjectType;
  externalId: string;
  rating: number;
  title: string | null;
  body: string | null;
  verified: boolean;
  status: 'published' | 'under_review' | 'rejected';
  author: { externalUserId: string; displayName: string | null; role: ReviewerRole | null };
  helpfulCount: number;
  response: { text: string; byDisplayName: string; at: string } | null;
  createdAt: string;
}

Error handling

Methods throw a ReviewsApiError on network failure or when the server returns an error/!ok response. The error carries an optional numeric code (the Parse error code or HTTP status).

import { ReviewsApiError } from '@duabalabs/reviews-sdk';
 
try {
  await reviews.submitReview({ /* … */ });
} catch (e) {
  if (e instanceof ReviewsApiError) {
    console.error('reviews error', e.code, e.message);
    // common: rate limit ("slow down"), invalid app key, out-of-scope subjectType, validation
  }
  // degrade gracefully — don't block your checkout/page on a reviews failure
}

Common server-side errors: invalid/inactive app key, out-of-scope subjectType (the credential’s scopes don’t include it), rate limit exceeded (per-app rateLimitPerMin), and validation (rating not 1–5, missing subjectType/externalId/author.externalUserId).

Environment

Env varMaps to config / headerSecret?
THREEYANAA_API_URLserverUrlno
THREEYANAA_PARSE_APP_IDappIdX-Parse-Application-Idno
THREEYANAA_PARSE_JS_KEYjavascriptKeyX-Parse-Javascript-Keyno
THREEYANAA_APP_KEYappKeyX-App-Keyyes

Worked example — Sellub

Sellub integrates the reviews platform server-side from its storefront. The pattern (mirrored in sellub-storefront/src/lib/reviews/client.ts, which will swap to this package once published):

Verified-purchase product review — after an order settles, Sellub submits a review on behalf of the buyer, marked verified:

// server action / route handler — never client-side
await reviews.submitReview({
  subjectType: 'product',
  externalId: order.productId,               // Sellub's own product id
  subjectDisplayName: product.name,
  subjectUrl: `https://sellub.com/p/${product.slug}`,
  author: {
    externalUserId: order.customerId,         // Sellub's user id
    displayName: order.customerName,
    role: 'buyer',
  },
  rating,
  title,
  body,
  verified: true,                             // gated on a real purchase
});

Shop review — the same call with subjectType: 'shop' and the seller’s channel id as externalId.

Read on the product page (server component, fail-soft):

const { items, aggregate } = await reviews.getReviews({
  subjectType: 'product',
  externalId: product.id,
  sort: 'helpful',
});
// render aggregate.avgRating / aggregate.reviewCount + items

Product grid — one batched call for all visible cards:

const aggs = await reviews.getAggregates(
  products.map((p) => ({ subjectType: 'product', externalId: p.id })),
);

Sellub’s credential is provisioned with scopes: ['product','shop','supplier','courier'], so submitting a courier or supplier review later needs no new key. 3y3anaa, as the host, can then surface Sellub’s reviews of a business alongside every other app’s via getSubjectReviews.