@marteye/studiojs
v1.2.1
Published
MartEye Studio JavaScript SDK
Readme
MartEye Studio JavaScript SDK (@marteye/studiojs)
This library is a JavaScript SDK that allows you to easily integrate with MartEye Studio's API. It provides methods to interact with various resources, manage data, and leverage the powerful features offered by MartEye Studio.
Installation
You can install the library using npm or yarn:
Using npm:
npm install @marteye/studiojsUsing yarn:
yarn add @marteye/studiojsUsage
Importing the Library
Using ES Modules:
import StudioJS from "@marteye/studiojs";Or using CommonJS:
let StudioJS = require("@marteye/studiojs");Initializing the SDK
Before using the library, initialize it with your API key. For more information on obtaining an API key, please contact your MartEye representative.
let studio = StudioJS({
apiKey: "YOUR_API_KEY_HERE",
});Handling errors
Failed requests throw a StudioApiError. It extends Error, so existing
try/catch code keeps working, but it also carries the fields you need to
decide what to do next instead of matching on message text.
import { StudioApiError } from "@marteye/studiojs";
try {
await studio.lots.update(marketId, saleId, lotId, { remarks: "sold as seen" });
} catch (e) {
if (StudioApiError.isStudioApiError(e)) {
e.status; // HTTP status, or 0 for a client-side timeout
e.code; // "LOT_SAVE_PRECONDITION_FAILED", "NOT_FOUND", …
e.fields; // paths that failed, e.g. ["lotNumber", "attributes.weight"]
e.retryable; // whether replaying the identical request could ever succeed
}
throw e;
}retryable is the important one for queued work: only a transient server fault
is worth replaying unchanged. Everything else needs the caller to do something
different first, so a queue that retries on those will never drain.
Offline-friendly mutations
These options exist so a client can queue mutations while offline and drain them later without creating duplicates or clobbering newer edits.
Supply your own lot ID
Generate the ID client-side and a retried create keeps its identity. Replaying the same ID returns the stored lot with 200 instead of overwriting it, so a retry carrying a drifted payload cannot rewrite what already landed.
await studio.lots.create(marketId, saleId, {
lotId: locallyGeneratedId,
lotNumber: "42",
});Guard an update against concurrent edits
Send the values you started from. If any of them have moved, the update is refused and nothing is written.
try {
await studio.lots.update(marketId, saleId, lotId, {
remarks: "edited on the phone",
expectedValues: { productCode: "SHEEP", lotNumber: "42" },
});
} catch (e) {
if (e.code === "LOT_SAVE_PRECONDITION_FAILED") {
// Someone changed e.fields since this edit began — refetch and re-apply.
}
}Only the keys you supply are compared. An absent key means "no expectation";
an explicit null means "I expected this to be empty".
Handle a lot number that is already taken
This is the common offline collision: the office built the catalogue while the device was in the pen, so lot 42 already exists — with a different id and with a seller and product code the device knows nothing about.
A create never silently becomes an update of that lot. The device has no
expectedValues to send (it believed the lot didn't exist), so nothing would
protect the existing seller or price from being overwritten by a payload that
was never aware of them. Instead the conflict names the lot:
try {
await studio.lots.create(marketId, saleId, {
lotId: locallyGeneratedId,
lotNumber: "42",
});
} catch (e) {
if (e.code === "LOT_NUMBER_CONFLICT") {
e.existingLotId; // the lot already holding number 42
}
}Deciding what happens next belongs to the client, which knows things the server doesn't — whether the operator meant this lot, and what local work is attached to it. Prefer asking. To merge scanned work onto the existing lot, sync the tags across rather than updating the lot wholesale:
await studio.eartags.sync(marketId, saleId, {
parentId: e.existingLotId,
removedEartags: [],
// Superseded by scannedItemsAdded, but still required on the wire.
scannedEartagsAdded: [],
scannedItemsAdded: locallyScannedItems,
missingEartagsAdded: 0,
faultyEarTagsAdded: 0,
expectedValues: /* … derived from a fresh read of that lot … */,
});That path is guarded: if the existing lot has drifted since you read it, the merge is refused rather than silently applied.
Delete without racing yourself
idempotent makes deleting an already-deleted lot succeed, so a replayed
delete clears the queue. Invoice and auction protections still reject the
delete — only the missing-resource outcome converts to success.
await studio.lots.delete(marketId, saleId, lotId, { idempotent: true });Files and media uploads
StudioJS includes file upload helpers on studio.files. They reuse the same Studio API key or Firebase ID token that initialized the SDK, so callers do not pass a second upload token.
import Studio from "@marteye/studiojs";
const studio = Studio({
apiKey: "YOUR_STUDIO_TOKEN_OR_API_KEY",
});Select production or staging uploads
The SDK picks the correct Studio upload endpoint from your project environment. You do not configure upload service URLs yourself.
Resolution priority:
EXPO_PUBLIC_STUDIO_ENVNEXT_PUBLIC_STUDIO_ENVSTUDIO_ENVNODE_ENV
Supported values:
productionorprodstaging,stage,development, ordev
Examples:
# Next.js browser/server bundle
NEXT_PUBLIC_STUDIO_ENV=staging
# Expo browser/native bundle
EXPO_PUBLIC_STUDIO_ENV=staging
# Node.js
STUDIO_ENV=stagingUpload a lot file
Pass a File or Blob-like object. The SDK chooses a single or multipart upload automatically; large files are uploaded as multipart without changing this API.
await studio.files.upload({
marketId: "market_123",
saleId: "sale_123",
lotId: "lot_123",
attributeId: "media",
file,
});Override the destination file name when needed:
await studio.files.upload({
marketId: "market_123",
saleId: "sale_123",
lotId: "lot_123",
attributeId: "media",
file,
fileName: "front-view.jpg",
});Force multipart only when you need to:
await studio.files.upload({
marketId: "market_123",
saleId: "sale_123",
lotId: "lot_123",
attributeId: "media",
file,
strategy: "multipart",
onProgress(progress) {
console.log(progress.bytesUploaded, progress.totalBytes);
},
});Upload from a URL
Pass sourceUrl instead of file. The URL must be directly fetchable; redirecting URLs are rejected.
await studio.files.upload({
marketId: "market_123",
saleId: "sale_123",
lotId: "lot_123",
attributeId: "media",
sourceUrl: "https://example.com/media/photo.jpg",
});Optional fileName overrides the name inferred from the URL. Pass a file name when you want to immediately set the uploaded file on a lot attribute.
await studio.files.upload({
marketId: "market_123",
saleId: "sale_123",
lotId: "lot_123",
attributeId: "media",
sourceUrl: "https://example.com/media/photo.jpg",
fileName: "catalogue-photo.jpg",
});Set the uploaded file on a lot
upload() stores the file and returns its asset id and URLs. Attach it to a lot by updating the attribute afterwards:
const upload = await studio.files.upload({
marketId,
saleId,
lotId,
attributeId: "media",
file,
});
await studio.lots.update(marketId, saleId, lotId, {
attributes: {
media: {
id: upload.assetId,
fileName: file.name,
fileType: file.type,
url: upload.expected.original?.url ?? "",
},
},
});Upload to a custom path
For non-lot files, pass a market-prefixed path and choose transform or direct mode.
await studio.files.upload({
marketId: "market_123",
mode: "transform",
path: "market_123/users/user_123/avatar.jpg",
file,
});URL uploads support lot and transform uploads:
await studio.files.upload({
marketId: "market_123",
mode: "transform",
path: "market_123/users/user_123",
sourceUrl: "https://example.com/avatar.jpg",
fileName: "avatar.jpg",
});Private customer documents
Private customer documents use Firebase user authentication rather than a Studio API key. Create the standalone client with the current Firebase ID token:
import { createCustomerDocumentsClient } from "@marteye/studiojs";
const customerDocuments = createCustomerDocumentsClient({
authToken: await firebaseUser.getIdToken(),
});The token is not refreshed automatically. After expiry or a 401, obtain a new
ID token and create a new client. Do not persist or log the token.
The client exposes:
listDocuments,createDocument,getDocument, andupdateDocument.uploadDocument,getUploadStatus, andcancelUpload.viewFile, which returns an authenticated streamingResponsefrom Studio.deleteFile, which returns202while deletion is pending or204once the Studio attachment is absent.
Uploads accept a Blob, ArrayBuffer, or typed-array view up to 25 MiB. Images
are converted by Media Crate; PDFs remain direct. viewFile and deleteFile
always go through Studio, so the SDK never receives private Media Crate action
tokens, R2 paths, credentials, or signed URLs.
Each returned file may include createdBy, the Firebase UID that originally
uploaded it. This field is set from Media Crate's trusted callback and cannot be
provided or changed by SDK create/update requests.
Contributing
Contributions are welcome! Please open an issue or submit a pull request if you have suggestions or improvements.
License
MIT
