@svadmin/appwrite 0.3.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/package.json +33 -0
- package/src/appwrite.test.ts +227 -0
- package/src/auth-provider.ts +111 -0
- package/src/data-provider.ts +138 -0
- package/src/index.ts +5 -0
- package/src/live-provider.ts +61 -0
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@svadmin/appwrite",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Appwrite adapters — DataProvider, AuthProvider, LiveProvider for svadmin",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"files": [
|
|
8
|
+
"src/**/*"
|
|
9
|
+
],
|
|
10
|
+
"main": "src/index.ts",
|
|
11
|
+
"types": "src/index.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./src/index.ts",
|
|
15
|
+
"default": "./src/index.ts"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"@svadmin/core": "^0.3.0",
|
|
20
|
+
"appwrite": ">=14.0.0"
|
|
21
|
+
},
|
|
22
|
+
"keywords": ["svadmin", "appwrite", "data-provider", "auth-provider", "live-provider"],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "zuohuadong",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/zuohuadong/svadmin.git"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/zuohuadong/svadmin#readme",
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/zuohuadong/svadmin/issues"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// @svadmin/appwrite — Unit Tests
|
|
2
|
+
import { describe, test, expect, mock } from 'bun:test';
|
|
3
|
+
import { createAppwriteDataProvider } from './data-provider';
|
|
4
|
+
import { createAppwriteAuthProvider } from './auth-provider';
|
|
5
|
+
import { createAppwriteLiveProvider } from './live-provider';
|
|
6
|
+
|
|
7
|
+
// ─── Mock Appwrite Databases ────────────────────────────────
|
|
8
|
+
|
|
9
|
+
function createMockDatabases() {
|
|
10
|
+
return {
|
|
11
|
+
listDocuments: mock(async (_db: string, _col: string, _q?: string[]) => ({
|
|
12
|
+
documents: [{ $id: '1', name: 'Test' }, { $id: '2', name: 'Test2' }],
|
|
13
|
+
total: 2,
|
|
14
|
+
})),
|
|
15
|
+
getDocument: mock(async (_db: string, _col: string, _id: string) => ({
|
|
16
|
+
$id: _id, name: 'Test',
|
|
17
|
+
})),
|
|
18
|
+
createDocument: mock(async (_db: string, _col: string, _docId: string, data: Record<string, unknown>) => ({
|
|
19
|
+
$id: 'new-1', ...data,
|
|
20
|
+
})),
|
|
21
|
+
updateDocument: mock(async (_db: string, _col: string, _docId: string, data: Record<string, unknown>) => ({
|
|
22
|
+
$id: _docId, ...data,
|
|
23
|
+
})),
|
|
24
|
+
deleteDocument: mock(async () => {}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ─── Mock Appwrite Account ──────────────────────────────────
|
|
29
|
+
|
|
30
|
+
function createMockAccount(overrides: Record<string, unknown> = {}) {
|
|
31
|
+
return {
|
|
32
|
+
createEmailPasswordSession: mock(async () => ({ $id: 'session-1' })),
|
|
33
|
+
deleteSession: mock(async () => {}),
|
|
34
|
+
get: mock(async () => ({ $id: 'user-1', name: 'Admin', email: 'admin@test.com', prefs: {} })),
|
|
35
|
+
create: mock(async () => ({ $id: 'user-2' })),
|
|
36
|
+
createRecovery: mock(async () => ({})),
|
|
37
|
+
updatePassword: mock(async () => ({})),
|
|
38
|
+
...overrides,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── DataProvider Tests ─────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
describe('Appwrite DataProvider', () => {
|
|
45
|
+
test('getList returns data with total', async () => {
|
|
46
|
+
const db = createMockDatabases();
|
|
47
|
+
const dp = createAppwriteDataProvider({ databases: db, databaseId: 'main' });
|
|
48
|
+
const result = await dp.getList({ resource: 'posts' });
|
|
49
|
+
expect(result.data).toHaveLength(2);
|
|
50
|
+
expect(result.total).toBe(2);
|
|
51
|
+
expect(db.listDocuments).toHaveBeenCalledTimes(1);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('getList passes pagination as queries', async () => {
|
|
55
|
+
const db = createMockDatabases();
|
|
56
|
+
const dp = createAppwriteDataProvider({ databases: db, databaseId: 'main' });
|
|
57
|
+
await dp.getList({ resource: 'posts', pagination: { current: 2, pageSize: 5 } });
|
|
58
|
+
const queries = db.listDocuments.mock.calls[0][2] as string[];
|
|
59
|
+
expect(queries).toContain('limit(5)');
|
|
60
|
+
expect(queries).toContain('offset(5)');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('getList passes sorters as queries', async () => {
|
|
64
|
+
const db = createMockDatabases();
|
|
65
|
+
const dp = createAppwriteDataProvider({ databases: db, databaseId: 'main' });
|
|
66
|
+
await dp.getList({ resource: 'posts', sorters: [{ field: 'name', order: 'desc' }] });
|
|
67
|
+
const queries = db.listDocuments.mock.calls[0][2] as string[];
|
|
68
|
+
expect(queries.some((q: string) => q.includes('orderDesc'))).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('getList passes filters as queries', async () => {
|
|
72
|
+
const db = createMockDatabases();
|
|
73
|
+
const dp = createAppwriteDataProvider({ databases: db, databaseId: 'main' });
|
|
74
|
+
await dp.getList({ resource: 'posts', filters: [{ field: 'status', operator: 'eq', value: 'active' }] });
|
|
75
|
+
const queries = db.listDocuments.mock.calls[0][2] as string[];
|
|
76
|
+
expect(queries.some((q: string) => q.includes('equal'))).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('getOne returns single record', async () => {
|
|
80
|
+
const db = createMockDatabases();
|
|
81
|
+
const dp = createAppwriteDataProvider({ databases: db, databaseId: 'main' });
|
|
82
|
+
const result = await dp.getOne({ resource: 'posts', id: '1' });
|
|
83
|
+
expect(result.data).toHaveProperty('$id', '1');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('create returns new record', async () => {
|
|
87
|
+
const db = createMockDatabases();
|
|
88
|
+
const dp = createAppwriteDataProvider({ databases: db, databaseId: 'main' });
|
|
89
|
+
const result = await dp.create({ resource: 'posts', variables: { name: 'New Post' } });
|
|
90
|
+
expect(result.data).toHaveProperty('name', 'New Post');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('update returns updated record', async () => {
|
|
94
|
+
const db = createMockDatabases();
|
|
95
|
+
const dp = createAppwriteDataProvider({ databases: db, databaseId: 'main' });
|
|
96
|
+
const result = await dp.update({ resource: 'posts', id: '1', variables: { name: 'Updated' } });
|
|
97
|
+
expect(result.data).toHaveProperty('name', 'Updated');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('deleteOne calls deleteDocument', async () => {
|
|
101
|
+
const db = createMockDatabases();
|
|
102
|
+
const dp = createAppwriteDataProvider({ databases: db, databaseId: 'main' });
|
|
103
|
+
await dp.deleteOne({ resource: 'posts', id: '1' });
|
|
104
|
+
expect(db.deleteDocument).toHaveBeenCalledTimes(1);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('getMany fetches by ids', async () => {
|
|
108
|
+
const db = createMockDatabases();
|
|
109
|
+
const dp = createAppwriteDataProvider({ databases: db, databaseId: 'main' });
|
|
110
|
+
const result = await dp.getMany!({ resource: 'posts', ids: ['1', '2'] });
|
|
111
|
+
expect(result.data).toHaveLength(2);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('deleteMany deletes each id', async () => {
|
|
115
|
+
const db = createMockDatabases();
|
|
116
|
+
const dp = createAppwriteDataProvider({ databases: db, databaseId: 'main' });
|
|
117
|
+
await dp.deleteMany!({ resource: 'posts', ids: ['1', '2'] });
|
|
118
|
+
expect(db.deleteDocument).toHaveBeenCalledTimes(2);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ─── AuthProvider Tests ─────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
describe('Appwrite AuthProvider', () => {
|
|
125
|
+
test('login success', async () => {
|
|
126
|
+
const account = createMockAccount();
|
|
127
|
+
const auth = createAppwriteAuthProvider({ account });
|
|
128
|
+
const result = await auth.login({ email: 'admin@test.com', password: 'pass' });
|
|
129
|
+
expect(result.success).toBe(true);
|
|
130
|
+
expect(result.redirectTo).toBe('/');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('login failure', async () => {
|
|
134
|
+
const account = createMockAccount({
|
|
135
|
+
createEmailPasswordSession: mock(async () => { throw new Error('Invalid credentials'); }),
|
|
136
|
+
});
|
|
137
|
+
const auth = createAppwriteAuthProvider({ account });
|
|
138
|
+
const result = await auth.login({ email: 'bad', password: 'bad' });
|
|
139
|
+
expect(result.success).toBe(false);
|
|
140
|
+
expect(result.error?.message).toBe('Invalid credentials');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('logout success', async () => {
|
|
144
|
+
const account = createMockAccount();
|
|
145
|
+
const auth = createAppwriteAuthProvider({ account });
|
|
146
|
+
const result = await auth.logout();
|
|
147
|
+
expect(result.success).toBe(true);
|
|
148
|
+
expect(result.redirectTo).toBe('/login');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('check returns authenticated', async () => {
|
|
152
|
+
const account = createMockAccount();
|
|
153
|
+
const auth = createAppwriteAuthProvider({ account });
|
|
154
|
+
const result = await auth.check();
|
|
155
|
+
expect(result.authenticated).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('check returns not authenticated on error', async () => {
|
|
159
|
+
const account = createMockAccount({
|
|
160
|
+
get: mock(async () => { throw new Error('Unauthorized'); }),
|
|
161
|
+
});
|
|
162
|
+
const auth = createAppwriteAuthProvider({ account });
|
|
163
|
+
const result = await auth.check();
|
|
164
|
+
expect(result.authenticated).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('getIdentity returns user', async () => {
|
|
168
|
+
const account = createMockAccount();
|
|
169
|
+
const auth = createAppwriteAuthProvider({ account });
|
|
170
|
+
const identity = await auth.getIdentity();
|
|
171
|
+
expect(identity).not.toBeNull();
|
|
172
|
+
expect(identity!.name).toBe('Admin');
|
|
173
|
+
expect(identity!.email).toBe('admin@test.com');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('register success', async () => {
|
|
177
|
+
const account = createMockAccount();
|
|
178
|
+
const auth = createAppwriteAuthProvider({ account });
|
|
179
|
+
const result = await auth.register!({ email: 'new@test.com', password: 'pass123' });
|
|
180
|
+
expect(result.success).toBe(true);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('onError with 401', async () => {
|
|
184
|
+
const account = createMockAccount();
|
|
185
|
+
const auth = createAppwriteAuthProvider({ account });
|
|
186
|
+
const result = await auth.onError!(new Error('401 Unauthorized'));
|
|
187
|
+
expect(result.logout).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// ─── LiveProvider Tests ─────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
describe('Appwrite LiveProvider', () => {
|
|
194
|
+
test('subscribe calls client.subscribe with correct channel', () => {
|
|
195
|
+
const unsubFn = mock(() => {});
|
|
196
|
+
const mockClient = {
|
|
197
|
+
subscribe: mock((_channels: string, _cb: unknown) => unsubFn),
|
|
198
|
+
};
|
|
199
|
+
const lp = createAppwriteLiveProvider({ client: mockClient, databaseId: 'main' });
|
|
200
|
+
const cb = mock(() => {});
|
|
201
|
+
const unsub = lp.subscribe({ resource: 'posts', callback: cb });
|
|
202
|
+
|
|
203
|
+
expect(mockClient.subscribe).toHaveBeenCalledTimes(1);
|
|
204
|
+
const channel = mockClient.subscribe.mock.calls[0][0];
|
|
205
|
+
expect(channel).toBe('databases.main.collections.posts.documents');
|
|
206
|
+
|
|
207
|
+
unsub();
|
|
208
|
+
expect(unsubFn).toHaveBeenCalledTimes(1);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('subscribe maps create event to INSERT', () => {
|
|
212
|
+
let handler: ((payload: unknown) => void) | null = null;
|
|
213
|
+
const mockClient = {
|
|
214
|
+
subscribe: mock((_channels: string, cb: (payload: unknown) => void) => {
|
|
215
|
+
handler = cb;
|
|
216
|
+
return () => {};
|
|
217
|
+
}),
|
|
218
|
+
};
|
|
219
|
+
const lp = createAppwriteLiveProvider({ client: mockClient, databaseId: 'main' });
|
|
220
|
+
const cb = mock(() => {});
|
|
221
|
+
lp.subscribe({ resource: 'posts', callback: cb });
|
|
222
|
+
|
|
223
|
+
handler!({ events: ['databases.main.collections.posts.documents.*.create'], payload: { $id: '1' } });
|
|
224
|
+
expect(cb).toHaveBeenCalledTimes(1);
|
|
225
|
+
expect(cb.mock.calls[0][0].type).toBe('INSERT');
|
|
226
|
+
});
|
|
227
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// @svadmin/appwrite — Appwrite AuthProvider
|
|
2
|
+
import type { AuthProvider, Identity, AuthActionResult, CheckResult } from '@svadmin/core';
|
|
3
|
+
|
|
4
|
+
interface AppwriteAuthOptions {
|
|
5
|
+
/** Appwrite Account instance */
|
|
6
|
+
account: unknown; // Appwrite.Account
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Duck-typed Appwrite Account interface
|
|
10
|
+
interface AppwriteAccount {
|
|
11
|
+
createEmailPasswordSession: (email: string, password: string) => Promise<Record<string, unknown>>;
|
|
12
|
+
deleteSession: (sessionId: string) => Promise<void>;
|
|
13
|
+
get: () => Promise<{ $id: string; name: string; email: string; prefs?: Record<string, unknown> }>;
|
|
14
|
+
create: (userId: string, email: string, password: string, name?: string) => Promise<Record<string, unknown>>;
|
|
15
|
+
createRecovery: (email: string, url: string) => Promise<Record<string, unknown>>;
|
|
16
|
+
updatePassword: (password: string, oldPassword?: string) => Promise<Record<string, unknown>>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create an AuthProvider backed by Appwrite Account.
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
* ```ts
|
|
24
|
+
* import { Account, Client } from 'appwrite';
|
|
25
|
+
* import { createAppwriteAuthProvider } from '@svadmin/appwrite';
|
|
26
|
+
*
|
|
27
|
+
* const client = new Client().setEndpoint('https://cloud.appwrite.io/v1').setProject('PROJECT_ID');
|
|
28
|
+
* const account = new Account(client);
|
|
29
|
+
* const authProvider = createAppwriteAuthProvider({ account });
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function createAppwriteAuthProvider(options: AppwriteAuthOptions): AuthProvider {
|
|
33
|
+
const account = options.account as AppwriteAccount;
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
async login({ email, password }: Record<string, unknown>): Promise<AuthActionResult> {
|
|
37
|
+
try {
|
|
38
|
+
await account.createEmailPasswordSession(email as string, password as string);
|
|
39
|
+
return { success: true, redirectTo: '/' };
|
|
40
|
+
} catch (err) {
|
|
41
|
+
return { success: false, error: { message: (err as Error).message } };
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
async logout(): Promise<AuthActionResult> {
|
|
46
|
+
try {
|
|
47
|
+
await account.deleteSession('current');
|
|
48
|
+
return { success: true, redirectTo: '/login' };
|
|
49
|
+
} catch (err) {
|
|
50
|
+
return { success: false, error: { message: (err as Error).message } };
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
async check(): Promise<CheckResult> {
|
|
55
|
+
try {
|
|
56
|
+
await account.get();
|
|
57
|
+
return { authenticated: true };
|
|
58
|
+
} catch {
|
|
59
|
+
return { authenticated: false, redirectTo: '/login', logout: true };
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
async getIdentity(): Promise<Identity | null> {
|
|
64
|
+
try {
|
|
65
|
+
const user = await account.get();
|
|
66
|
+
return {
|
|
67
|
+
id: user.$id,
|
|
68
|
+
name: user.name || user.email.split('@')[0],
|
|
69
|
+
email: user.email,
|
|
70
|
+
avatar: user.prefs?.avatar as string | undefined,
|
|
71
|
+
};
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
async register({ email, password, name }: Record<string, unknown>): Promise<AuthActionResult> {
|
|
78
|
+
try {
|
|
79
|
+
await account.create('unique()', email as string, password as string, name as string | undefined);
|
|
80
|
+
return { success: true, redirectTo: '/login' };
|
|
81
|
+
} catch (err) {
|
|
82
|
+
return { success: false, error: { message: (err as Error).message } };
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
async forgotPassword({ email }: Record<string, unknown>): Promise<AuthActionResult> {
|
|
87
|
+
try {
|
|
88
|
+
await account.createRecovery(email as string, `${globalThis.location?.origin ?? ''}/update-password`);
|
|
89
|
+
return { success: true };
|
|
90
|
+
} catch (err) {
|
|
91
|
+
return { success: false, error: { message: (err as Error).message } };
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
async updatePassword({ password, oldPassword }: Record<string, unknown>): Promise<AuthActionResult> {
|
|
96
|
+
try {
|
|
97
|
+
await account.updatePassword(password as string, oldPassword as string | undefined);
|
|
98
|
+
return { success: true, redirectTo: '/' };
|
|
99
|
+
} catch (err) {
|
|
100
|
+
return { success: false, error: { message: (err as Error).message } };
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
async onError(error: unknown): Promise<{ redirectTo?: string; logout?: boolean }> {
|
|
105
|
+
if (error instanceof Error && (error.message.includes('401') || error.message.includes('unauthorized'))) {
|
|
106
|
+
return { redirectTo: '/login', logout: true };
|
|
107
|
+
}
|
|
108
|
+
return {};
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// @svadmin/appwrite — Appwrite DataProvider
|
|
2
|
+
// Maps svadmin CRUD operations to Appwrite SDK
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
DataProvider, GetListParams, GetListResult, GetOneParams, GetOneResult,
|
|
6
|
+
CreateParams, CreateResult, UpdateParams, UpdateResult, DeleteParams, DeleteResult,
|
|
7
|
+
GetManyParams, GetManyResult, DeleteManyParams, DeleteManyResult, BaseRecord,
|
|
8
|
+
} from '@svadmin/core';
|
|
9
|
+
|
|
10
|
+
interface AppwriteProviderOptions {
|
|
11
|
+
/** Appwrite Databases instance */
|
|
12
|
+
databases: unknown; // Appwrite.Databases
|
|
13
|
+
/** Database ID */
|
|
14
|
+
databaseId: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Duck-typed Appwrite SDK interfaces to avoid hard dependency
|
|
18
|
+
interface AppwriteDatabases {
|
|
19
|
+
listDocuments: (dbId: string, collectionId: string, queries?: string[]) => Promise<{ documents: Record<string, unknown>[]; total: number }>;
|
|
20
|
+
getDocument: (dbId: string, collectionId: string, docId: string) => Promise<Record<string, unknown>>;
|
|
21
|
+
createDocument: (dbId: string, collectionId: string, docId: string, data: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
22
|
+
updateDocument: (dbId: string, collectionId: string, docId: string, data: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
23
|
+
deleteDocument: (dbId: string, collectionId: string, docId: string) => Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Map svadmin CrudOperator to Appwrite Query string.
|
|
28
|
+
* Uses Appwrite's query builder format: Query.equal('field', value)
|
|
29
|
+
*/
|
|
30
|
+
function mapFilter(field: string, operator: string, value: unknown): string | null {
|
|
31
|
+
switch (operator) {
|
|
32
|
+
case 'eq': return `equal("${field}", ${JSON.stringify(value)})`;
|
|
33
|
+
case 'ne': return `notEqual("${field}", ${JSON.stringify(value)})`;
|
|
34
|
+
case 'lt': return `lessThan("${field}", ${JSON.stringify(value)})`;
|
|
35
|
+
case 'gt': return `greaterThan("${field}", ${JSON.stringify(value)})`;
|
|
36
|
+
case 'lte': return `lessThanEqual("${field}", ${JSON.stringify(value)})`;
|
|
37
|
+
case 'gte': return `greaterThanEqual("${field}", ${JSON.stringify(value)})`;
|
|
38
|
+
case 'contains': return `search("${field}", ${JSON.stringify(value)})`;
|
|
39
|
+
case 'in': return `equal("${field}", ${JSON.stringify(value)})`;
|
|
40
|
+
case 'null': return `isNull("${field}")`;
|
|
41
|
+
case 'nnull': return `isNotNull("${field}")`;
|
|
42
|
+
case 'between': {
|
|
43
|
+
const [min, max] = value as [unknown, unknown];
|
|
44
|
+
return `between("${field}", ${JSON.stringify(min)}, ${JSON.stringify(max)})`;
|
|
45
|
+
}
|
|
46
|
+
default: return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create a DataProvider backed by Appwrite.
|
|
52
|
+
*
|
|
53
|
+
* Usage:
|
|
54
|
+
* ```ts
|
|
55
|
+
* import { Databases, Client } from 'appwrite';
|
|
56
|
+
* import { createAppwriteDataProvider } from '@svadmin/appwrite';
|
|
57
|
+
*
|
|
58
|
+
* const client = new Client().setEndpoint('https://cloud.appwrite.io/v1').setProject('PROJECT_ID');
|
|
59
|
+
* const databases = new Databases(client);
|
|
60
|
+
* const dataProvider = createAppwriteDataProvider({ databases, databaseId: 'main' });
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export function createAppwriteDataProvider(options: AppwriteProviderOptions): DataProvider {
|
|
64
|
+
const { databases, databaseId } = options;
|
|
65
|
+
const db = databases as AppwriteDatabases;
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
getApiUrl: () => 'appwrite',
|
|
69
|
+
|
|
70
|
+
async getList<TData extends BaseRecord = BaseRecord>(params: GetListParams): Promise<GetListResult<TData>> {
|
|
71
|
+
const { resource, pagination, sorters, filters } = params;
|
|
72
|
+
const queries: string[] = [];
|
|
73
|
+
|
|
74
|
+
if (pagination?.current && pagination?.pageSize) {
|
|
75
|
+
const offset = (pagination.current - 1) * pagination.pageSize;
|
|
76
|
+
queries.push(`limit(${pagination.pageSize})`);
|
|
77
|
+
queries.push(`offset(${offset})`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (sorters?.length) {
|
|
81
|
+
for (const sort of sorters) {
|
|
82
|
+
queries.push(sort.order === 'asc'
|
|
83
|
+
? `orderAsc("${sort.field}")`
|
|
84
|
+
: `orderDesc("${sort.field}")`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (filters?.length) {
|
|
90
|
+
for (const filter of filters) {
|
|
91
|
+
const q = mapFilter(filter.field, filter.operator, filter.value);
|
|
92
|
+
if (q) queries.push(q);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const result = await db.listDocuments(databaseId, resource, queries);
|
|
97
|
+
return {
|
|
98
|
+
data: result.documents as TData[],
|
|
99
|
+
total: result.total,
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
async getOne<TData extends BaseRecord = BaseRecord>(params: GetOneParams): Promise<GetOneResult<TData>> {
|
|
104
|
+
const doc = await db.getDocument(databaseId, params.resource, String(params.id));
|
|
105
|
+
return { data: doc as TData };
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
async create<TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: CreateParams<TVariables>): Promise<CreateResult<TData>> {
|
|
109
|
+
const doc = await db.createDocument(databaseId, params.resource, 'unique()', params.variables as Record<string, unknown>);
|
|
110
|
+
return { data: doc as TData };
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
async update<TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: UpdateParams<TVariables>): Promise<UpdateResult<TData>> {
|
|
114
|
+
const doc = await db.updateDocument(databaseId, params.resource, String(params.id), params.variables as Record<string, unknown>);
|
|
115
|
+
return { data: doc as TData };
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
async deleteOne<TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: DeleteParams<TVariables>): Promise<DeleteResult<TData>> {
|
|
119
|
+
await db.deleteDocument(databaseId, params.resource, String(params.id));
|
|
120
|
+
return { data: { id: params.id } as unknown as TData };
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
async getMany<TData extends BaseRecord = BaseRecord>(params: GetManyParams): Promise<GetManyResult<TData>> {
|
|
124
|
+
const queries = [`equal("$id", ${JSON.stringify(params.ids.map(String))})`];
|
|
125
|
+
const result = await db.listDocuments(databaseId, params.resource, queries);
|
|
126
|
+
return { data: result.documents as TData[] };
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
async deleteMany<TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: DeleteManyParams<TVariables>): Promise<DeleteManyResult<TData>> {
|
|
130
|
+
const results: unknown[] = [];
|
|
131
|
+
for (const id of params.ids) {
|
|
132
|
+
await db.deleteDocument(databaseId, params.resource, String(id));
|
|
133
|
+
results.push({ id });
|
|
134
|
+
}
|
|
135
|
+
return { data: results as TData[] };
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// @svadmin/appwrite — Appwrite LiveProvider (Realtime)
|
|
2
|
+
import type { LiveProvider, LiveEvent } from '@svadmin/core';
|
|
3
|
+
|
|
4
|
+
interface AppwriteLiveOptions {
|
|
5
|
+
/** Appwrite Client instance (must have realtime support) */
|
|
6
|
+
client: unknown; // Appwrite.Client
|
|
7
|
+
/** Database ID for document subscriptions */
|
|
8
|
+
databaseId: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Duck-typed Appwrite realtime interface
|
|
12
|
+
interface AppwriteClient {
|
|
13
|
+
subscribe: (channels: string | string[], callback: (payload: AppwriteRealtimePayload) => void) => { (): void };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface AppwriteRealtimePayload {
|
|
17
|
+
events: string[];
|
|
18
|
+
payload: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Map Appwrite realtime events to svadmin LiveEvent types.
|
|
23
|
+
*/
|
|
24
|
+
function mapEventType(events: string[]): LiveEvent['type'] {
|
|
25
|
+
const eventStr = events.join(',');
|
|
26
|
+
if (eventStr.includes('.create')) return 'INSERT';
|
|
27
|
+
if (eventStr.includes('.update')) return 'UPDATE';
|
|
28
|
+
if (eventStr.includes('.delete')) return 'DELETE';
|
|
29
|
+
return 'UPDATE';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create a LiveProvider backed by Appwrite Realtime.
|
|
34
|
+
*
|
|
35
|
+
* Usage:
|
|
36
|
+
* ```ts
|
|
37
|
+
* import { Client } from 'appwrite';
|
|
38
|
+
* import { createAppwriteLiveProvider } from '@svadmin/appwrite';
|
|
39
|
+
*
|
|
40
|
+
* const client = new Client().setEndpoint('https://cloud.appwrite.io/v1').setProject('PROJECT_ID');
|
|
41
|
+
* const liveProvider = createAppwriteLiveProvider({ client, databaseId: 'main' });
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function createAppwriteLiveProvider(options: AppwriteLiveOptions): LiveProvider {
|
|
45
|
+
const client = options.client as AppwriteClient;
|
|
46
|
+
const { databaseId } = options;
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
subscribe({ resource, callback }) {
|
|
50
|
+
const channel = `databases.${databaseId}.collections.${resource}.documents`;
|
|
51
|
+
const unsubscribe = client.subscribe(channel, (response) => {
|
|
52
|
+
callback({
|
|
53
|
+
type: mapEventType(response.events),
|
|
54
|
+
resource,
|
|
55
|
+
payload: response.payload,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
return unsubscribe;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|