Custom adapters
Sobj is designed to be highly modular. Connecting to a custom backend — such as local filesystem storage, an internal server API, or a custom database — is as swift as writing a plain JavaScript object.
🦴 Anatomy of an Adapter
An adapter is a function or helper that returns an object satisfying the SobjAdapter contract. It consists of two parts:
- Capabilities definition: A compile-time assertion declaring which operations the adapter supports.
- Operations implementation: Asynchronous functions translating Sobj calls to your backend's operations.
🧪 Example: Building a LocalStorage Adapter
Here is a fully functional custom adapter mapping Sobj keys to the browser's localStorage.
📋 1. Define Capabilities
Capabilities register what operations your adapter implements. Required methods (put, get, head, delete, list) must be marked true. Optional features (like copy, presign, multipart) can be marked false.
import type { SobjAdapter, SobjCapabilities } from 'sobj/adapter'
const capabilities = {
copy: false,
delete: true,
get: true,
head: true,
list: true,
multipart: false,
presign: false,
put: true,
versioning: false,
} as const satisfies SobjCapabilities
type LocalCapabilities = typeof capabilities⚙️ 2. Implement the Operations
Implement the functions matching your capabilities.
import type { SobjObject, SobjMetadata, SobjList } from 'sobj/types'
const localStorageAdapter: SobjAdapter<LocalCapabilities> = {
async put(key, body, options) {
// Standardize input body to a string for local storage
const content = typeof body === 'string' ? body : new TextDecoder().decode(body as any)
localStorage.setItem(key, content)
return {
key,
size: content.length,
contentType: options?.contentType ?? 'text/plain',
} satisfies SobjMetadata
},
async get(key) {
const value = localStorage.getItem(key)
if (value === null) return null // Return null if not found (don't throw)
const encoder = new TextEncoder()
const stream = new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode(value))
controller.close()
}
})
return {
body: stream,
metadata: {
key,
size: value.length,
}
} satisfies SobjObject
},
async head(key) {
const value = localStorage.getItem(key)
if (value === null) return null
return {
key,
size: value.length,
} satisfies SobjMetadata
},
async delete(key) {
localStorage.removeItem(key)
},
async list(options) {
const keys = Object.keys(localStorage).filter(k =>
options?.prefix ? k.startsWith(options.prefix) : true
)
const objects = keys.map(k => ({
key: k,
size: localStorage.getItem(k)?.length ?? 0
}))
return {
objects,
prefixes: [],
hasMore: false,
} satisfies SobjList
}
}🏭 3. Consume the adapter
import { sobj } from 'sobj'
// Consume it!
const storage = sobj(localStorageAdapter)
await storage.put('theme', 'dark')⚠️ Error Handling in Adapters
When an active operation detects a structural failure (e.g. invalid parameters or authorization errors), it should throw a designated SobjError subclass. This allows the host application to catch errors in a provider-agnostic way:
import { SobjPermissionError, SobjNetworkError } from 'sobj/errors'
// If auth token expired in your custom backend api:
throw new SobjPermissionError('Invalid token credentials')
// If fetch request timed out:
throw new SobjNetworkError('Server unreachable')For more details on error matching, read the Errors Reference.