document-manager-sdk
v2.0.11
Published
TypeScript SDK for Lakehouse Document Manager – simple, typed, React-ready
Downloads
215
Maintainers
Readme
document-manager-sdk
TypeScript SDK for the Lakehouse Document Manager API. Designed to be simple, fully typed, and React-friendly. All internal API plumbing is hidden – you only call high-level methods.
Installation
npm install document-manager-sdkQuick Start
import { DocumentManagerSDK } from "document-manager-sdk";
const sdk = new DocumentManagerSDK({
domain: "https://api-dms.lakehouse.anybim.vn",
});Configuration
type DocumentManagerConfig = {
/** Base API URL (no trailing slash). */
domain: string;
/** Default bucket name used for all operations. */
bucketName: string;
/** Returns the access token (raw string or "Bearer …"). */
getAccessToken?: () => Promise<string> | string;
/** Request timeout in milliseconds (default: 30 000). */
timeout?: number;
/** Extra headers added to every backend request. */
defaultHeaders?: Record<string, string>;
// Auth config – required only when using auth methods
/** Base URL of the identity server, e.g. "https://identity.anybim.vn". */
identityDomain?: string;
/** OAuth client ID. Default: "admin". */
clientId?: string;
/** OAuth client secret. Required for login() and refreshTokenWithOrganization(). */
clientSecret?: string;
};Authentication
The SDK includes a full authentication module so your frontend never needs to write auth boilerplate.
Setup (with auth)
import { DocumentManagerSDK } from "document-manager-sdk";
const sdk = new DocumentManagerSDK({
domain: "https://api-dms.lakehouse.anybim.vn",
identityDomain: "https://identity.anybim.vn",
clientId: "admin",
clientSecret: process.env.CLIENT_SECRET, // never hardcode in frontend bundles
});login(username, password)
Authenticate with the identity server using the OAuth 2.0 Resource Owner Password Credentials grant.
const auth = await sdk.login("[email protected]", "password");
// auth.accessToken, auth.refreshToken, auth.expiresIn, auth.tokenTypegetOrganizations(accessToken)
Fetch all organizations the current user belongs to.
const orgs = await sdk.getOrganizations(auth.accessToken);
// orgs[0].guid, orgs[0].companyName, orgs[0].invitationStatus, …getCurrentClientPreference(accessToken)
Read the user's stored client preferences, including their default organization GUID.
const pref = await sdk.getCurrentClientPreference(auth.accessToken);
// pref.defaultOrganization – GUID or nullresolveOrganizationId(accessToken)
Automatically determine the best organization GUID for the user:
- Read
defaultOrganizationfrom preferences. - Fall back to the first organization in the list.
- Return
""when no organization is found.
const orgId = await sdk.resolveOrganizationId(auth.accessToken);refreshTokenWithOrganization({ refreshToken, orgId })
Exchange a refresh token for a new access token scoped to a specific organization.
const orgToken = await sdk.refreshTokenWithOrganization({
refreshToken: auth.refreshToken,
orgId,
remember: true, // optional, default true
});
// orgToken.accessToken is now org-scopedloginWithOrganization(username, password) ← recommended
High-level convenience method that runs the complete login + org flow in one call:
- Login with username/password → initial token.
- Fetch the user's organization list.
- Resolve the best
orgId(default org, or first in list). - Refresh the token with
orgId→ org-scoped access token.
const session = await sdk.loginWithOrganization("[email protected]", "password");
console.log(session.accessToken); // org-scoped token, ready to use
console.log(session.orgId); // resolved organization GUID
console.log(session.organizations); // full list of user's organizationsSteps B–D are non-fatal — a session is always returned even if org resolution fails.
React example
const [session, setSession] = useState(null);
async function handleLogin(username: string, password: string) {
const session = await sdk.loginWithOrganization(username, password);
setSession(session);
// Configure subsequent SDK calls to use the org-scoped token
const scopedSdk = new DocumentManagerSDK({
domain: "https://api-dms.lakehouse.anybim.vn",
});
const result = await scopedSdk.uploadFile(file, {
companyId: "1",
projectId: "...",
orgId: session.orgId,
});
}Auth error types
| Error | When thrown |
|---|---|
| ValidationError | Missing username/password or config (identityDomain, clientSecret) |
| AuthenticationError | Credentials rejected / 401 |
| AuthorizationError | 403 from the identity server |
| OrganizationError | Organization list response is not a valid array |
| NetworkError | Network timeout or connectivity failure |
API Reference
uploadFile(file, options)
Uploads a browser File object. The SDK:
- Validates all inputs.
- Checks whether the file already exists in the project.
- Selects single upload (≤ 5 MB) or multipart upload (> 5 MB) automatically.
- Executes the full pipeline internally.
- Returns a normalised result.
const result = await sdk.uploadFile(file, {
companyId: "1",
projectId: "08dc133e-8168-42b8-8baf-5e5d72a0a00b",
orgId: "08dc133e-6dd1-49eb-84e3-d98a2eb3a649",
parentId: "6927bf543c3d0b51d8694a0b", // optional
isOrganization: false, // optional, default false
displayName: "My Report.pdf", // optional, falls back to file.name
});
console.log(result.fileId); // UUID assigned by the backend
console.log(result.uploadType); // "single" | "multipart"
console.log(result.cloudLink); // storage pathReturn type:
type UploadResult = {
success: boolean;
fileId: string;
documentId?: string;
fileName: string;
size: number;
mimeType: string;
bucketName: string;
cloudLink?: string;
version?: string;
uploadType: "single" | "multipart";
raw?: { checkExists; init; confirm; initMultipart; uploadedParts; completeMultipart; };
};React example
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
try {
const result = await sdk.uploadFile(file, {
companyId: "1",
projectId: "08dc133e-8168-42b8-8baf-5e5d72a0a00b",
orgId: "08dc133e-6dd1-49eb-84e3-d98a2eb3a649",
parentId: "6927bf543c3d0b51d8694a0b",
});
console.log("Uploaded:", result);
} catch (err) {
if (err instanceof FileAlreadyExistsError) {
alert(`File already exists (id: ${err.existingFileId})`);
} else {
console.error(err);
}
}
};getDocumentInfo(fileId)
Fetches full metadata for a document.
const info = await sdk.getDocumentInfo("eb5e587a-c8ef-4a60-b20f-3640852427dc");
console.log(info.displayName); // "report.pdf"
console.log(info.owner?.email); // "[email protected]"
console.log(info.path); // breadcrumb path arrayReturn type:
type DocumentInfo = {
id: string;
fileId: string;
name: string;
displayName: string;
mimeType: string;
size: number;
bucketName: string;
cloudLink: string;
version: string;
parentId?: string;
owner?: { id: string; name: string; email?: string; icon?: string | null; };
createDate?: string;
lastModified?: string;
workspace?: string;
path?: Array<{ id: string; name: string }>;
raw?: unknown;
};getFilesAndFolders(params)
List files and folders under a parent folder.
Calls GET /Label/files/{parentId}?page=&pageSize=. The SDK attaches the
Authorization header, auto-refreshes the token, and normalizes the response
into a typed structure that matches the search result shape so UI components
can be reused across browse and search views.
const result = await sdk.getFilesAndFolders({
parentId: "6927bf543c3d0b51d8694a0b",
page: 1,
pageSize: 20,
});
for (const item of result.items) {
console.log(item.type, item.name); // "folder" | "file" + display name
}
console.log(result.pagination.queryCount); // total items
console.log(result.workspace); // "Shared with me"Return type:
type GetFilesAndFoldersResult = { // alias → SearchFilesAndFoldersResult
items: DmsLabelItem[]; // alias → SearchResultItem[]
pagination: DmsPagination; // alias → SearchPagination
path: Array<{ id?: string; name?: string }>;
ownerRootId: string | null;
workspace?: string;
raw?: unknown;
};
type DmsLabelItem = {
id: string; name: string; type: string; isDeleted: boolean;
color?: string; createdDate?: string; lastModified?: string;
children?: Array<{ idChildren?: string; type?: string }>;
createUser?: DmsLabelUser;
transferedUser?: unknown;
clientId?: string | null;
parentId?: string | null;
labelCode?: string;
isOrganization?: boolean;
isRootOrganization?: boolean;
raw?: unknown;
};| Param | Required | Description |
|---|---|---|
| parentId | yes | MongoDB ObjectId of the parent folder |
| page | no | Page number (1-based, default: 1) |
| pageSize | no | Items per page (default: 20) |
Error types:
| Error | When thrown |
|---|---|
| ValidationError | parentId missing or pagination params ≤ 0 |
| AuthenticationError | Token missing or rejected |
| FileBrowserError | API call failed or response has unexpected structure |
getProjectRootFiles(params)
List files and folders at the root level of a project. The SDK resolves the
root folder ID automatically — the caller only provides the projectId.
const result = await sdk.getProjectRootFiles({
projectId: "08dc133e-8168-42b8-8baf-5e5d72a0a00b",
page: 1,
pageSize: 20,
});
console.log(result.items); // DmsLabelItem[]
console.log(result.workspace); // workspace stringInternal flow:
- Calls
GET /Document/getFolderRootId/{projectId}→ resolvesparentId. - Calls
getFilesAndFolders({ parentId, page, pageSize }).
Error types:
| Error | When thrown |
|---|---|
| ValidationError | projectId missing |
| FileBrowserError | Project has no root folder (call resolveUploadParentId() to create one) |
| AuthenticationError | Token missing or rejected |
moveFilesToTrash(params) / moveFileToTrash(fileId)
Move one or more files to the trash (soft delete — reversible via restoreFiles).
Calls PATCH /Document/multi/delete with isDeleted: true.
// Batch
const results = await sdk.moveFilesToTrash({
fileIds: ["f158bcc7-2a43-499e-b99e-98eb01fb6f78"],
});
console.log(results[0].success); // true
console.log(results[0].isDeleted); // true
// Single-file convenience method
const result = await sdk.moveFileToTrash("f158bcc7-2a43-499e-b99e-98eb01fb6f78");restoreFiles(params) / restoreFile(fileId)
Restore one or more files from the trash.
Calls PATCH /Document/multi/delete with isDeleted: false.
// Batch
const results = await sdk.restoreFiles({
fileIds: ["f158bcc7-2a43-499e-b99e-98eb01fb6f78"],
});
console.log(results[0].isDeleted); // false
// Single-file convenience method
const result = await sdk.restoreFile("f158bcc7-2a43-499e-b99e-98eb01fb6f78");forceDeleteFiles(params) / forceDeleteFile(fileId)
Permanently delete one or more files. This action cannot be undone.
Calls DELETE /Document/multi/force-delete?companyId=<resolved>.
The SDK automatically resolves companyId from the current access token — the caller never provides it.
// Batch
const results = await sdk.forceDeleteFiles({
fileIds: ["0d44ff92-a566-469e-bff8-85ab81c81ea1"],
});
// Single-file convenience method
const result = await sdk.forceDeleteFile("0d44ff92-a566-469e-bff8-85ab81c81ea1");Return type (all three methods):
type DocumentDeleteResult = {
success: boolean;
message?: string;
documentId?: string; // MongoDB ObjectId of the affected document
isDeleted?: boolean; // true = in trash, false = restored
raw?: unknown;
};Error types
| Error | When thrown |
|---|---|
| ValidationError | fileIds missing, empty, or contains blank entries |
| AuthenticationError | Token missing or rejected |
| OrganizationError | companyId could not be resolved (force-delete only) |
| TrashOperationError | moveFilesToTrash or restoreFiles API call failed |
| ForceDeleteError | forceDeleteFiles API call failed |
searchFilesAndFolders(params)
Search for files and folders within a project by name and/or content.
The SDK automatically:
- Validates all inputs.
- Maps
keyword→fileNameKey+contentKeywhen onlykeywordis provided. - Refreshes the access token if needed.
- Decodes
OrgIdfrom the token and resolves the numericcompanyId. - Builds the correct query string and calls
GET /api/Search/search. - Returns normalized, typed results with the same shape as
getFilesAndFolders().
Search by keyword (shortcut)
const result = await sdk.searchFilesAndFolders({
projectId: "08dc133e-8168-42b8-8baf-5e5d72a0a00b",
keyword: "té",
});
console.log(result.items); // SearchResultItem[]
console.log(result.pagination); // { page, pageCount, pageSize, queryCount, ... }
console.log(result.workspace); // "Shared with me"keyword is a shortcut — it sets both fileNameKey and contentKey to the same value. The SDK builds the query fileNameKey=té&contentKey=té automatically.
Search by separate keys
const result = await sdk.searchFilesAndFolders({
projectId: "08dc133e-8168-42b8-8baf-5e5d72a0a00b",
fileNameKey: "Converter", // search by name
contentKey: "design", // search by content
page: 1,
pageSize: 20,
});Return type
type SearchFilesAndFoldersResult = {
items: SearchResultItem[];
pagination: {
page: number;
pageCount: number;
pageSize: number;
queryCount: number;
firstRowIndex: number;
lastRowIndex: number;
};
path: Array<{ id?: string; name?: string }>;
ownerRootId: string | null;
workspace?: string;
raw?: unknown;
};
type SearchResultItem = {
id: string;
name: string;
type: string; // "file" | "folder"
isDeleted: boolean;
color?: string;
createdDate?: string;
lastModified?: string;
children?: Array<{ idChildren?: string; type?: string }>;
createUser?: { id: string; name: string; email?: string; icon?: string | null };
transferedUser?: unknown;
clientId?: string | null;
parentId?: string | null;
labelCode?: string;
isOrganization?: boolean;
isRootOrganization?: boolean;
raw?: unknown;
};Input parameters
| Param | Required | Description |
|---|---|---|
| projectId | yes | Project GUID to search within |
| keyword | no | Shortcut — sets both fileNameKey and contentKey |
| fileNameKey | no | Search by file / folder name |
| contentKey | no | Search by document content |
| page | no | Page number (1-based, default: 1) |
| pageSize | no | Items per page (default: 20) |
At least one of keyword, fileNameKey, or contentKey is required.
Error types
| Error | When thrown |
|---|---|
| ValidationError | projectId missing, no search key, or page/pageSize ≤ 0 |
| AuthenticationError | Token missing or could not be refreshed |
| FileSearchError | companyId could not be resolved, or the API returned an error / unexpected structure |
React search box example
const [keyword, setKeyword] = useState("");
const [results, setResults] = useState<SearchFilesAndFoldersResult | null>(null);
async function handleSearch() {
const result = await sdk.searchFilesAndFolders({
projectId: "08dc133e-8168-42b8-8baf-5e5d72a0a00b",
keyword,
});
setResults(result);
}Error Handling
Import specific error classes to handle different failure modes:
import {
FileAlreadyExistsError,
ValidationError,
AuthenticationError,
AuthorizationError,
NotFoundError,
SingleUploadError,
MultipartUploadError,
UploadConfirmError,
DocumentInfoError,
NetworkError,
} from "document-manager-sdk";| Error class | When thrown |
|---|---|
| ValidationError | Missing or invalid input parameters |
| FileAlreadyExistsError | File already exists in the project (check before upload) |
| AuthenticationError | Token invalid / 401 from backend |
| AuthorizationError | Insufficient permissions / 403 |
| NotFoundError | Resource not found / 404 |
| OrganizationError | Organization list response is not a valid array |
| SingleUploadError | Single upload init or binary PUT failed |
| MultipartUploadError | Any multipart step failed |
| UploadConfirmError | Confirm metadata API call failed |
| DocumentInfoError | getDocumentInfo API call failed |
| FileSearchError | searchFilesAndFolders API call failed or response has unexpected structure |
| FileBrowserError | getFilesAndFolders or getProjectRootFiles failed |
| TrashOperationError | moveFilesToTrash or restoreFiles API call failed |
| ForceDeleteError | forceDeleteFiles permanent delete failed |
| NetworkError | Network timeout or connectivity error |
All errors extend SDKError which provides .statusCode, .details, and .message.
Internal Architecture
DocumentManagerSDK ← public facade
├── HttpClient ← fetch wrapper (auth, retry, timeout, error mapping)
├── UploadService ← orchestrates the full upload pipeline
│ ├── SingleUploadService ← init → PUT binary → confirm (≤ 5 MB)
│ └── MultipartUploadService ← init → slice → part URLs → PUT parts → complete → confirm (> 5 MB)
├── DocumentService ← getDocumentInfo and future document operations
├── FileBrowserService ← getFilesAndFolders / searchFilesAndFolders (shared parse logic)
├── TrashService ← moveFilesToTrash / restoreFiles / forceDeleteFiles
└── AuthService ← identity server integration
├── login ← ROPC grant
├── getCurrentClientPreference ← read defaultOrganization
├── getOrganizations ← fetch org list
├── resolveOrganizationId ← defaultOrg → first org → ""
├── refreshTokenWithOrganization ← refresh_token grant + orgId
└── loginWithOrganization ← full flow (A→B→C→D)Users of the SDK never interact with any of these internal classes.
Upload pipeline (single, ≤ 5 MB)
GET /api/Upload/check-db– check file existencePOST /api/Upload/single/init– obtain presigned PUT URLPUT <presigned-url>– upload binary (no auth header)POST /api/Upload/confirm– register document in DB
Upload pipeline (multipart, > 5 MB)
GET /api/Upload/check-db– check file existencePOST /api/upload/multipart/init– start multipart session- File is sliced into 5 MB chunks
- For each part:
GET /api/upload/multipart/presigned-url→PUT <presigned-url> POST /api/upload/multipart/complete– assemble partsPOST /api/Upload/confirm– register document in DB
Development
npm install
npm run build # build ESM + CJS + .d.ts into dist/
npm run build:check # type-check without emitting
npm run clean # delete dist/
npm test # run Jest (52 tests)
npm run test:coverage # with coverage reportBuild output
dist/
├── index.js ← CommonJS bundle
├── index.js.map ← source map
├── index.mjs ← ESM bundle
├── index.mjs.map ← source map
├── index.d.ts ← TypeScript declarations (CJS)
└── index.d.mts ← TypeScript declarations (ESM)The build is powered by tsup (esbuild under the hood) – fast and zero-config.
Publishing to npm
First-time setup
# 1. Create an npm account at https://www.npmjs.com
# 2. Login from the terminal
npm login
# 3. (Optional) If publishing as a scoped package, update name in package.json:
# "@your-org/document-manager-sdk"Change the package name
Edit package.json → "name" field before the first publish:
{
"name": "@your-org/document-manager-sdk"
}Publish (manual)
# Runs: lint → test → build → publish in sequence
npm publishprepublishOnly is configured to automatically run all checks before each publish.
Version bump + publish (one command)
npm run release:patch # 2.0.0 → 2.0.1
npm run release:minor # 2.0.0 → 2.1.0
npm run release:major # 2.0.0 → 3.0.0Each command runs npm version <bump> (creates a git tag + updates package.json) then npm publish.
Dry run (inspect what will be published)
npm pack --dry-runExpected output: only dist/, README.md, CHANGELOG.md, LICENSE.
Verify the published package
After publishing, test the install:
mkdir /tmp/sdk-test && cd /tmp/sdk-test
npm init -y
npm install document-manager-sdk
node -e "const s = require('document-manager-sdk'); console.log(Object.keys(s))"Requirements
- Node.js ≥ 18 (for native
fetch) - TypeScript ≥ 5
