Survey Service Deep Dive
Overview
The Survey Service handles all survey-related business logic on the server side. It provides standard CRUD operations for surveys plus response submission and retrieval. Surveys are stored in a PostgreSQL database using Drizzle ORM.
This service is server-only and should never be imported in client components. Client components should use surveyApiClient from lib/api/survey-api.client.ts instead.
Source Files
| File | Path |
|---|---|
| Service | template/lib/services/survey.service.ts |
| API Client (client-side) | template/lib/api/survey-api.client.ts |
| DB Queries | template/lib/db/queries/survey.queries.ts |
| Types | template/lib/types/survey.ts |
| Migration | template/lib/db/migrations/0013_add_surveys_table.sql |
Architecture
Client Components
|
surveyApiClient (/api/surveys/*)
|
API Route Handlers
|
SurveyService (server-only)
|
DB Queries (survey.queries.ts)
|
PostgreSQL (surveys, survey_responses tables)
Survey Lifecycle
Draft --> Published --> Closed
|
+--> Responses submitted while published
- Draft: Survey is being edited, not visible to users
- Published: Survey is live and accepting responses. Sets
publishedAttimestamp - Closed: Survey is no longer accepting responses. Sets
closedAttimestamp
Method Reference
create(data: CreateSurveyData): Promise<Survey>
Creates a new survey with an auto-generated URL slug.
Parameters:
interface CreateSurveyData {
title: string;
description?: string;
type: string;
itemId?: string;
status?: 'draft' | 'published' | 'closed';
surveyJson: any; // SurveyJS-compatible JSON definition
}
Implementation details:
- Generate slug from title using Unicode-safe slug generation
- Check for slug uniqueness; if duplicate, append incrementing number
- If status is
published, setpublishedAtto now - If status is
closed, setclosedAtto now
Error handling: Database errors are caught and classified:
DATABASE_URL/connect ECONNREFUSED/databaseerrors throw'Database not configured'- Other errors propagate unchanged
getBySlug(slug: string): Promise<Survey | null>
Retrieves a survey by its URL slug. Returns null if not found.
getOne(id: string): Promise<Survey | null>
Retrieves a survey by its database ID. Returns null if not found.
getMany(filters?, userId?): Promise<SurveyListResult>
Retrieves surveys with pagination and filtering. Uses efficient JOINs to get response count and completion status in a single query.
Parameters:
filters(SurveyFilters) -- Page, limit, status, search, etc.userId-- If provided, includes whether the user has completed each survey
Returns:
{
surveys: Survey[];
total: number;
totalPages: number;
page: number;
}
Error classification:
relation ... does not exist-- Suggests runningpnpm db:migrate- Connection errors -- Throws
'Database connection failed'
update(id: string, data: UpdateSurveyData): Promise<Survey>
Updates a survey. Handles status transition logic:
- If changing to
published(from non-published): setspublishedAt - If changing to
closed(from non-closed): setsclosedAt - If slug is changed, checks for uniqueness (appends number if duplicate)
Throws 'Survey not found' if the survey does not exist.
delete(id: string): Promise<void>
Deletes a survey. Safety check: refuses to delete surveys that have responses. Throws 'Cannot delete survey with N responses' if responses exist.
submitResponse(data: SubmitResponseData): Promise<SurveyResponse>
Submits a response to a published survey.
Parameters:
interface SubmitResponseData {
surveyId: string;
userId?: string;
itemId?: string;
data: any; // Response data matching surveyJson schema
ipAddress?: string;
userAgent?: string;
}
Validation:
- Survey must exist
- Survey must have status
published(throws if draft or closed)
Sets completedAt to the current timestamp.
getResponses(surveyId: string, filters?: ResponseFilters): Promise<SurveyResponse[]>
Retrieves responses for a specific survey with optional filtering.
getResponseById(id: string): Promise<SurveyResponse | null>
Retrieves a single response by ID.
Slug Generation
The generateSlug method handles Unicode characters:
- Normalize to NFD (separates diacritical marks)
- Strip combining diacritical marks (
U+0300toU+036F) - Lowercase
- Replace spaces/underscores with hyphens
- Remove non-alphanumeric characters (except hyphens)
- Collapse multiple hyphens
- Trim leading/trailing hyphens
- Fallback to
'survey'if result is empty (e.g., all non-Latin input)
Examples:
"Cafe Survey"becomescafe-survey"Nino's Test"becomesninos-test
ensureUniqueSlug(baseSlug, ignoreId?): Promise<string>
Appends an incrementing number to the base slug until it finds a unique one: base-2, base-3, etc.
Singleton Access
import { surveyService } from '@/lib/services/survey.service';
const survey = await surveyService.create({
title: 'User Satisfaction Survey',
type: 'feedback',
surveyJson: { /* SurveyJS definition */ },
status: 'draft',
});
Error Handling
The service implements contextual error handling for database issues:
- Missing table: Detected via
relation ... does not exist. Advises running migrations. - Connection errors: Detected via
DATABASE_URL,ECONNREFUSED,ENOTFOUND,connectionkeywords. - Duplicate slug: Automatically resolved by appending a number.
- Delete protection: Surveys with responses cannot be deleted.
- Status validation: Only published surveys accept responses.
Usage Examples
import { surveyService } from '@/lib/services/survey.service';
// Create a survey
const survey = await surveyService.create({
title: 'Product Feedback',
description: 'Tell us what you think',
type: 'feedback',
itemId: 'product-123',
status: 'draft',
surveyJson: {
pages: [{
elements: [{
type: 'rating',
name: 'satisfaction',
title: 'How satisfied are you?',
}],
}],
},
});
// Publish the survey
await surveyService.update(survey.id, { status: 'published' });
// Submit a response
await surveyService.submitResponse({
surveyId: survey.id,
userId: 'user_123',
data: { satisfaction: 5 },
ipAddress: '1.2.3.4',
userAgent: 'Mozilla/5.0...',
});
// List surveys with response counts
const result = await surveyService.getMany(
{ page: 1, limit: 10, status: 'published' },
'user_123'
);
// Close the survey
await surveyService.update(survey.id, { status: 'closed' });