@everystack/admin 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 +282 -0
  2. package/package.json +53 -0
  3. package/src/AdminRoot.tsx +34 -0
  4. package/src/adapters/EverystackAdapter.ts +218 -0
  5. package/src/adapters/types.ts +78 -0
  6. package/src/components/AvatarFieldInput.tsx +100 -0
  7. package/src/components/ImageDisplay.tsx +57 -0
  8. package/src/components/ImageFieldInput.tsx +140 -0
  9. package/src/components/dashboard/Brush.tsx +139 -0
  10. package/src/components/dashboard/DimensionBars.tsx +67 -0
  11. package/src/components/dashboard/DimensionDonut.tsx +98 -0
  12. package/src/components/dashboard/EventTable.tsx +248 -0
  13. package/src/components/dashboard/StatDials.tsx +67 -0
  14. package/src/components/dashboard/TimeHistogram.tsx +158 -0
  15. package/src/context.ts +75 -0
  16. package/src/hooks/useAdminConfig.ts +22 -0
  17. package/src/hooks/useAdminData.ts +132 -0
  18. package/src/hooks/useAggregates.ts +290 -0
  19. package/src/hooks/useRpc.ts +124 -0
  20. package/src/hooks/useStorageConfig.ts +16 -0
  21. package/src/index.ts +138 -0
  22. package/src/presets/twitter.ts +267 -0
  23. package/src/providers/AdminConfigProvider.tsx +18 -0
  24. package/src/providers/DataAdapterProvider.tsx +18 -0
  25. package/src/rpc/resource-dashboard-rpc.ts +416 -0
  26. package/src/screens/AdminDashboard.tsx +217 -0
  27. package/src/screens/AdminLayout.tsx +97 -0
  28. package/src/screens/ResourceCreate.tsx +243 -0
  29. package/src/screens/ResourceDashboard.tsx +278 -0
  30. package/src/screens/ResourceEdit.tsx +283 -0
  31. package/src/screens/ResourceList.tsx +153 -0
  32. package/src/screens/ResourceShow.tsx +171 -0
  33. package/src/screens/index.ts +14 -0
  34. package/src/types.ts +358 -0
  35. package/src/utils/naming.ts +105 -0
  36. package/src/utils/pick-image-web.ts +81 -0
  37. package/src/utils/resolve-image-url.ts +43 -0
  38. package/src/utils/upload-file.ts +93 -0
