@nlabs/reaktor
v1.8.0
Published
GraphQL and ArangoDB application platform for typed actions, authentication, billing, media, messaging, and realtime services.
Maintainers
Readme
Reaktor
A powerful GraphQL API framework built with TypeScript, ArangoDB, and AWS Lambda. Reaktor provides a complete backend solution for social platforms with user management, posts, messaging, reactions, and more.
Features
- GraphQL API - Built with graphql-compose for type-safe schema composition
- ArangoDB - High-performance multi-model database with graph capabilities
- TypeScript - Full type safety throughout the codebase
- Modular Architecture - Clean separation of concerns with actions, adapters, and utilities
- Zod Validation - Runtime type checking and data validation
- JWT Authentication - Secure session management
- AWS Lambda Ready - Serverless deployment support
- Comprehensive Testing - Jest-based testing with utilities
- Internationalization - Database-driven i18n system
- Feature flags - Code-owned boolean and fixed-choice flags with database overrides (guide)
- Secure billing setup - Stripe-hosted card collection without raw card data passing through Reaktor (guide)
Peer Dependencies
This package requires the following peer dependencies to be installed in your project:
npm install graphql@^16.10.0 graphql-compose@^9.1.0Getting Started
- Download the lastest version of ArangoDb and install.
- Clone the repo and install the necessary node modules:
$ npm install # Install Node modules listed in ./package.json (may take a while the first time)
$ npm run build # Compile TypeScript
$ npm test # Run testsFor detailed setup instructions, see Development Guide.
Architecture
Reaktor follows a clean, layered architecture designed for maintainability and scalability:
New Modular Structure
The codebase has been refactored to follow modern patterns:
- Shared Utilities: Common functionality extracted into reusable utility functions
- Single-Responsibility Functions: Actions are broken down into focused, composable functions
- Type Safety: Comprehensive TypeScript types and Zod validation throughout
- Consistent Error Handling: Standardized error logging and exception handling
- Constants Over Magic Strings: Centralized constants for collections, edges, and enums
Query Builder System
Reaktor provides a flexible query system built on ArangoDB's AQL:
- Composable Queries: Build complex queries from simple building blocks
- Type-Safe Parameters: All queries use parameterization to prevent injection attacks
- Flexible Filtering: Filter by any field including custom properties
- Advanced Operators: Support for comparison, string, and array operators
- Nested Conditions: Complex AND/OR logic for sophisticated queries
See Query System Documentation for detailed examples.
Refactored Action Pattern
Actions now follow a consistent, maintainable pattern:
export const addPost = async (context: ApiContext, input: PostInputType): Promise<PostType> => {
// Validate and parse input
const parsedPost = parseAndPreparePost(input, context.session.userId);
// Perform database operation
const savedPost = await insertPost(context, parsedPost);
// Handle related operations
return await handleTagsForPost(context, savedPost, input.tags);
};Benefits:
- Easier to test and maintain
- Clear separation of concerns
- Reusable helper functions
- Consistent error handling
Layer Architecture
┌─────────────────────────────────────────────────────────────┐
│ GraphQL Layer │
│ (queries/, mutations/, objectTypes/) │
└────────────────┬────────────────────────────────────────────┘
│
┌────────────────▼────────────────────────────────────────────┐
│ Actions Layer │
│ (actions/) - Domain-specific business logic │
└────────────────┬────────────────────────────────────────────┘
│
┌────────────────▼────────────────────────────────────────────┐
│ Adapters Layer │
│ (adapters/) - Data transformation & validation │
└────────────────┬────────────────────────────────────────────┘
│
┌────────────────▼────────────────────────────────────────────┐
│ Utilities Layer │
│ (utils/) - Shared helpers & database utilities │
└────────────────┬────────────────────────────────────────────┘
│
┌────────────────▼────────────────────────────────────────────┐
│ ArangoDB Database │
│ Collections & Edges - Document and graph storage │
└─────────────────────────────────────────────────────────────┘For comprehensive architecture documentation, see Architecture Guide.
Usage
npm run dev
Runs the webpack build system just like in compile but enables HMR. The webpack dev server can be found at localhost:3000.
npm run compile
Runs the Webpack build system with your current NODE_ENV and compiles the application to disk (~/dist). Production builds will fail on eslint errors (but not on warnings).
npm run test
Runs unit tests with Karma.
npm run deploy
Helper script to run tests and then, on success, compile your application.
npm run db:get:s3
Get a copy of the latest database data from S3 and import into your local database
npm run db:get:direct
Get a copy of the latest database data from the production database and import into your local database
npm run db:save
Get a copy of the latest database data from the production database and save to S3
Configuration
Basic project configuration can be found in ~/src/config.ts. The configuration system supports environment-based settings and can be customized via environment variables.
Required Environment Variables
SESSION_SECRET- Required - Secret key used for JWT token signing and verification. This must be set for authentication to work.
Example:
export SESSION_SECRET="your-secret-key-here"ArangoDB Configuration
The ArangoDB connection can be configured using the following environment variables:
ARANGODB_URL- The ArangoDB server URL (default:http://localhost)ARANGODB_PORT- The ArangoDB server port (default:8529)ARANGODB_DATABASE- The database name (default:reaktor)ARANGODB_USERNAME- The database username (default:root)ARANGODB_PASSWORD- The database password (default:password)
Example:
export SESSION_SECRET="your-secret-key-here"
export ARANGODB_URL="http://my-arango-server"
export ARANGODB_PORT="8530"
export ARANGODB_DATABASE="my-database"
export ARANGODB_USERNAME="myuser"
export ARANGODB_PASSWORD="mypassword"The configuration is environment-aware and supports local, development, test, and production environments. The active environment is determined by the NODE_ENV or stage environment variable.
Anonymous RUM Configuration
The public rum.track mutation sanitizes GothamJS journey events and sends them to Amazon CloudWatch RUM with PutRumEvents. Reaktor does not persist RUM events in ArangoDB.
AWS_REGION- AWS region containing the CloudWatch RUM app monitors.AWS_RUM_APP_MONITORS- JSON object mapping each public application ID to its CloudWatch RUM app-monitor UUID. Values may also be objects withid,name,version,domain, and optional resource-policyaliasfields.
The Lambda or server role must allow rum:PutRumEvents for the configured app monitors. APIs that create tagged monitors must also allow rum:CreateAppMonitor, rum:GetAppMonitor, rum:GetAppMonitorData, and rum:TagResource. The same mapping can be supplied programmatically at aws.rum.appMonitors using Config.set().
Each event includes its sanitized public appId in the event details and as the applicationId metadata attribute so CloudWatch RUM filters, Logs Insights, and custom dashboards can filter shared-monitor data. Use a separate app monitor per application when AWS-native metrics and access boundaries must remain isolated.
When supplied as event properties, Reaktor promotes browserName, browserVersion, browserLanguage, pageId/pageID, parentPageId/parentPage, interaction, deviceType, osName, and osVersion into canonical AWS RUM metadata fields.
Reaktor does not send authentication state, cookies, IP addresses, user agents, referrers, form values, common identity fields, or client-supplied geolocation. AWS can enrich accepted RUM events with coarse location metadata such as country, subdivision, and locality. Journey IDs remain memory-only in the browser and reset when the application runtime restarts.
Programmatic Configuration
You can also override configuration programmatically using the Config.set() method:
import {Config} from '@nlabs/reaktor';
Config.set({
arangodb: {
url: 'http://custom-server',
port: '8530',
database: 'custom-db'
}
}, 'development');Structure
Reaktor follows a modular, domain-driven structure optimized for GraphQL API development:
└── src
├── actions/ # Business logic for each domain (refactored & modular)
│ ├── users.ts # User management actions
│ ├── posts.ts # Post management actions
│ ├── groups.ts # Group management actions
│ ├── reactions.ts # Reaction handling
│ └── ... # Other domain actions
├── adapters/ # Data transformation and parsing with Zod
│ ├── userAdapter.ts # User data validation
│ ├── postAdapter.ts # Post data validation
│ └── ... # Other adapters
├── handlers/ # Request handlers
│ └── graphqlHandler.ts # GraphQL request processing
├── mutations/ # GraphQL mutations
│ ├── users.ts # User mutations
│ ├── posts.ts # Post mutations
│ └── ... # Other mutations
├── objectTypes/ # GraphQL type definitions
│ ├── user.ts # User type schema
│ ├── post.ts # Post type schema
│ └── ... # Other type definitions
├── queries/ # GraphQL query resolvers
│ ├── users/ # User queries
│ ├── posts/ # Post queries
│ └── ... # Other queries
├── types/ # TypeScript type definitions
│ ├── users.types.ts # User interfaces
│ ├── posts.types.ts # Post interfaces
│ └── ... # Other type definitions
├── utils/ # Shared utilities (EXPANDED)
│ ├── arangodbUtils.ts # Database connection & helpers
│ ├── sessionUtils.ts # JWT authentication
│ ├── analyticsUtils.ts # Error logging & monitoring
│ ├── schemaUtils.ts # GraphQL schema utilities
│ ├── testUtils.ts # Testing helpers
│ └── ... # Other utilities
├── config.ts # Environment configuration
└── index.ts # Main entry pointKey Directories
- actions/: Domain-specific business logic with single-responsibility functions
- adapters/: Zod-based validation and data transformation layer
- handlers/: Request entry points (GraphQL, Lambda)
- mutations/: GraphQL mutation resolvers
- queries/: GraphQL query resolvers organized by domain
- objectTypes/: GraphQL type schemas with graphql-compose
- types/: TypeScript interfaces and type definitions
- utils/: Shared utilities for database, auth, logging, etc.
Flexible Query System
Reaktor includes a powerful, flexible query system that allows you to search and filter any collection by any property, including custom document fields.
Basic Query
// Get posts by filters
const aqlQry = aql`
FOR p IN posts
FILTER p.type == ${'event'}
FILTER p.privacy == ${'public'}
SORT p.added DESC
LIMIT 0, 20
RETURN p
`;Advanced Filtering
Nested Conditions (OR/AND):
const aqlQry = aql`
FOR p IN posts
FILTER p.type == 'post'
FILTER (p.userId == ${userId} OR p.privacy == 'public')
SORT p.added DESC
LIMIT 0, 20
RETURN p
`;Custom Properties:
const aqlQry = aql`
FOR p IN posts
FILTER p.customStatus == 'active'
FILTER p.customPriority > 5
RETURN p
`;Available Operators:
==- Equal!=- Not equal>- Greater than>=- Greater than or equal<- Less than<=- Less than or equalLIKE- Pattern matching (use with wildcards)CONTAINS- String containsIN- Value in arrayNOT IN- Value not in array
Multi-Collection Search
Search across multiple collections:
import {searchContent} from './src/actions/search.js';
const searchResults = await searchContent(context, {
query: 'nodejs conference',
collections: ['posts', 'users', 'groups'],
limit: { from: 0, to: 50 }
});GraphQL Query
query {
posts(
type: "event"
privacy: "public"
from: 0
to: 20
sort: "added"
direction: "DESC"
) {
_key
content
type
privacy
added
user {
name
avatar
}
}
}For complete query system documentation, see Query System Guide.
Architecture Patterns
Shared AQL Helpers
Common AQL query patterns are implemented as reusable functions in action files:
// Example: Building distance queries
const buildDistanceQuery = (lat: number, lng: number): string =>
`LET distance = DISTANCE(
${lat},
${lng},
NOT_NULL(p.latitude, 0),
NOT_NULL(p.longitude, 0))
`;
// Example: Building reaction count queries
const buildReactionCountQuery = (reactionName: string): string =>
`LET ${reactionName}Count = FIRST(
FOR post, r IN INBOUND p._id hasReaction
FILTER r.name == "${reactionName}"
COLLECT WITH COUNT INTO count
RETURN count
)`;Error Handling
All actions use consistent error handling:
import { logError, logException } from './src/utils/analyticsUtils.js';
try {
// Database operation
const result = await performDatabaseOperation();
return result;
} catch (error) {
logError({
action: 'functionName',
category: 'domain',
label: ErrorTypes.DATABASE_ERROR
}, error, context);
throw error;
}Constants
Use constants instead of magic strings:
// Collection names
const COLLECTIONS = {
POSTS: 'posts',
USERS: 'users',
GROUPS: 'groups'
} as const;
// Edge names
const EDGES = {
HAS_REACTION: 'hasReaction',
HAS_CONNECTION: 'hasConnection',
IS_TAGGED: 'isTagged'
} as const;
// Privacy levels
export const PrivacyLevel = {
PUBLIC: 'public',
PRIVATE: 'private',
GROUP: 'group'
} as const;
// Use in queries
const aqlQry = aql`
FOR p IN ${COLLECTIONS.POSTS}
FOR r IN INBOUND p._id ${EDGES.HAS_REACTION}
FILTER r.name == 'like'
RETURN p
`;Modular Functions
Functions are now single-responsibility and composable:
// Before: One large function
export const addPost = async (context, input) => {
// 100+ lines of mixed logic
};
// After: Composed of smaller functions
export const addPost = async (context: ApiContext, input: PostInputType): Promise<PostType> => {
const parsedPost = parseAndPreparePost(input, context.session.userId);
const savedPost = await insertPost(context, parsedPost);
return await handleTagsForPost(context, savedPost, input.tags);
};Query Guidelines
Avoid manual query building:
// Manual string concatenation (avoid this)
const aqlQry = `FOR p IN posts
FILTER p.type == "${type}" && p.privacy == "public"
RETURN p`;Use parameterized queries:
// Use AQL template literals for safety
import {aql} from 'arangojs';
const aqlQry = aql`
FOR p IN posts
FILTER p.type == ${type}
FILTER p.privacy == 'public'
RETURN p
`;Using helper functions:
// Leverage existing action functions
import {getPostsByLatest} from './src/actions/posts.js';
const results = await getPostsByLatest(context, {
type: type,
privacy: 'public',
from: 0,
to: 20
});Best Practices
- Use parameterized queries: Always use
aqltemplate literals - Leverage adapters: Parse data with Zod schemas for validation
- Use constants: Define collection/edge names as constants
- Error handling: Use
logError()andlogException()consistently - Type safety: Specify TypeScript types for all functions
Database
- Install the latest ArangoDB binary for your platform.
- Create the database
reaktor(for development) andreaktor-test(for testing). - Configure connection in
.envfile (see Configuration section above).
Environment setup:
# In .env file
ARANGODB_URL=http://localhost
ARANGODB_PORT=8529
ARANGODB_DATABASE=reaktor
ARANGODB_USERNAME=root
ARANGODB_PASSWORD=passwordTesting
Reaktor uses Jest for testing with comprehensive test utilities.
Running Tests
# Run all tests
npm test
# Run tests in watch mode
npm test -- --watch
# Run specific test file
npm test -- src/actions/users.test.tsWriting Tests
Use the provided test utilities for consistent test data:
import {describe, it, expect, beforeAll} from '@jest/globals';
import {createMockContext, createMockUser} from '../utils/testUtils.js';
import {addUser, getUser} from './users.js';
describe('User Actions', () => {
let context;
beforeAll(() => {
context = createMockContext();
});
it('should create and retrieve a user', async () => {
const userData = createMockUser({email: '[email protected]'});
const created = await addUser(context, userData);
expect(created.email).toBe('[email protected]');
const retrieved = await getUser(context, created._key);
expect(retrieved?._key).toBe(created._key);
});
});See Test Utilities Guide for detailed documentation on test helpers.
Releases
Releases are automated from Conventional Commit messages when files under
packages/reaktor change on main:
fix:creates a patch release.feat:creates a minor release.feat!:or aBREAKING CHANGE:footer creates a major release.- Other commit types do not create a release by default.
Release Please opens a version and changelog pull request. Merging that pull
request creates the GitHub release and publishes @nlabs/reaktor to npm after
the package test, lint, build, and declaration checks pass. The repository must
provide an NPM_TOKEN Actions secret with publish access to the private package.
Available Action Functions
All action functions have been refactored for better modularity and maintainability. Common patterns are now extracted into shared utilities.
Secure billing
Use Stripe Checkout to save a card without sending its number, expiration date, or security code through Reaktor. The signed-in user's flow is:
- Call
users.createBillingSetupSession(returnUrl). - Redirect the browser to the returned Stripe Checkout URL.
- After Stripe returns with
billing=success, callusers.completeBillingSetupSession(sessionId)using the returnedsession_id. - Use
users.deleteBillingCardif the customer removes the saved card.
Set STRIPE_RETURN_URL_ORIGINS to the comma-separated HTTPS origins allowed to receive Stripe redirects. See the secure billing guide for complete GraphQL examples, local-development rules, errors, and migration instructions for the removed addCreditCard and saveUserBillingCard APIs.
Posts
import {
addPost,
updatePost,
deletePost,
getPost,
getPostsByLatest,
getPostsByReactions,
getPostsByTags,
getPostsByUser,
getPostsByArea
} from './src/actions/posts.js';
// Add a post
const newPost = await addPost(context, {
content: 'Hello World',
type: 'post',
privacy: 'public',
tags: [{name: 'nodejs'}]
});
// Get posts by user
const userPosts = await getPostsByUser(context, userId, {
from: 0,
to: 20
});
// Get posts by location
const nearbyPosts = await getPostsByArea(context, {
latitude: 37.7749,
longitude: -122.4194,
radius: 50, // km
from: 0,
to: 20
});Reactions
import {
addReaction,
deleteReaction,
getReactionCount,
getReactionsByItem,
hasReaction
} from './src/actions/reactions.js';
// Add a reaction
await addReaction(context, 'posts/123', {
name: 'like',
value: '1'
});
// Get reaction counts
const counts = await getReactionCount(context, 'posts/123', {
reactionName: 'like'
});
// Check if user has reacted
const liked = await hasReaction(context, 'posts/123', 'like');Users
import {
getUser,
getUserList,
addUser,
updateUser,
signIn,
signOut,
getUsersByLatest,
getUsersByTags,
getSessionUser
} from './src/actions/users.js';
// Get users with filters
const users = await getUserList(context, {
from: 0,
to: 20
});
// Sign in
const authResult = await signIn(context, {
email: '[email protected]',
password: 'password123'
});
// Get current user from session
const currentUser = await getSessionUser(context, authToken);Groups
import {
getGroupList,
getGroupListByUser,
getGroupListByTags,
getGroupById,
addGroup,
updateGroup,
deleteGroup
} from './src/actions/groups.js';
// Get groups
const groups = await getGroupList(context, {
from: 0,
to: 20
});
// Get user's groups
const userGroups = await getGroupListByUser(context, userId, {
from: 0,
to: 20
});Tags
import {
getTags,
getTagsByItem,
addTag,
addTagToItem,
updateTag,
deleteTag,
extractTags
} from './src/actions/tags.js';
// Add tags to an item
await addTagToItem(context, 'posts/123', [
{name: 'nodejs'},
{name: 'javascript'}
]);
// Extract tags from text
const extractedTags = await extractTags(context, 'Learning #nodejs and #graphql');Messages & Conversations
import {
getConversations,
getConversation,
addDirectConversation,
addUserToConversation
} from './src/actions/conversations.js';
// Get user's conversations
const conversations = await getConversations(context, {
from: 0,
to: 20
});
// Start a direct conversation
const conversation = await addDirectConversation(context, {
userId: otherUserId,
content: 'Hello!'
});See individual action files in src/actions/ for complete API documentation and available options.
GraphQL Schema Utilities
The src/utils/schemaUtils.ts file provides utilities for working with GraphQL schemas programmatically. These utilities help generate custom GraphQL schema definitions from existing Reaktor types.
composeSchemaFromTypes(types: any[]): string
Composes a GraphQL schema string from an array of GraphQL types that support the toSDL() method.
Parameters:
types- Array of GraphQL types withtoSDL()method
Returns: A string containing the concatenated GraphQL schema definitions
Example:
import {composeSchemaFromTypes} from 'utils/schemaUtils';
import {UserType, PostType} from './types';
const schema = composeSchemaFromTypes([UserType, PostType]);
console.log(schema);
// Output: "type User { ... }\ntype Post { ... }"getReaktorMutationFields(fieldNames: string[]): string
Extracts and formats GraphQL mutation field definitions from the Reaktor user mutations schema.
Parameters:
fieldNames- Array of mutation field names to extract
Returns: A formatted string containing the GraphQL field definitions with arguments and return types
Example:
import {getReaktorMutationFields} from 'utils/schemaUtils';
const fields = getReaktorMutationFields(['signIn', 'signUp']);
console.log(fields);
// Output:
// signIn(email: String!, password: String!): AuthResponse
// signUp(email: String!, password: String!, username: String): UsergetReaktorQueryFields(fieldNames: string[]): string
Extracts and formats GraphQL query field definitions from the Reaktor user queries schema.
Parameters:
fieldNames- Array of query field names to extract
Returns: A formatted string containing the GraphQL field definitions with arguments and return types
Example:
import {getReaktorQueryFields} from 'utils/schemaUtils';
const fields = getReaktorQueryFields(['getUser', 'listPosts']);
console.log(fields);
// Output:
// getUser(id: ID!): User
// listPosts(limit: Int, offset: Int): [Post]buildCustomMutations(fieldNames: string[]): string
Builds a complete GraphQL Mutation type definition from selected Reaktor mutation fields.
Parameters:
fieldNames- Array of mutation field names to include in the type
Returns: A complete GraphQL Mutation type definition as a string
Example:
import {buildCustomMutations} from 'utils/schemaUtils';
const mutationType = buildCustomMutations(['signIn', 'signUp']);
console.log(mutationType);
// Output:
// type Mutation {
// signIn(email: String!, password: String!): AuthResponse
// signUp(email: String!, password: String!, username: String): User
// }buildCustomQueries(fieldNames: string[]): string
Builds a complete GraphQL Query type definition from selected Reaktor query fields.
Parameters:
fieldNames- Array of query field names to include in the type
Returns: A complete GraphQL Query type definition as a string
Example:
import {buildCustomQueries} from 'utils/schemaUtils';
const queryType = buildCustomQueries(['getUser', 'listPosts']);
console.log(queryType);
// Output:
// type Query {
// getUser(id: ID!): User
// listPosts(limit: Int, offset: Int): [Post]
// }These utilities are particularly useful when you need to generate custom GraphQL schemas programmatically, such as for API documentation, schema stitching, or creating federated GraphQL services.
GraphQL Handler Usage
Using createGraphqlHandler
The createGraphqlHandler function is the main entry point for handling GraphQL requests in Reaktor. It builds the GraphQL context, applies middleware, and executes queries/mutations using your composed schema.
Source of truth:
- Implementation:
src/handlers/graphqlHandler.ts - Architecture notes:
docs/ARCHITECTURE.md
Example Usage
import {createGraphqlHandler} from './src/handlers/graphqlHandler.js';
// Create a handler instance with default Reaktor schema
const handler = createGraphqlHandler({
context: {}
});
// Use in an AWS Lambda connected to API Gateway HTTP API (payload v2)
export const lambdaHandler = async (event) => {
return await handler(event);
};Custom Composer / Extensions
import {SchemaComposer} from 'graphql-compose';
import {createGraphqlHandler} from './src/handlers/graphqlHandler.js';
const appComposer = new SchemaComposer();
const handler = createGraphqlHandler({
composer: appComposer,
context: {},
extraQueries: {
appHealth: {
type: 'Boolean!',
resolve: () => true
}
},
extraMutations: {},
middleware: []
});How Context Works
- The handler builds a context object for each request, including config, session, and request data.
- This context is passed to all resolvers as the third argument.
- You can customize context by passing initial values with
createGraphqlHandler({context}).
Typical Integration
- Export the handler from AWS Lambda and wire it to API Gateway HTTP API payload v2 events.
See src/handlers/graphqlHandler.ts for implementation details.
Available Action Functions
Below is a list of available functions for each main action group. These can be used in resolvers, business logic, or for direct API calls.
Users
- createToken
- getUserOptional
- parseUserOptions
- addUser
- updateUser
- forgotPassword
- resetPassword
- confirmCode
- deleteUser
- deactivateUser
- getDisplayName
- getSessionUser
- getUser
- getUsers
- getUsersByReactions
- getUsersByTags
- getUsersByLatest
- getUsersByConnection
- refreshSession
- signIn
- signOut
- getActiveUserCount
- getUserByToken
Payments
- completeBillingSetupSession
- createBillingSetupSession
- deleteUserBillingCard
- upgradeUserToPlus
Groups
- getGroupList
- getGroupListByUser
- getGroupListByTags
- getGroupById
- getGroupDetails
- createGroupEdge
- addGroup
- updateGroup
- deleteGroup
- getGroupsByReaction
- isGrouped
Posts
- parsePostOptions
- getPostOptional
- getPost
- getPostsByArea
- getPostsByLatest
- getPostsByReactions
- getPostsByTags
- getPostsByUser
- getPostComments
- addPost
- updatePost
- deletePost
- createPostEdge
Tags
- getTags
- getTagsByItem
- getTag
- getTagsByName
- addTag
- addTagToItem
- updateTag
- deleteTag
- deleteTagFromEdge
- deleteTagFromItem
- extractTags
- updateTagsInItem
Conversations
- parseConversationOptions
- getConversations
- getDirectConversation
- addDirectConversation
- getConnectionUsers
- getConversationUsers
- getConversation
- updateConversation
- addUserToConversation
- deleteUserFromConversation
Reactions
- parseReactionOptions
- addReaction
- deleteReaction
- deleteReactionByItem
- updateReaction
- getReactionCount
- getReactionCountByUser
- getGroupUsersByReaction
- getReactionsByItem
- getItemsByReaction
- hasReaction
Notifications
- getApnProvider
- pushNotification
- clearBadges
Images
- parseImageOptions
- getImageOptional
- getImagesByUser
- getImageCountByItem
- getImagesByItem
- getImagesByGroup
- getImagesByReactions
- getImage
- getPathUserImages
- getAppImageUrl
- getImageUrl
- resizeSaveImage
- addImage
- addImageEdge
- updateImage
- deleteImage
Flexible Query System
Reaktor provides a comprehensive, flexible search and filtering system that allows querying any collection by any property, including custom document properties. This system is built with security, performance, and type safety in mind.
Overview
The flexible query system consists of:
- Query Builder (
src/utils/queryBuilder.ts) - Type-safe query construction - AQL Helpers (
src/utils/aqlHelpers.ts) - Secure AQL generation utilities - Query Actions (
src/actions/query.ts) - Business logic for executing queries - GraphQL Resolvers (
src/queries/query/query.ts) - GraphQL API endpoints
Features
- ✅ Dynamic filter construction for any collection
- ✅ Multiple comparison operators:
==,!=,>,>=,<,<=,LIKE,IN,NOT IN,CONTAINS - ✅ Nested AND/OR conditions
- ✅ Custom property filtering (for documents with dynamic fields)
- ✅ Full-text search across multiple collections
- ✅ Sorting and pagination
- ✅ Type-safe filter definitions
- ✅ AQL injection prevention
- ✅ Aggregation operations (COUNT, SUM, AVG, MIN, MAX)
Basic Query Example
import {executeQuery} from '@nlabs/reaktor';
// Find all active posts from the last 24 hours
const results = await executeQuery(context, {
collection: 'posts',
filters: {
logic: 'AND',
conditions: [
{ field: 'status', operator: '==', value: 'active' },
{ field: 'added', operator: '>', value: Date.now() - 86400000 }
]
},
sort: [{ field: 'added', direction: 'DESC' }],
limit: { from: 0, to: 20 }
});GraphQL Query Example
query {
query {
query(options: {
collection: "posts",
filters: {
logic: "AND",
conditions: [
{ field: "status", operator: "==", value: "published" },
{ field: "type", operator: "IN", value: ["article", "blog"] }
]
},
sort: [{ field: "added", direction: "DESC" }],
limit: { from: 0, to: 10 }
})
}
}Available Operators
Comparison Operators
==- Equal to!=- Not equal to>- Greater than>=- Greater than or equal to<- Less than<=- Less than or equal to
String Operators
LIKE- Pattern matching (supports wildcards with%)CONTAINS- Contains substring (case-insensitive by default)
Array Operators
IN- Value exists in arrayNOT IN- Value does not exist in array
Complex Filter Example
// Find posts that are either:
// - Events happening in the next week
// - Articles published in the last month
const results = await executeQuery(context, {
collection: 'posts',
filters: {
logic: 'OR',
conditions: [
{
logic: 'AND',
conditions: [
{ field: 'type', operator: '==', value: 'event' },
{ field: 'eventDate', operator: '>', value: Date.now() },
{ field: 'eventDate', operator: '<', value: Date.now() + 7 * 86400000 }
]
},
{
logic: 'AND',
conditions: [
{ field: 'type', operator: '==', value: 'article' },
{ field: 'publishedAt', operator: '>', value: Date.now() - 30 * 86400000 }
]
}
]
},
sort: [{ field: 'added', direction: 'DESC' }],
limit: { from: 0, to: 50 }
});Multi-Collection Search
Search across multiple collections with a single query:
import {executeSearch} from '@nlabs/reaktor';
const searchResults = await executeSearch(context, {
collections: ['posts', 'users', 'groups'],
query: 'nodejs',
searchFields: ['name', 'content', 'description'],
filters: {
logic: 'AND',
conditions: [
{ field: 'status', operator: '==', value: 'active' }
]
},
limit: { from: 0, to: 50 }
});Aggregation Operations
Perform aggregations on any collection:
import {executeAggregation} from '@nlabs/reaktor';
// Count active users
const userCount = await executeAggregation(context, {
collection: 'users',
type: 'COUNT',
filters: {
logic: 'AND',
conditions: [
{ field: 'status', operator: '==', value: 'active' }
]
}
});
// Calculate total revenue
const totalRevenue = await executeAggregation(context, {
collection: 'payments',
type: 'SUM',
field: 'amount',
filters: {
logic: 'AND',
conditions: [
{ field: 'status', operator: '==', value: 'completed' }
]
}
});
// Get average rating
const avgRating = await executeAggregation(context, {
collection: 'reviews',
type: 'AVG',
field: 'rating'
});Custom Property Filtering
Query documents with dynamic/custom fields:
const results = await executeQuery(context, {
collection: 'posts',
filters: {
logic: 'AND',
conditions: [
{ field: 'type', operator: '==', value: 'event' },
{ field: 'customField.status', operator: '==', value: 'confirmed' },
{ field: 'customField.capacity', operator: '>=', value: 100 }
]
},
includeCustomFields: true
});Field Selection
Return only specific fields to optimize performance:
const results = await executeQuery(context, {
collection: 'users',
fields: ['_id', 'name', 'email', 'status'],
filters: {
logic: 'AND',
conditions: [
{ field: 'status', operator: '==', value: 'active' }
]
}
});Performance Tips
- Use Indexes - Ensure your ArangoDB collections have appropriate indexes for frequently filtered fields
- Limit Results - Always use pagination to avoid loading large datasets
- Select Fields - Use the
fieldsparameter to return only needed data - Filter Early - Apply filters before sorting for better performance
- Avoid LIKE - Use CONTAINS or full-text search when possible
// Good - Efficient with indexes
{
collection: 'posts',
filters: {
logic: 'AND',
conditions: [
{ field: 'status', operator: '==', value: 'active' },
{ field: 'added', operator: '>', value: timestamp }
]
},
fields: ['_id', 'title', 'added'],
limit: { from: 0, to: 20 }
}
// Avoid - Slower without proper indexes
{
collection: 'posts',
filters: {
logic: 'AND',
conditions: [
{ field: 'content', operator: 'LIKE', value: '%search%' }
]
}
}Error Handling
All query functions throw descriptive errors:
try {
const results = await executeQuery(context, options);
} catch (error) {
// Error types:
// - Invalid collection name
// - Invalid field name
// - Invalid operator
// - Database connection error
// - Query execution error
console.error('Query failed:', error.message);
}Common Use Cases
1. Search Users by Multiple Criteria
const users = await executeQuery(context, {
collection: 'users',
filters: {
logic: 'AND',
conditions: [
{ field: 'status', operator: '==', value: 'active' },
{ field: 'age', operator: '>=', value: 18 },
{ field: 'role', operator: 'IN', value: ['admin', 'moderator'] }
]
}
});2. Find Recent Posts with Tags
const posts = await executeQuery(context, {
collection: 'posts',
filters: {
logic: 'AND',
conditions: [
{ field: 'added', operator: '>', value: Date.now() - 7 * 86400000 },
{ field: 'tags', operator: 'CONTAINS', value: 'javascript' }
]
},
sort: [{ field: 'added', direction: 'DESC' }],
limit: { from: 0, to: 10 }
});3. Case-Insensitive Email Search
const users = await executeQuery(context, {
collection: 'users',
filters: {
logic: 'AND',
conditions: [
{
field: 'email',
operator: 'CONTAINS',
value: 'example.com',
caseSensitive: false
}
]
}
});API Reference
executeQuery(context, options)
Execute a flexible query on any collection.
Parameters:
context: ApiContext- API context with session and database infooptions: QueryOptions- Query configuration
Returns: Promise<any[]> - Array of matching documents
executeSearch(context, options)
Perform multi-collection search.
Parameters:
context: ApiContext- API contextoptions: SearchOptions- Search configuration
Returns: Promise<any[]> - Array of search results
executeAggregation(context, options)
Execute aggregation operations.
Parameters:
context: ApiContext- API contextoptions: AggregationOptions- Aggregation configuration
Returns: Promise<any> - Aggregation result
For more details, see the TypeScript definitions in src/types/arangodb.types.ts.
Refer to the respective files in src/actions/ for full function signatures and usage details.
Documentation
For comprehensive guides and detailed documentation, see:
- Architecture Guide - System overview, data flow, security model, and extension points
- Query System Guide - Complete query system documentation with examples and best practices
- Development Guide - Setup instructions, coding patterns, and contribution guidelines
- Database i18n Guide - Internationalization system documentation
- Test Utilities Guide - Testing patterns and helper functions
Additional Resources
Contributing
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Follow the coding conventions in Development Guide
- Write tests for new functionality
- Ensure all tests pass (
npm test) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
Reaktor is proprietary commercial software. A valid commercial agreement with Nitrogen Labs, Inc. is required to use, copy, modify, or distribute this package. See LICENSE for the governing terms.
