@taruvi/sdk 1.5.0-beta.1 → 1.5.0-beta.2
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/README.md +58 -1295
- package/package.json +10 -2
- package/.claude/settings.local.json +0 -19
- package/.github/worflows/publish.yml +0 -57
- package/.github/workflows/publish.yml +0 -58
- package/.kiro/settings/lsp.json +0 -198
- package/MODULE_NAMING_CHANGES.md +0 -81
- package/PARAMETER_NAMING_CHANGES.md +0 -106
- package/USAGE_EXAMPLE.md +0 -86
- package/src/client.ts +0 -88
- package/src/index.ts +0 -51
- package/src/lib/analytics/AnalyticsClient.ts +0 -24
- package/src/lib/analytics/types.ts +0 -8
- package/src/lib/app/AppClient.ts +0 -54
- package/src/lib/app/types.ts +0 -50
- package/src/lib/auth/AuthClient.ts +0 -126
- package/src/lib/auth/types.ts +0 -123
- package/src/lib/database/DatabaseClient.ts +0 -306
- package/src/lib/database/types.ts +0 -156
- package/src/lib/functions/FunctionsClient.ts +0 -27
- package/src/lib/functions/types.ts +0 -27
- package/src/lib/policy/PolicyClient.ts +0 -79
- package/src/lib/policy/types.ts +0 -39
- package/src/lib/secrets/SecretsClient.ts +0 -75
- package/src/lib/secrets/types.ts +0 -59
- package/src/lib/settings/SettingsClient.ts +0 -22
- package/src/lib/settings/types.ts +0 -9
- package/src/lib/storage/StorageClient.ts +0 -131
- package/src/lib/storage/types.ts +0 -86
- package/src/lib/users/UserClient.ts +0 -63
- package/src/lib/users/types.ts +0 -123
- package/src/lib-internal/errors/ErrorClient.ts +0 -114
- package/src/lib-internal/errors/index.ts +0 -3
- package/src/lib-internal/errors/types.ts +0 -29
- package/src/lib-internal/http/HttpClient.ts +0 -116
- package/src/lib-internal/http/types.ts +0 -12
- package/src/lib-internal/routes/AnalyticsRoutes.ts +0 -3
- package/src/lib-internal/routes/AppRoutes.ts +0 -9
- package/src/lib-internal/routes/AuthRoutes.ts +0 -0
- package/src/lib-internal/routes/DatabaseRoutes.ts +0 -10
- package/src/lib-internal/routes/FunctionRoutes.ts +0 -3
- package/src/lib-internal/routes/PolicyRoutes.ts +0 -4
- package/src/lib-internal/routes/SecretsRoutes.ts +0 -5
- package/src/lib-internal/routes/SettingsRoutes.ts +0 -4
- package/src/lib-internal/routes/StorageRoutes.ts +0 -15
- package/src/lib-internal/routes/UserRoutes.ts +0 -12
- package/src/lib-internal/routes/index.ts +0 -0
- package/src/lib-internal/token/TokenClient.ts +0 -108
- package/src/lib-internal/token/types.ts +0 -0
- package/src/types.ts +0 -104
- package/src/utils/enums.ts +0 -24
- package/src/utils/utils.ts +0 -38
- package/tests/fixtures/mockClient.ts +0 -19
- package/tests/mocks/db.json +0 -1
- package/tests/unit/analytics/AnalyticsClient.test.ts +0 -84
- package/tests/unit/app/AppClient.test.ts +0 -114
- package/tests/unit/auth/AuthClient.test.ts +0 -91
- package/tests/unit/client/Client.test.ts +0 -87
- package/tests/unit/database/DatabaseClient.test.ts +0 -652
- package/tests/unit/edge-cases/robustness.test.ts +0 -258
- package/tests/unit/errors/errors.test.ts +0 -236
- package/tests/unit/functions/FunctionsClient.test.ts +0 -99
- package/tests/unit/policy/PolicyClient.test.ts +0 -180
- package/tests/unit/secrets/SecretsClient.test.ts +0 -146
- package/tests/unit/settings/SettingsClient.test.ts +0 -50
- package/tests/unit/storage/StorageClient.test.ts +0 -252
- package/tests/unit/users/UserClient.test.ts +0 -150
- package/tsconfig.json +0 -44
- package/vitest.config.ts +0 -7
|
@@ -1,652 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
-
import { Database } from '../../../src/lib/database/DatabaseClient.js'
|
|
3
|
-
import { Client } from '../../../src/client.js'
|
|
4
|
-
import type { BackendFilterTreeRoot, DatabaseResponse, DatabaseSingleResponse, EdgeResponse } from '../../../src/lib/database/types.js'
|
|
5
|
-
import type { TaruviResponse } from '../../../src/types.js'
|
|
6
|
-
|
|
7
|
-
// Mock the Client
|
|
8
|
-
const mockHttpClient = {
|
|
9
|
-
get: vi.fn(),
|
|
10
|
-
post: vi.fn(),
|
|
11
|
-
patch: vi.fn(),
|
|
12
|
-
delete: vi.fn()
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const mockClient = {
|
|
16
|
-
getConfig: () => ({ apiKey: 'test-key', appSlug: 'test-app', apiUrl: 'https://api.test.com' }),
|
|
17
|
-
httpClient: mockHttpClient
|
|
18
|
-
} as unknown as Client
|
|
19
|
-
|
|
20
|
-
describe('Database', () => {
|
|
21
|
-
beforeEach(() => {
|
|
22
|
-
vi.clearAllMocks()
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
describe('from()', () => {
|
|
26
|
-
it('returns new Database instance with table name', () => {
|
|
27
|
-
const db = new Database(mockClient)
|
|
28
|
-
const result = db.from('accounts')
|
|
29
|
-
expect(result).toBeInstanceOf(Database)
|
|
30
|
-
})
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
describe('filters() flat (triple-arg)', () => {
|
|
34
|
-
it('eq operator uses field name without suffix', async () => {
|
|
35
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
36
|
-
await new Database(mockClient).from('accounts').filters('status', 'eq', 'active').execute()
|
|
37
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('status=active'))
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it('gt operator appends __gt suffix', async () => {
|
|
41
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
42
|
-
await new Database(mockClient).from('accounts').filters('age', 'gt', 18).execute()
|
|
43
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('age__gt=18'))
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
it('gte operator appends __gte suffix', async () => {
|
|
47
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
48
|
-
await new Database(mockClient).from('accounts').filters('age', 'gte', 18).execute()
|
|
49
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('age__gte=18'))
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('lt operator appends __lt suffix', async () => {
|
|
53
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
54
|
-
await new Database(mockClient).from('accounts').filters('age', 'lt', 65).execute()
|
|
55
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('age__lt=65'))
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('lte operator appends __lte suffix', async () => {
|
|
59
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
60
|
-
await new Database(mockClient).from('accounts').filters('age', 'lte', 65).execute()
|
|
61
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('age__lte=65'))
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('icontains operator appends __icontains suffix', async () => {
|
|
65
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
66
|
-
await new Database(mockClient).from('accounts').filters('name', 'icontains', 'john').execute()
|
|
67
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('name__icontains=john'))
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
it('in operator joins array values with comma', async () => {
|
|
71
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
72
|
-
await new Database(mockClient).from('accounts').filters('status', 'in', ['active', 'pending']).execute()
|
|
73
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('status__in=active%2Cpending'))
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
it('null operator appends __null suffix', async () => {
|
|
77
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
78
|
-
await new Database(mockClient).from('accounts').filters('deleted_at', 'null', true).execute()
|
|
79
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('deleted_at__null=true'))
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it('multiple filters chain correctly', async () => {
|
|
83
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
84
|
-
await new Database(mockClient)
|
|
85
|
-
.from('accounts')
|
|
86
|
-
.filters('status', 'eq', 'active')
|
|
87
|
-
.filters('age', 'gte', 18)
|
|
88
|
-
.execute()
|
|
89
|
-
const url = mockHttpClient.get.mock.calls[0][0]
|
|
90
|
-
expect(url).toContain('status=active')
|
|
91
|
-
expect(url).toContain('age__gte=18')
|
|
92
|
-
})
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
describe('filters() JSON tree', () => {
|
|
96
|
-
const normativeTree: BackendFilterTreeRoot = [
|
|
97
|
-
{
|
|
98
|
-
operator: 'and',
|
|
99
|
-
value: [
|
|
100
|
-
{ field: 'is_active', operator: 'eq', value: true },
|
|
101
|
-
{ field: 'hire_date', operator: 'lt', value: '2021-01-01' },
|
|
102
|
-
{
|
|
103
|
-
operator: 'or',
|
|
104
|
-
value: [
|
|
105
|
-
{ field: 'salary', operator: 'gte', value: 200000 },
|
|
106
|
-
{
|
|
107
|
-
operator: 'and',
|
|
108
|
-
value: [
|
|
109
|
-
{ field: 'title', operator: 'containss', value: 'VP' },
|
|
110
|
-
{ field: 'bonus', operator: 'gte', value: 50000 },
|
|
111
|
-
],
|
|
112
|
-
},
|
|
113
|
-
],
|
|
114
|
-
},
|
|
115
|
-
],
|
|
116
|
-
},
|
|
117
|
-
]
|
|
118
|
-
|
|
119
|
-
it('serializes tree into filters query param (round-trip)', async () => {
|
|
120
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
121
|
-
await new Database(mockClient).from('accounts').filters(normativeTree).execute()
|
|
122
|
-
const url = mockHttpClient.get.mock.calls[0][0] as string
|
|
123
|
-
const q = url.includes('?') ? url.split('?')[1] : ''
|
|
124
|
-
const params = new URLSearchParams(q)
|
|
125
|
-
const raw = params.get('filters')
|
|
126
|
-
expect(raw).toBeTruthy()
|
|
127
|
-
expect(JSON.parse(decodeURIComponent(raw!))).toEqual(normativeTree)
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
it('chains JSON filters with populate', async () => {
|
|
131
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
132
|
-
await new Database(mockClient)
|
|
133
|
-
.from('accounts')
|
|
134
|
-
.filters(normativeTree)
|
|
135
|
-
.populate(['customer'])
|
|
136
|
-
.execute()
|
|
137
|
-
const url = mockHttpClient.get.mock.calls[0][0] as string
|
|
138
|
-
expect(url).toContain('populate=customer')
|
|
139
|
-
const q = url.split('?')[1]
|
|
140
|
-
expect(new URLSearchParams(q).get('filters')).toBeTruthy()
|
|
141
|
-
})
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
describe('orderBy()', () => {
|
|
145
|
-
it('asc order uses field name without prefix', async () => {
|
|
146
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
147
|
-
await new Database(mockClient).from('accounts').orderBy('created_at', 'asc').execute()
|
|
148
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('ordering=created_at'))
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
it('desc order adds - prefix', async () => {
|
|
152
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
153
|
-
await new Database(mockClient).from('accounts').orderBy('created_at', 'desc').execute()
|
|
154
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('ordering=-created_at'))
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
it('defaults to asc when order not specified', async () => {
|
|
158
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
159
|
-
await new Database(mockClient).from('accounts').orderBy('name').execute()
|
|
160
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringMatching(/ordering=name(?!-)/))
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
it('comma-joins multiple fields from array (mixed asc/desc)', async () => {
|
|
164
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
165
|
-
await new Database(mockClient)
|
|
166
|
-
.from('accounts')
|
|
167
|
-
.orderBy([
|
|
168
|
-
{ field: 'salary', order: 'desc' },
|
|
169
|
-
{ field: 'hire_date', order: 'asc' },
|
|
170
|
-
])
|
|
171
|
-
.execute()
|
|
172
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith(
|
|
173
|
-
expect.stringContaining('ordering=-salary%2Chire_date')
|
|
174
|
-
)
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
it('accepts pre-built ordering string', async () => {
|
|
178
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
179
|
-
await new Database(mockClient).from('accounts').orderBy('-a,b').execute()
|
|
180
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('ordering=-a%2Cb'))
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
it('chains with filters without dropping ordering', async () => {
|
|
184
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
185
|
-
await new Database(mockClient)
|
|
186
|
-
.from('accounts')
|
|
187
|
-
.filters('status', 'eq', 'active')
|
|
188
|
-
.orderBy([{ field: 'name', order: 'asc' }])
|
|
189
|
-
.execute()
|
|
190
|
-
const url = mockHttpClient.get.mock.calls[0][0] as string
|
|
191
|
-
expect(url).toContain('status=active')
|
|
192
|
-
expect(url).toContain('ordering=name')
|
|
193
|
-
})
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
describe('pagination', () => {
|
|
197
|
-
it('page() sets page number', async () => {
|
|
198
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
199
|
-
await new Database(mockClient).from('accounts').page(2).execute()
|
|
200
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('page=2'))
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
it('pageSize() sets page_size', async () => {
|
|
204
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
205
|
-
await new Database(mockClient).from('accounts').pageSize(20).execute()
|
|
206
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('page_size=20'))
|
|
207
|
-
})
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
describe('populate()', () => {
|
|
211
|
-
it('joins array with comma', async () => {
|
|
212
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
213
|
-
await new Database(mockClient).from('orders').populate(['customer', 'items']).execute()
|
|
214
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('populate=customer%2Citems'))
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
it('populateAll sets wildcard populate', async () => {
|
|
218
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
219
|
-
await new Database(mockClient).from('orders').populateAll().execute()
|
|
220
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('populate=*'))
|
|
221
|
-
})
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
describe('CRUD operations', () => {
|
|
225
|
-
it('get() calls httpClient.get with record ID in URL', async () => {
|
|
226
|
-
mockHttpClient.get.mockResolvedValue({ id: '123' })
|
|
227
|
-
await new Database(mockClient).from('accounts').get('123').execute()
|
|
228
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('/123/'))
|
|
229
|
-
})
|
|
230
|
-
|
|
231
|
-
it('create() calls httpClient.post with body', async () => {
|
|
232
|
-
const body = { name: 'Test', status: 'active' }
|
|
233
|
-
mockHttpClient.post.mockResolvedValue({ id: '1', ...body })
|
|
234
|
-
await new Database(mockClient).from('accounts').create(body).execute()
|
|
235
|
-
expect(mockHttpClient.post).toHaveBeenCalledWith(expect.any(String), body)
|
|
236
|
-
})
|
|
237
|
-
|
|
238
|
-
it('update() calls httpClient.patch with body', async () => {
|
|
239
|
-
const body = { status: 'inactive' }
|
|
240
|
-
mockHttpClient.patch.mockResolvedValue({ id: '123', ...body })
|
|
241
|
-
await new Database(mockClient).from('accounts').get('123').update(body).execute()
|
|
242
|
-
expect(mockHttpClient.patch).toHaveBeenCalledWith(expect.stringContaining('/123/'), body)
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
it('update() without recordId throws error', async () => {
|
|
246
|
-
const body = { status: 'inactive' }
|
|
247
|
-
await expect(
|
|
248
|
-
new Database(mockClient).from('accounts').update(body).execute()
|
|
249
|
-
).rejects.toThrow('PATCH operation requires a record ID')
|
|
250
|
-
})
|
|
251
|
-
|
|
252
|
-
it('delete() calls httpClient.delete', async () => {
|
|
253
|
-
mockHttpClient.delete.mockResolvedValue({})
|
|
254
|
-
await new Database(mockClient).from('accounts').delete('123').execute()
|
|
255
|
-
expect(mockHttpClient.delete).toHaveBeenCalledWith(expect.stringContaining('/123/'))
|
|
256
|
-
})
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
describe('execute()', () => {
|
|
260
|
-
it('throws error without table name', async () => {
|
|
261
|
-
await expect(new Database(mockClient).execute()).rejects.toThrow('Table name is required')
|
|
262
|
-
})
|
|
263
|
-
|
|
264
|
-
it('calls httpClient.get for list query', async () => {
|
|
265
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
266
|
-
await new Database(mockClient).from('accounts').execute()
|
|
267
|
-
expect(mockHttpClient.get).toHaveBeenCalled()
|
|
268
|
-
})
|
|
269
|
-
})
|
|
270
|
-
|
|
271
|
-
describe('first()', () => {
|
|
272
|
-
it('returns first item from array', async () => {
|
|
273
|
-
mockHttpClient.get.mockResolvedValue({ data: [{ id: '1' }, { id: '2' }] })
|
|
274
|
-
const result = await new Database(mockClient).from('accounts').first()
|
|
275
|
-
expect(result).toEqual({ id: '1' })
|
|
276
|
-
})
|
|
277
|
-
|
|
278
|
-
it('returns null for empty array', async () => {
|
|
279
|
-
mockHttpClient.get.mockResolvedValue({ data: [] })
|
|
280
|
-
const result = await new Database(mockClient).from('accounts').first()
|
|
281
|
-
expect(result).toBeNull()
|
|
282
|
-
})
|
|
283
|
-
|
|
284
|
-
it('returns single item if not array', async () => {
|
|
285
|
-
mockHttpClient.get.mockResolvedValue({ data: { id: '1' } })
|
|
286
|
-
const result = await new Database(mockClient).from('accounts').get('1').first()
|
|
287
|
-
expect(result).toEqual({ id: '1' })
|
|
288
|
-
})
|
|
289
|
-
})
|
|
290
|
-
|
|
291
|
-
describe('count()', () => {
|
|
292
|
-
it('returns array length', async () => {
|
|
293
|
-
mockHttpClient.get.mockResolvedValue({ data: [{ id: '1' }, { id: '2' }, { id: '3' }] })
|
|
294
|
-
const result = await new Database(mockClient).from('accounts').count()
|
|
295
|
-
expect(result).toBe(3)
|
|
296
|
-
})
|
|
297
|
-
|
|
298
|
-
it('returns 0 for non-array', async () => {
|
|
299
|
-
mockHttpClient.get.mockResolvedValue({ data: { id: '1' } })
|
|
300
|
-
const result = await new Database(mockClient).from('accounts').get('1').count()
|
|
301
|
-
expect(result).toBe(0)
|
|
302
|
-
})
|
|
303
|
-
})
|
|
304
|
-
|
|
305
|
-
describe('URL building', () => {
|
|
306
|
-
it('builds correct base URL with app slug and table', async () => {
|
|
307
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
308
|
-
await new Database(mockClient).from('accounts').execute()
|
|
309
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith('api/apps/test-app/datatables/accounts/data/')
|
|
310
|
-
})
|
|
311
|
-
|
|
312
|
-
it('builds correct URL with record ID', async () => {
|
|
313
|
-
mockHttpClient.get.mockResolvedValue({})
|
|
314
|
-
await new Database(mockClient).from('accounts').get('abc-123').execute()
|
|
315
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith('api/apps/test-app/datatables/accounts/data/abc-123/')
|
|
316
|
-
})
|
|
317
|
-
|
|
318
|
-
it('appends query string with filters', async () => {
|
|
319
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
320
|
-
await new Database(mockClient)
|
|
321
|
-
.from('accounts')
|
|
322
|
-
.filters('status', 'eq', 'active')
|
|
323
|
-
.page(1)
|
|
324
|
-
.pageSize(10)
|
|
325
|
-
.execute()
|
|
326
|
-
const url = mockHttpClient.get.mock.calls[0][0]
|
|
327
|
-
expect(url).toContain('?')
|
|
328
|
-
expect(url).toContain('status=active')
|
|
329
|
-
expect(url).toContain('page=1')
|
|
330
|
-
expect(url).toContain('page_size=10')
|
|
331
|
-
})
|
|
332
|
-
})
|
|
333
|
-
|
|
334
|
-
describe('response handling', () => {
|
|
335
|
-
it('returns list response matching DatabaseResponse type', async () => {
|
|
336
|
-
const mockResponse: DatabaseResponse = {
|
|
337
|
-
status: 'success',
|
|
338
|
-
message: 'Data retrieved successfully',
|
|
339
|
-
data: [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }],
|
|
340
|
-
total: 2,
|
|
341
|
-
pagination: { offset: 0, limit: 20, count: 2, current_page: 1, total_pages: 1, has_next: false, has_previous: false }
|
|
342
|
-
}
|
|
343
|
-
mockHttpClient.get.mockResolvedValue(mockResponse)
|
|
344
|
-
const result = await new Database(mockClient).from('accounts').execute() as DatabaseResponse
|
|
345
|
-
expect(result.status).toBe('success')
|
|
346
|
-
expect(result.data).toHaveLength(2)
|
|
347
|
-
expect(result.total).toBe(2)
|
|
348
|
-
expect(result.pagination!.current_page).toBe(1)
|
|
349
|
-
})
|
|
350
|
-
|
|
351
|
-
it('returns single record matching DatabaseSingleResponse type', async () => {
|
|
352
|
-
const mockResponse: DatabaseSingleResponse = {
|
|
353
|
-
status: 'success',
|
|
354
|
-
message: 'Record retrieved successfully',
|
|
355
|
-
data: { id: 1, name: 'Alice', email: 'alice@example.com' }
|
|
356
|
-
}
|
|
357
|
-
mockHttpClient.get.mockResolvedValue(mockResponse)
|
|
358
|
-
const result = await new Database(mockClient).from('accounts').get('1').execute() as DatabaseSingleResponse
|
|
359
|
-
expect(result.data.id).toBe(1)
|
|
360
|
-
expect(result.data.name).toBe('Alice')
|
|
361
|
-
})
|
|
362
|
-
|
|
363
|
-
it('returns created record response', async () => {
|
|
364
|
-
const mockResponse: DatabaseSingleResponse = {
|
|
365
|
-
status: 'success',
|
|
366
|
-
message: 'Record created successfully',
|
|
367
|
-
data: { id: 3, name: 'Carol', status: 'active' }
|
|
368
|
-
}
|
|
369
|
-
mockHttpClient.post.mockResolvedValue(mockResponse)
|
|
370
|
-
const result = await new Database(mockClient).from('accounts').create({ name: 'Carol', status: 'active' }).execute() as DatabaseSingleResponse
|
|
371
|
-
expect(result.data.id).toBe(3)
|
|
372
|
-
})
|
|
373
|
-
|
|
374
|
-
it('returns updated record response', async () => {
|
|
375
|
-
const mockResponse: DatabaseSingleResponse = {
|
|
376
|
-
status: 'success',
|
|
377
|
-
message: 'Record updated successfully',
|
|
378
|
-
data: { id: 1, name: 'Alice Updated' }
|
|
379
|
-
}
|
|
380
|
-
mockHttpClient.patch.mockResolvedValue(mockResponse)
|
|
381
|
-
const result = await new Database(mockClient).from('accounts').get('1').update({ name: 'Alice Updated' }).execute() as DatabaseSingleResponse
|
|
382
|
-
expect(result.data.name).toBe('Alice Updated')
|
|
383
|
-
})
|
|
384
|
-
|
|
385
|
-
it('returns delete response', async () => {
|
|
386
|
-
const mockResponse = { status: 'success', message: 'Record deleted successfully' }
|
|
387
|
-
mockHttpClient.delete.mockResolvedValue(mockResponse)
|
|
388
|
-
const result = await new Database(mockClient).from('accounts').delete('1').execute()
|
|
389
|
-
expect((result as any).status).toBe('success')
|
|
390
|
-
})
|
|
391
|
-
})
|
|
392
|
-
|
|
393
|
-
describe('graph traversal', () => {
|
|
394
|
-
it('include() sets include param', async () => {
|
|
395
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
396
|
-
await new Database(mockClient).from('employees').get('1').include('descendants').execute()
|
|
397
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('include=descendants'))
|
|
398
|
-
})
|
|
399
|
-
|
|
400
|
-
it('include() supports ancestors', async () => {
|
|
401
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
402
|
-
await new Database(mockClient).from('employees').get('4').include('ancestors').execute()
|
|
403
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('include=ancestors'))
|
|
404
|
-
})
|
|
405
|
-
|
|
406
|
-
it('include() supports both', async () => {
|
|
407
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
408
|
-
await new Database(mockClient).from('employees').get('2').include('both').execute()
|
|
409
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('include=both'))
|
|
410
|
-
})
|
|
411
|
-
|
|
412
|
-
it('depth() sets depth param', async () => {
|
|
413
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
414
|
-
await new Database(mockClient).from('employees').get('1').include('descendants').depth(3).execute()
|
|
415
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('depth=3'))
|
|
416
|
-
})
|
|
417
|
-
|
|
418
|
-
it('format() sets format param to tree', async () => {
|
|
419
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
420
|
-
await new Database(mockClient).from('employees').get('1').format('tree').depth(3).execute()
|
|
421
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('format=tree'))
|
|
422
|
-
})
|
|
423
|
-
|
|
424
|
-
it('format() sets format param to graph', async () => {
|
|
425
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
426
|
-
await new Database(mockClient).from('employees').format('graph').execute()
|
|
427
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('format=graph'))
|
|
428
|
-
})
|
|
429
|
-
|
|
430
|
-
it('types() sets relationship_type as repeated params', async () => {
|
|
431
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
432
|
-
await new Database(mockClient).from('employees').format('graph').types(['manager', 'dotted_line']).execute()
|
|
433
|
-
const url = mockHttpClient.get.mock.calls[0][0]
|
|
434
|
-
expect(url).toContain('relationship_type=manager')
|
|
435
|
-
expect(url).toContain('relationship_type=dotted_line')
|
|
436
|
-
})
|
|
437
|
-
|
|
438
|
-
it('types() with single type', async () => {
|
|
439
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
440
|
-
await new Database(mockClient).from('employees').format('graph').types(['manager']).execute()
|
|
441
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('relationship_type=manager'))
|
|
442
|
-
})
|
|
443
|
-
|
|
444
|
-
it('combines include, depth, and get', async () => {
|
|
445
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
446
|
-
await new Database(mockClient).from('employees').get('1').include('descendants').depth(3).execute()
|
|
447
|
-
const url = mockHttpClient.get.mock.calls[0][0]
|
|
448
|
-
expect(url).toContain('/1/')
|
|
449
|
-
expect(url).toContain('include=descendants')
|
|
450
|
-
expect(url).toContain('depth=3')
|
|
451
|
-
})
|
|
452
|
-
|
|
453
|
-
it('combines format, types, and depth', async () => {
|
|
454
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
455
|
-
await new Database(mockClient).from('employees').format('graph').types(['manager']).depth(2).execute()
|
|
456
|
-
const url = mockHttpClient.get.mock.calls[0][0]
|
|
457
|
-
expect(url).toContain('format=graph')
|
|
458
|
-
expect(url).toContain('relationship_type=manager')
|
|
459
|
-
expect(url).toContain('depth=2')
|
|
460
|
-
})
|
|
461
|
-
})
|
|
462
|
-
|
|
463
|
-
describe('graph response handling', () => {
|
|
464
|
-
it('returns descendants response', async () => {
|
|
465
|
-
const mockResponse = {
|
|
466
|
-
status: 'success',
|
|
467
|
-
data: {
|
|
468
|
-
data: { id: 1, name: 'Alice Chen', title: 'CEO' },
|
|
469
|
-
reports: [
|
|
470
|
-
{ id: 2, name: 'Bob Smith', _depth: 1, _relationship_type: 'manager' },
|
|
471
|
-
{ id: 3, name: 'Carol White', _depth: 1, _relationship_type: 'manager' }
|
|
472
|
-
]
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
mockHttpClient.get.mockResolvedValue(mockResponse)
|
|
476
|
-
const result = await new Database(mockClient).from('employees').get('1').include('descendants').execute()
|
|
477
|
-
expect((result as any).data.reports).toHaveLength(2)
|
|
478
|
-
})
|
|
479
|
-
|
|
480
|
-
it('returns tree format with nested children', async () => {
|
|
481
|
-
const mockResponse = {
|
|
482
|
-
status: 'success',
|
|
483
|
-
data: [{
|
|
484
|
-
id: 1, name: 'Alice Chen', _depth: 0,
|
|
485
|
-
children: [
|
|
486
|
-
{ id: 2, name: 'Bob Smith', _depth: 1, children: [] },
|
|
487
|
-
{ id: 3, name: 'Carol White', _depth: 1, children: [] }
|
|
488
|
-
]
|
|
489
|
-
}],
|
|
490
|
-
total: 3
|
|
491
|
-
}
|
|
492
|
-
mockHttpClient.get.mockResolvedValue(mockResponse)
|
|
493
|
-
const result = await new Database(mockClient).from('employees').get('1').format('tree').depth(2).execute()
|
|
494
|
-
expect((result as any).data[0].children).toHaveLength(2)
|
|
495
|
-
})
|
|
496
|
-
|
|
497
|
-
it('returns graph format with nodes and edges', async () => {
|
|
498
|
-
const mockResponse = {
|
|
499
|
-
status: 'success',
|
|
500
|
-
data: {
|
|
501
|
-
nodes: [{ id: 1, name: 'Alice Chen' }, { id: 2, name: 'Bob Smith' }],
|
|
502
|
-
edges: [{ id: 9, from_id: 2, to_id: 1, type: 'manager' }]
|
|
503
|
-
},
|
|
504
|
-
total: 2
|
|
505
|
-
}
|
|
506
|
-
mockHttpClient.get.mockResolvedValue(mockResponse)
|
|
507
|
-
const result = await new Database(mockClient).from('employees').format('graph').types(['manager']).execute()
|
|
508
|
-
expect((result as any).data.nodes).toHaveLength(2)
|
|
509
|
-
expect((result as any).data.edges).toHaveLength(1)
|
|
510
|
-
})
|
|
511
|
-
})
|
|
512
|
-
|
|
513
|
-
describe('upsert()', () => {
|
|
514
|
-
it('calls httpClient.post with body to upsert URL', async () => {
|
|
515
|
-
const body = { name: 'Test', status: 'active' }
|
|
516
|
-
mockHttpClient.post.mockResolvedValue({ id: '1', ...body })
|
|
517
|
-
await new Database(mockClient).from('accounts').upsert(body).execute()
|
|
518
|
-
expect(mockHttpClient.post).toHaveBeenCalledWith(
|
|
519
|
-
'api/apps/test-app/datatables/accounts/data/upsert/',
|
|
520
|
-
body
|
|
521
|
-
)
|
|
522
|
-
})
|
|
523
|
-
|
|
524
|
-
it('builds correct upsert URL with array body', async () => {
|
|
525
|
-
const body = [{ name: 'A' }, { name: 'B' }]
|
|
526
|
-
mockHttpClient.post.mockResolvedValue({ data: body })
|
|
527
|
-
await new Database(mockClient).from('accounts').upsert(body).execute()
|
|
528
|
-
expect(mockHttpClient.post).toHaveBeenCalledWith(
|
|
529
|
-
'api/apps/test-app/datatables/accounts/data/upsert/',
|
|
530
|
-
body
|
|
531
|
-
)
|
|
532
|
-
})
|
|
533
|
-
})
|
|
534
|
-
|
|
535
|
-
describe('bulkUpdate()', () => {
|
|
536
|
-
it('calls httpClient.patch with array body without recordId', async () => {
|
|
537
|
-
const body = [{ id: '1', status: 'inactive' }, { id: '2', status: 'active' }]
|
|
538
|
-
mockHttpClient.patch.mockResolvedValue({ data: body })
|
|
539
|
-
await new Database(mockClient).from('accounts').bulkUpdate(body).execute()
|
|
540
|
-
expect(mockHttpClient.patch).toHaveBeenCalledWith(
|
|
541
|
-
'api/apps/test-app/datatables/accounts/data/',
|
|
542
|
-
body
|
|
543
|
-
)
|
|
544
|
-
})
|
|
545
|
-
|
|
546
|
-
it('does not throw without recordId when body is array', async () => {
|
|
547
|
-
const body = [{ id: '1', name: 'Updated' }]
|
|
548
|
-
mockHttpClient.patch.mockResolvedValue({ data: body })
|
|
549
|
-
await expect(
|
|
550
|
-
new Database(mockClient).from('accounts').bulkUpdate(body).execute()
|
|
551
|
-
).resolves.toBeDefined()
|
|
552
|
-
})
|
|
553
|
-
})
|
|
554
|
-
|
|
555
|
-
describe('bulkDelete()', () => {
|
|
556
|
-
it('calls httpClient.delete with ids in query string', async () => {
|
|
557
|
-
mockHttpClient.delete.mockResolvedValue({ status: 'success' })
|
|
558
|
-
await new Database(mockClient).from('accounts').bulkDelete(['1']).execute()
|
|
559
|
-
expect(mockHttpClient.delete).toHaveBeenCalledWith(
|
|
560
|
-
expect.stringContaining('ids=1')
|
|
561
|
-
)
|
|
562
|
-
})
|
|
563
|
-
|
|
564
|
-
it('joins multiple ids with comma', async () => {
|
|
565
|
-
mockHttpClient.delete.mockResolvedValue({ status: 'success' })
|
|
566
|
-
await new Database(mockClient).from('accounts').bulkDelete(['1', '2', '3']).execute()
|
|
567
|
-
expect(mockHttpClient.delete).toHaveBeenCalledWith(
|
|
568
|
-
expect.stringContaining('ids=1%2C2%2C3')
|
|
569
|
-
)
|
|
570
|
-
})
|
|
571
|
-
})
|
|
572
|
-
|
|
573
|
-
describe('deleteFiltered()', () => {
|
|
574
|
-
it('calls httpClient.delete with filter params in query string', async () => {
|
|
575
|
-
mockHttpClient.delete.mockResolvedValue({ status: 'success' })
|
|
576
|
-
await new Database(mockClient).from('accounts').filters('status', 'eq', 'inactive').deleteFiltered().execute()
|
|
577
|
-
const url = mockHttpClient.delete.mock.calls[0][0]
|
|
578
|
-
expect(url).toContain('status=inactive')
|
|
579
|
-
expect(url).not.toContain('/undefined/')
|
|
580
|
-
})
|
|
581
|
-
|
|
582
|
-
it('hits collection endpoint without recordId', async () => {
|
|
583
|
-
mockHttpClient.delete.mockResolvedValue({ status: 'success' })
|
|
584
|
-
await new Database(mockClient).from('accounts').filters('age', 'lt', 18).deleteFiltered().execute()
|
|
585
|
-
expect(mockHttpClient.delete).toHaveBeenCalledWith(
|
|
586
|
-
expect.stringContaining('api/apps/test-app/datatables/accounts/data/?')
|
|
587
|
-
)
|
|
588
|
-
})
|
|
589
|
-
})
|
|
590
|
-
|
|
591
|
-
describe('edges()', () => {
|
|
592
|
-
it('targets _edges table for list', async () => {
|
|
593
|
-
mockHttpClient.get.mockResolvedValue({ edges: [], total: 0 })
|
|
594
|
-
await new Database(mockClient).from('employees').edges().execute()
|
|
595
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith('api/apps/test-app/datatables/employees_edges/data/')
|
|
596
|
-
})
|
|
597
|
-
|
|
598
|
-
it('create() calls POST on edges route', async () => {
|
|
599
|
-
const edges = [
|
|
600
|
-
{ from_id: 5, to_id: 2, type: 'manager' },
|
|
601
|
-
{ from_id: 5, to_id: 3, type: 'dotted_line', metadata: { project: 'AI' } }
|
|
602
|
-
]
|
|
603
|
-
mockHttpClient.post.mockResolvedValue({ status: 'success', data: edges, total: 2 })
|
|
604
|
-
await new Database(mockClient).from('employees').edges().create(edges).execute()
|
|
605
|
-
expect(mockHttpClient.post).toHaveBeenCalledWith(
|
|
606
|
-
'api/apps/test-app/datatables/employees_edges/data/',
|
|
607
|
-
edges
|
|
608
|
-
)
|
|
609
|
-
})
|
|
610
|
-
|
|
611
|
-
it('update() calls PATCH with edge ID', async () => {
|
|
612
|
-
const edge = { from_id: 5, to_id: 3, type: 'dotted_line' }
|
|
613
|
-
mockHttpClient.patch.mockResolvedValue({ id: 9, ...edge })
|
|
614
|
-
await new Database(mockClient).from('employees').edges().get('9').update(edge).execute()
|
|
615
|
-
expect(mockHttpClient.patch).toHaveBeenCalledWith(
|
|
616
|
-
'api/apps/test-app/datatables/employees_edges/data/9/',
|
|
617
|
-
edge
|
|
618
|
-
)
|
|
619
|
-
})
|
|
620
|
-
|
|
621
|
-
it('delete() calls DELETE with edge_ids body', async () => {
|
|
622
|
-
mockHttpClient.delete.mockResolvedValue({ deleted: 2 })
|
|
623
|
-
await new Database(mockClient).from('employees').edges().delete([9, 10]).execute()
|
|
624
|
-
expect(mockHttpClient.delete).toHaveBeenCalledWith(
|
|
625
|
-
'api/apps/test-app/datatables/employees_edges/data/',
|
|
626
|
-
{ edge_ids: [9, 10] }
|
|
627
|
-
)
|
|
628
|
-
})
|
|
629
|
-
|
|
630
|
-
it('returns created edges matching EdgeResponse', async () => {
|
|
631
|
-
const mockResponse: TaruviResponse<EdgeResponse[]> = {
|
|
632
|
-
status: 'success',
|
|
633
|
-
message: 'Edges created successfully',
|
|
634
|
-
data: [{ id: 10, from_id: 5, to_id: 2, type: 'manager', metadata: {} }],
|
|
635
|
-
total: 1
|
|
636
|
-
}
|
|
637
|
-
mockHttpClient.post.mockResolvedValue(mockResponse)
|
|
638
|
-
const result = await new Database(mockClient).from('employees').edges().create([{ from_id: 5, to_id: 2, type: 'manager' }]).execute() as TaruviResponse<EdgeResponse[]>
|
|
639
|
-
expect(result.data).toHaveLength(1)
|
|
640
|
-
expect(result.data[0].id).toBe(10)
|
|
641
|
-
})
|
|
642
|
-
|
|
643
|
-
it('does not affect non-edge queries', async () => {
|
|
644
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
645
|
-
const base = new Database(mockClient).from('employees')
|
|
646
|
-
await base.edges().execute()
|
|
647
|
-
await base.execute()
|
|
648
|
-
expect(mockHttpClient.get).toHaveBeenNthCalledWith(1, 'api/apps/test-app/datatables/employees_edges/data/')
|
|
649
|
-
expect(mockHttpClient.get).toHaveBeenNthCalledWith(2, 'api/apps/test-app/datatables/employees/data/')
|
|
650
|
-
})
|
|
651
|
-
})
|
|
652
|
-
})
|