@tapstack/db 1.0.7 → 3.0.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 (48) hide show
  1. package/README.md +655 -0
  2. package/dist/adapters/index.d.ts +6 -0
  3. package/dist/adapters/index.js +22 -0
  4. package/dist/adapters/nodejs.adapter.d.ts +63 -0
  5. package/dist/adapters/nodejs.adapter.js +204 -0
  6. package/dist/adapters/types.d.ts +77 -0
  7. package/dist/adapters/types.js +19 -0
  8. package/dist/index.d.ts +101 -21
  9. package/dist/index.js +114 -41
  10. package/dist/modules/automations.d.ts +109 -0
  11. package/dist/modules/automations.js +59 -0
  12. package/dist/modules/conversations.d.ts +82 -0
  13. package/dist/modules/conversations.js +54 -0
  14. package/dist/modules/fields.d.ts +30 -9
  15. package/dist/modules/fields.js +31 -13
  16. package/dist/modules/files.d.ts +68 -0
  17. package/dist/modules/files.js +115 -0
  18. package/dist/modules/index.d.ts +12 -0
  19. package/dist/modules/index.js +28 -0
  20. package/dist/modules/objects.d.ts +30 -9
  21. package/dist/modules/objects.js +35 -13
  22. package/dist/modules/organizations.d.ts +69 -0
  23. package/dist/modules/organizations.js +83 -0
  24. package/dist/modules/records.d.ts +47 -5
  25. package/dist/modules/records.js +70 -5
  26. package/dist/modules/workspaces.d.ts +44 -0
  27. package/dist/modules/workspaces.js +57 -0
  28. package/dist/types.d.ts +159 -10
  29. package/dist/types.js +19 -0
  30. package/package.json +16 -7
  31. package/src/__tests__/client.test.ts +305 -49
  32. package/src/adapters/index.ts +13 -0
  33. package/src/adapters/nodejs.adapter.ts +298 -0
  34. package/src/adapters/types.ts +108 -0
  35. package/src/index.ts +132 -44
  36. package/src/modules/automations.ts +157 -0
  37. package/src/modules/conversations.ts +134 -0
  38. package/src/modules/fields.ts +64 -14
  39. package/src/modules/files.ts +144 -0
  40. package/src/modules/index.ts +19 -0
  41. package/src/modules/objects.ts +46 -14
  42. package/src/modules/organizations.ts +137 -0
  43. package/src/modules/records.ts +119 -6
  44. package/src/modules/workspaces.ts +95 -0
  45. package/src/types.ts +229 -9
  46. package/dist/request.d.ts +0 -2
  47. package/dist/request.js +0 -20
  48. package/src/request.ts +0 -14
@@ -1,84 +1,340 @@
1
- import axios from 'axios';
2
- import { TapstackDBClient } from '../index';
1
+ import {
2
+ TapstackDBClient,
3
+ IInstanceAdapter,
4
+ createClient,
5
+ createCustomClient,
6
+ } from '../index';
3
7
 
