@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.
Files changed (38) hide show
  1. package/README.md +329 -0
  2. package/package.json +42 -0
  3. package/src/__tests__/jwt-refresh.test.ts +195 -0
  4. package/src/__tests__/query-params.test.ts +144 -0
  5. package/src/__tests__/url-builder.test.ts +121 -0
  6. package/src/constants/endpoints.ts +39 -0
  7. package/src/index.ts +109 -0
  8. package/src/lib/api-client.ts +89 -0
  9. package/src/lib/contacts-api.ts +111 -0
  10. package/src/lib/cors.ts +122 -0
  11. package/src/lib/entities-api.ts +123 -0
  12. package/src/lib/env.ts +35 -0
  13. package/src/lib/error-handler.ts +138 -0
  14. package/src/lib/errors.ts +381 -0
  15. package/src/lib/fetch-wrapper.ts +188 -0
  16. package/src/lib/llm-sanitize.ts +145 -0
  17. package/src/lib/messages-api.ts +273 -0
  18. package/src/lib/messages-api.ts.backup +273 -0
  19. package/src/lib/organizations-api.ts +132 -0
  20. package/src/lib/rate-limit.ts +91 -0
  21. package/src/lib/sanitize.ts +39 -0
  22. package/src/lib/workflows-api.ts +159 -0
  23. package/src/middleware/index.ts +12 -0
  24. package/src/middleware/with-auth.ts +90 -0
  25. package/src/middleware/with-error-handling.ts +83 -0
  26. package/src/middleware/with-validation.ts +110 -0
  27. package/src/types/api.ts +38 -0
  28. package/src/types/contact.ts +49 -0
  29. package/src/types/entity.ts +153 -0
  30. package/src/types/error.ts +129 -0
  31. package/src/types/funnel.ts +133 -0
  32. package/src/types/index.ts +95 -0
  33. package/src/types/organization.ts +49 -0
  34. package/src/types/response.ts +44 -0
  35. package/src/types/workflow.ts +69 -0
  36. package/src/utils/index.ts +13 -0
  37. package/src/utils/query-params.ts +79 -0
  38. package/src/utils/url-builder.ts +78 -0
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Funnel types matching Django Funnel models
3
+ *
4
+ * Funnels are brutally generic - they can represent:
5
+ * - Lead scoring funnels
6
+ * - Investor qualification stages
7
+ * - Customer segmentation
8
+ * - Any multi-stage entity classification
9
+ */
10
+
11
+ export type FunnelStatus = 'draft' | 'active' | 'archived';
12
+ export type EntityType = 'contact' | 'organization';
13
+
14
+ /**
15
+ * Filter rule for funnel stage
16
+ */
17
+ export interface FunnelStageRule {
18
+ id: number;
19
+ fieldPath: string;
20
+ operator: 'eq' | 'ne' | 'gt' | 'lt' | 'gte' | 'lte' | 'contains' | 'notContains' | 'startswith' | 'endswith' | 'in' | 'notIn' | 'isnull' | 'isnotnull' | 'arrayContains' | 'arrayOverlaps';
21
+ value: string | number | boolean | null;
22
+ logic?: 'AND' | 'OR';
23
+ }
24
+
25
+ /**
26
+ * Funnel stage with rules
27
+ */
28
+ export interface FunnelStage {
29
+ id: number;
30
+ name: string;
31
+ description?: string;
32
+ order: number;
33
+ rules: FunnelStageRule[];
34
+ entityCount?: number;
35
+ createdAt: string;
36
+ updatedAt: string;
37
+ }
38
+
39
+ /**
40
+ * Funnel - brutally generic entity classification system
41
+ */
42
+ export interface Funnel {
43
+ id: string;
44
+ name: string;
45
+ description: string;
46
+ status: FunnelStatus;
47
+ tags?: string[];
48
+ entityType: EntityType;
49
+ /**
50
+ * @deprecated Legacy field for backward compatibility
51
+ */
52
+ inputType?: 'contacts' | 'organizations' | 'both' | 'any';
53
+ stages: FunnelStage[];
54
+ stageCount: number;
55
+ totalRuns: number;
56
+ lastRunAt: string | null;
57
+ createdBy: string;
58
+ createdByName: string | null;
59
+ createdAt: string;
60
+ updatedAt: string;
61
+ }
62
+
63
+ /**
64
+ * Funnel result for a specific entity
65
+ */
66
+ export interface FunnelResult {
67
+ id: number;
68
+ funnel: string;
69
+ entity: string;
70
+ entityType: EntityType;
71
+ stage: number | null;
72
+ stageName: string | null;
73
+ score: number | null;
74
+ metadata: Record<string, any>;
75
+ createdAt: string;
76
+ updatedAt: string;
77
+ }
78
+
79
+ /**
80
+ * Funnel execution run
81
+ */
82
+ export interface FunnelRun {
83
+ id: string;
84
+ funnel: string;
85
+ status: 'pending' | 'running' | 'completed' | 'failed';
86
+ totalEntities: number;
87
+ processedEntities: number;
88
+ resultsByStage: Record<string, number>;
89
+ startedAt: string | null;
90
+ completedAt: string | null;
91
+ errorMessage: string | null;
92
+ createdBy: string;
93
+ createdAt: string;
94
+ }
95
+
96
+ /**
97
+ * Filters for listing funnels
98
+ */
99
+ export interface FunnelFilters {
100
+ status?: FunnelStatus;
101
+ entityType?: EntityType;
102
+ search?: string;
103
+ tags?: string[];
104
+ createdBy?: string;
105
+ page?: number;
106
+ pageSize?: number;
107
+ ordering?: string;
108
+ }
109
+
110
+ /**
111
+ * Input for creating a funnel
112
+ */
113
+ export interface CreateFunnelInput {
114
+ name: string;
115
+ description?: string;
116
+ status?: FunnelStatus;
117
+ tags?: string[];
118
+ entityType: EntityType;
119
+ stages?: Omit<FunnelStage, 'id' | 'createdAt' | 'updatedAt' | 'entityCount'>[];
120
+ }
121
+
122
+ /**
123
+ * Input for updating a funnel
124
+ */
125
+ export interface UpdateFunnelInput extends Partial<CreateFunnelInput> {}
126
+
127
+ /**
128
+ * Input for executing a funnel
129
+ */
130
+ export interface ExecuteFunnelInput {
131
+ entityIds?: string[];
132
+ filters?: Record<string, any>;
133
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Type exports for @startsimpli/api
3
+ */
4
+
5
+ // API types
6
+ export type {
7
+ PaginatedResponse,
8
+ PaginationParams,
9
+ SortParams,
10
+ ApiRequestConfig,
11
+ HttpMethod,
12
+ FetchOptions,
13
+ } from './api';
14
+ // DRF-specific ApiError (aliased to avoid conflict with canonical ApiError below)
15
+ export type { ApiError as DRFApiError } from './api';
16
+
17
+ // Canonical response envelope types (used by all StartSimpli apps)
18
+ export type {
19
+ ApiError,
20
+ ApiResponse,
21
+ AsyncState,
22
+ AsyncStatus,
23
+ } from './response';
24
+ export { ErrorCodes } from './response';
25
+
26
+ // Entity types
27
+ export type {
28
+ EntityType,
29
+ Entity,
30
+ Tag,
31
+ EntityTag,
32
+ Metric,
33
+ Profile,
34
+ Attribute,
35
+ Relationship,
36
+ RoleAssignment,
37
+ Event,
38
+ EventParticipant,
39
+ Source,
40
+ WritableAssertions,
41
+ } from './entity';
42
+
43
+ // Contact types
44
+ export type {
45
+ Contact,
46
+ CreateContactRequest,
47
+ UpdateContactRequest,
48
+ ContactFilters,
49
+ } from './contact';
50
+
51
+ // Organization types
52
+ export type {
53
+ Organization,
54
+ CreateOrganizationRequest,
55
+ UpdateOrganizationRequest,
56
+ OrganizationFilters,
57
+ } from './organization';
58
+
59
+ // Funnel types
60
+ export type {
61
+ FunnelStatus,
62
+ FunnelStageRule,
63
+ FunnelStage,
64
+ Funnel,
65
+ FunnelResult,
66
+ FunnelRun,
67
+ FunnelFilters,
68
+ CreateFunnelInput,
69
+ UpdateFunnelInput,
70
+ ExecuteFunnelInput,
71
+ } from './funnel';
72
+
73
+ // Workflow types
74
+ export type {
75
+ WorkflowStatus,
76
+ Workflow,
77
+ WorkflowExecution,
78
+ WorkflowFilters,
79
+ ExecuteWorkflowInput,
80
+ CreateWorkflowInput,
81
+ UpdateWorkflowInput,
82
+ } from './workflow';
83
+
84
+ // Error types
85
+ export type {
86
+ FieldError,
87
+ StandardErrorResponse,
88
+ } from './error';
89
+ export {
90
+ ErrorCode,
91
+ ErrorMessages,
92
+ getErrorCodeFromStatus,
93
+ FieldErrorSchema,
94
+ StandardErrorResponseSchema,
95
+ } from './error';
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Organization types matching Django Organization model
3
+ */
4
+
5
+ import type { Entity, WritableAssertions } from './entity';
6
+
7
+ export interface Organization extends Entity {
8
+ name: string;
9
+ domain?: string | null;
10
+ description?: string | null;
11
+ location?: string | null;
12
+
13
+ // Computed fields from assertions
14
+ tier?: number | null;
15
+ stage?: string | null;
16
+ focusAreas?: string[];
17
+ checkSizeMin?: number | null;
18
+ checkSizeMax?: number | null;
19
+ aum?: number | string | null;
20
+ portfolioCount?: number | null;
21
+ linkedin?: string | null;
22
+ twitter?: string | null;
23
+ crunchbase?: string | null;
24
+ website?: string | null;
25
+ foundedYear?: number | null;
26
+ }
27
+
28
+ export interface CreateOrganizationRequest extends WritableAssertions {
29
+ name: string;
30
+ domain?: string;
31
+ description?: string;
32
+ location?: string;
33
+ }
34
+
35
+ export interface UpdateOrganizationRequest extends Partial<CreateOrganizationRequest> {}
36
+
37
+ export interface OrganizationFilters {
38
+ [key: string]: unknown;
39
+ search?: string;
40
+ tier?: number;
41
+ stage?: string;
42
+ focusArea?: string;
43
+ checkSizeMinGte?: number;
44
+ checkSizeMaxLte?: number;
45
+ aumGte?: number;
46
+ location?: string;
47
+ tagCategory?: string;
48
+ tagName?: string;
49
+ }
@@ -0,0 +1,44 @@
1
+ // Canonical API response envelope used by all StartSimpli apps
2
+ export interface ApiError {
3
+ code: string;
4
+ message: string;
5
+ details?: unknown;
6
+ }
7
+
8
+ export interface ApiResponse<T = unknown> {
9
+ success: boolean;
10
+ data?: T;
11
+ error?: ApiError;
12
+ }
13
+
14
+ // Standard error codes (apps may define additional domain-specific codes)
15
+ export enum ErrorCodes {
16
+ UNAUTHORIZED = 'UNAUTHORIZED',
17
+ FORBIDDEN = 'FORBIDDEN',
18
+ NOT_FOUND = 'NOT_FOUND',
19
+ VALIDATION_ERROR = 'VALIDATION_ERROR',
20
+ CONFLICT = 'CONFLICT',
21
+ INTERNAL_ERROR = 'INTERNAL_ERROR',
22
+ SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE',
23
+ RATE_LIMITED = 'RATE_LIMITED',
24
+ }
25
+
26
+ export interface PaginationParams {
27
+ page?: number;
28
+ pageSize?: number;
29
+ }
30
+
31
+ export interface PaginatedResponse<T> {
32
+ results: T[];
33
+ count: number;
34
+ next: string | null;
35
+ previous: string | null;
36
+ }
37
+
38
+ // Async state pattern for React components
39
+ export type AsyncStatus = 'idle' | 'loading' | 'success' | 'error';
40
+ export interface AsyncState<T> {
41
+ status: AsyncStatus;
42
+ data: T | null;
43
+ error: string | null;
44
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Workflow types matching Django Workflow models
3
+ *
4
+ * Workflows are n8n-style node-based automation flows
5
+ */
6
+
7
+ export type WorkflowStatus = 'draft' | 'active' | 'paused';
8
+
9
+ export interface Workflow {
10
+ id: string;
11
+ name: string;
12
+ description: string;
13
+ team: string;
14
+ createdBy: string | null;
15
+ nodes: any[];
16
+ connections: Record<string, any>;
17
+ settings: Record<string, any>;
18
+ staticData: Record<string, any>;
19
+ isActive: boolean;
20
+ version: number;
21
+ isTemplate: boolean;
22
+ templateSource: string | null;
23
+ executionsCount: number;
24
+ createdAt: string;
25
+ updatedAt: string;
26
+ }
27
+
28
+ export interface WorkflowExecution {
29
+ id: string;
30
+ workflow: string;
31
+ workflowVersion: number;
32
+ triggeredBy: string;
33
+ status: 'pending' | 'running' | 'completed' | 'failed' | 'paused';
34
+ mode: 'manual' | 'trigger' | 'webhook';
35
+ contextData: Record<string, any>;
36
+ state: Record<string, any>;
37
+ startedAt: string | null;
38
+ completedAt: string | null;
39
+ waitUntil: string | null;
40
+ errorMessage: string | null;
41
+ createdAt: string;
42
+ updatedAt: string;
43
+ }
44
+
45
+ export interface WorkflowFilters {
46
+ team?: string;
47
+ isActive?: boolean;
48
+ isTemplate?: boolean;
49
+ page?: number;
50
+ pageSize?: number;
51
+ ordering?: string;
52
+ }
53
+
54
+ export interface ExecuteWorkflowInput {
55
+ contextData?: Record<string, any>;
56
+ mode?: 'manual' | 'trigger' | 'webhook';
57
+ }
58
+
59
+ export interface CreateWorkflowInput {
60
+ name: string;
61
+ description?: string;
62
+ team: string;
63
+ nodes?: any[];
64
+ connections?: Record<string, any>;
65
+ settings?: Record<string, any>;
66
+ isActive?: boolean;
67
+ }
68
+
69
+ export interface UpdateWorkflowInput extends Partial<CreateWorkflowInput> {}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Utility exports for @startsimpli/api
3
+ */
4
+
5
+ export { buildUrl, buildQueryString, normalizeId } from './url-builder';
6
+ export type { UrlBuilderOptions } from './url-builder';
7
+
8
+ export {
9
+ buildFilterParams,
10
+ buildOrderingParam,
11
+ mergeQueryParams,
12
+ } from './query-params';
13
+ export type { DjangoFilterParams } from './query-params';
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Django query parameter helpers
3
+ */
4
+
5
+ import type { PaginationParams, SortParams } from '../types';
6
+
7
+ export interface DjangoFilterParams extends PaginationParams, SortParams {
8
+ search?: string;
9
+ [key: string]: unknown;
10
+ }
11
+
12
+ /**
13
+ * Build Django filter params from filters object
14
+ */
15
+ export function buildFilterParams<T extends Record<string, unknown>>(
16
+ filters: T
17
+ ): Record<string, unknown> {
18
+ const params: Record<string, unknown> = {};
19
+
20
+ Object.entries(filters).forEach(([key, value]) => {
21
+ if (value === undefined || value === null) {
22
+ return;
23
+ }
24
+
25
+ // Handle boolean filters (e.g., has_email=true)
26
+ if (typeof value === 'boolean') {
27
+ params[key] = value;
28
+ return;
29
+ }
30
+
31
+ // Handle array filters (e.g., tier__in=[1,2,3])
32
+ if (Array.isArray(value)) {
33
+ if (value.length > 0) {
34
+ params[`${key}__in`] = value.join(',');
35
+ }
36
+ return; // Skip empty arrays
37
+ }
38
+
39
+ // Handle range filters (e.g., check_size_min__gte=100000)
40
+ if (key.endsWith('_gte') || key.endsWith('_lte') || key.endsWith('_gt') || key.endsWith('_lt')) {
41
+ params[key] = value;
42
+ return;
43
+ }
44
+
45
+ // Default: exact match
46
+ params[key] = value;
47
+ });
48
+
49
+ return params;
50
+ }
51
+
52
+ /**
53
+ * Build Django ordering param from sort config
54
+ */
55
+ export function buildOrderingParam(
56
+ field?: string,
57
+ direction: 'asc' | 'desc' = 'asc'
58
+ ): string | undefined {
59
+ if (!field) {
60
+ return undefined;
61
+ }
62
+
63
+ return direction === 'desc' ? `-${field}` : field;
64
+ }
65
+
66
+ /**
67
+ * Merge pagination, sorting, and filter params
68
+ */
69
+ export function mergeQueryParams(
70
+ pagination?: PaginationParams,
71
+ sorting?: SortParams,
72
+ filters?: Record<string, unknown>
73
+ ): Record<string, unknown> {
74
+ return {
75
+ ...filters,
76
+ ...pagination,
77
+ ...sorting,
78
+ };
79
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * URL construction utilities for Django REST API
3
+ */
4
+
5
+ export interface UrlBuilderOptions {
6
+ baseUrl: string;
7
+ endpoint: string;
8
+ params?: Record<string, unknown>;
9
+ id?: string | number;
10
+ }
11
+
12
+ /**
13
+ * Build API URL with query parameters
14
+ */
15
+ export function buildUrl({ baseUrl, endpoint, params, id }: UrlBuilderOptions): string {
16
+ // Remove trailing slash from baseUrl, leading slash from endpoint
17
+ const cleanBase = baseUrl.replace(/\/$/, '');
18
+ const cleanEndpoint = endpoint.replace(/^\//, '');
19
+
20
+ // Build path with optional ID
21
+ let path = `${cleanBase}/${cleanEndpoint}`;
22
+ if (id !== undefined) {
23
+ path += `/${id}`;
24
+ }
25
+
26
+ // Add trailing slash (Django convention)
27
+ if (!path.endsWith('/')) {
28
+ path += '/';
29
+ }
30
+
31
+ // Add query parameters
32
+ if (params && Object.keys(params).length > 0) {
33
+ const queryString = buildQueryString(params);
34
+ if (queryString) {
35
+ path += `?${queryString}`;
36
+ }
37
+ }
38
+
39
+ return path;
40
+ }
41
+
42
+ /**
43
+ * Build query string from params object
44
+ */
45
+ export function buildQueryString(params: Record<string, unknown>): string {
46
+ const searchParams = new URLSearchParams();
47
+
48
+ Object.entries(params).forEach(([key, value]) => {
49
+ if (value === undefined || value === null) {
50
+ return;
51
+ }
52
+
53
+ if (Array.isArray(value)) {
54
+ // Django handles arrays with comma-separated values
55
+ value.forEach((item) => {
56
+ searchParams.append(key, String(item));
57
+ });
58
+ } else if (typeof value === 'object') {
59
+ // Serialize objects as JSON
60
+ searchParams.append(key, JSON.stringify(value));
61
+ } else {
62
+ searchParams.append(key, String(value));
63
+ }
64
+ });
65
+
66
+ return searchParams.toString();
67
+ }
68
+
69
+ /**
70
+ * Extract ID from URL or return as-is
71
+ */
72
+ export function normalizeId(id: string | number): string | number {
73
+ if (typeof id === 'string' && id.includes('/')) {
74
+ const parts = id.split('/').filter(Boolean);
75
+ return parts[parts.length - 1];
76
+ }
77
+ return id;
78
+ }