Members
Class
# constant BaseService
Export BaseService with observability mixin applied.
This demonstrates the mixin composition pattern. RawBaseService provides core CRUD operations, while withObservable wraps it to add automatic:
- Timing (operation duration tracking)
- Logging (structured logs for all operations)
- Metrics (success/failure counts, duration stats)
- Event emission (observability.service events)
How Mixins Work: withObservable() is a higher-order function that:
- Takes a service class (RawBaseService)
- Returns a new class that extends it
- Wraps key methods (getById, create, update, etc.)
- Adds pre/post hooks for timing and logging
- Emits events for monitoring
What This Means for Your Code:
const iconService = new IconService();
const icon = await iconService.getById(123);
// Automatically logged:
// "icon-service.getById success 45ms { id: 123 }"
// Automatically emitted:
// Event: observability.service
// { service: 'icon', operation: 'getById', phase: 'success', durationMs: 45 }
// Automatically tracked:
// Metric: operation_duration_ms=45 operation=getById service=icon result=success
In Production: The full composition would include more mixins:
const BaseService =
withObservable(
withCacheable(
withPluggable(
withAccessControl(
withSoftDeletable(
withActivatable(
RawBaseService
))))));
- See:
-
- withObservable For observability mixin documentation
# constant BaseService
CartService
- Extends BaseService so all the standard CRUD flows are consistent.
- Uses CartRepository for carts persistence.
- Uses CartItemRepository for cart_items persistence (same module boundary).
- Uses other modules via their init
Service() for product reads.
# constant IconRepository
Icon repository with cursor pagination support.
Extends RawIconRepository with withCursorPagination mixin to provide efficient keyset-based pagination for large icon datasets (750K+ icons).
Examples
// Cursor paginate icons
const result = await iconRepo.cursorPaginate({
filters: {
price: 'free',
tagIds: [1, 2, 3],
styleId: 5
},
cursor: null,
limit: 20,
sortBy: 'createdAt',
sortOrder: 'desc'
});
// Next page (using cursor from previous page)
const page2 = await iconRepo.cursorPaginate({
filters: { price: 'free' },
cursor: 'eyJpZCI6MTAyMCwiY3JlYXRlZEF0IjoiMjAyNC0wMS0xNVQxMDowMDowMFoifQ==',
limit: 20
});
# categoriesPlugin
Categories Plugin Provides CRUD endpoints for managing categories. Routes:
- GET /category/:page/:pageSize
- GET /category/
- GET /category/:id
- POST /category
- PATCH /category/:id
- DELETE /category/:id Each route supports appropriate query parameters and request bodies as defined in the schemas.
# familiesPlugin
Families Plugin Provides CRUD endpoints for managing families. Routes:
- GET /family/:userId/:page/:pageSize
- GET /family/:page/:pageSize
- GET /family/
- GET /family/:id
- POST /family
- PATCH /family/:id
- DELETE /family/:id Each route supports appropriate query parameters and request bodies as defined in the schemas.
# iconsPlugin
Icons Plugin Provides CRUD endpoints for managing icons. Routes:
- GET /icon/:userId/:page/:pageSize
- GET /icon/:page/:pageSize
- GET /icon
- GET /icon/:id
- POST /icon
- PATCH /icon/:id
- DELETE /icon/:id Each route supports appropriate query parameters and request bodies as defined in the schemas.
# imagesPlugin
Images Plugin Provides CRUD endpoints for managing images. Routes:
- GET /image/:entityType/:entityId/:page/:pageSize
- GET /image/:page/:pageSize
- GET /image/:id
- POST /image
- PATCH /image/:id
- DELETE /image/:id Each route supports appropriate query parameters and request bodies as defined in the schemas.
number
# constant kDEFAULT_MAX_LIMIT
Maximum limit for pagination to prevent excessive data transfer.
# setsPlugin
Sets Plugin Provides CRUD endpoints for managing sets. Routes:
- GET /set/:userId/:page/:pageSize
- GET /set/:page/:pageSize
- GET /set/
- GET /set/:id
- POST /set
- PATCH /set/:id
- DELETE /set/:id Each route supports appropriate query parameters and request bodies as defined in the schemas.
# tagsPlugin
Tags Plugin Provides CRUD endpoints for managing tags. Routes:
- GET /tag/:page/:pageSize
- GET /tag
- GET /tag/:id
- POST /tag
- PATCH /tag/:id
- DELETE /tag/:id Each route supports appropriate query parameters and request bodies as defined in the schemas.
Methods
# protected _applyArrayPositionCursor(query, cursorData, iconIdsOrder, sortOrder) → {Object}
Apply array position cursor condition (for Elasticsearch relevance sorting).
When sorting by Elasticsearch relevance, results are ordered by their position in the iconIdsOrder array. The cursor contains the array position, and we need to filter for items that come after that position.
Parameters:
| Name | Type | Description |
|---|---|---|
query |
Object
|
Objection.js query builder |
cursorData |
Object
|
Decoded cursor data with arrayPosition |
iconIdsOrder |
Array.<number>
|
Ordered array of icon IDs from Elasticsearch |
sortOrder |
string
|
'asc' or 'desc' |
Modified query
Object
Example
// Cursor: { arrayPosition: 20, id: 5005 }
// iconIdsOrder: [1001, 2003, 5005, 3002] (from Elasticsearch)
// Query: WHERE array_position(ARRAY[iconIdsOrder], id) > 20
# protected _applyArrayPositionSort(query, iconIdsOrder, sortOrder) → {Object}
Apply array position sorting (for Elasticsearch relevance).
Orders results by their position in the iconIdsOrder array, which preserves the relevance ranking from Elasticsearch.
Parameters:
| Name | Type | Description |
|---|---|---|
query |
Object
|
Objection.js query builder |
iconIdsOrder |
Array.<number>
|
Ordered array of icon IDs from Elasticsearch |
sortOrder |
string
|
'asc' or 'desc' |
Modified query
Object
Example
// iconIdsOrder: [1001, 2003, 5005, 3002] (from Elasticsearch)
// ORDER BY array_position(ARRAY[iconIdsOrder], id) ASC
# protected _applyCursorCondition(query, cursorData, sortField, sortOrder) → {Object}
Apply cursor condition to query (keyset WHERE clause).
Builds the keyset condition for efficient pagination:
- For DESC: WHERE (sort_field, id) < (cursor_value, cursor_id)
- For ASC: WHERE (sort_field, id) > (cursor_value, cursor_id)
This uses composite index and is O(log n) instead of O(n).
Parameters:
| Name | Type | Description |
|---|---|---|
query |
Object
|
Objection.js query builder |
cursorData |
Object
|
Decoded cursor data |
sortField |
string
|
Database field to sort by (snake_case) |
sortOrder |
string
|
'asc' or 'desc' |
Modified query
Object
# protected _applyFilters(query, filters) → {Object}
Apply filters to query.
This is meant to be overridden in subclasses to handle domain-specific filters. The base implementation does nothing.
Subclasses should apply filters like:
- price (free, premium, all)
- tagIds (array of tag IDs)
- styleId (design style)
- userId (creator)
- searchTerm (text search)
- etc.
Parameters:
| Name | Type | Description |
|---|---|---|
query |
Object
|
Objection.js query builder |
filters |
Object
|
Filter criteria |
Modified query
Object
Example
// Override in IconRepository
_applyFilters(query, filters) {
if (filters.price) {
query.where('price', filters.price);
}
if (filters.tagIds && filters.tagIds.length > 0) {
query.whereIn('tags.id', filters.tagIds);
}
return query;
}
# protected _createArrayPositionCursor(entity, iconIdsOrder) → {string}
Create cursor from entity for array position sorting.
For array position sorting, the cursor contains:
- arrayPosition: Position in the iconIdsOrder array
- id: Icon ID (for validation)
- sortType: 'arrayPosition' (to distinguish from field-based cursors)
Parameters:
| Name | Type | Description |
|---|---|---|
entity |
Object
|
Entity instance or plain object |
iconIdsOrder |
Array.<number>
|
Ordered array of icon IDs |
Encoded cursor token
string
Example
// Entity: { id: 5005, name: 'home-icon' }
// iconIdsOrder: [1001, 2003, 5005, 3002] (from Elasticsearch)
// Cursor: { arrayPosition: 3, id: 5005, sortType: 'arrayPosition' }
# protected _toCamelCase(str) → {string}
Convert snake_case to camelCase.
Parameters:
| Name | Type | Description |
|---|---|---|
str |
string
|
snake_case string |
camelCase string
string
# protected _toSnakeCase(str) → {string}
Convert camelCase to snake_case.
Parameters:
| Name | Type | Description |
|---|---|---|
str |
string
|
camelCase string |
snake_case string
string
# assertService()
Tiny guard to make missing service configs fail loudly & clearly.
# buildSchemas(entityClass) → {Object}
Build JSON schemas for common response types from an entity class.
Generates schemas for single entities, arrays of entities, paginated responses, and delete confirmations. These schemas are used for Fastify route response validation.
Parameters:
| Name | Type | Description |
|---|---|---|
entityClass |
function
|
Entity class with static getJsonSchema() method |
Object containing:
- entitySchema: Schema for single entity
- listSchema: Schema for array of entities
- paginatedSchema: Schema for paginated response with metadata
- deletedSchema: Schema for delete confirmation { deleted: boolean }
Object
Example
const schemas = buildSchemas(IconEntity);
// schemas.paginatedSchema can be used in route response validation
# createEntityFromModel(ModelClass, extraMethodsopt, optionsopt) → {function}
Factory function to create an immutable Entity class from an Objection.js Model.
This is the primary way to define entities in the system. It automatically:
- Converts snake_case database columns to camelCase entity properties
- Derives a JSON schema from the Model's jsonSchema
- Filters hidden fields (passwords, tokens, etc.)
- Materializes related entities (lazy-loaded to avoid circular dependencies)
- Freezes instances for immutability
- Validates data against the derived schema
Why This Pattern?
- Separation of Concerns: Database models (Objection) vs. domain entities (business logic)
- Immutability: Frozen entities prevent accidental mutations
- Type Safety: JSON schema provides runtime validation
- Security: Hidden fields prevent sensitive data leakage
- Flexibility: Related entities can be included/excluded dynamically
Parameters:
| Name | Type | Attributes | Default | Description |
|---|---|---|---|---|
ModelClass |
Object
|
Objection.js Model class with jsonSchema |
||
extraMethods |
Object
|
<optional> |
{} | Additional methods to add to entity instances |
options |
Object
|
<optional> |
{} | Entity configuration options |
hiddenFields |
Array.<string>
|
<optional> |
[] | Fields to hide (e.g., ['password', 'resetToken']) |
allowedColumns |
Array.<string>
|
<optional> |
[] | If provided, only these fields are included (whitelist) |
relatedEntities |
Object.<string, function()>
|
<optional> |
{} | Related entity loaders (lazy functions) |
freeze |
boolean
|
<optional> |
true | Whether to freeze instances (default: true) |
Entity class extending BaseEntity with derived schema
function
Examples
// Basic entity
const IconEntity = createEntityFromModel(IconModel, {}, {
hiddenFields: ['internalId']
});
const icon = new IconEntity({ id: 1, name: 'home', internal_id: 'secret' });
console.log(icon.name); // 'home' (camelCase)
console.log(icon.internalId); // undefined (hidden)
Object.isFrozen(icon); // true
// Entity with related entities (lazy-loaded to avoid circular deps)
const IconEntity = createEntityFromModel(IconModel, {
// Extra methods
isPublished() {
return this.isActive && !this.isDeleted;
}
}, {
hiddenFields: ['secretKey'],
relatedEntities: {
set: () => require('./SetEntity'), // Lazy load
images: () => require('./ImageEntity')
}
});
const iconData = {
id: 1,
name: 'home',
set: { id: 10, name: 'Material' },
images: [{ id: 100, url: '/img.png' }]
};
const icon = new IconEntity(iconData);
console.log(icon.set instanceof SetEntity); // true
console.log(icon.images[0] instanceof ImageEntity); // true
console.log(icon.isPublished()); // true (custom method)
// Entity with allowedColumns (whitelist)
const PublicIconEntity = createEntityFromModel(IconModel, {}, {
allowedColumns: ['id', 'name', 'svgPath', 'isActive']
// Only these fields will be included, all others dropped
});
# createItem(config) → {function}
Factory function to generate a create (POST) route.
Creates a POST route that accepts JSON body, validates it, and creates a new item. Request body is passed directly to service.create().
Parameters:
| Name | Type | Attributes | Default | Description |
|---|---|---|---|---|
config |
Object
|
Route configuration |
||
path |
string
|
<optional> |
'/' | Route path |
service |
Object
|
Service instance with create() method |
||
schema |
Object
|
Fastify route schema for validation |
||
entityClass |
function
|
<optional> |
Entity class (not currently used) |
|
name |
string
|
<optional> |
'resource' | Resource name for error messages |
preHandler |
Array
|
function
|
<optional> |
[] | Fastify preHandler hooks |
Async function that registers the route with Fastify
function
Example
await createItem({
route: '/',
service: iconService,
schema: schemas.CreateSchema,
name: 'icon'
})(fastify);
# async cursorPaginate(options) → {Promise.<Object>|Array|Object|boolean|boolean|string|null|string|null|number}
Paginate using cursor-based keyset pagination.
Builds a query with:
- Apply all filters FIRST (WHERE clauses)
- Apply cursor condition (keyset WHERE)
- Sort by specified fields + id for uniqueness
- Limit results
- Generate cursors for next/prev pages
Keyset Condition:
For sortBy='createdAt', sortOrder='desc':
WHERE (created_at, id) < (cursor.createdAt, cursor.id)
This uses a composite index and is O(log n) instead of O(n) like OFFSET.
Parameters:
| Name | Type | Attributes | Default | Description |
|---|---|---|---|---|
options |
Object
|
Pagination options |
||
filters |
Object
|
<optional> |
{} | Filter criteria |
cursor |
string
|
<optional> |
null | Cursor token from previous page |
limit |
number
|
<optional> |
20 | Page size (max 100) |
sortBy |
string
|
<optional> |
'createdAt' | Field to sort by |
sortOrder |
string
|
<optional> |
'desc' | 'asc' or 'desc' |
includeTotalCount |
boolean
|
<optional> |
false | Whether to count total (expensive!) |
entityClass |
function
|
<optional> |
Entity class for results |
|
entityOptions |
Object
|
<optional> |
{} | Entity construction options |
trx |
Object
|
<optional> |
Knex transaction |
Pagination result
Promise.<Object>
result.results - Entity instances for current page
Array
result.pageInfo - Pagination metadata
Object
result.pageInfo.hasNextPage - True if more results exist
boolean
result.pageInfo.hasPreviousPage - True if previous page exists
boolean
result.pageInfo.startCursor - Cursor for first item
string
|
null
result.pageInfo.endCursor - Cursor for last item
string
|
null
[result.pageInfo.totalCount] - Total count (if includeTotalCount=true)
number
Examples
// First page (newest icons)
const page1 = await repo.cursorPaginate({
filters: { price: 'free' },
cursor: null,
limit: 20,
sortBy: 'createdAt',
sortOrder: 'desc'
});
// Next page (using cursor from previous page)
const page2 = await repo.cursorPaginate({
filters: { price: 'free' },
cursor: 'eyJpZCI6MTAyMCwiY3JlYXRlZEF0IjoiMjAyNC0wMS0xNVQxMDowMDowMFoifQ==',
limit: 20,
sortBy: 'createdAt',
sortOrder: 'desc'
});
// With multiple filters (search facets)
const results = await repo.cursorPaginate({
filters: {
price: 'free',
tagIds: [1, 2, 3],
styleId: 5,
userId: 123,
searchTerm: 'home'
},
cursor: null,
limit: 20
});
// Ascending sort (oldest first)
const oldestFirst = await repo.cursorPaginate({
filters: {},
cursor: null,
limit: 20,
sortBy: 'createdAt',
sortOrder: 'asc'
});
# deleteItem(config) → {function}
Factory function to generate a delete (DELETE) route.
Creates a DELETE route that removes an item by ID. Returns 404 if not found. Returns { deleted: true } on success, { deleted: false } if deletion failed.
Parameters:
| Name | Type | Attributes | Default | Description |
|---|---|---|---|---|
config |
Object
|
Route configuration |
||
path |
string
|
<optional> |
'/:id' | Route path with ID parameter |
idParam |
string
|
<optional> |
'id' | Name of ID parameter in path |
parseId |
function
|
<optional> |
Function to parse ID from string (default: Number) |
|
service |
Object
|
Service instance with getById() and delete() methods |
||
schema |
Object
|
Fastify route schema for validation |
||
entityClass |
function
|
<optional> |
Entity class (not currently used) |
|
name |
string
|
<optional> |
'resource' | Resource name for error messages |
preHandler |
Array
|
function
|
<optional> |
[] | Fastify preHandler hooks |
Async function that registers the route with Fastify
function
Example
await deleteItem({
route: '/:id',
service: iconService,
schema: schemas.DeleteSchema,
name: 'icon'
})(fastify);
# ensureTrailingSlash(path) → {string}
Ensure a path ends with a trailing slash.
Parameters:
| Name | Type | Description |
|---|---|---|
path |
string
|
string
# getItem(config) → {function}
Factory function to generate a get-by-ID route.
Creates a GET route that fetches a single item by ID. Returns 404 if not found. Supports custom ID parameter names and parsing functions.
Parameters:
| Name | Type | Attributes | Default | Description |
|---|---|---|---|---|
config |
Object
|
Route configuration |
||
path |
string
|
<optional> |
'/:id' | Route path with ID parameter |
idParam |
string
|
<optional> |
'id' | Name of ID parameter in path |
parseId |
function
|
<optional> |
Function to parse ID from string (default: Number) |
|
service |
Object
|
Service instance with getById() method |
||
schema |
Object
|
Fastify route schema for validation |
||
entityClass |
function
|
<optional> |
Entity class (not currently used) |
|
name |
string
|
<optional> |
'resource' | Resource name for error messages |
preHandler |
Array
|
function
|
<optional> |
[] | Fastify preHandler hooks |
Async function that registers the route with Fastify
function
Examples
await getItem({
route: '/:id',
service: iconService,
schema: schemas.GetItemSchema,
name: 'icon'
})(fastify);
// With UUID parsing
await getItem({
route: '/:uuid',
idParam: 'uuid',
parseId: (v) => v.toString(),
service: userService,
schema: schemas.GetUserSchema,
name: 'user'
})(fastify);
# initCartItemService() → {CartItemService}
Initialize the cart item service
- If the initialization fails
Error
- The initialized cart item service
# initCartService() → {CartService}
Initializes the CartService with injected dependencies.
# initCategoryService() → {CategoryService}
Initializes the CategoryService with injected dependencies.
# initDownloadService() → {DownloadService}
Initializes the DownloadService with injected dependencies.
# initEntityToCategoriesService() → {EntityToCategoriesService}
Initializes the EntityToCategoriesService with injected dependencies.
EntityToCategoriesService
# initEntityToTagsService() → {EntityToTagsService}
Initializes the EntityToTagsService with injected dependencies.
EntityToTagsService
# initFavoriteService() → {FavoriteService}
Initializes the FavoriteService with injected dependencies.
# initIconService() → {IconService}
Initializes the IconService with injected dependencies.
# initImageService() → {ImageService}
Initializes the ImageService with injected dependencies.
ImageService
# initImageTypeService() → {ImageTypeService}
Initializes the ImageTypeService with injected dependencies.
ImageTypeService
# initPaymentTypeService() → {PaymentTypeService}
Initializes the PaymentTypeService with injected dependencies.
PaymentTypeService
# initProductTypeService() → {ProductTypeService}
Initializes the ProductTypeService with injected dependencies.
# initPurchasedItemService() → {PurchasedItemService}
Initializes the PurchasedItemService with injected dependencies.
PurchasedItemService
# initStyleService() → {StyleService}
Initializes the StyleService with injected dependencies.
StyleService
# initTagService() → {TagService}
Initializes the TagService with injected dependencies.
# initTransactionCategoryService() → {TransactionCategoryService}
Initializes the TransactionCategoryService with injected dependencies.
# initTransactionItemService() → {TransactionItemService}
Initializes the TransactionItemService with injected dependencies.
# initTransactionService(params) → {TransactionService}
Initialize the transaction service
Parameters:
| Name | Type | Description |
|---|---|---|
params |
Object
|
|
DB |
Object
|
The database instance |
ModelsRegistry |
Object
|
The models registry instance |
couponCode |
string
|
The coupon code to be used |
- If the coupon code is invalid
Error
- The initialized transaction service
# initTransactionTypeService() → {TransactionTypeService}
Initializes the TransactionTypeService with injected dependencies.
# initTransactionsViewService() → {TransactionsViewService}
Initializes the TransactionsViewService with injected dependencies.
# list(config) → {function}
Factory function to generate a paginated list route with filtering.
Creates a GET route that returns paginated results with filtering support. The route delegates to service.paginate() and supports dynamic WHERE clauses via the getWhere function.
Parameters:
| Name | Type | Attributes | Default | Description |
|---|---|---|---|---|
config |
Object
|
Route configuration |
||
route |
string
|
Route path (e.g., '/:page/:pageSize') |
||
service |
Object
|
Service instance with paginate() method |
||
schema |
Object
|
Fastify route schema for validation |
||
getWhere |
function
|
<optional> |
Function to build WHERE clause from request Receives (req) and returns object of filter conditions |
|
page |
number
|
<optional> |
1 | Default page number |
pageSize |
number
|
<optional> |
100 | Default page size |
preHandler |
Array
|
function
|
<optional> |
[] | Fastify preHandler hooks (e.g., authentication, authorization) |
Async function that registers the route with Fastify
function
Examples
// Generate a list route with filtering
await list({
route: '/:page/:pageSize',
service: iconService,
schema: schemas.IconPaginatedSchema,
getWhere: (req) => {
const filters = {};
if (req.query.setId) filters.setId = Number(req.query.setId);
if (req.query.isActive !== undefined) filters.isActive = req.query.isActive;
return filters;
}
})(fastify);
// With authentication
await list({
route: '/:page/:pageSize',
service: iconService,
schema: schemas.IconPaginatedSchema,
preHandler: [authenticate, authorize([UserRoles.Admin])]
})(fastify);
# makeCrudPlugin(config) → {function}
Higher-level factory to create a complete CRUD plugin from route definitions.
Generates multiple CRUD routes (list, getItem, createItem, patchItem, deleteItem) from an array of route configurations. Simplifies plugin creation by providing common service and entity once and defining multiple routes declaratively.
Parameters:
| Name | Type | Description |
|---|---|---|
config |
Object
|
Plugin configuration |
service |
Object
|
Service instance shared across all routes |
entityClass |
function
|
Entity class shared across all routes |
name |
string
|
Resource name for error messages (e.g., 'icon') |
routes |
Array.<Object>
|
Array of route configurations |
routes[].kind |
string
|
Route type: 'list', 'getItem', 'createItem', 'patchItem', 'deleteItem' |
routes[ |
Object
|
Additional route-specific config (merged with common config) |
If service, entityClass, or name is missing
Error
If unsupported route kind is specified
Error
Async Fastify plugin function
function
Example
const iconCrudPlugin = makeCrudPlugin({
service: iconService,
entityClass: IconEntity,
name: 'icon',
routes: [
{
kind: 'list',
route: '/:page/:pageSize',
schema: schemas.IconPaginatedSchema,
getWhere: (req) => ({ setId: req.query.setId })
},
{
kind: 'getItem',
path: '/:id',
schema: schemas.GetItemSchema
},
{
kind: 'createItem',
path: '/',
schema: schemas.CreateSchema,
preHandler: [authenticate]
}
]
});
// Register plugin with Fastify
await fastify.register(iconCrudPlugin, { prefix: '/icons' });
# mapKeys(obj, fn) → {Object}
Map an object's keys using a transform function.
Parameters:
| Name | Type | Description |
|---|---|---|
obj |
Object
|
|
fn |
function
|
Object
# patchItem(config) → {function}
Factory function to generate a patch (PATCH/update) route.
Creates a PATCH route that updates an existing item by ID. Returns 404 if not found. Fetches the item before and after update to ensure existence and return fresh data.
Parameters:
| Name | Type | Attributes | Default | Description |
|---|---|---|---|---|
config |
Object
|
Route configuration |
||
path |
string
|
<optional> |
'/:id' | Route path with ID parameter |
idParam |
string
|
<optional> |
'id' | Name of ID parameter in path |
parseId |
function
|
<optional> |
Function to parse ID from string (default: Number) |
|
service |
Object
|
Service instance with getById() and update() methods |
||
schema |
Object
|
Fastify route schema for validation |
||
entityClass |
function
|
<optional> |
Entity class (not currently used) |
|
name |
string
|
<optional> |
'resource' | Resource name for error messages |
preHandler |
Array
|
function
|
<optional> |
[] | Fastify preHandler hooks |
Async function that registers the route with Fastify
function
Example
await patchItem({
route: '/:id',
service: iconService,
schema: schemas.UpdateSchema,
name: 'icon'
})(fastify);
# withCursorPagination(BaseClass) → {function}
Mixin to add cursor pagination to a repository class.
Adds cursorPaginate() method that implements keyset pagination with:
- Multi-field sort support (e.g., created_at DESC, id DESC)
- Filter application before cursor (search facets)
- Cursor encoding/decoding
- Metadata (hasNext, hasPrev, cursors)
- Forward and backward paging
Method Added:
async cursorPaginate(options)
Options:
filters- Object with filter criteria (price, tagIds, styleId, etc.)cursor- Encoded cursor token from previous pagelimit- Page size (default: 20, max: 100)sortBy- Field to sort by (default: 'createdAt')sortOrder- 'asc' or 'desc' (default: 'desc')includeTotalCount- Whether to count total matches (expensive!)entityClass- Entity class for resultsentityOptions- Options for entity constructiontrx- Knex transaction
Performance:
- First page (no cursor): O(log n + limit)
- Subsequent pages (with cursor): O(log n + limit)
- Total count (if requested): O(n) - use sparingly!
Parameters:
| Name | Type | Description |
|---|---|---|
BaseClass |
function
|
Repository class to extend |
Enhanced repository class with cursor pagination
function
Examples
// Create repository with cursor pagination
class RawIconRepository {
constructor(opts) {
this.model = IconModel;
}
}
const IconRepository = withCursorPagination(RawIconRepository);
// Use in service
const result = await iconRepo.cursorPaginate({
filters: {
price: 'free',
tagIds: [1, 2, 3]
},
cursor: null, // First page
limit: 20,
sortBy: 'createdAt',
sortOrder: 'desc'
});
Type Definitions
Object
# CategoryPaginatedSchema
Properties:
| Name | Type | Attributes | Description |
|---|---|---|---|
results |
Array.<CategorySchema>
|
||
total |
number
|
||
page |
number
|
||
pageSize |
number
|
||
totalPages |
number
|
||
cacheHit |
boolean
|
<optional> |
Object
# CategorySchema
Properties:
| Name | Type | Description |
|---|---|---|
id |
number
|
|
name |
string
|
|
isActive |
boolean
|
|
createdAt |
string
|
(date-time) |
updatedAt |
string
|
(date-time) |
Object
# CreateFamilySchema
Properties:
| Name | Type | Description |
|---|---|---|
name |
string
|
|
price |
number
|
|
description |
string
|
|
licenseId |
number
|
|
teamId |
number
|
|
uniqueId |
string
|
|
userId |
number
|
|
sort |
number
|
|
isActive |
boolean
|
Object
# CreateIconSchema
Properties:
| Name | Type | Description |
|---|---|---|
name |
string
|
|
price |
number
|
|
width |
number
|
|
height |
number
|
|
setId |
number
|
|
styleId |
number
|
|
teamId |
number
|
|
userId |
number
|
|
uniqueId |
string
|
|
licenseId |
number
|
|
isActive |
boolean
|
Object
# CreateImageSchema
Properties:
| Name | Type | Description |
|---|---|---|
entityId |
number
|
|
entityType |
string
|
|
imageTypeId |
number
|
|
imageHash |
string
|
|
visibility |
string
|
|
access |
string
|
|
name |
string
|
|
fileType |
string
|
|
url |
string
|
|
uniqueId |
string
|
Object
# CreateSetSchema
Properties:
| Name | Type | Description |
|---|---|---|
name |
string
|
|
price |
number
|
|
familyId |
number
|
|
licenseId |
number
|
|
typeId |
number
|
|
styleId |
number
|
|
teamId |
number
|
|
uniqueId |
string
|
|
userId |
number
|
|
description |
string
|
|
sort |
number
|
|
isActive |
boolean
|
Object
# DeleteCategorySchema
Properties:
| Name | Type | Description |
|---|---|---|
id |
number
|
|
200 |
CategoryDeletedSchema
|
|
404 |
Object
|
Object
# FamilyPaginatedSchema
Properties:
| Name | Type | Description |
|---|---|---|
results |
Array.<FamilySchema>
|
|
total |
number
|
|
page |
number
|
|
pageSize |
number
|
|
totalPages |
number
|
|
cacheHit |
boolean
|
Object
# FamilySchema
Properties:
| Name | Type | Description |
|---|---|---|
id |
number
|
|
name |
string
|
|
price |
number
|
|
description |
string
|
|
licenseId |
number
|
|
teamId |
number
|
|
uniqueId |
string
|
|
userId |
number
|
|
sort |
number
|
|
isActive |
boolean
|
|
createdAt |
string
|
(date-time) |
updatedAt |
string
|
(date-time) |
isDeleted |
boolean
|
Object
# GetCategorySchema
Properties:
| Name | Type | Description |
|---|---|---|
id |
number
|
|
200 |
CategorySchema
|
|
404 |
Object
|
Object
# IconPaginatedSchema
Properties:
| Name | Type | Description |
|---|---|---|
results |
Array.<IconSchema>
|
|
total |
number
|
|
page |
number
|
|
pageSize |
number
|
|
totalPages |
number
|
|
cacheHit |
boolean
|
Object
# IconSchema
Properties:
| Name | Type | Description |
|---|---|---|
id |
number
|
|
name |
string
|
|
price |
number
|
|
width |
number
|
|
height |
number
|
|
setId |
number
|
|
styleId |
number
|
|
teamId |
number
|
|
userId |
number
|
|
uniqueId |
string
|
|
licenseId |
number
|
|
isActive |
boolean
|
|
createdAt |
string
|
(date-time) |
updatedAt |
string
|
(date-time) |
isDeleted |
boolean
|
Object
# ImagePaginatedSchema
Properties:
| Name | Type | Description |
|---|---|---|
results |
Array.<ImageSchema>
|
|
total |
number
|
|
page |
number
|
|
pageSize |
number
|
|
totalPages |
number
|
|
cacheHit |
boolean
|
Object
# ImageSchema
Properties:
| Name | Type | Description |
|---|---|---|
id |
number
|
|
entityId |
number
|
|
entityType |
string
|
|
imageTypeId |
number
|
|
imageHash |
string
|
|
visibility |
string
|
|
access |
string
|
|
name |
string
|
|
fileType |
string
|
|
url |
string
|
|
uniqueId |
string
|
|
createdAt |
string
|
(date-time) |
updatedAt |
string
|
(date-time) |
isDeleted |
boolean
|
Object
# ListCategoriesSchema
Properties:
| Name | Type | Attributes | Description |
|---|---|---|---|
page |
number
|
<optional> |
|
pageSize |
number
|
<optional> |
|
isActive |
boolean
|
<optional> |
|
200 |
Array.<CategorySchema>
|
Object
# ListFamiliesSchema
Properties:
| Name | Type | Attributes | Description |
|---|---|---|---|
page |
number
|
<optional> |
|
pageSize |
number
|
<optional> |
|
userId |
number
|
<optional> |
|
teamId |
number
|
<optional> |
|
licenseId |
number
|
<optional> |
|
isActive |
boolean
|
<optional> |
|
isDeleted |
boolean
|
<optional> |
|
200 |
Array.<FamilySchema>
|
Object
# ListIconsSchema
Properties:
| Name | Type | Attributes | Description |
|---|---|---|---|
page |
number
|
<optional> |
|
pageSize |
number
|
<optional> |
|
setId |
number
|
<optional> |
|
styleId |
number
|
<optional> |
|
teamId |
number
|
<optional> |
|
userId |
number
|
<optional> |
|
isActive |
boolean
|
<optional> |
|
isDeleted |
boolean
|
<optional> |
|
200 |
Array.<IconSchema>
|
Object
# ListImagesSchema
Properties:
| Name | Type | Attributes | Description |
|---|---|---|---|
page |
number
|
<optional> |
|
pageSize |
number
|
<optional> |
|
entityType |
string
|
<optional> |
|
entityId |
number
|
<optional> |
|
isDeleted |
boolean
|
<optional> |
|
200 |
Array.<ImageSchema>
|
Object
# ListSetsSchema
Properties:
| Name | Type | Attributes | Description |
|---|---|---|---|
page |
number
|
<optional> |
|
pageSize |
number
|
<optional> |
|
familyId |
number
|
<optional> |
|
isActive |
boolean
|
<optional> |
|
200 |
Array.<SetSchema>
|
Object
# ListTagsSchema
Properties:
| Name | Type | Attributes | Description |
|---|---|---|---|
page |
number
|
<optional> |
|
pageSize |
number
|
<optional> |
|
isActive |
boolean
|
<optional> |
|
200 |
Array.<TagSchema>
|
Object
# SetPaginatedSchema
Properties:
| Name | Type | Description |
|---|---|---|
results |
Array.<SetSchema>
|
|
total |
number
|
|
page |
number
|
|
pageSize |
number
|
|
totalPages |
number
|
|
cacheHit |
boolean
|
Object
# SetSchema
Properties:
| Name | Type | Description |
|---|---|---|
id |
number
|
|
name |
string
|
|
price |
number
|
|
familyId |
number
|
|
licenseId |
number
|
|
typeId |
number
|
|
styleId |
number
|
|
teamId |
number
|
|
uniqueId |
string
|
|
userId |
number
|
|
description |
string
|
|
sort |
number
|
|
isActive |
boolean
|
|
createdAt |
string
|
(date-time) |
updatedAt |
string
|
(date-time) |
isDeleted |
boolean
|
Object
# TagPaginatedSchema
Properties:
| Name | Type | Description |
|---|---|---|
results |
Array.<TagSchema>
|
|
total |
number
|
|
page |
number
|
|
pageSize |
number
|
|
totalPages |
number
|
|
cacheHit |
boolean
|
Object
# TagSchema
Properties:
| Name | Type | Description |
|---|---|---|
id |
number
|
|
name |
string
|
|
isActive |
boolean
|
|
createdAt |
string
|
(date-time) |
updatedAt |
string
|
(date-time) |
Object
# UpdateCategorySchema
Properties:
| Name | Type | Description |
|---|---|---|
id |
number
|
|
body |
Object
|
|
200 |
CategorySchema
|
|
404 |
Object
|