Collections (CRUD & Queries)
Collections are MACHHUB’s typed data tables. The SDK exposes them through
sdk.collection(name), which returns a fluent query builder for reading and a small set
of methods for writing. This page covers CRUD, the query API, JSON-array filtering,
field selection, relation expansion, pagination, and batch operations.
CRUD operations
Section titled “CRUD operations”Create
Section titled “Create”const sdk = await getOrInitializeSDK();
const newProduct = await sdk.collection('products').create({ name: 'Wireless Mouse', sku: 'MOUSE-001', price: 29.99, quantity: 100, categoryId: { Table: "myapp.categories", ID: "CAT-002" }});
console.log('Created:', newProduct);// Get allconst allProducts = await sdk.collection('products').getAll();
// Get one by full RecordIDconst product = await sdk.collection('products') .getOne('myapp.products:PROD-001');
// Get first match of a queryconst firstLowStock = await sdk.collection('products') .filter('quantity', '<', 10) .first();
// Countconst totalProducts = await sdk.collection('products').count();Update (PATCH — partial update)
Section titled “Update (PATCH — partial update)”update() is a PATCH: only the fields you provide change; everything else is left
untouched. It requires the full RecordID.
// Update only price (other fields unchanged)const updated = await sdk.collection('products').update( 'myapp.products:PROD-001', { price: 34.99 });
// Update a reference fieldawait sdk.collection('products').update( 'myapp.products:PROD-001', { categoryId: { Table: "myapp.categories", ID: "CAT-003" } });Delete
Section titled “Delete”await sdk.collection('products').delete('myapp.products:PROD-001');The fluent query API
Section titled “The fluent query API”Chain query methods on sdk.collection(name) and end the chain with a terminal method
(getAll, first, count, or getOne).
const sdk = await getOrInitializeSDK();const collection = sdk.collection('records');
// Single filterconst activeRecords = await collection .filter('state', '=', 'active') .getAll();
// Multiple (AND) filtersconst filteredRecords = await collection .filter('state', '=', 'active') .filter('amount', '>', 100) .filter('score', '>=', 10) .getAll();
// OR filtersconst openOrPending = await collection .orFilter('state', '=', 'open') .orFilter('state', '=', 'pending') .getAll();
// Sortingconst sortedRecords = await collection .sort('amount', 'desc') .getAll();
// Paginationconst paginatedRecords = await collection .offset(0) .limit(10) .getAll();
// Full chainconst results = await collection .filter('type', '=', 'standard') .orFilter('state', '=', 'active') .orFilter('state', '=', 'pending') .sort('amount', 'asc') .offset(20) .limit(10) .getAll();Query operators
Section titled “Query operators”| Operator | Description | Example |
|---|---|---|
= | Equal to | .filter('state', '=', 'active') |
!= | Not equal to | .filter('state', '!=', 'archived') |
> | Greater than | .filter('amount', '>', 100) |
< | Less than | .filter('score', '<', 10) |
>= | Greater than or equal | .filter('amount', '>=', 50) |
<= | Less than or equal | .filter('score', '<=', 100) |
CONTAINS | String contains | .filter('title', 'CONTAINS', 'sample') |
IN | Value in array | .filter('state', 'IN', ['active', 'pending']) |
Terminal methods
Section titled “Terminal methods”// First matching record (or null)const firstActive = await collection .filter('state', '=', 'active') .first();
// Count matching recordsconst activeCount = await collection .filter('state', '=', 'active') .count();
// Single record by full idconst record = await collection.getOne('myapp.records:REC-001');
// getOne with a RecordID objectimport { RecordIDToString } from '@machhub-dev/sdk-ts';const recordId = RecordIDToString({ Table: "myapp.records", ID: "REC-001" });const sameRecord = await collection.getOne(recordId);Filtering inside JSON-array fields
Section titled “Filtering inside JSON-array fields”Some collections store lines as a json field — an array of objects embedded in the
record (for example purchaseOrders.orderLines). Because these are json (not
relation) fields, plain filter() cannot reach inside them. Use filterInArray(),
which pushes the predicate into the database so only matching parent records are
returned.
.filterInArray(arrayField: string, subField: string, operator: BasicOperator, value: any)BasicOperator supports the same set as filter: = != < <= > >=
CONTAINS IN.
// ❌ BEFORE — load everything, filter in TypeScript (inefficient)const allPOs = await sdk.collection('purchaseOrders').getAll();const matchingPOs = allPOs.filter(po => po.orderLines?.some((line: any) => line.itemId === 'myapp.items:ITEM-001'));
// ✅ AFTER — filter pushed to the database, only matching records returnedconst matchingPOs = await sdk.collection('purchaseOrders') .filterInArray('orderLines', 'itemId', '=', 'myapp.items:ITEM-001') .getAll();You can combine filterInArray() with regular filters, and chain several sub-field
predicates:
// Open sales orders that contain a specific itemconst sos = await sdk.collection('salesOrders') .filterInArray('orderLines', 'itemId', '=', 'myapp.items:ITEM-001') .filter('status', '=', 'open') .getAll();
// Lines for an item priced under 50const specificOrders = await sdk.collection('purchaseOrders') .filterInArray('orderLines', 'itemId', '=', 'myapp.items:ITEM-001') .filterInArray('orderLines', 'unitPrice', '<', 50) .getAll();Selecting specific fields
Section titled “Selecting specific fields”Use fields to fetch only the columns you need and reduce payload size. It accepts an
array or a comma-separated string.
// As an arrayconst products = await sdk.collection('products').getAll({ fields: ['id', 'name', 'price']});
// As a comma-separated stringconst sameProducts = await sdk.collection('products').getAll({ fields: 'id,name,price'});Expanding related records
Section titled “Expanding related records”By default a reference field returns a RecordID. To fetch the full related record, use
expand. The SDK offers two equivalent forms — a chained .expand(...) method and
an expand option on getAll/getOne. Both accept a single field name or an array;
use whichever reads better.
// Chained formconst productsWithCategory = await sdk .collection('products') .expand('categoryId') .getAll();
// Option formconst sameResult = await sdk.collection('products').getAll({ expand: 'categoryId'});
console.log(sameResult[0].categoryId);// { id: "CAT-001", name: "Electronics", description: "..." }Expand multiple relations, and combine with filters, sorting, and field selection:
// Multiple relationsconst orders = await sdk.collection('orders').getAll({ expand: ['customerId', 'productId', 'warehouseId']});
orders.forEach(order => { console.log(order.customerId.name); console.log(order.productId.name); console.log(order.warehouseId.location);});
// Expand within a full query chainconst results = await sdk.collection('orders') .filter('status', '=', 'pending') .sort('created_dt', 'desc') .limit(50) .getAll({ expand: ['customerId', 'productId'] });
// Combine fields + expandconst trimmed = await sdk.collection('products').getAll({ fields: ['id', 'name', 'price', 'categoryId'], expand: 'categoryId'});Pagination with total count
Section titled “Pagination with total count”Run a count() for the total and a paged getAll() for the rows. offset is the row
to start at; limit is the page size.
async function getPaginatedProducts(page: number, pageSize: number) { const sdk = await getOrInitializeSDK();
// Total count (same filters as the page) const total = await sdk.collection('products') .filter('status', '=', 'active') .count();
// Page of data const products = await sdk.collection('products') .filter('status', '=', 'active') .sort('name', 'asc') .offset(page * pageSize) .limit(pageSize) .getAll();
return { products, total, page, pageSize, totalPages: Math.ceil(total / pageSize) };}Batch operations
Section titled “Batch operations”There is no single bulk endpoint — process records in chunks and collect successes and
failures. The pattern below runs each chunk with Promise.all, supports
continueOnError, and builds the full id for updates and deletes.
// filepath: src/services/batch-operations.service.ts (excerpt)async batchCreate<T>( collectionName: string, records: Partial<T>[], options?: { continueOnError?: boolean; chunkSize?: number }): Promise<BatchResults<T>> { const results: BatchResults<T> = { successful: [], failed: [], total: records.length, successCount: 0, failedCount: 0 };
const sdk = await this.getSDK(); const chunkSize = options?.chunkSize || 10; const continueOnError = options?.continueOnError !== false;
for (let i = 0; i < records.length; i += chunkSize) { const chunk = records.slice(i, i + chunkSize); const chunkResults = await Promise.all(chunk.map(async (record) => { try { const created = await sdk.collection(collectionName).create(record); return { success: true, data: created }; } catch (error: any) { return { success: false, error: error.message || 'Unknown error', data: record }; } }));
for (const result of chunkResults) { if (result.success && result.data) { results.successful.push(result.data); results.successCount++; } else { results.failed.push({ data: result.data, error: result.error || 'Unknown error' }); results.failedCount++; if (!continueOnError) return results; } } }
return results;}import { batchOperationsService } from './services/batch-operations.service';
const createResults = await batchOperationsService.batchCreate('products', [ { name: 'Product 1', price: 10 }, { name: 'Product 2', price: 20 }, { name: 'Product 3', price: 30 }], { chunkSize: 10, continueOnError: true });
console.log(`Created: ${createResults.successCount}, Failed: ${createResults.failedCount}`);Field types
Section titled “Field types”| Type | Use case | Example fields |
|---|---|---|
string | Plain text, codes, free-form statuses | name, description, sku |
enum | Predefined set of allowed string values | priority, category, orderStatus |
url | URL strings (validated) | website, documentUrl |
file | File references | image, attachment, logo |
editor | Rich text / HTML | description, content, notes |
number | Integers or decimals | quantity, price, age, rating |
boolean | True/false flags | isActive, isVerified, enabled |
date | Date and time values | createdAt, dueDate, timestamp |
json | JSON objects/arrays — query with filterInArray | orderLines, metadata, config |
record | Record ID (for the id field only) | id |
relation | Reference to other collections | categoryId, userId, orderId |
For an enum field, pass a plain string that matches one of the schema’s enumOptions
— no special conversion needed (a TypeScript union type is a natural fit). For a
relation field, pass a { Table, ID } object. For a file field, pass a File
object on write; see File Handling.
Checklist
Section titled “Checklist”-
update/deleteuse the full RecordID (application_id.collection:id). - Relation fields written as
{ Table, ID }objects. -
updateunderstood as a PATCH (partial), not a full replace. - Raw operators (
'=','CONTAINS', …) andoffset/limitused. -
filterInArrayused forjsonarray fields instead of loading everything. -
expandused only when full related records are needed. -
fieldsused to trim payloads.
- Learn the identifier rules in Record IDs.
- Upload and retrieve files in File Handling.
- Organize this logic with the service-layer architecture.