@lyeve-labs/client 0.2.1

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.
@@ -0,0 +1,465 @@
1
+ /**
2
+ * Framework-agnostic HTTP client for the LyEve CMS API.
3
+ *
4
+ * The `createClient` factory accepts any `fetch`-compatible function
5
+ * (globalThis.fetch, SvelteKit's event.fetch, a Node.js polyfill) and
6
+ * returns a typed client with get/post/put/delete methods that handle
7
+ * JSON serialization, error mapping, and timeout.
8
+ */
9
+ declare class ApiError extends Error {
10
+ readonly status: number;
11
+ constructor(status: number, message: string);
12
+ }
13
+ type HttpClient = ReturnType<typeof createClient>;
14
+ declare function createClient(fetchFn: typeof fetch, defaultHeaders?: Record<string, string>): {
15
+ get: <T>(url: string, init?: RequestInit) => Promise<T>;
16
+ post: <T>(url: string, body: unknown, init?: RequestInit) => Promise<T>;
17
+ put: <T>(url: string, body: unknown, init?: RequestInit) => Promise<T>;
18
+ patch: <T>(url: string, body: unknown, init?: RequestInit) => Promise<T>;
19
+ delete: <T>(url: string, init?: RequestInit) => Promise<T>;
20
+ };
21
+
22
+ interface Schema {
23
+ name: string;
24
+ display_name: string;
25
+ fields: SchemaField[];
26
+ /** Whether to include a created_at system field (TIMESTAMPTZ, auto-set on insert). */
27
+ with_created_at?: boolean;
28
+ /** Whether to include an updated_at system field (TIMESTAMPTZ, auto-set on update). */
29
+ with_updated_at?: boolean;
30
+ /** Whether content entries support draft/published/archived status. */
31
+ with_draft_publish?: boolean;
32
+ /** Whether to include a deleted_at soft-delete column. */
33
+ with_soft_delete?: boolean;
34
+ /** Whether to support per-row localization. */
35
+ with_localization?: boolean;
36
+ /** Canvas position : stored client-side in localStorage. */
37
+ _pos?: {
38
+ x: number;
39
+ y: number;
40
+ };
41
+ }
42
+ interface SchemaField {
43
+ /** Client-side stable id for drag-and-drop : not sent to the server. */
44
+ id?: string;
45
+ name: string;
46
+ field_type: FieldType;
47
+ required: boolean;
48
+ unique: boolean;
49
+ indexed: boolean;
50
+ default?: unknown;
51
+ relation_to?: string;
52
+ /** 'belongs_to' (FK on this table) | 'has_one' | 'has_many' | 'many_to_many' (pivot table). */
53
+ relation_type?: "belongs_to" | "has_one" | "has_many" | "many_to_many";
54
+ /** Override the auto-generated pivot table name (many_to_many only). */
55
+ relation_through?: string;
56
+ /** Override the auto-generated FK column name (belongs_to only). */
57
+ relation_fk_name?: string;
58
+ /** Set by the server : these fields cannot be edited or removed in the UI. */
59
+ system?: boolean;
60
+ }
61
+ type FieldType = "text" | "rich_text" | "number" | "boolean" | "date" | "datetime" | "json" | "relation" | "media" | "email" | "url" | "uid";
62
+ interface Content {
63
+ id: string;
64
+ schema_name: string;
65
+ slug?: string;
66
+ status?: string;
67
+ data: Record<string, unknown>;
68
+ tenant_id?: string;
69
+ created_at: string;
70
+ updated_at: string;
71
+ }
72
+ interface APIKey {
73
+ id: string;
74
+ name: string;
75
+ roles: string[];
76
+ schemas: string[];
77
+ enabled: boolean;
78
+ monthly_limit: number;
79
+ created_at: string;
80
+ expires_at: string | null;
81
+ }
82
+ interface CreateAPIKeyResponse extends APIKey {
83
+ raw_key: string;
84
+ }
85
+ interface Webhook {
86
+ id: string;
87
+ name: string;
88
+ url: string;
89
+ /** Subset of: before_create | after_create | before_update | after_update | before_delete | after_delete. */
90
+ events: string[];
91
+ /** Empty array = all schemas. */
92
+ schemas: string[];
93
+ enabled: boolean;
94
+ created_at: string;
95
+ updated_at: string;
96
+ }
97
+ interface WebhookTestResult {
98
+ success: boolean;
99
+ status_code: number;
100
+ message: string;
101
+ }
102
+ interface WebhookDelivery {
103
+ id: string;
104
+ webhook_id: string;
105
+ event_type: string;
106
+ schema_name: string;
107
+ status_code?: number;
108
+ success: boolean;
109
+ duration_ms?: number;
110
+ request_body: string;
111
+ error?: string;
112
+ attempted_at: string;
113
+ retry_count: number;
114
+ next_retry_at?: string;
115
+ }
116
+ interface RetryDeliveryResult {
117
+ success: boolean;
118
+ status_code: number;
119
+ message: string;
120
+ delivery_id: string;
121
+ }
122
+ interface RetryConfig$1 {
123
+ id: string;
124
+ webhook_id: string;
125
+ max_attempts: number;
126
+ base_delay_ms: number;
127
+ max_delay_ms: number;
128
+ strategy: "exponential" | "fixed" | "linear";
129
+ enabled: boolean;
130
+ created_at: string;
131
+ updated_at: string;
132
+ }
133
+ type RetryConfigInput = Partial<Pick<RetryConfig$1, "max_attempts" | "base_delay_ms" | "max_delay_ms" | "strategy" | "enabled">>;
134
+ type DLQStatus = "pending" | "replayed" | "dismissed";
135
+ interface DeadLetter {
136
+ id: string;
137
+ webhook_id: string;
138
+ webhook_name: string;
139
+ webhook_url: string;
140
+ event_type: string;
141
+ schema_name: string;
142
+ request_body: string;
143
+ last_status_code?: number;
144
+ last_error?: string;
145
+ total_attempts: number;
146
+ status: DLQStatus;
147
+ dead_at: string;
148
+ replayed_at?: string;
149
+ created_at: string;
150
+ }
151
+ interface PaginatedResponse<T> {
152
+ items: T[];
153
+ total: number;
154
+ limit: number;
155
+ offset: number;
156
+ }
157
+ interface WebhookHealthStats {
158
+ webhook_id: string;
159
+ webhook_name: string;
160
+ total_attempts: number;
161
+ success_count: number;
162
+ failed_count: number;
163
+ exhausted_count: number;
164
+ dlq_pending: number;
165
+ average_duration_ms?: number;
166
+ healthy: boolean;
167
+ }
168
+ interface GlobalHealthStats {
169
+ total_webhooks: number;
170
+ healthy_webhooks: number;
171
+ unhealthy_webhooks: number;
172
+ pending_retries: number;
173
+ total_dlq: number;
174
+ pending_dlq: number;
175
+ }
176
+ interface IncomingWebhook {
177
+ id: string;
178
+ name: string;
179
+ schema_name: string;
180
+ field_map: Record<string, string>;
181
+ enabled: boolean;
182
+ allowed_ips: string[];
183
+ created_at: string;
184
+ updated_at: string;
185
+ }
186
+ interface User {
187
+ id: string;
188
+ email: string;
189
+ roles: string[];
190
+ tenant_id: string;
191
+ disabled: boolean;
192
+ created_at: string;
193
+ expires_at?: string | null;
194
+ }
195
+ interface ListResponse<T> {
196
+ items: T[];
197
+ total?: number;
198
+ }
199
+ interface OAuthProvider {
200
+ id: string;
201
+ name: string;
202
+ client_id: string;
203
+ /** Not returned by the server : only sent on create/update. */
204
+ client_secret?: string;
205
+ issuer_url: string;
206
+ scopes: string[];
207
+ roles_claim: string;
208
+ default_roles: string[];
209
+ enabled: boolean;
210
+ created_at: string;
211
+ updated_at: string;
212
+ }
213
+ interface Permission {
214
+ id: string;
215
+ role: string;
216
+ schema_name: string;
217
+ /** Subset of: create | read | update | delete. */
218
+ actions: string[];
219
+ /** Field names stripped from read responses for this role. */
220
+ field_mask: string[];
221
+ created_at: string;
222
+ }
223
+ /**
224
+ * License entitlement snapshot : mirrors the Go `license.Snapshot` returned by
225
+ * GET /api/admin/entitlements. Drives free-tier UI gating and upgrade prompts.
226
+ */
227
+ interface Entitlements {
228
+ /** Active plan, "free" when unlicensed. */
229
+ plan: string;
230
+ /** License state: free | active | grace | expired. */
231
+ state: string;
232
+ /** Entitled feature ids (e.g. "feature:rbac", "feature:schema_ui"). */
233
+ features: string[];
234
+ /** License expiry (ISO 8601), null when unlicensed or perpetual. */
235
+ expires_at: string | null;
236
+ /** Full days until expiry, -1 when unlicensed or perpetual. */
237
+ days_remaining: number;
238
+ }
239
+
240
+ /**
241
+ * Async iterator for cursor-based content pagination.
242
+ *
243
+ * Walks the Content API's cursor endpoint, yielding records one at a time.
244
+ * Each call to `.next()` returns the next record.
245
+ *
246
+ * ## Usage
247
+ * ```ts
248
+ * const paginator = new PaginationIterator<MyRecord>({
249
+ * fetchPage: (cursor) => api.getPage(cursor),
250
+ * });
251
+ *
252
+ * for await (const record of paginator) {
253
+ * console.log(record);
254
+ * }
255
+ *
256
+ * // With AbortController:
257
+ * const ac = new AbortController();
258
+ * setTimeout(() => ac.abort(), 5000);
259
+ * for await (const record of paginator.withSignal(ac.signal)) {
260
+ * // stops after 5 seconds
261
+ * }
262
+ * ```
263
+ */
264
+ /** Response shape from a cursor-paginated endpoint. */
265
+ interface CursorPage<T> {
266
+ items: T[];
267
+ /** Present when more pages are available. */
268
+ next_cursor?: string;
269
+ }
270
+ /** Function that fetches a single cursor page. */
271
+ type PageFetcher<T> = (cursor?: string) => Promise<CursorPage<T>>;
272
+ interface PaginationConfig<T> {
273
+ /** Function that fetches a single cursor page. */
274
+ fetchPage: PageFetcher<T>;
275
+ }
276
+ /**
277
+ * Async iterable iterator over paginated records.
278
+ *
279
+ * Implements `AsyncIterableIterator<T>` so it can be used directly in
280
+ * `for await...of` loops. Also exposes `.withSignal()` for AbortController
281
+ * integration.
282
+ */
283
+ declare class PaginationIterator<T> implements AsyncIterableIterator<T> {
284
+ #private;
285
+ constructor(config: PaginationConfig<T>);
286
+ /**
287
+ * Return a new iterator sharing the same underlying state but with a
288
+ * different AbortSignal. Useful when the signal is only known at the
289
+ * call site, not at construction time.
290
+ */
291
+ withSignal(signal: AbortSignal): PaginationIterator<T>;
292
+ [Symbol.asyncIterator](): AsyncIterableIterator<T>;
293
+ next(): Promise<IteratorResult<T>>;
294
+ return?(value?: unknown): Promise<IteratorResult<T>>;
295
+ throw?(e?: unknown): Promise<IteratorResult<T>>;
296
+ }
297
+
298
+ /**
299
+ * Fluent query builder for LyEve CMS Content API queries.
300
+ *
301
+ * Produces ContentQuery objects with a chainable API.
302
+ *
303
+ * ## Usage
304
+ * ```ts
305
+ * import { query } from '@lyeve/cms-client';
306
+ *
307
+ * const q = query('posts')
308
+ * .where('status', 'eq:published')
309
+ * .sort('-created_at')
310
+ * .limit(20)
311
+ * .build();
312
+ * ```
313
+ */
314
+ type ContentStatus = "draft" | "published" | "archived";
315
+ interface ContentQuery {
316
+ /** Filter by status. */
317
+ status?: ContentStatus;
318
+ /** Max records per page. */
319
+ limit?: number;
320
+ /** Offset for offset-based pagination. Mutually exclusive with cursor. */
321
+ offset?: number;
322
+ /** Cursor for cursor-based pagination. Mutually exclusive with offset. */
323
+ cursor?: string;
324
+ /** Field-level filters in `field=op:value` format. */
325
+ filters?: Record<string, string>;
326
+ /** Sort field(s), comma-separated. Prefix with `-` for descending. */
327
+ sort?: string;
328
+ }
329
+ /**
330
+ * Builder for ContentQuery objects.
331
+ *
332
+ * Each field can only hold one filter (Record<string, string>). Calling
333
+ * where() (or whereEq/whereGt etc.) with an already-set field name
334
+ * overwrites the previous filter. For range queries on the same field,
335
+ * use the server's range filter syntax in a single call.
336
+ */
337
+ declare class QueryBuilder {
338
+ #private;
339
+ constructor(schema: string);
340
+ /** The schema/collection this query targets. */
341
+ get schema(): string;
342
+ /** Filter by content status (published, draft, archived). */
343
+ whereStatus(s: ContentStatus | undefined): this;
344
+ /** Add a field filter in `op:value` format. */
345
+ where(field: string, value: string): this;
346
+ /** Add a field-level equality filter. */
347
+ whereEq(field: string, value: string | number | boolean): this;
348
+ /** Add a field-level "in" filter (value is a JSON array). */
349
+ whereIn(field: string, values: (string | number)[]): this;
350
+ /** Greater-than comparison. */
351
+ whereGt(field: string, value: number): this;
352
+ /** Greater-than-or-equal comparison. */
353
+ whereGte(field: string, value: number): this;
354
+ /** Less-than comparison. */
355
+ whereLt(field: string, value: number): this;
356
+ /** Less-than-or-equal comparison. */
357
+ whereLte(field: string, value: number): this;
358
+ /** Full-text search (if the schema supports it). */
359
+ whereSearch(field: string, query: string): this;
360
+ /** Set the max records per page. */
361
+ limit(n: number | undefined): this;
362
+ /** Set the offset for offset-based pagination. Clears cursor. */
363
+ offset(n: number | undefined): this;
364
+ /** Set the cursor for cursor-based pagination. Clears offset. */
365
+ cursor(c: string | undefined): this;
366
+ /** Set sort fields. Prefix with `-` for descending. */
367
+ sort(...fields: string[]): this;
368
+ /** Clear all filters, keeping only the schema name. */
369
+ resetFilters(): this;
370
+ /** Produce the ContentQuery object. */
371
+ build(): ContentQuery;
372
+ }
373
+ /**
374
+ * Create a new query builder for the given schema.
375
+ * Equivalent to `new QueryBuilder(schema)`.
376
+ */
377
+ declare function query(schema: string): QueryBuilder;
378
+
379
+ /**
380
+ * Automatic retry with exponential backoff and jitter.
381
+ *
382
+ * Wraps fetch to retry on transient errors (429, 5xx, network failures).
383
+ * Uses "full jitter" backoff: `random(0, min(cap, base * 2^attempt))`.
384
+ *
385
+ * Usage:
386
+ * const fetchWithRetry = createRetryFetch(fetch, {
387
+ * maxRetries: 3,
388
+ * baseDelay: 1000,
389
+ * maxDelay: 30000,
390
+ * retryOn: [429, 500, 502, 503, 504],
391
+ * onRetry: (attempt, err, delay) => console.warn(`Retry ${attempt} in ${delay}ms`),
392
+ * });
393
+ * const res = await fetchWithRetry(url, init);
394
+ */
395
+ interface RetryConfig {
396
+ /** Maximum number of retry attempts (default: 3). */
397
+ maxRetries?: number;
398
+ /** Base delay in ms before first retry (default: 1000). */
399
+ baseDelay?: number;
400
+ /** Maximum delay in ms between retries (default: 30000). */
401
+ maxDelay?: number;
402
+ /** HTTP status codes that trigger a retry (default: [429, 500, 502, 503, 504]). */
403
+ retryOn?: number[];
404
+ /** If true, retry on network/abort errors as well (default: true). */
405
+ retryOnNetworkError?: boolean;
406
+ /** Called before each retry with (attempt, error, delayMs). */
407
+ onRetry?: (attempt: number, error: unknown, delayMs: number) => void;
408
+ }
409
+ /**
410
+ * Create a fetch wrapper that automatically retries on transient failures.
411
+ *
412
+ * Retry logic:
413
+ * 1. If the response status is in `retryOn`, read+discard the body, then retry.
414
+ * 2. If the request threw a network/abort error and `retryOnNetworkError` is true,
415
+ * retry.
416
+ * 3. Exponential backoff with full jitter.
417
+ * 4. Respects AbortSignal : aborts cancel the current attempt and skip remaining
418
+ * retries.
419
+ */
420
+ declare function createRetryFetch(fetchImpl: typeof globalThis.fetch, config?: RetryConfig): typeof globalThis.fetch;
421
+
422
+ /**
423
+ * In-flight request deduplication.
424
+ *
425
+ * When multiple callers request the same URL+method+body combination
426
+ * concurrently, only one network request is made. All callers receive
427
+ * the same response (the promise is shared).
428
+ *
429
+ * Cache entries are evicted when the shared promise settles (success or
430
+ * failure). Late subscribers that arrive after eviction start fresh.
431
+ */
432
+ /**
433
+ * Deduplicator for in-flight HTTP requests.
434
+ *
435
+ * ## Usage
436
+ * ```ts
437
+ * const dedupe = new RequestDeduplicator();
438
+ *
439
+ * async function fetchDeduped(url: string, init?: RequestInit): Promise<Response> {
440
+ * const method = init?.method ?? 'GET';
441
+ * const key = `${method}:${url}:${JSON.stringify(init?.body ?? '')}`;
442
+ * return dedupe.dedup(key, () => fetch(url, init), init?.signal);
443
+ * }
444
+ * ```
445
+ */
446
+ declare class RequestDeduplicator {
447
+ #private;
448
+ /**
449
+ * Execute `factory` once for the given key. Concurrent callers with
450
+ * the same key receive the same promise. The entry is evicted after
451
+ * the shared promise settles.
452
+ *
453
+ * @param key - Unique request key (method + URL + stable body hash).
454
+ * @param factory - The network operation to perform.
455
+ * @param signal - Optional AbortSignal. If it fires, this caller gets
456
+ * an AbortError. The shared request continues.
457
+ */
458
+ dedup<T>(key: string, factory: () => Promise<T>, signal?: AbortSignal): Promise<T>;
459
+ /** How many unique requests are currently in flight. */
460
+ get inflight(): number;
461
+ /** Remove all pending deduplication entries. */
462
+ clear(): void;
463
+ }
464
+
465
+ export { type APIKey, ApiError, type Content, type ContentQuery, type ContentStatus, type CreateAPIKeyResponse, type CursorPage, type DLQStatus, type DeadLetter, type Entitlements, type FieldType, type GlobalHealthStats, type HttpClient, type IncomingWebhook, type ListResponse, type OAuthProvider, type PageFetcher, type PaginatedResponse, type PaginationConfig, PaginationIterator, type Permission, QueryBuilder, RequestDeduplicator, type RetryConfig$1 as RetryConfig, type RetryConfigInput, type RetryDeliveryResult, type RetryConfig as RetryFetchConfig, type Schema, type SchemaField, type User, type Webhook, type WebhookDelivery, type WebhookHealthStats, type WebhookTestResult, createClient, createRetryFetch, query };