Overview
Credo provides a flexible storage layer with two main components:
- StorageService: Low-level storage interface
- Repository: High-level data access with event emission
StorageService
The StorageService interface defines the contract for storage implementations.
Interface
interface StorageService<T extends BaseRecord> {
save(agentContext: AgentContext, record: T): Promise<void>
update(agentContext: AgentContext, record: T): Promise<void>
delete(agentContext: AgentContext, record: T): Promise<void>
deleteById(agentContext: AgentContext, recordClass: BaseRecordConstructor<T>, id: string): Promise<void>
getById(agentContext: AgentContext, recordClass: BaseRecordConstructor<T>, id: string): Promise<T>
getAll(agentContext: AgentContext, recordClass: BaseRecordConstructor<T>): Promise<T[]>
findByQuery(
agentContext: AgentContext,
recordClass: BaseRecordConstructor<T>,
query: Query<T>,
queryOptions?: QueryOptions
): Promise<T[]>
}
Query Options
type QueryOptions = {
limit?: number
offset?: number
cursor?: {
before?: string
after?: string
}
}
Query Syntax
// Simple query
const query: Query<DidCommConnectionRecord> = {
state: DidCommConnectionState.Completed,
theirLabel: 'Alice',
}
// Advanced query with operators
const advancedQuery: Query<DidCommConnectionRecord> = {
$or: [
{ state: DidCommConnectionState.Completed },
{ state: DidCommConnectionState.ResponseSent },
],
$and: [
{ role: DidCommConnectionRole.Inviter },
],
}
Repository
The Repository class provides high-level access to stored records with automatic event emission.
Constructor
import { Repository } from '@credo-ts/core'
const repository = new Repository(
recordClass,
storageService,
eventEmitter
)
Methods
save()
Save a new record.
await repository.save(agentContext, record)
Returns: Promise<void>
Throws: RecordDuplicateError if record with ID already exists
Emits: RecordSavedEvent
update()
Update an existing record.
await repository.update(agentContext, record)
Returns: Promise<void>
Throws: RecordNotFoundError if record doesn’t exist
Emits: RecordUpdatedEvent
updateByIdWithLock()
Update a record with locking support (if storage supports it).
const updatedRecord = await repository.updateByIdWithLock(
agentContext,
recordId,
async (record) => {
record.metadata.set('counter', record.metadata.get('counter') + 1)
return record
}
)
updateCallback
(record: T) => Promise<T>
required
Callback that receives the record and returns the updated record
Returns: Promise<T>
Emits: RecordUpdatedEvent
delete()
Delete a record.
await repository.delete(agentContext, record)
Returns: Promise<void>
Emits: RecordDeletedEvent
deleteById()
Delete a record by ID.
await repository.deleteById(agentContext, recordId)
Returns: Promise<void>
Throws: RecordNotFoundError if record doesn’t exist
Emits: RecordDeletedEvent
getById()
Retrieve a record by ID.
const record = await repository.getById(agentContext, recordId)
Returns: Promise<T>
Throws: RecordNotFoundError if record doesn’t exist
findById()
Find a record by ID (returns null if not found).
const record = await repository.findById(agentContext, recordId)
if (record) {
console.log('Found record')
} else {
console.log('Record not found')
}
Returns: Promise<T | null>
getAll()
Retrieve all records.
const records = await repository.getAll(agentContext)
Returns: Promise<T[]>
findByQuery()
Find records by query.
const records = await repository.findByQuery(
agentContext,
{
state: DidCommConnectionState.Completed,
},
{
limit: 10,
offset: 0,
}
)
Query options (limit, offset, cursor)
Returns: Promise<T[]>
findSingleByQuery()
Find a single record by query.
const record = await repository.findSingleByQuery(
agentContext,
{ did: 'did:key:...' }
)
if (record) {
console.log('Found record')
}
Options including optional cache key
Returns: Promise<T | null>
Throws: RecordDuplicateError if multiple records match
getSingleByQuery()
Get a single record by query (throws if not found).
const record = await repository.getSingleByQuery(
agentContext,
{ did: 'did:key:...' }
)
Returns: Promise<T>
Throws: RecordNotFoundError if no record matches, RecordDuplicateError if multiple records match
supportsLocking()
Check if the storage service supports locking.
if (repository.supportsLocking(agentContext)) {
// Use updateByIdWithLock for concurrent updates
} else {
// Use regular update
}
Returns: boolean
Creating Custom Repositories
import { BaseRecord, Repository, injectable, inject } from '@credo-ts/core'
class MyCustomRecord extends BaseRecord {
public static readonly type = 'MyCustomRecord'
public name!: string
public value!: string
public getTags() {
return {
...this._tags,
name: this.name,
}
}
}
@injectable()
class MyCustomRepository extends Repository<MyCustomRecord> {
public constructor(
@inject(InjectionSymbols.StorageService) storageService: StorageService<MyCustomRecord>,
eventEmitter: EventEmitter
) {
super(MyCustomRecord, storageService, eventEmitter)
}
// Add custom methods
public async findByName(agentContext: AgentContext, name: string) {
return this.findSingleByQuery(agentContext, { name })
}
}
Storage Events
import { RepositoryEventTypes } from '@credo-ts/core'
agent.events.on(RepositoryEventTypes.RecordSaved, (event) => {
console.log('Record saved:', event.payload.record)
})
agent.events.on(RepositoryEventTypes.RecordUpdated, (event) => {
console.log('Record updated:', event.payload.record)
})
agent.events.on(RepositoryEventTypes.RecordDeleted, (event) => {
console.log('Record deleted:', event.payload.record)
})
Storage Implementations
Credo supports multiple storage backends:
Askar (Recommended)
import { AskarModule } from '@credo-ts/askar'
import { ariesAskar } from '@hyperledger/aries-askar-nodejs'
const agent = new Agent({
// ...
modules: {
askar: new AskarModule({ ariesAskar }),
},
})
Drizzle
import { DrizzleStorageModule } from '@credo-ts/drizzle-storage'
const agent = new Agent({
// ...
modules: {
drizzleStorage: new DrizzleStorageModule({
// Database configuration
}),
},
})
See Also