4
- jest.mock('axios');
8
+ /**
9
+ * Mock adapter for testing
10
+ */
11
+ class MockAdapter implements IInstanceAdapter {
12
+ public requests: Array<{
13
+ method: string;
14
+ path: string;
15
+ options?: { body?: unknown; params?: Record<string, string> };
16
+ }> = [];
17
+ public uploads: Array<{ path: string; file: File | Blob; metadata?: Record<string, string> }> = [];
18
+ private workspaceId: string | null = null;
19
+ public mockResponse: unknown = {};
5
20
 
6
- const mockedCreate = axios.create as jest.Mock;
21
+ async request<T>(
22
+ method: string,
23
+ path: string,
24
+ options?: { body?: unknown; params?: Record<string, string> }
25
+ ): Promise<T> {
26
+ this.requests.push({ method, path, options });
27
+ return this.mockResponse as T;
28
+ }
7
29
 
8
- describe('TapstackDBClient', () => {
9
- const apiClient = {
10
- get: jest.fn(),
11
- post: jest.fn(),
12
- put: jest.fn(),
13
- delete: jest.fn(),
14
- };
30
+ async upload<T>(
31
+ path: string,
32
+ file: File | Blob,
33
+ metadata?: Record<string, string>
34
+ ): Promise<T> {
35
+ this.uploads.push({ path, file, metadata });
36
+ return this.mockResponse as T;
37
+ }
38
+
39
+ setWorkspaceId(id: string | null): void {
40
+ this.workspaceId = id;
41
+ }
42
+
43
+ getWorkspaceId(): string | null {
44
+ return this.workspaceId;
45
+ }
15
46
 
47
+ clearRequests(): void {
48
+ this.requests = [];
49
+ this.uploads = [];
50
+ }
51
+ }
52
+
53
+ describe('TapstackDBClient', () => {
54
+ let adapter: MockAdapter;
16
55
  let client: TapstackDBClient;
17
56
 
18
57
  beforeEach(() => {
19
- mockedCreate.mockReturnValue(apiClient);
20
- client = new TapstackDBClient({ baseURL: 'http://example.com', apiKey: 'token' });
58
+ adapter = new MockAdapter();
59
+ client = new TapstackDBClient(adapter);
21
60
  });
22
61
 
23
62
  afterEach(() => {
24
- jest.clearAllMocks();
63
+ adapter.clearRequests();
25
64
  });
26
65
 
66
+ describe('Objects Module', () => {
67
+ it('list() calls GET schema/objects', async () => {
68
+ adapter.mockResponse = { objects: [] };
69
+ await client.objects.list();
70
+ expect(adapter.requests).toHaveLength(1);
71
+ expect(adapter.requests[0]).toEqual({
72
+ method: 'GET',
73
+ path: 'schema/objects',
74
+ options: undefined,
75
+ });
76
+ });
77
+
78
+ it('get() calls GET schema/objects/:slug', async () => {
79
+ adapter.mockResponse = { id: '1', slug: 'contacts' };
80
+ await client.objects.get('contacts');
81
+ expect(adapter.requests[0]).toEqual({
82
+ method: 'GET',
83
+ path: 'schema/objects/contacts',
84
+ options: undefined,
85
+ });
86
+ });
27
87
 
28
- it('getObject calls axios.get with correct path', async () => {
29
- await client.getObject('item');
30
- expect(apiClient.get).toHaveBeenCalledWith('/system/item');
88
+ it('create() calls POST schema/objects', async () => {
89
+ const payload = { singleName: 'Contact', pluralName: 'Contacts' };
90
+ adapter.mockResponse = { id: '1', ...payload };
91
+ await client.objects.create(payload);
92
+ expect(adapter.requests[0]).toEqual({
93
+ method: 'POST',
94
+ path: 'schema/objects',
95
+ options: { body: payload },
96
+ });
97
+ });
98
+
99
+ it('update() calls PATCH schema/objects/:slug', async () => {
100
+ const payload = { singleName: 'Updated Contact' };
101
+ await client.objects.update('contacts', payload);
102
+ expect(adapter.requests[0]).toEqual({
103
+ method: 'PATCH',
104
+ path: 'schema/objects/contacts',
105
+ options: { body: payload },
106
+ });
107
+ });
108
+
109
+ it('delete() calls DELETE schema/objects/:slug', async () => {
110
+ await client.objects.delete('contacts');
111
+ expect(adapter.requests[0]).toEqual({
112
+ method: 'DELETE',
113
+ path: 'schema/objects/contacts',
114
+ options: { params: { soft: 'true' } },
115
+ });
116
+ });
31
117
  });
32
118
 
33
- it('getAllObjects calls axios.get on /system', async () => {
34
- await client.getAllObjects();
35
- expect(apiClient.get).toHaveBeenCalledWith('/system');
119
+ describe('Fields Module', () => {
120
+ it('list() calls GET schema/objects/:slug/fields', async () => {
121
+ adapter.mockResponse = { fields: [] };
122
+ await client.fields.list('contacts');
123
+ expect(adapter.requests[0]).toEqual({
124
+ method: 'GET',
125
+ path: 'schema/objects/contacts/fields',
126
+ options: undefined,
127
+ });
128
+ });
129
+
130
+ it('create() calls POST schema/objects/:slug/fields', async () => {
131
+ const payload = { label: 'Email', value: 'email', type: 'email' as const };
132
+ await client.fields.create('contacts', payload);
133
+ expect(adapter.requests[0]).toEqual({
134
+ method: 'POST',
135
+ path: 'schema/objects/contacts/fields',
136
+ options: { body: payload },
137
+ });
138
+ });
139
+
140
+ it('update() calls PATCH schema/objects/:slug/fields/:fieldId', async () => {
141
+ const payload = { label: 'Updated Email' };
142
+ await client.fields.update('contacts', 'field-1', payload);
143
+ expect(adapter.requests[0]).toEqual({
144
+ method: 'PATCH',
145
+ path: 'schema/objects/contacts/fields/field-1',
146
+ options: { body: payload },
147
+ });
148
+ });
149
+
150
+ it('delete() calls DELETE schema/objects/:slug/fields/:fieldId', async () => {
151
+ await client.fields.delete('contacts', 'field-1');
152
+ expect(adapter.requests[0]).toEqual({
153
+ method: 'DELETE',
154
+ path: 'schema/objects/contacts/fields/field-1',
155
+ options: { params: { soft: 'true' } },
156
+ });
157
+ });
36
158
  });
37
159
 
38
- it('createObject posts to /system', async () => {
39
- const data = { name: 'test' };
40
- await client.createObject(data);
41
- expect(apiClient.post).toHaveBeenCalledWith('/system', data);
160
+ describe('Records Module', () => {
161
+ it('list() calls GET records/:slug', async () => {
162
+ adapter.mockResponse = { items: [], total: 0 };
163
+ await client.records.list('contacts');
164
+ expect(adapter.requests[0]).toEqual({
165
+ method: 'GET',
166
+ path: 'records/contacts',
167
+ options: { params: {} },
168
+ });
169
+ });
170
+
171
+ it('list() with options adds query params', async () => {
172
+ adapter.mockResponse = { items: [], total: 0 };
173
+ await client.records.list('contacts', {
174
+ page: 2,
175
+ pageSize: 10,
176
+ sortBy: 'createdAt',
177
+ sortOrder: 'desc',
178
+ });
179
+ expect(adapter.requests[0].options?.params).toEqual({
180
+ page: '2',
181
+ pageSize: '10',
182
+ sortBy: 'createdAt',
183
+ sortOrder: 'desc',
184
+ });
185
+ });
186
+
187
+ it('get() calls GET records/:slug/:id', async () => {
188
+ await client.records.get('contacts', 'rec-1');
189
+ expect(adapter.requests[0]).toEqual({
190
+ method: 'GET',
191
+ path: 'records/contacts/rec-1',
192
+ options: undefined,
193
+ });
194
+ });
195
+
196
+ it('create() calls POST records/:slug', async () => {
197
+ const payload = { data: { name: 'John' } };
198
+ await client.records.create('contacts', payload);
199
+ expect(adapter.requests[0]).toEqual({
200
+ method: 'POST',
201
+ path: 'records/contacts',
202
+ options: { body: payload },
203
+ });
204
+ });
205
+
206
+ it('update() calls PATCH records/:slug/:id', async () => {
207
+ const payload = { data: { name: 'Jane' } };
208
+ await client.records.update('contacts', 'rec-1', payload);
209
+ expect(adapter.requests[0]).toEqual({
210
+ method: 'PATCH',
211
+ path: 'records/contacts/rec-1',
212
+ options: { body: payload },
213
+ });
214
+ });
215
+
216
+ it('delete() calls DELETE records/:slug/:id', async () => {
217
+ await client.records.delete('contacts', 'rec-1');
218
+ expect(adapter.requests[0]).toEqual({
219
+ method: 'DELETE',
220
+ path: 'records/contacts/rec-1',
221
+ options: { params: { soft: 'true' } },
222
+ });
223
+ });
42
224
  });
43
225
 
44
- it('updateObject puts to correct path', async () => {
45
- await client.updateObject('item', { a: 1 });
46
- expect(apiClient.put).toHaveBeenCalledWith('/system/item', { a: 1 });
226
+ describe('Organizations Module', () => {
227
+ it('list() calls GET organizations', async () => {
228
+ adapter.mockResponse = { organizations: [] };
229
+ await client.organizations.list();
230
+ expect(adapter.requests[0]).toEqual({
231
+ method: 'GET',
232
+ path: 'organizations',
233
+ options: undefined,
234
+ });
235
+ });
236
+
237
+ it('create() calls POST organizations', async () => {
238
+ const payload = { name: 'Acme Corp' };
239
+ await client.organizations.create(payload);
240
+ expect(adapter.requests[0]).toEqual({
241
+ method: 'POST',
242
+ path: 'organizations',
243
+ options: { body: payload },
244
+ });
245
+ });
47
246
  });
48
247
 
49
- it('deleteObject deletes from correct path', async () => {
50
- await client.deleteObject('item');
51
- expect(apiClient.delete).toHaveBeenCalledWith('/system/item');
248
+ describe('Workspaces Module', () => {
249
+ it('list() calls GET workspaces', async () => {
250
+ adapter.mockResponse = { workspaces: [] };
251
+ await client.workspaces.list();
252
+ expect(adapter.requests[0]).toEqual({
253
+ method: 'GET',
254
+ path: 'workspaces',
255
+ options: undefined,
256
+ });
257
+ });
258
+
259
+ it('create() calls POST workspaces/org/:orgId', async () => {
260
+ const payload = { name: 'Production' };
261
+ await client.workspaces.create('org-1', payload);
262
+ expect(adapter.requests[0]).toEqual({
263
+ method: 'POST',
264
+ path: 'workspaces/org/org-1',
265
+ options: { body: payload },
266
+ });
267
+ });
52
268
  });
53
269
 
54
- it('getFields calls axios.get with fields path', async () => {
55
- await client.getFields('item');
56
- expect(apiClient.get).toHaveBeenCalledWith('/system/item/fields');
270
+ describe('Files Module', () => {
271
+ it('list() calls GET files/list', async () => {
272
+ adapter.mockResponse = { files: [], total: 0 };
273
+ await client.files.list();
274
+ expect(adapter.requests[0]).toEqual({
275
+ method: 'GET',
276
+ path: 'files/list',
277
+ options: { params: {} },
278
+ });
279
+ });
280
+
281
+ it('upload() uses adapter.upload', async () => {
282
+ const file = new Blob(['test'], { type: 'text/plain' });
283
+ await client.files.upload(file, { folderId: 'folder-1' });
284
+ expect(adapter.uploads).toHaveLength(1);
285
+ expect(adapter.uploads[0]).toEqual({
286
+ path: 'files/upload',
287
+ file,
288
+ metadata: { folderId: 'folder-1' },
289
+ });
290
+ });
57
291
  });
58
292
 
59
- it('getField calls axios.get with field id', async () => {
60
- await client.getField('item', '1');
61
- expect(apiClient.get).toHaveBeenCalledWith('/system/item/fields/1');
293
+ describe('Workspace Context', () => {
294
+ it('setWorkspaceId updates adapter workspace', () => {
295
+ client.setWorkspaceId('ws-123');
296
+ expect(adapter.getWorkspaceId()).toBe('ws-123');
297
+ });
298
+
299
+ it('getWorkspaceId returns current workspace', () => {
300
+ adapter.setWorkspaceId('ws-456');
301
+ expect(client.getWorkspaceId()).toBe('ws-456');
302
+ });
62
303
  });
304
+ });
63
305
 
64
- it('createField posts to fields path', async () => {
65
- await client.createField('item', { name: 'field' });
66
- expect(apiClient.post).toHaveBeenCalledWith('/system/item/fields', { name: 'field' });
306
+ describe('Factory Functions', () => {
307
+ // Mock fetch for adapter tests
308
+ const mockFetch = jest.fn();
309
+ global.fetch = mockFetch;
310
+
311
+ beforeEach(() => {
312
+ mockFetch.mockReset();
313
+ mockFetch.mockResolvedValue({
314
+ status: 200,
315
+ json: () => Promise.resolve({ success: true, data: {} }),
316
+ });
67
317
  });
68
318
 
69
- it('updateField puts to specific field path', async () => {
70
- await client.updateField('item', '1', { name: 'f' });
71
- expect(apiClient.put).toHaveBeenCalledWith('/system/item/fields/1', { name: 'f' });
319
+ it('createClient creates a client with default API URL', () => {
320
+ const client = createClient({
321
+ apiKey: 'api-key',
322
+ });
323
+ expect(client).toBeInstanceOf(TapstackDBClient);
72
324
  });
73
325
 
74
- it('deleteField deletes the field path', async () => {
75
- await client.deleteField('item', '1');
76
- expect(apiClient.delete).toHaveBeenCalledWith('/system/item/fields/1');
326
+ it('createClient creates a client with custom base URL', () => {
327
+ const client = createClient({
328
+ baseUrl: 'https://api.example.com',
329
+ apiKey: 'api-key',
330
+ });
331
+ expect(client).toBeInstanceOf(TapstackDBClient);
77
332
  });
78
333
 
79
- it('query posts to slug', async () => {
80
- const variables = { a: 1 };
81
- await client.query('item', variables);
82
- expect(apiClient.post).toHaveBeenCalledWith('/item', variables);
334
+ it('createCustomClient creates a client with custom adapter', () => {
335
+ const adapter = new MockAdapter();
336
+ const client = createCustomClient(adapter);
337
+ expect(client).toBeInstanceOf(TapstackDBClient);
338
+ expect(client.getAdapter()).toBe(adapter);
83
339
  });
84
340
  });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Adapters Index
3
+ * Export all adapter types and implementations
4
+ */
5
+
6
+ export * from './types';
7
+ export * from './nodejs.adapter';
8
+
9
+
10
+
11
+
12
+
13
+