A fluent, type-safe TypeScript client library for the TeamDesk/DBFlex REST API.
TeDasuke (手助け) means "helping hand" in Japanese - and that's exactly what this library provides for working with the TeamDesk API.
- 🎯 Fluent API - Chainable methods that read like English
- 🔒 Type-safe - Full TypeScript support with generic types
- 🚀 Modern - Built for Deno with Node.js compatibility via JSR
- 📦 Zero dependencies - Uses native Deno/Web APIs
- 🎨 Intuitive - Simple things are simple, complex things are possible
- 🔄 Auto-pagination - Built-in support for fetching large datasets
- ✨ Clean errors - Rich error context for debugging
import { TeamDeskClient } from "jsr:@rick/tedasuke";npx jsr add @rick/tedasukeimport { TeamDeskClient } from "@rick/tedasuke";import { TeamDeskClient } from "jsr:@rick/tedasuke";
// Create a client (defaults to TeamDesk API)
const client = new TeamDeskClient({
appId: 12345,
token: Deno.env.get("TD_TOKEN")!,
useBearerAuth: true, // Optional: use Bearer token auth (recommended for security)
});
// Fetch data with a fluent API
const orders = await client
.table("Orders")
.select(["OrderID", "Total", "CustomerName"])
.filter('[Status]="Active"')
.sort("OrderDate", "DESC")
.limit(100)
.execute();
console.log(`Found ${orders.length} orders`);// Select all records from a table
const clients = await client
.table("Clients")
.select()
.execute();
// Select specific columns
const orders = await client
.table("Orders")
.select(["OrderID", "Total", "CustomerName"])
.execute();
// Filtering with TeamDesk formula syntax
// See: https://www.teamdesk.net/help/working-with-formulas/
const activeOrders = await client
.table("Orders")
.select()
.filter('[Status]="Active" and [Total]>1000')
.execute();
// Sorting (URL: ?sort=OrderDate//DESC)
const recentOrders = await client
.table("Orders")
.select()
.sort("OrderDate", "DESC")
.limit(50)
.execute();
// Sort by column with spaces (URL: ?sort=Last%20Modified//DESC)
const recentlyModified = await client
.table("Records")
.select()
.sort("Last Modified", "DESC")
.limit(100)
.execute();
// Pagination (URL: ?skip=100&top=100)
const page2 = await client
.table("Orders")
.select()
.skip(100) // Skip first 100 records
.limit(100) // Return next 100 records
.execute();Views are pre-configured queries in the TeamDesk UI. TeDasuke makes them easy to use:
// Access a view
const activeOrders = await client
.table("Orders")
.view("Active Orders")
.select()
.execute();
// Views with pagination
const topProjects = await client
.table("Projects")
.view("Top Projects")
.select()
.limit(10)
.execute();Add TypeScript types for autocomplete and type checking:
interface Order {
"@row.id": number;
OrderID: string;
CustomerName: string;
Total: number;
Status: string;
OrderDate: string;
}
const orders = await client
.table<Order>("Orders")
.select()
.execute();
// Now you get autocomplete and type safety
orders.forEach((order) => {
console.log(order.CustomerName); // TypeScript knows this exists
});Fetch all records automatically with batch processing:
// Automatically handles pagination in 500-record batches
for await (const batch of client.table("Orders").select().selectAll()) {
console.log(`Processing batch of ${batch.length} orders`);
// Process each batch
}// Create records
const results = await client
.table("Clients")
.create([
{ CompanyName: "Acme Corp", Industry: "Tech" },
{ CompanyName: "Globex Inc", Industry: "Manufacturing" },
]);
// Check results
results.forEach((result) => {
if (result.success) {
console.log(`Created record ${result.id} with key ${result.key}`);
} else {
console.error("Failed:", result.errors.map((e) => e.message).join("; "));
}
});
// Update records by @row.id (preferred)
const updated = await client
.table("Clients")
.update([
{ "@row.id": 123, Status: "Active" },
]);
// Update records by key (string — automatically converted to @row.id)
const updated2 = await client
.table("Clients")
.update([
{ key: "123", Status: "Active" },
]);
// Upsert (create or update based on match column)
const upserted = await client
.table("Contacts")
.upsert(
[{ Email: "john@example.com", Name: "John Doe" }],
"Email", // Match on Email column
);
// Check if record was created or updated
upserted.forEach((result) => {
if (result.success) {
console.log(`${result.action} record ${result.id}`); // "created" or "updated"
}
});
// Disable workflow triggers
const quietResult = await client
.table("Clients")
.create(
[{ CompanyName: "Test Corp" }],
{ workflow: false }, // Won't trigger TeamDesk workflow rules
);Note on record identification: The TeamDesk API uses
@row.id(a numeric internal ID) to identify records for updates. TeDasuke accepts either@row.id(number) orkey(string) — if you passkey, it is automatically converted to a numeric@row.id. The@row.idvalue is returned in every select query result.
All write operations (create, update, upsert) return result objects with
the following properties:
interface CreateResult<T> {
success: boolean; // true if HTTP status 200-299
status: number; // HTTP status code (200, 201, etc.)
data: Partial<T>; // The record data
id?: number; // Row ID of the record
key?: string; // Key value of the record
errors: ApiError[]; // Any errors that occurred
}
interface UpsertResult<T> extends CreateResult<T> {
action: "created" | "updated"; // Whether record was created or updated
}Example usage:
const results = await client.table("Web Lead").upsert([
{
f_1655786: "John",
f_1655792: "Doe",
f_1655893: "john@example.com",
},
]);
const result = results[0];
if (result.success) {
console.log(`Success! Record ${result.action} with ID: ${result.id}`);
// Redirect to success page, etc.
} else {
// errors is an array of { column?: string, message: string }
const messages = result.errors.map((e) =>
e.column ? `${e.column}: ${e.message}` : e.message
);
console.error("Failed:", messages.join("; "));
}TeDasuke provides rich error context:
import {
AuthenticationError,
TeamDeskError,
ValidationError,
} from "jsr:@rick/tedasuke";
try {
const data = await client
.table("Orders")
.select()
.execute();
} catch (error) {
if (error instanceof AuthenticationError) {
console.error("Authentication failed:", error.message);
} else if (error instanceof ValidationError) {
console.error("Validation error:", error.message);
console.error("Details:", error.details);
} else if (error instanceof TeamDeskError) {
console.error("TeamDesk error:", error.message);
console.error("Status:", error.status);
console.error("URL:", error.url);
}
}TeDasuke includes automatic caching to disk for build resilience. Caching is enabled by default and falls back to cached data when the API is unavailable.
Default behavior:
import { TeamDeskClient } from "jsr:@rick/tedasuke";
// Caching is automatic - defaults to "./_tdcache" directory
const client = new TeamDeskClient({
appId: 12345,
token: Deno.env.get("API_KEY")!,
});Custom cache directory:
const client = new TeamDeskClient({
appId: 12345,
token: Deno.env.get("API_KEY")!,
cacheDir: "src/_data/_tdcache", // Custom location
});Disable caching:
const client = new TeamDeskClient({
appId: 12345,
token: Deno.env.get("API_KEY")!,
cacheDir: null, // or false - disables caching
});Using fetchWithCache helper:
import { fetchWithCache, TeamDeskClient } from "jsr:@rick/tedasuke";
const client = new TeamDeskClient({
appId: 12345,
token: Deno.env.get("API_KEY")!,
cacheDir: "./_tdcache", // Configure once
});
// Pass client directly - it uses the configured cache directory
const result = await fetchWithCache(
client,
"orders",
() => client.table("Orders").select().execute(),
);
if (result.fromCache) {
console.warn(
`Using cached data (${result.cacheAge?.toFixed(1)} minutes old)`,
);
}
export const orders = result.data;This ensures your static site builds succeed even when the API is temporarily unavailable.
Perfect for static site generation with Lume:
// In your _data/prodb.ts file
import { TeamDeskClient } from "jsr:@rick/tedasuke";
const td = new TeamDeskClient({
appId: 12345,
token: Deno.env.get("API_KEY")!,
baseUrl: "https://my.dbflex.net/secure/api/v2", // for DBFlex
});
// These exports become available in your Lume templates
export const holidays = await td
.table("Work Holiday")
.view("API Holidays Today or Later")
.select()
.execute();
export const projects = await td
.table("Web Project")
.view("API List All")
.select()
.limit(3)
.execute();
export const contacts = await td
.table("Web Japan Contact and App")
.select()
.execute();Main client class for interacting with TeamDesk.
Constructor options:
appId(string | number) - Your TeamDesk application IDtoken(string) - API token (preferred authentication method)user(string) - Username for basic auth (alternative to token)password(string) - Password for basic auth (alternative to token)baseUrl(string) - API base URL (defaults tohttps://www.teamdesk.net/secure/api/v2; override for DBFlexhttps://my.dbflex.net/secure/api/v2or custom domains)cacheDir(string | null | false) - Cache directory path (defaults to./_tdcache; set tonullorfalseto disable)debug(boolean) - Enable debug logging (optional)
Methods:
table<T>(tableName: string)- Get a TableClient for the specified tabledescribe()- Get database schema information
Client for a specific table.
Methods:
select(columns?: string[])- Start building a SELECT queryview(viewName: string)- Access a view on this tablecreate(records, options?)- Create new records, returnsCreateResult<T>[]update(records, options?)- Update existing records (identify by@row.idorkey), returnsUpdateResult<T>[]upsert(records, matchColumn?, options?)- Create or update records, returnsUpsertResult<T>[]delete(key, options?)- Delete a record, returnsDeleteResult
Fluent query builder for SELECT operations.
Methods:
filter(expression: string)- Add a filter (TeamDesk formula language)sort(column: string, direction?: 'ASC' | 'DESC')- Add sortinglimit(n: number)- Limit results (max 500)skip(n: number)- Skip records for paginationexecute()- Execute the query and return resultsselectAll()- Auto-paginate through all results
Client for a specific view.
Methods:
select(columns?: string[])- Start building a query on this view
TeDasuke uses TeamDesk's formula language for filters. Here are some examples:
// Equality
.filter('[Status]="Active"')
// Comparison
.filter('[Amount]>1000')
.filter('[Date]>="2024-01-01"')
// Logical operators
.filter('[Status]="Active" and [Amount]>1000')
.filter('[Status]="Active" or [Status]="Pending"')
// Functions
.filter('IsBlank([ShippedDate])')
.filter('Contains([Description], "urgent")')
.filter('Year([Date])=2024')TeDasuke methods map to TeamDesk/DBFlex REST API URL parameters:
| Method | URL Parameter | Example | Description |
|---|---|---|---|
.limit(n) |
?top=n |
?top=100 |
Limit results to n records (max 500) |
.skip(n) |
?skip=n |
?skip=100 |
Skip first n records (for pagination) |
.sort(column, direction) |
?sort=Column//DIR |
?sort=Last%20Modified//DESC |
Sort by column ASC or DESC |
.filter(formula) |
?filter=... |
?filter=[Status]="Active" |
Filter using TeamDesk formulas |
// Get last 100 most recently modified records
const recent = await client
.table("Records")
.select()
.sort("Last Modified", "DESC")
.limit(100)
.execute();
// URL: /Records/select.json?sort=Last%20Modified//DESC&top=100
// Pagination: Get page 3 (records 201-300)
const page3 = await client
.table("Orders")
.select()
.skip(200)
.limit(100)
.execute();
// URL: /Orders/select.json?skip=200&top=100
// Complex filtering
const filtered = await client
.table("Tasks")
.select()
.filter('[Priority]="High" and [Status]<>"Completed"')
.sort("Due Date", "ASC")
.limit(50)
.execute();
// URL: /Tasks/select.json?filter=[Priority]="High" and [Status]<>"Completed"&sort=Due%20Date//ASC&top=50TeamDeskError- Base error classAuthenticationError- Authentication failed (401, 403)ValidationError- Validation failed (400, 422)NotFoundError- Resource not found (404)RateLimitError- Rate limit exceeded (429)ServerError- Server error (500+)
TeDasuke supports two authentication methods:
Token-based (recommended):
const client = new TeamDeskClient({
appId: 12345,
token: "your-api-token",
});Token-based with Bearer Auth (recommended for production):
const client = new TeamDeskClient({
appId: 12345,
token: "your-api-token",
useBearerAuth: true, // Sends token as "Authorization: Bearer" header
});When useBearerAuth is enabled, the token is sent as an Authorization: Bearer
header instead of being included in the URL path. This prevents the token from
appearing in server logs and is the recommended approach for production
environments.
Basic auth:
const client = new TeamDeskClient({
appId: 12345,
user: "username",
password: "password",
});Note: Basic auth does not support useBearerAuth mode.
For DBFlex or custom installations:
const client = new TeamDeskClient({
appId: 12345,
token: "your-token",
baseUrl: "https://my.dbflex.net/secure/api/v2",
});Enable debug logging to see all API requests:
const client = new TeamDeskClient({
appId: 12345,
token: "your-token",
debug: true,
});# Run examples
deno task example
# Run type checking
deno task check
# Run linter
deno task lint
# Format code
deno task fmt
# Run all preflight checks
deno task preflightSet up environment variables:
export TD_APP_ID=12345
export TD_TOKEN="your-api-token"Run the basic example:
deno task exampleThis package is published to JSR (JavaScript Registry):
# Dry run to validate
deno task publish
# Publish for real
deno publishMIT
Contributions welcome! Please open an issue or PR on GitHub.
Built by Rick Cogley for use with Lume SSG, but designed as a general-purpose TeamDesk client library.
TeamDesk is a product of ForeSoft Corporation.