package/README.md ADDED
@@ -0,0 +1,282 @@
1
+ # @everystack/admin
2
+
3
+ Declarative admin dashboard framework for PostgREST APIs. Configuration-driven CRUD screens, dashboard widgets, image field renderers, and data hooks — all wired to `@everystack/api`.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add @everystack/admin @everystack/api @everystack/ui react react-native
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ Define your admin configuration and render with `AdminRoot`:
14
+
15
+ ```tsx
16
+ import { AdminRoot, EverystackAdapter } from '@everystack/admin';
17
+ import { createClient } from '@everystack/api/client';
18
+ import type { AdminConfig } from '@everystack/admin';
19
+
20
+ const client = createClient({ baseUrl: '/api', getToken: () => token });
21
+ const adapter = new EverystackAdapter({ client });
22
+
23
+ const config: AdminConfig = {
24
+ resources: {
25
+ users: {
26
+ label: 'Users',
27
+ fields: {
28
+ id: { type: 'text', label: 'ID' },
29
+ name: { type: 'text', label: 'Name' },
30
+ email: { type: 'text', label: 'Email' },
31
+ avatar: { type: 'image', label: 'Avatar' },
32
+ createdAt: { type: 'datetime', label: 'Created' },
33
+ },
34
+ listFields: ['name', 'email', 'createdAt'],
35
+ searchFields: ['name', 'email'],
36
+ },
37
+ posts: {
38
+ label: 'Posts',
39
+ fields: {
40
+ id: { type: 'text', label: 'ID' },
41
+ title: { type: 'text', label: 'Title' },
42
+ content: { type: 'textarea', label: 'Content' },
43
+ status: { type: 'select', label: 'Status', options: ['draft', 'published'] },
44
+ },
45
+ },
46
+ },
47
+ menu: [
48
+ { label: 'Users', resource: 'users', icon: 'users' },
49
+ { label: 'Posts', resource: 'posts', icon: 'file-text' },
50
+ ],
51
+ };
52
+
53
+ function AdminApp() {
54
+ return <AdminRoot config={config} adapter={adapter} />;
55
+ }
56
+ ```
57
+
58
+ ## Entry Points
59
+
60
+ | Import | Description |
61
+ |--------|-------------|
62
+ | `@everystack/admin` | Config types, hooks, screens, adapter, components |
63
+ | `@everystack/admin/presets/twitter` | Pre-built Twitter clone admin preset |
64
+
65
+ ## Admin Configuration
66
+
67
+ ### Field Types
68
+
69
+ | Type | Description |
70
+ |------|-------------|
71
+ | `text` | Single-line text input |
72
+ | `textarea` | Multi-line text |
73
+ | `number` | Numeric input |
74
+ | `boolean` | Toggle switch |
75
+ | `select` | Dropdown with `options` array |
76
+ | `datetime` | Date/time display |
77
+ | `image` | Image upload with `ImageFieldInput` |
78
+ | `avatar` | Avatar upload with `AvatarFieldInput` |
79
+ | `json` | JSON display |
80
+ | `relation` | Foreign key reference |
81
+
82
+ ### Resource Config
83
+
84
+ ```typescript
85
+ interface ResourceConfig {
86
+ label: string;
87
+ fields: Record<string, FieldDefinition>;
88
+ listFields?: string[]; // Columns shown in list view
89
+ searchFields?: string[]; // Fields searchable via text input
90
+ defaultSort?: string; // Default ordering (e.g., 'createdAt.desc')
91
+ dashboard?: ResourceDashboardConfig; // Per-resource analytics
92
+ }
93
+ ```
94
+
95
+ ## Data Adapter
96
+
97
+ The `EverystackAdapter` bridges admin hooks to the `@everystack/api` client:
98
+
99
+ ```typescript
100
+ import { EverystackAdapter } from '@everystack/admin';
101
+ import { createClient } from '@everystack/api/client';
102
+
103
+ const client = createClient({ baseUrl: '/api', getToken: () => token });
104
+
105
+ const adapter = new EverystackAdapter({
106
+ client,
107
+ // Optional: custom fetch for RPC calls
108
+ rpcFetch: async (name, body) => { /* ... */ },
109
+ });
110
+ ```
111
+
112
+ The adapter implements the `DataAdapter` interface:
113
+
114
+ ```typescript
115
+ interface DataAdapter {
116
+ list(resource: string, params: ListParams): Promise<ListResult>;
117
+ getOne(resource: string, id: string): Promise<DataRecord>;
118
+ create(resource: string, data: DataRecord): Promise<DataRecord>;
119
+ update(resource: string, id: string, data: DataRecord): Promise<DataRecord>;
120
+ delete(resource: string, id: string): Promise<void>;
121
+ aggregate(resource: string, params: AggregateParams): Promise<AggregateResult>;
122
+ rpc(name: string, body: unknown): Promise<unknown>;
123
+ subscribe?(resource: string, callback: (event: SubscriptionEvent) => void): () => void;
124
+ }
125
+ ```
126
+
127
+ ## Hooks
128
+
129
+ ### useAdminData
130
+
131
+ CRUD data hook for any resource:
132
+
133
+ ```tsx
134
+ const { data, total, isLoading, error, refetch } = useAdminData('posts', {
135
+ page: 1,
136
+ perPage: 20,
137
+ sort: 'createdAt.desc',
138
+ filters: { status: 'eq.published' },
139
+ });
140
+ ```
141
+
142
+ ### useAggregates
143
+
144
+ Server-side aggregation hooks:
145
+
146
+ ```tsx
147
+ const { data: count } = useCount('posts');
148
+ const { data: grouped } = useGroupedCount('posts', 'status');
149
+ const { data: stats } = useStats('posts', 'views', ['sum', 'avg', 'max']);
150
+ ```
151
+
152
+ ### useRpc
153
+
154
+ Call server-side RPC functions:
155
+
156
+ ```tsx
157
+ const { data, isLoading, call } = useRpc('dashboard-stats', {
158
+ autoFetch: true,
159
+ body: { period: '7d' },
160
+ });
161
+ ```
162
+
163
+ ### useStorageConfig
164
+
165
+ Access storage configuration for image uploads:
166
+
167
+ ```tsx
168
+ const { uploadUrl, cdnUrl } = useStorageConfig();
169
+ ```
170
+
171
+ ## Screens
172
+
173
+ Built-in screens that render from config:
174
+
175
+ | Component | Description |
176
+ |-----------|-------------|
177
+ | `AdminLayout` | Navigation sidebar + content area |
178
+ | `AdminDashboard` | Overview with stat cards |
179
+ | `ResourceList` | Paginated table with filters |
180
+ | `ResourceShow` | Read-only record detail |
181
+ | `ResourceCreate` | Create form |
182
+ | `ResourceEdit` | Edit form |
183
+ | `ResourceDashboard` | Per-resource analytics |
184
+
185
+ ### Manual Screen Usage
186
+
187
+ ```tsx
188
+ import { AdminLayout, ResourceList, ResourceEdit } from '@everystack/admin';
189
+
190
+ // In your router:
191
+ <AdminLayout config={config}>
192
+ <ResourceList resource="posts" config={config.resources.posts} />
193
+ </AdminLayout>
194
+ ```
195
+
196
+ ## Image Fields
197
+
198
+ Image upload field renderers for use in create/edit forms:
199
+
200
+ ```tsx
201
+ import { ImageFieldInput, AvatarFieldInput, ImageDisplay } from '@everystack/admin';
202
+
203
+ // In a form
204
+ <ImageFieldInput
205
+ value={formData.coverImage}
206
+ onChange={(key) => setFormData({ ...formData, coverImage: key })}
207
+ storageConfig={{ uploadUrl: '/api/storage', cdnUrl: 'https://cdn.example.com' }}
208
+ />
209
+
210
+ <AvatarFieldInput
211
+ value={formData.avatar}
212
+ onChange={(key) => setFormData({ ...formData, avatar: key })}
213
+ storageConfig={storageConfig}
214
+ />
215
+
216
+ // Read-only display
217
+ <ImageDisplay value={record.coverImage} cdnUrl="https://cdn.example.com" />
218
+ ```
219
+
220
+ ## Dashboard Widgets
221
+
222
+ SVG-based chart components for analytics:
223
+
224
+ ```tsx
225
+ import { StatDials, DimensionBars, TimeHistogram, EventTable } from '@everystack/admin';
226
+
227
+ <StatDials data={[{ label: 'Users', value: 1234, trend: 12 }]} />
228
+ <DimensionBars data={[{ label: 'iOS', value: 60 }, { label: 'Android', value: 40 }]} />
229
+ <TimeHistogram data={timeSeriesData} />
230
+ <EventTable columns={columns} data={events} />
231
+ ```
232
+
233
+ ## Presets
234
+
235
+ Pre-built admin configurations for common schemas:
236
+
237
+ ```typescript
238
+ import { twitterPreset } from '@everystack/admin/presets/twitter';
239
+
240
+ const config = twitterPreset({
241
+ // Override any resource config
242
+ resources: {
243
+ posts: { label: 'Tweets' },
244
+ },
245
+ });
246
+ ```
247
+
248
+ ## Utilities
249
+
250
+ ```typescript
251
+ import { toCamelCase, toSnakeCase, resolveImageUrl, uploadFile } from '@everystack/admin';
252
+
253
+ // Naming conversion
254
+ toCamelCase('created_at'); // 'createdAt'
255
+ toSnakeCase('createdAt'); // 'created_at'
256
+
257
+ // Image URL resolution
258
+ const url = resolveImageUrl('uploads/abc.jpg', { cdnUrl: 'https://cdn.example.com' });
259
+
260
+ // File upload
261
+ const result = await uploadFile(file, {
262
+ uploadUrl: '/api/storage/presign',
263
+ getToken: () => token,
264
+ });
265
+ ```
266
+
267
+ ## Peer Dependencies
268
+
269
+ | Package | Version |
270
+ |---------|---------|
271
+ | `@everystack/api` | `>=0.1.0` |
272
+ | `@everystack/ui` | `>=0.1.0` |
273
+ | `react` | `>=18.0.0` |
274
+ | `react-native` | `>=0.72.0` |
275
+
276
+ ---
277
+
278
+ Part of [everystack](https://github.com/scalable-technology/everystack) — a self-hosted application stack for Expo apps on AWS.
279
+
280
+ ## License
281
+
282
+ MIT
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@everystack/admin",
3
+ "version": "0.1.0",
4
+ "description": "Declarative admin dashboard framework for PostgREST APIs",
5
+ "license": "MIT",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "files": [
10
+ "src",
11
+ "README.md"
12
+ ],
13
+ "type": "module",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./src/index.ts",
17
+ "default": "./src/index.ts"
18
+ },
19
+ "./presets/twitter": {
20
+ "types": "./src/presets/twitter.ts",
21
+ "default": "./src/presets/twitter.ts"
22
+ }
23
+ },
24
+ "dependencies": {
25
+ "d3-scale": "^4.0.2",
26
+ "d3-time-format": "^4.1.0"
27
+ },
28
+ "peerDependencies": {
29
+ "@everystack/api": ">=0.1.0",
30
+ "@everystack/ui": ">=0.1.0",
31
+ "react": ">=18.0.0",
32
+ "react-native": ">=0.72.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/d3-scale": "^4.0.9",
36
+ "@types/d3-time-format": "^4.0.3",
37
+ "@types/jest": "^29.5.14",
38
+ "@types/node": "^22.0.0",
39
+ "@types/react": "~19.2.14",
40
+ "jest": "^29.7.0",
41
+ "react": "^19.0.0",
42
+ "react-native": ">=0.72.0",
43
+ "ts-jest": "^29.3.0",
44
+ "typescript": "^5.7.0",
45
+ "@everystack/api": "0.1.0",
46
+ "@everystack/ui": "0.1.0"
47
+ },
48
+ "scripts": {
49
+ "test": "NODE_OPTIONS='--experimental-vm-modules' jest",
50
+ "build": "tsc --build",
51
+ "lint": "tsc --noEmit"
52
+ }
53
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * AdminRoot
3
+ *
4
+ * Root component for the admin dashboard.
5
+ * Combines AdminConfigProvider and DataAdapterProvider.
6
+ */
7
+
8
+ import React from 'react';
9
+ import type { AdminConfig } from './types';
10
+ import type { DataAdapter } from './adapters/types';
11
+ import { AdminConfigProvider } from './providers/AdminConfigProvider';
12
+ import { DataAdapterProvider } from './providers/DataAdapterProvider';
13
+ import { StorageConfigContext } from './hooks/useStorageConfig';
14
+
15
+ export interface AdminRootProps {
16
+ /** Admin configuration */
17
+ config: AdminConfig;
18
+ /** Data adapter for backend communication */
19
+ adapter: DataAdapter;
20
+ /** Children to render (typically expo-router's Slot or navigator) */
21
+ children?: React.ReactNode;
22
+ }
23
+
24
+ export function AdminRoot({ config, adapter, children }: AdminRootProps) {
25
+ return (
26
+ <AdminConfigProvider config={config}>
27
+ <DataAdapterProvider adapter={adapter}>
28
+ <StorageConfigContext.Provider value={config.storage ?? null}>
29
+ {children}
30
+ </StorageConfigContext.Provider>
31
+ </DataAdapterProvider>
32
+ </AdminConfigProvider>
33
+ );
34
+ }
@@ -0,0 +1,218 @@
1
+ /**
2
+ * EverystackAdapter — bridges the admin DataAdapter interface to everystack/client.
3
+ *
4
+ * Translates the admin's ListParams (page/perPage/sort/order/filter)
5
+ * into PostgREST-compatible queries via the everystack client.
6
+ */
7
+
8
+ import { createClient, type Client, type ClientOptions, type QueryBuilder } from '@everystack/api/client';
9
+ import type { DataAdapter } from './types';
10
+ import type {
11
+ ListParams,
12
+ ListResult,
13
+ DataRecord,
14
+ AggregateParams,
15
+ AggregateResult,
16
+ } from '../types';
17
+
18
+ /** Known PostgREST filter operators */
19
+ const POSTGREST_OPERATORS = new Set([
20
+ 'eq', 'neq', 'gt', 'gte', 'lt', 'lte',
21
+ 'like', 'ilike', 'is', 'in', 'not', 'cs', 'cd',
22
+ 'sl', 'sr', 'nxl', 'nxr', 'adj', 'ov', 'fts',
23
+ ]);
24
+
25
+ export interface EverystackAdapterOptions extends ClientOptions {}
26
+
27
+ export class EverystackAdapter implements DataAdapter {
28
+ private client: Client;
29
+
30
+ constructor(options: EverystackAdapterOptions) {
31
+ this.client = createClient(options);
32
+ }
33
+
34
+ async fetchList<T extends DataRecord = DataRecord>(
35
+ resource: string,
36
+ params: ListParams,
37
+ ): Promise<ListResult<T>> {
38
+ const page = params.page ?? 1;
39
+ const perPage = params.perPage ?? 25;
40
+ const offset = (page - 1) * perPage;
41
+
42
+ let builder = this.client.from(resource);
43
+
44
+ // Apply filters
45
+ builder = applyFilters(builder, params.filter);
46
+
47
+ // Apply sort
48
+ if (params.sort) {
49
+ builder = builder.order(params.sort, params.order ?? 'asc');
50
+ }
51
+
52
+ // Apply pagination
53
+ builder = builder.limit(perPage).offset(offset);
54
+
55
+ const result = await builder.executeWithCount();
56
+
57
+ if (result.error) {
58
+ throw new Error(result.error.message);
59
+ }
60
+
61
+ return {
62
+ data: (result.data ?? []) as T[],
63
+ total: result.total,
64
+ page,
65
+ perPage,
66
+ };
67
+ }
68
+
69
+ async fetchOne<T extends DataRecord = DataRecord>(
70
+ resource: string,
71
+ id: string | number,
72
+ ): Promise<T> {
73
+ const result = await this.client.from(resource).eq('id', String(id)).single();
74
+
75
+ if (result.error) {
76
+ throw new Error(result.error.status === 406
77
+ ? `${resource}/${id} not found`
78
+ : result.error.message);
79
+ }
80
+
81
+ return result.data as T;
82
+ }
83
+
84
+ async create<T extends DataRecord = DataRecord>(
85
+ resource: string,
86
+ data: Omit<T, 'id'>,
87
+ ): Promise<T> {
88
+ const result = await this.client.from(resource).insert(data as Record<string, unknown>);
89
+
90
+ if (result.error) {
91
+ throw new Error(result.error.message);
92
+ }
93
+
94
+ return result.data as T;
95
+ }
96
+
97
+ async update<T extends DataRecord = DataRecord>(
98
+ resource: string,
99
+ id: string | number,
100
+ data: Partial<T>,
101
+ ): Promise<T> {
102
+ const result = await this.client
103
+ .from(resource)
104
+ .eq('id', String(id))
105
+ .update(data as Record<string, unknown>);
106
+
107
+ if (result.error) {
108
+ throw new Error(result.error.message);
109
+ }
110
+
111
+ const rows = result.data as T[];
112
+ return rows[0];
113
+ }
114
+
115
+ async delete(resource: string, id: string | number): Promise<void> {
116
+ const result = await this.client.from(resource).eq('id', String(id)).remove();
117
+
118
+ if (result.error) {
119
+ throw new Error(result.error.message);
120
+ }
121
+ }
122
+
123
+ async aggregate(resource: string, params: AggregateParams): Promise<AggregateResult> {
124
+ const selectParts: string[] = [];
125
+
126
+ // Add groupBy columns first
127
+ if (params.groupBy) {
128
+ selectParts.push(...params.groupBy);
129
+ }
130
+
131
+ // Build aggregate select expressions
132
+ for (const agg of params.aggregates) {
133
+ if (agg.column) {
134
+ // column.fn() — e.g., amount.sum()
135
+ selectParts.push(`${agg.column}.${agg.function}()`);
136
+ } else {
137
+ // count() with no column
138
+ selectParts.push(`${agg.function}()`);
139
+ }
140
+ }
141
+
142
+ let builder = this.client.from(resource).select(selectParts.join(','));
143
+
144
+ // Apply filters
145
+ builder = applyFilters(builder, params.filter);
146
+
147
+ const result = await builder.execute();
148
+
149
+ if (result.error) {
150
+ throw new Error(result.error.message);
151
+ }
152
+
153
+ return {
154
+ data: (result.data ?? []) as Record<string, unknown>[],
155
+ meta: {
156
+ aggregate: true,
157
+ groupBy: params.groupBy,
158
+ },
159
+ };
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Apply admin filter params to a query builder.
165
+ * Detects PostgREST-formatted values (e.g., "eq.active", "gt.100")
166
+ * vs plain values (e.g., "active") and wraps plain values with eq.
167
+ */
168
+ function applyFilters(builder: QueryBuilder, filter?: Record<string, unknown>): QueryBuilder {
169
+ if (!filter) return builder;
170
+
171
+ for (const [key, value] of Object.entries(filter)) {
172
+ if (value == null) continue;
173
+
174
+ const strValue = String(value);
175
+
176
+ if (isPostgrestFilter(strValue)) {
177
+ // Already formatted — extract operator and value
178
+ const dotIndex = strValue.indexOf('.');
179
+ const op = strValue.substring(0, dotIndex);
180
+ const val = strValue.substring(dotIndex + 1);
181
+
182
+ switch (op) {
183
+ case 'eq': builder = builder.eq(key, val); break;
184
+ case 'neq': builder = builder.neq(key, val); break;
185
+ case 'gt': builder = builder.gt(key, val); break;
186
+ case 'gte': builder = builder.gte(key, val); break;
187
+ case 'lt': builder = builder.lt(key, val); break;
188
+ case 'lte': builder = builder.lte(key, val); break;
189
+ case 'like': builder = builder.like(key, val); break;
190
+ case 'ilike': builder = builder.ilike(key, val); break;
191
+ case 'is': builder = builder.is(key, val); break;
192
+ case 'in': {
193
+ const values = val.replace(/^\(/, '').replace(/\)$/, '').split(',');
194
+ builder = builder.in(key, values);
195
+ break;
196
+ }
197
+ default:
198
+ // Unknown operator — use eq with full value
199
+ builder = builder.eq(key, strValue);
200
+ }
201
+ } else {
202
+ // Plain value — wrap with eq
203
+ builder = builder.eq(key, strValue);
204
+ }
205
+ }
206
+
207
+ return builder;
208
+ }
209
+
210
+ /**
211
+ * Check if a filter value is already in PostgREST format (operator.value).
212
+ */
213
+ function isPostgrestFilter(value: string): boolean {
214
+ const dotIndex = value.indexOf('.');
215
+ if (dotIndex <= 0) return false;
216
+ const prefix = value.substring(0, dotIndex);
217
+ return POSTGREST_OPERATORS.has(prefix);
218
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Data Adapter Interface
3
+ *
4
+ * The DataAdapter provides a backend-agnostic abstraction for data operations.
5
+ * Implement this interface to connect the admin to any data source.
6
+ */
7
+
8
+ import type {
9
+ ListParams,
10
+ ListResult,
11
+ DataRecord,
12
+ AggregateParams,
13
+ AggregateResult,
14
+ } from '../types';
15
+
16
+ // =============================================================================
17
+ // Adapter Interface
18
+ // =============================================================================
19
+
20
+ export interface DataAdapter {
21
+ /**
22
+ * Fetch a list of records with pagination, sorting, and filtering
23
+ */
24
+ fetchList<T extends DataRecord = DataRecord>(
25
+ resource: string,
26
+ params: ListParams
27
+ ): Promise<ListResult<T>>;
28
+
29
+ /**
30
+ * Fetch a single record by ID
31
+ */
32
+ fetchOne<T extends DataRecord = DataRecord>(resource: string, id: string | number): Promise<T>;
33
+
34
+ /**
35
+ * Create a new record
36
+ */
37
+ create<T extends DataRecord = DataRecord>(resource: string, data: Omit<T, 'id'>): Promise<T>;
38
+
39
+ /**
40
+ * Update an existing record
41
+ */
42
+ update<T extends DataRecord = DataRecord>(
43
+ resource: string,
44
+ id: string | number,
45
+ data: Partial<T>
46
+ ): Promise<T>;
47
+
48
+ /**
49
+ * Delete a record
50
+ */
51
+ delete(resource: string, id: string | number): Promise<void>;
52
+
53
+ /**
54
+ * Fetch aggregate data (count, sum, avg, min, max) with optional grouping
55
+ */
56
+ aggregate?(resource: string, params: AggregateParams): Promise<AggregateResult>;
57
+
58
+ /**
59
+ * Subscribe to real-time updates (optional)
60
+ */
61
+ subscribe?<T extends DataRecord = DataRecord>(
62
+ resource: string,
63
+ callback: (event: SubscriptionEvent<T>) => void
64
+ ): () => void;
65
+ }
66
+
67
+ // =============================================================================
68
+ // Subscription Types
69
+ // =============================================================================
70
+
71
+ export type SubscriptionEventType = 'insert' | 'update' | 'delete';
72
+
73
+ export interface SubscriptionEvent<T extends DataRecord = DataRecord> {
74
+ type: SubscriptionEventType;
75
+ resource: string;
76
+ record: T;
77
+ previousRecord?: T;
78
+ }