@startsimpli/api 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +329 -0
- package/package.json +42 -0
- package/src/__tests__/jwt-refresh.test.ts +195 -0
- package/src/__tests__/query-params.test.ts +144 -0
- package/src/__tests__/url-builder.test.ts +121 -0
- package/src/constants/endpoints.ts +39 -0
- package/src/index.ts +109 -0
- package/src/lib/api-client.ts +89 -0
- package/src/lib/contacts-api.ts +111 -0
- package/src/lib/cors.ts +122 -0
- package/src/lib/entities-api.ts +123 -0
- package/src/lib/env.ts +35 -0
- package/src/lib/error-handler.ts +138 -0
- package/src/lib/errors.ts +381 -0
- package/src/lib/fetch-wrapper.ts +188 -0
- package/src/lib/llm-sanitize.ts +145 -0
- package/src/lib/messages-api.ts +273 -0
- package/src/lib/messages-api.ts.backup +273 -0
- package/src/lib/organizations-api.ts +132 -0
- package/src/lib/rate-limit.ts +91 -0
- package/src/lib/sanitize.ts +39 -0
- package/src/lib/workflows-api.ts +159 -0
- package/src/middleware/index.ts +12 -0
- package/src/middleware/with-auth.ts +90 -0
- package/src/middleware/with-error-handling.ts +83 -0
- package/src/middleware/with-validation.ts +110 -0
- package/src/types/api.ts +38 -0
- package/src/types/contact.ts +49 -0
- package/src/types/entity.ts +153 -0
- package/src/types/error.ts +129 -0
- package/src/types/funnel.ts +133 -0
- package/src/types/index.ts +95 -0
- package/src/types/organization.ts +49 -0
- package/src/types/response.ts +44 -0
- package/src/types/workflow.ts +69 -0
- package/src/utils/index.ts +13 -0
- package/src/utils/query-params.ts +79 -0
- package/src/utils/url-builder.ts +78 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth middleware for Next.js API routes
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { NextRequest } from 'next/server';
|
|
6
|
+
import { NextResponse } from 'next/server';
|
|
7
|
+
|
|
8
|
+
export interface AuthContext {
|
|
9
|
+
userId?: string;
|
|
10
|
+
token?: string;
|
|
11
|
+
isAuthenticated: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type ApiHandler<T = unknown> = (
|
|
15
|
+
request: NextRequest,
|
|
16
|
+
context: AuthContext
|
|
17
|
+
) => Promise<NextResponse<T>> | NextResponse<T>;
|
|
18
|
+
|
|
19
|
+
export interface WithAuthOptions {
|
|
20
|
+
required?: boolean;
|
|
21
|
+
getToken?: (request: NextRequest) => Promise<string | null> | string | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Auth middleware for Next.js API routes
|
|
26
|
+
*/
|
|
27
|
+
export function withAuth<T = unknown>(
|
|
28
|
+
handler: ApiHandler<T>,
|
|
29
|
+
options: WithAuthOptions = {}
|
|
30
|
+
): (request: NextRequest) => Promise<NextResponse<T>> {
|
|
31
|
+
return async (request: NextRequest) => {
|
|
32
|
+
const { required = true, getToken } = options;
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
// Get token from request
|
|
36
|
+
let token: string | null = null;
|
|
37
|
+
|
|
38
|
+
if (getToken) {
|
|
39
|
+
token = await getToken(request);
|
|
40
|
+
} else {
|
|
41
|
+
// Default: extract from Authorization header
|
|
42
|
+
const authHeader = request.headers.get('authorization');
|
|
43
|
+
if (authHeader?.startsWith('Bearer ')) {
|
|
44
|
+
token = authHeader.substring(7);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Build auth context
|
|
49
|
+
const context: AuthContext = {
|
|
50
|
+
token: token || undefined,
|
|
51
|
+
isAuthenticated: !!token,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Check if auth is required
|
|
55
|
+
if (required && !context.isAuthenticated) {
|
|
56
|
+
return NextResponse.json(
|
|
57
|
+
{ error: 'Unauthorized', message: 'Authentication required' },
|
|
58
|
+
{ status: 401 }
|
|
59
|
+
) as NextResponse<T>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Call handler with context
|
|
63
|
+
return await handler(request, context);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error('Auth middleware error:', error);
|
|
66
|
+
return NextResponse.json(
|
|
67
|
+
{ error: 'Internal Server Error', message: 'Authentication failed' },
|
|
68
|
+
{ status: 500 }
|
|
69
|
+
) as NextResponse<T>;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Extract user ID from JWT token (without verification)
|
|
76
|
+
* NOTE: This is for convenience only - always verify tokens on the backend
|
|
77
|
+
*/
|
|
78
|
+
export function extractUserIdFromToken(token: string): string | null {
|
|
79
|
+
try {
|
|
80
|
+
const parts = token.split('.');
|
|
81
|
+
if (parts.length !== 3) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const payload = JSON.parse(atob(parts[1]));
|
|
86
|
+
return payload.sub || payload.user_id || payload.userId || null;
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error handling middleware for Next.js API routes
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { NextRequest } from 'next/server';
|
|
6
|
+
import { NextResponse } from 'next/server';
|
|
7
|
+
import { ApiException } from '../lib/error-handler';
|
|
8
|
+
|
|
9
|
+
export type ErrorApiHandler<T = unknown> = (
|
|
10
|
+
request: NextRequest
|
|
11
|
+
) => Promise<NextResponse<T>> | NextResponse<T>;
|
|
12
|
+
|
|
13
|
+
export interface ErrorResponseBody {
|
|
14
|
+
error: string;
|
|
15
|
+
message: string;
|
|
16
|
+
errors?: Record<string, string[]>;
|
|
17
|
+
status?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Error handling middleware for Next.js API routes
|
|
22
|
+
*/
|
|
23
|
+
export function withErrorHandling<T = unknown>(
|
|
24
|
+
handler: ErrorApiHandler<T>
|
|
25
|
+
): (request: NextRequest) => Promise<NextResponse<T | ErrorResponseBody>> {
|
|
26
|
+
return async (request: NextRequest) => {
|
|
27
|
+
try {
|
|
28
|
+
return await handler(request);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error('API error:', error);
|
|
31
|
+
|
|
32
|
+
// Handle ApiException
|
|
33
|
+
if (error instanceof ApiException) {
|
|
34
|
+
return NextResponse.json(
|
|
35
|
+
{
|
|
36
|
+
error: error.name,
|
|
37
|
+
message: error.message,
|
|
38
|
+
errors: error.errors,
|
|
39
|
+
status: error.status,
|
|
40
|
+
},
|
|
41
|
+
{ status: error.status || 500 }
|
|
42
|
+
) as NextResponse<ErrorResponseBody>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Handle validation errors (Zod, etc.)
|
|
46
|
+
if (error && typeof error === 'object' && 'issues' in error) {
|
|
47
|
+
const issues = (error as { issues: Array<{ path: string[]; message: string }> })
|
|
48
|
+
.issues;
|
|
49
|
+
|
|
50
|
+
const errors: Record<string, string[]> = {};
|
|
51
|
+
issues.forEach((issue) => {
|
|
52
|
+
const field = issue.path.join('.');
|
|
53
|
+
if (!errors[field]) {
|
|
54
|
+
errors[field] = [];
|
|
55
|
+
}
|
|
56
|
+
errors[field].push(issue.message);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return NextResponse.json(
|
|
60
|
+
{
|
|
61
|
+
error: 'ValidationError',
|
|
62
|
+
message: 'Validation failed',
|
|
63
|
+
errors,
|
|
64
|
+
status: 400,
|
|
65
|
+
},
|
|
66
|
+
{ status: 400 }
|
|
67
|
+
) as NextResponse<ErrorResponseBody>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Handle generic errors
|
|
71
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
72
|
+
|
|
73
|
+
return NextResponse.json(
|
|
74
|
+
{
|
|
75
|
+
error: 'InternalServerError',
|
|
76
|
+
message,
|
|
77
|
+
status: 500,
|
|
78
|
+
},
|
|
79
|
+
{ status: 500 }
|
|
80
|
+
) as NextResponse<ErrorResponseBody>;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation middleware for Next.js API routes using Zod
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { NextRequest } from 'next/server';
|
|
6
|
+
import { NextResponse } from 'next/server';
|
|
7
|
+
import { z, type ZodSchema } from 'zod';
|
|
8
|
+
|
|
9
|
+
export type ValidatedApiHandler<TBody = unknown, TQuery = unknown> = (
|
|
10
|
+
request: NextRequest,
|
|
11
|
+
validated: { body?: TBody; query?: TQuery }
|
|
12
|
+
) => Promise<NextResponse> | NextResponse;
|
|
13
|
+
|
|
14
|
+
export interface ValidationSchemas {
|
|
15
|
+
body?: ZodSchema;
|
|
16
|
+
query?: ZodSchema;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Validation middleware for Next.js API routes
|
|
21
|
+
*/
|
|
22
|
+
export function withValidation<TBody = unknown, TQuery = unknown>(
|
|
23
|
+
handler: ValidatedApiHandler<TBody, TQuery>,
|
|
24
|
+
schemas: ValidationSchemas
|
|
25
|
+
): (request: NextRequest) => Promise<NextResponse> {
|
|
26
|
+
return async (request: NextRequest) => {
|
|
27
|
+
try {
|
|
28
|
+
const validated: { body?: TBody; query?: TQuery } = {};
|
|
29
|
+
|
|
30
|
+
// Validate body
|
|
31
|
+
if (schemas.body) {
|
|
32
|
+
const contentType = request.headers.get('content-type');
|
|
33
|
+
if (!contentType?.includes('application/json')) {
|
|
34
|
+
return NextResponse.json(
|
|
35
|
+
{ error: 'ValidationError', message: 'Content-Type must be application/json' },
|
|
36
|
+
{ status: 400 }
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const body = await request.json();
|
|
41
|
+
validated.body = schemas.body.parse(body) as TBody;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Validate query params
|
|
45
|
+
if (schemas.query) {
|
|
46
|
+
const { searchParams } = new URL(request.url);
|
|
47
|
+
const query: Record<string, unknown> = {};
|
|
48
|
+
|
|
49
|
+
searchParams.forEach((value, key) => {
|
|
50
|
+
// Handle array params (key repeated multiple times)
|
|
51
|
+
if (key in query) {
|
|
52
|
+
if (Array.isArray(query[key])) {
|
|
53
|
+
(query[key] as unknown[]).push(value);
|
|
54
|
+
} else {
|
|
55
|
+
query[key] = [query[key], value];
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
query[key] = value;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
validated.query = schemas.query.parse(query) as TQuery;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Call handler with validated data
|
|
66
|
+
return await handler(request, validated);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (error instanceof z.ZodError) {
|
|
69
|
+
const errors: Record<string, string[]> = {};
|
|
70
|
+
error.issues.forEach((issue) => {
|
|
71
|
+
const field = issue.path.join('.');
|
|
72
|
+
if (!errors[field]) {
|
|
73
|
+
errors[field] = [];
|
|
74
|
+
}
|
|
75
|
+
errors[field].push(issue.message);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return NextResponse.json(
|
|
79
|
+
{
|
|
80
|
+
error: 'ValidationError',
|
|
81
|
+
message: 'Validation failed',
|
|
82
|
+
errors,
|
|
83
|
+
},
|
|
84
|
+
{ status: 400 }
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Combine validation with other middleware
|
|
95
|
+
*/
|
|
96
|
+
export function composeMiddleware<TBody = unknown, TQuery = unknown>(
|
|
97
|
+
handler: ValidatedApiHandler<TBody, TQuery>,
|
|
98
|
+
...middlewares: Array<
|
|
99
|
+
(handler: ValidatedApiHandler<TBody, TQuery>) => ValidatedApiHandler<TBody, TQuery>
|
|
100
|
+
>
|
|
101
|
+
): (request: NextRequest) => Promise<NextResponse> {
|
|
102
|
+
const composed = middlewares.reduce(
|
|
103
|
+
(acc, middleware) => middleware(acc),
|
|
104
|
+
handler
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return async (request: NextRequest) => {
|
|
108
|
+
return composed(request, {});
|
|
109
|
+
};
|
|
110
|
+
}
|
package/src/types/api.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common API types for Django REST Framework
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface PaginatedResponse<T> {
|
|
6
|
+
count: number;
|
|
7
|
+
next: string | null;
|
|
8
|
+
previous: string | null;
|
|
9
|
+
results: T[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ApiError {
|
|
13
|
+
detail?: string;
|
|
14
|
+
message?: string;
|
|
15
|
+
errors?: Record<string, string[]>;
|
|
16
|
+
status?: number;
|
|
17
|
+
statusText?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PaginationParams {
|
|
21
|
+
page?: number;
|
|
22
|
+
pageSize?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SortParams {
|
|
26
|
+
ordering?: string; // e.g., '-createdAt', 'name'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ApiRequestConfig {
|
|
30
|
+
headers?: HeadersInit;
|
|
31
|
+
signal?: AbortSignal;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
35
|
+
|
|
36
|
+
export interface FetchOptions extends RequestInit {
|
|
37
|
+
params?: Record<string, unknown>;
|
|
38
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contact types matching Django Contact model
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Entity, WritableAssertions } from './entity';
|
|
6
|
+
|
|
7
|
+
export interface Contact extends Entity {
|
|
8
|
+
name: string;
|
|
9
|
+
email?: string | null;
|
|
10
|
+
phone?: string | null;
|
|
11
|
+
title?: string | null;
|
|
12
|
+
companyName?: string | null;
|
|
13
|
+
location?: string | null;
|
|
14
|
+
notes?: string | null;
|
|
15
|
+
|
|
16
|
+
// Computed fields from assertions
|
|
17
|
+
tier?: number | null;
|
|
18
|
+
status?: string | null;
|
|
19
|
+
linkedin?: string | null;
|
|
20
|
+
twitter?: string | null;
|
|
21
|
+
website?: string | null;
|
|
22
|
+
firmId?: string | null;
|
|
23
|
+
firmName?: string | null;
|
|
24
|
+
enrichmentScore?: number | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CreateContactRequest extends WritableAssertions {
|
|
28
|
+
name: string;
|
|
29
|
+
email?: string;
|
|
30
|
+
phone?: string;
|
|
31
|
+
title?: string;
|
|
32
|
+
companyName?: string;
|
|
33
|
+
location?: string;
|
|
34
|
+
notes?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface UpdateContactRequest extends Partial<CreateContactRequest> {}
|
|
38
|
+
|
|
39
|
+
export interface ContactFilters {
|
|
40
|
+
[key: string]: unknown;
|
|
41
|
+
search?: string;
|
|
42
|
+
tier?: number;
|
|
43
|
+
status?: string;
|
|
44
|
+
firmId?: string;
|
|
45
|
+
tagCategory?: string;
|
|
46
|
+
tagName?: string;
|
|
47
|
+
hasEmail?: boolean;
|
|
48
|
+
hasLinkedin?: boolean;
|
|
49
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entity system types matching Django core models
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type EntityType = 'contact' | 'organization';
|
|
6
|
+
|
|
7
|
+
export interface Entity {
|
|
8
|
+
id: string; // external_id (UUID)
|
|
9
|
+
entityType: EntityType;
|
|
10
|
+
createdAt: string;
|
|
11
|
+
updatedAt: string;
|
|
12
|
+
tags?: EntityTag[];
|
|
13
|
+
metrics?: Metric[];
|
|
14
|
+
profiles?: Profile[];
|
|
15
|
+
attributes?: Attribute[];
|
|
16
|
+
relationshipsFrom?: Relationship[];
|
|
17
|
+
relationshipsTo?: Relationship[];
|
|
18
|
+
roleAssignments?: RoleAssignment[];
|
|
19
|
+
events?: Event[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface Tag {
|
|
23
|
+
id: number;
|
|
24
|
+
category: string;
|
|
25
|
+
name: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
fullPath: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface EntityTag {
|
|
31
|
+
id: number;
|
|
32
|
+
category: string;
|
|
33
|
+
name: string;
|
|
34
|
+
tagDescription?: string;
|
|
35
|
+
confidence: number;
|
|
36
|
+
appliedAt: string;
|
|
37
|
+
appliedBy?: string;
|
|
38
|
+
metadata?: Record<string, unknown>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface Metric {
|
|
42
|
+
id: number;
|
|
43
|
+
type: string;
|
|
44
|
+
subtype: string;
|
|
45
|
+
value: number | string | null;
|
|
46
|
+
valueNumeric?: number | null;
|
|
47
|
+
valueText?: string | null;
|
|
48
|
+
unit?: string;
|
|
49
|
+
asOfDate?: string;
|
|
50
|
+
periodStart?: string;
|
|
51
|
+
confidence: number;
|
|
52
|
+
metadata?: Record<string, unknown>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface Profile {
|
|
56
|
+
id: number;
|
|
57
|
+
type: string;
|
|
58
|
+
subtype: string;
|
|
59
|
+
identifier: string;
|
|
60
|
+
identifierType: string;
|
|
61
|
+
displayName: string;
|
|
62
|
+
verified: boolean;
|
|
63
|
+
verifiedAt?: string;
|
|
64
|
+
metadata?: Record<string, unknown>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface Attribute {
|
|
68
|
+
id: number;
|
|
69
|
+
type: string;
|
|
70
|
+
subtype: string;
|
|
71
|
+
value: string | Record<string, unknown> | null;
|
|
72
|
+
valueText?: string | null;
|
|
73
|
+
valueJson?: Record<string, unknown> | null;
|
|
74
|
+
valueType: 'text' | 'json';
|
|
75
|
+
validFrom?: string;
|
|
76
|
+
validTo?: string;
|
|
77
|
+
isCurrent: boolean;
|
|
78
|
+
confidence: number;
|
|
79
|
+
metadata?: Record<string, unknown>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface Relationship {
|
|
83
|
+
id: number;
|
|
84
|
+
fromEntityId: string;
|
|
85
|
+
toEntityId: string;
|
|
86
|
+
type: string;
|
|
87
|
+
subtype?: string;
|
|
88
|
+
validFrom?: string;
|
|
89
|
+
validTo?: string;
|
|
90
|
+
isCurrent: boolean;
|
|
91
|
+
durationDays?: number;
|
|
92
|
+
strength?: number;
|
|
93
|
+
confidence: number;
|
|
94
|
+
metadata?: Record<string, unknown>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface RoleAssignment {
|
|
98
|
+
id: number;
|
|
99
|
+
contactId: string;
|
|
100
|
+
orgId: string;
|
|
101
|
+
type: string;
|
|
102
|
+
subtype?: string;
|
|
103
|
+
title?: string;
|
|
104
|
+
seniority?: string;
|
|
105
|
+
validFrom?: string;
|
|
106
|
+
validTo?: string;
|
|
107
|
+
isCurrent: boolean;
|
|
108
|
+
durationDays?: number;
|
|
109
|
+
confidence: number;
|
|
110
|
+
metadata?: Record<string, unknown>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface Event {
|
|
114
|
+
id: number;
|
|
115
|
+
type: string;
|
|
116
|
+
subtype?: string;
|
|
117
|
+
occurredAt?: string;
|
|
118
|
+
announcedAt?: string;
|
|
119
|
+
magnitude?: number;
|
|
120
|
+
confidence: number;
|
|
121
|
+
metadata?: Record<string, unknown>;
|
|
122
|
+
participants?: EventParticipant[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface EventParticipant {
|
|
126
|
+
id: number;
|
|
127
|
+
entityId: string;
|
|
128
|
+
roleType: string;
|
|
129
|
+
roleSubtype?: string;
|
|
130
|
+
metadata?: Record<string, unknown>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface Source {
|
|
134
|
+
id: number;
|
|
135
|
+
type: string;
|
|
136
|
+
subtype?: string;
|
|
137
|
+
identifier: string;
|
|
138
|
+
retrievedAt: string;
|
|
139
|
+
format: string;
|
|
140
|
+
confidence: number;
|
|
141
|
+
cost?: number;
|
|
142
|
+
metadata?: Record<string, unknown>;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Writable assertion formats for POST/PATCH
|
|
147
|
+
*/
|
|
148
|
+
export interface WritableAssertions {
|
|
149
|
+
writeTags?: Array<string | { category: string; name: string; confidence?: number }>;
|
|
150
|
+
writeMetrics?: Record<string, number | string>;
|
|
151
|
+
writeProfiles?: Record<string, string>;
|
|
152
|
+
writeAttributes?: Record<string, string | Record<string, unknown>>;
|
|
153
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standardized API error response types
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent error handling across frontend and backend
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Field-level validation error
|
|
11
|
+
*/
|
|
12
|
+
export interface FieldError {
|
|
13
|
+
field: string;
|
|
14
|
+
messages: string[];
|
|
15
|
+
code?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Standardized API error response
|
|
20
|
+
*/
|
|
21
|
+
export interface StandardErrorResponse {
|
|
22
|
+
/** Main error message (user-friendly) */
|
|
23
|
+
error: string;
|
|
24
|
+
|
|
25
|
+
/** Error type/code for programmatic handling */
|
|
26
|
+
code: string;
|
|
27
|
+
|
|
28
|
+
/** HTTP status code */
|
|
29
|
+
statusCode: number;
|
|
30
|
+
|
|
31
|
+
/** Field-level validation errors */
|
|
32
|
+
fieldErrors?: FieldError[];
|
|
33
|
+
|
|
34
|
+
/** Additional error details */
|
|
35
|
+
details?: Record<string, unknown>;
|
|
36
|
+
|
|
37
|
+
/** Request ID for debugging */
|
|
38
|
+
requestId?: string;
|
|
39
|
+
|
|
40
|
+
/** Timestamp of error */
|
|
41
|
+
timestamp?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Zod schema for runtime validation
|
|
46
|
+
*/
|
|
47
|
+
export const FieldErrorSchema = z.object({
|
|
48
|
+
field: z.string(),
|
|
49
|
+
messages: z.array(z.string()),
|
|
50
|
+
code: z.string().optional(),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const StandardErrorResponseSchema = z.object({
|
|
54
|
+
error: z.string(),
|
|
55
|
+
code: z.string(),
|
|
56
|
+
statusCode: z.number().int().min(400).max(599),
|
|
57
|
+
fieldErrors: z.array(FieldErrorSchema).optional(),
|
|
58
|
+
details: z.record(z.unknown()).optional(),
|
|
59
|
+
requestId: z.string().optional(),
|
|
60
|
+
timestamp: z.string().datetime().optional(),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Common error codes
|
|
65
|
+
*/
|
|
66
|
+
export enum ErrorCode {
|
|
67
|
+
// Client errors (4xx)
|
|
68
|
+
BAD_REQUEST = 'bad_request',
|
|
69
|
+
UNAUTHORIZED = 'unauthorized',
|
|
70
|
+
FORBIDDEN = 'forbidden',
|
|
71
|
+
NOT_FOUND = 'not_found',
|
|
72
|
+
METHOD_NOT_ALLOWED = 'method_not_allowed',
|
|
73
|
+
VALIDATION_ERROR = 'validation_error',
|
|
74
|
+
CONFLICT = 'conflict',
|
|
75
|
+
RATE_LIMITED = 'rate_limited',
|
|
76
|
+
|
|
77
|
+
// Server errors (5xx)
|
|
78
|
+
INTERNAL_ERROR = 'internal_error',
|
|
79
|
+
SERVICE_UNAVAILABLE = 'service_unavailable',
|
|
80
|
+
GATEWAY_TIMEOUT = 'gateway_timeout',
|
|
81
|
+
|
|
82
|
+
// Network errors
|
|
83
|
+
NETWORK_ERROR = 'network_error',
|
|
84
|
+
TIMEOUT = 'timeout',
|
|
85
|
+
|
|
86
|
+
// Unknown
|
|
87
|
+
UNKNOWN = 'unknown',
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Map HTTP status codes to error codes
|
|
92
|
+
*/
|
|
93
|
+
export function getErrorCodeFromStatus(status: number): ErrorCode {
|
|
94
|
+
const codeMap: Record<number, ErrorCode> = {
|
|
95
|
+
400: ErrorCode.BAD_REQUEST,
|
|
96
|
+
401: ErrorCode.UNAUTHORIZED,
|
|
97
|
+
403: ErrorCode.FORBIDDEN,
|
|
98
|
+
404: ErrorCode.NOT_FOUND,
|
|
99
|
+
405: ErrorCode.METHOD_NOT_ALLOWED,
|
|
100
|
+
409: ErrorCode.CONFLICT,
|
|
101
|
+
422: ErrorCode.VALIDATION_ERROR,
|
|
102
|
+
429: ErrorCode.RATE_LIMITED,
|
|
103
|
+
500: ErrorCode.INTERNAL_ERROR,
|
|
104
|
+
503: ErrorCode.SERVICE_UNAVAILABLE,
|
|
105
|
+
504: ErrorCode.GATEWAY_TIMEOUT,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return codeMap[status] || ErrorCode.UNKNOWN;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* User-friendly error messages
|
|
113
|
+
*/
|
|
114
|
+
export const ErrorMessages: Record<ErrorCode, string> = {
|
|
115
|
+
[ErrorCode.BAD_REQUEST]: 'The request was invalid. Please check your input.',
|
|
116
|
+
[ErrorCode.UNAUTHORIZED]: 'You need to log in to access this resource.',
|
|
117
|
+
[ErrorCode.FORBIDDEN]: "You don't have permission to access this resource.",
|
|
118
|
+
[ErrorCode.NOT_FOUND]: 'The requested resource was not found.',
|
|
119
|
+
[ErrorCode.METHOD_NOT_ALLOWED]: 'This operation is not allowed.',
|
|
120
|
+
[ErrorCode.VALIDATION_ERROR]: 'Please fix the errors in your form.',
|
|
121
|
+
[ErrorCode.CONFLICT]: 'This operation conflicts with existing data.',
|
|
122
|
+
[ErrorCode.RATE_LIMITED]: 'Too many requests. Please slow down.',
|
|
123
|
+
[ErrorCode.INTERNAL_ERROR]: 'An unexpected error occurred. Please try again.',
|
|
124
|
+
[ErrorCode.SERVICE_UNAVAILABLE]: 'The service is temporarily unavailable.',
|
|
125
|
+
[ErrorCode.GATEWAY_TIMEOUT]: 'The request took too long. Please try again.',
|
|
126
|
+
[ErrorCode.NETWORK_ERROR]: 'Network error. Please check your connection.',
|
|
127
|
+
[ErrorCode.TIMEOUT]: 'The request timed out. Please try again.',
|
|
128
|
+
[ErrorCode.UNKNOWN]: 'An unknown error occurred.',
|
|
129
|
+
};
|