@taruvi/sdk 1.3.9 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/index.ts +3 -7
- package/src/lib/database/DatabaseClient.ts +75 -37
- package/src/lib/database/types.ts +25 -1
- package/src/lib/policy/PolicyClient.ts +1 -1
- package/tests/unit/database/DatabaseClient.test.ts +184 -1
- package/tests/unit/edge-cases/robustness.test.ts +10 -11
- package/src/lib/graphs/GraphClient.ts +0 -106
- package/src/lib/graphs/types.ts +0 -33
- package/src/lib-internal/routes/GraphRoutes.ts +0 -14
- package/tests/unit/graphs/GraphClient.test.ts +0 -329
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -17,7 +17,6 @@ export { Secrets } from "./lib/secrets/SecretsClient.js"
|
|
|
17
17
|
export { Policy } from "./lib/policy/PolicyClient.js"
|
|
18
18
|
export { App } from "./lib/app/AppClient.js"
|
|
19
19
|
export { Analytics } from "./lib/analytics/AnalyticsClient.js"
|
|
20
|
-
export { Graph } from "./lib/graphs/GraphClient.js"
|
|
21
20
|
|
|
22
21
|
// Export core types
|
|
23
22
|
export type { TaruviConfig, TaruviResponse, PaginationInfo, StorageFilters, DatabaseFilters } from "./types.js"
|
|
@@ -27,7 +26,7 @@ export type { AuthTokens } from "./lib-internal/token/TokenClient.js"
|
|
|
27
26
|
export type { UserCreateRequest, UserData, UserUpdateRequest, UserListFilters, UserApp, UserResponse, UserListResponse, UserAppsResponse, AssignRolesRequest, RevokeRolesRequest, RolesResponse, UserGroup, UserPermission, UserRole, UserPreferences, UserPreferencesUpdate, UserPreferencesResponse } from "./lib/users/types.js"
|
|
28
27
|
|
|
29
28
|
// Policy types
|
|
30
|
-
export type { Principal, Resource, Resources, PolicyCheckResult, PolicyCheckBatchResult, ResourceCheckResponse } from "./lib/policy/types.js"
|
|
29
|
+
export type { Principal, Resource, Resources, PolicyCheckResult, PolicyCheckBatchResult, ResourceCheckResponse, GetAllowedActionsOptions } from "./lib/policy/types.js"
|
|
31
30
|
|
|
32
31
|
// App types
|
|
33
32
|
export type { RoleData, AppSettingsData, RoleResponse, RolesListResponse, AppSettingsResponse } from "./lib/app/types.js"
|
|
@@ -36,7 +35,7 @@ export type { RoleData, AppSettingsData, RoleResponse, RolesListResponse, AppSet
|
|
|
36
35
|
export type { FunctionRequest, FunctionResponse, FunctionInvocation } from "./lib/functions/types.js"
|
|
37
36
|
|
|
38
37
|
// Database types
|
|
39
|
-
export type { DatabaseRequest, DatabaseResponse, DatabaseSingleResponse, FilterOperator, SortOrder } from "./lib/database/types.js"
|
|
38
|
+
export type { DatabaseRequest, DatabaseResponse, DatabaseSingleResponse, FilterOperator, SortOrder, GraphInclude, GraphFormat, EdgeRequest, EdgeResponse, EdgeDeleteRequest } from "./lib/database/types.js"
|
|
40
39
|
|
|
41
40
|
// Storage types
|
|
42
41
|
export type { StorageRequest, StorageUpdateRequest, StorageObject, StorageResponse, StorageListResponse, StorageUploadBatchResponse, StorageDeleteBatchResponse } from "./lib/storage/types.js"
|
|
@@ -48,7 +47,4 @@ export type { SiteSettingsData, SettingsResponse } from "./lib/settings/types.js
|
|
|
48
47
|
export type { SecretCreateRequest, SecretUpdateRequest, SecretData, SecretResponse, SecretsListResponse, SecretsBatchResponse, SecretsBatchMetadataResponse, GetSecretOptions, GetSecretsOptions } from "./lib/secrets/types.js"
|
|
49
48
|
|
|
50
49
|
// Analytics types
|
|
51
|
-
export type { AnalyticsRequest, AnalyticsResponse } from "./lib/analytics/types.js"
|
|
52
|
-
|
|
53
|
-
// Graph types
|
|
54
|
-
export type { GraphInclude, GraphFormat, GraphQueryParams, EdgeRequest, EdgeResponse, EdgeDeleteRequest } from "./lib/graphs/types.js"
|
|
50
|
+
export type { AnalyticsRequest, AnalyticsResponse } from "./lib/analytics/types.js"
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import type { Client } from "../../client.js";
|
|
2
|
-
import { DatabaseRoutes
|
|
2
|
+
import { DatabaseRoutes } from "../../lib-internal/routes/DatabaseRoutes.js";
|
|
3
3
|
import { HttpMethod } from "../../lib-internal/http/types.js";
|
|
4
4
|
import type { TaruviConfig, DatabaseFilters, TaruviResponse } from "../../types.js";
|
|
5
|
-
import type { UrlParams, FilterOperator, SortOrder } from "./types.js";
|
|
5
|
+
import type { UrlParams, FilterOperator, SortOrder, GraphInclude, GraphFormat, EdgeRequest, EdgeDeleteRequest } from "./types.js";
|
|
6
6
|
import { buildQueryString } from "../../utils/utils.js";
|
|
7
7
|
|
|
8
|
+
interface GraphQueryParams {
|
|
9
|
+
include?: GraphInclude
|
|
10
|
+
depth?: number
|
|
11
|
+
format?: GraphFormat
|
|
12
|
+
relationship_type?: string[]
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
// Used to access app data
|
|
9
16
|
export class Database<T = Record<string, unknown>> {
|
|
10
17
|
private client: Client
|
|
@@ -13,28 +20,53 @@ export class Database<T = Record<string, unknown>> {
|
|
|
13
20
|
private operation: HttpMethod | undefined
|
|
14
21
|
private body: object | undefined
|
|
15
22
|
private queryParams: DatabaseFilters | undefined
|
|
23
|
+
private graphParams: GraphQueryParams
|
|
24
|
+
private isEdges: boolean
|
|
16
25
|
|
|
17
|
-
constructor(client: Client, urlParams: UrlParams = {}, operation?: HttpMethod | undefined, body?: object | undefined, queryParams?: DatabaseFilters) {
|
|
26
|
+
constructor(client: Client, urlParams: UrlParams = {}, operation?: HttpMethod | undefined, body?: object | undefined, queryParams?: DatabaseFilters, graphParams: GraphQueryParams = {}, isEdges: boolean = false) {
|
|
18
27
|
this.client = client
|
|
19
28
|
this.urlParams = urlParams
|
|
20
29
|
this.operation = operation
|
|
21
30
|
this.body = body
|
|
22
31
|
this.config = this.client.getConfig()
|
|
23
32
|
this.queryParams = queryParams
|
|
33
|
+
this.graphParams = graphParams
|
|
34
|
+
this.isEdges = isEdges
|
|
24
35
|
}
|
|
25
36
|
|
|
26
37
|
from<U = Record<string, unknown>>(dataTables: string): Database<U> {
|
|
27
|
-
return new Database<U>(this.client, { ...this.urlParams, dataTables }, undefined, undefined)
|
|
38
|
+
return new Database<U>(this.client, { ...this.urlParams, dataTables }, undefined, undefined, undefined, {}, this.isEdges)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
edges(): Database<T> {
|
|
42
|
+
return new Database<T>(this.client, { ...this.urlParams }, undefined, undefined, this.queryParams, { ...this.graphParams }, true)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Graph traversal methods
|
|
46
|
+
include(direction: GraphInclude): Database<T> {
|
|
47
|
+
return new Database<T>(this.client, { ...this.urlParams }, this.operation, this.body, this.queryParams, { ...this.graphParams, include: direction }, this.isEdges)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
depth(n: number): Database<T> {
|
|
51
|
+
return new Database<T>(this.client, { ...this.urlParams }, this.operation, this.body, this.queryParams, { ...this.graphParams, depth: n }, this.isEdges)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
format(fmt: GraphFormat): Database<T> {
|
|
55
|
+
return new Database<T>(this.client, { ...this.urlParams }, this.operation, this.body, this.queryParams, { ...this.graphParams, format: fmt }, this.isEdges)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
types(types: string[]): Database<T> {
|
|
59
|
+
return new Database<T>(this.client, { ...this.urlParams }, this.operation, this.body, this.queryParams, { ...this.graphParams, relationship_type: types }, this.isEdges)
|
|
28
60
|
}
|
|
29
61
|
|
|
62
|
+
// Filter & query methods
|
|
30
63
|
filter(field: string, operator: FilterOperator, value: string | number | boolean | (string | number)[]): Database<T> {
|
|
31
64
|
const filterKey = operator === 'eq' ? field : `${field}__${operator}`
|
|
32
|
-
// For 'in' and 'nin' operators, join array values with comma
|
|
33
65
|
const filterValue = Array.isArray(value) ? value.join(',') : value
|
|
34
66
|
return new Database<T>(this.client, { ...this.urlParams }, undefined, undefined, {
|
|
35
67
|
...this.queryParams,
|
|
36
68
|
[filterKey]: filterValue
|
|
37
|
-
})
|
|
69
|
+
}, { ...this.graphParams }, this.isEdges)
|
|
38
70
|
}
|
|
39
71
|
|
|
40
72
|
sort(field: string, order: SortOrder = 'asc'): Database<T> {
|
|
@@ -42,72 +74,77 @@ export class Database<T = Record<string, unknown>> {
|
|
|
42
74
|
return new Database<T>(this.client, { ...this.urlParams }, undefined, undefined, {
|
|
43
75
|
...this.queryParams,
|
|
44
76
|
ordering
|
|
45
|
-
})
|
|
77
|
+
}, { ...this.graphParams }, this.isEdges)
|
|
46
78
|
}
|
|
47
79
|
|
|
48
80
|
pageSize(size: number): Database<T> {
|
|
49
81
|
return new Database<T>(this.client, { ...this.urlParams }, undefined, undefined, {
|
|
50
82
|
...this.queryParams,
|
|
51
83
|
page_size: size
|
|
52
|
-
})
|
|
84
|
+
}, { ...this.graphParams }, this.isEdges)
|
|
53
85
|
}
|
|
54
86
|
|
|
55
87
|
page(num: number): Database<T> {
|
|
56
88
|
return new Database<T>(this.client, { ...this.urlParams }, undefined, undefined, {
|
|
57
89
|
...this.queryParams,
|
|
58
90
|
page: num
|
|
59
|
-
})
|
|
91
|
+
}, { ...this.graphParams }, this.isEdges)
|
|
60
92
|
}
|
|
61
93
|
|
|
62
94
|
populate(populate: string[]): Database<T> {
|
|
63
95
|
return new Database<T>(this.client, { ...this.urlParams }, undefined, undefined, {
|
|
64
96
|
...this.queryParams,
|
|
65
97
|
populate: populate.join(',')
|
|
66
|
-
})
|
|
98
|
+
}, { ...this.graphParams }, this.isEdges)
|
|
67
99
|
}
|
|
68
100
|
|
|
69
101
|
search(query: string): Database<T> {
|
|
70
102
|
return new Database<T>(this.client, { ...this.urlParams }, undefined, undefined, {
|
|
71
103
|
...this.queryParams,
|
|
72
104
|
search: query
|
|
73
|
-
})
|
|
105
|
+
}, { ...this.graphParams }, this.isEdges)
|
|
74
106
|
}
|
|
75
107
|
|
|
76
108
|
aggregate(...expressions: string[]): Database<T> {
|
|
77
109
|
return new Database<T>(this.client, { ...this.urlParams }, undefined, undefined, {
|
|
78
110
|
...this.queryParams,
|
|
79
111
|
_aggregate: expressions.join(',')
|
|
80
|
-
})
|
|
112
|
+
}, { ...this.graphParams }, this.isEdges)
|
|
81
113
|
}
|
|
82
114
|
|
|
83
115
|
groupBy(...fields: string[]): Database<T> {
|
|
84
116
|
return new Database<T>(this.client, { ...this.urlParams }, undefined, undefined, {
|
|
85
117
|
...this.queryParams,
|
|
86
118
|
_group_by: fields.join(',')
|
|
87
|
-
})
|
|
119
|
+
}, { ...this.graphParams }, this.isEdges)
|
|
88
120
|
}
|
|
89
121
|
|
|
90
122
|
having(condition: string): Database<T> {
|
|
91
123
|
return new Database<T>(this.client, { ...this.urlParams }, undefined, undefined, {
|
|
92
124
|
...this.queryParams,
|
|
93
125
|
_having: condition
|
|
94
|
-
})
|
|
126
|
+
}, { ...this.graphParams }, this.isEdges)
|
|
95
127
|
}
|
|
96
128
|
|
|
129
|
+
// CRUD methods
|
|
97
130
|
get(recordId: string): Database<T> {
|
|
98
|
-
return new Database<T>(this.client, { ...this.urlParams, recordId }, HttpMethod.GET)
|
|
131
|
+
return new Database<T>(this.client, { ...this.urlParams, recordId }, HttpMethod.GET, undefined, this.queryParams, { ...this.graphParams }, this.isEdges)
|
|
99
132
|
}
|
|
100
133
|
|
|
101
|
-
create(body: Partial<T> | Partial<T>[]): Database<T> {
|
|
102
|
-
return new Database<T>(this.client, { ...this.urlParams }, HttpMethod.POST, body as object)
|
|
134
|
+
create(body: Partial<T> | Partial<T>[] | EdgeRequest[]): Database<T> {
|
|
135
|
+
return new Database<T>(this.client, { ...this.urlParams }, HttpMethod.POST, body as object, this.queryParams, { ...this.graphParams }, this.isEdges)
|
|
103
136
|
}
|
|
104
137
|
|
|
105
|
-
update(body: Partial<T> |
|
|
106
|
-
return new Database<T>(this.client, { ...this.urlParams }, HttpMethod.PATCH, body as object)
|
|
138
|
+
update(body: Partial<T> | EdgeRequest): Database<T> {
|
|
139
|
+
return new Database<T>(this.client, { ...this.urlParams }, HttpMethod.PATCH, body as object, this.queryParams, { ...this.graphParams }, this.isEdges)
|
|
107
140
|
}
|
|
108
141
|
|
|
109
|
-
delete(
|
|
110
|
-
|
|
142
|
+
delete(recordIdOrEdgeIds: string | number[]): Database<T> {
|
|
143
|
+
if (Array.isArray(recordIdOrEdgeIds)) {
|
|
144
|
+
const body: EdgeDeleteRequest = { edge_ids: recordIdOrEdgeIds }
|
|
145
|
+
return new Database<T>(this.client, { ...this.urlParams }, HttpMethod.DELETE, body, this.queryParams, { ...this.graphParams }, this.isEdges)
|
|
146
|
+
}
|
|
147
|
+
return new Database<T>(this.client, { ...this.urlParams, recordId: recordIdOrEdgeIds }, HttpMethod.DELETE, undefined, this.queryParams, { ...this.graphParams }, this.isEdges)
|
|
111
148
|
}
|
|
112
149
|
|
|
113
150
|
async first(): Promise<T | null> {
|
|
@@ -127,22 +164,22 @@ export class Database<T = Record<string, unknown>> {
|
|
|
127
164
|
return Array.isArray(response.data) ? response.data.length : 0
|
|
128
165
|
}
|
|
129
166
|
|
|
167
|
+
private getTableName(): string {
|
|
168
|
+
const table = this.urlParams.dataTables
|
|
169
|
+
if (!table) throw new Error('Table name is required. Call .from(tableName) first.')
|
|
170
|
+
return this.isEdges ? `${table}_edges` : table
|
|
171
|
+
}
|
|
172
|
+
|
|
130
173
|
private buildRoute(): string {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (value && routeBuilder) {
|
|
138
|
-
acc += routeBuilder(value)
|
|
139
|
-
}
|
|
174
|
+
const tableName = this.getTableName()
|
|
175
|
+
const base = DatabaseRoutes.baseUrl(this.config.appSlug) +
|
|
176
|
+
DatabaseRoutes.dataTables(tableName) +
|
|
177
|
+
(this.urlParams.recordId ? DatabaseRoutes.recordId(this.urlParams.recordId) : '') +
|
|
178
|
+
'/'
|
|
140
179
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
buildQueryString(this.queryParams)
|
|
145
|
-
)
|
|
180
|
+
// Merge database filters and graph params into one query string
|
|
181
|
+
const allParams: Record<string, unknown> = { ...this.queryParams, ...this.graphParams }
|
|
182
|
+
return base + buildQueryString(allParams)
|
|
146
183
|
}
|
|
147
184
|
|
|
148
185
|
async execute(): Promise<TaruviResponse<T | T[]>> {
|
|
@@ -150,9 +187,7 @@ export class Database<T = Record<string, unknown>> {
|
|
|
150
187
|
throw new Error('Table name is required. Call .from(tableName) first.')
|
|
151
188
|
}
|
|
152
189
|
|
|
153
|
-
// Build the API URL
|
|
154
190
|
const url = this.buildRoute()
|
|
155
|
-
|
|
156
191
|
const operation = this.operation || HttpMethod.GET
|
|
157
192
|
|
|
158
193
|
switch (operation) {
|
|
@@ -166,6 +201,9 @@ export class Database<T = Record<string, unknown>> {
|
|
|
166
201
|
return await this.client.httpClient.patch(url, this.body)
|
|
167
202
|
|
|
168
203
|
case HttpMethod.DELETE:
|
|
204
|
+
if (this.body) {
|
|
205
|
+
return await this.client.httpClient.delete(url, this.body)
|
|
206
|
+
}
|
|
169
207
|
return await this.client.httpClient.delete(url)
|
|
170
208
|
|
|
171
209
|
case HttpMethod.GET:
|
|
@@ -63,4 +63,28 @@ export interface DatabaseRequest {
|
|
|
63
63
|
|
|
64
64
|
// Response types - uses standard wrapper
|
|
65
65
|
export type DatabaseResponse<T = unknown> = TaruviResponse<T[]>
|
|
66
|
-
export type DatabaseSingleResponse<T = unknown> = TaruviResponse<T>
|
|
66
|
+
export type DatabaseSingleResponse<T = unknown> = TaruviResponse<T>
|
|
67
|
+
|
|
68
|
+
// Graph traversal types
|
|
69
|
+
export type GraphInclude = 'descendants' | 'ancestors' | 'both'
|
|
70
|
+
export type GraphFormat = 'tree' | 'graph'
|
|
71
|
+
|
|
72
|
+
// Edge types
|
|
73
|
+
export interface EdgeRequest {
|
|
74
|
+
from_id: number | string
|
|
75
|
+
to_id: number | string
|
|
76
|
+
type: string
|
|
77
|
+
metadata?: Record<string, unknown>
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface EdgeResponse {
|
|
81
|
+
id: number
|
|
82
|
+
from_id: number | string
|
|
83
|
+
to_id: number | string
|
|
84
|
+
type: string
|
|
85
|
+
metadata?: Record<string, unknown>
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface EdgeDeleteRequest {
|
|
89
|
+
edge_ids: number[]
|
|
90
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Client } from "../../client.js"
|
|
2
2
|
import { PolicyRoutes } from "../../lib-internal/routes/PolicyRoutes.js"
|
|
3
3
|
import type { TaruviConfig } from "../../types.js"
|
|
4
|
-
import type { Resources, Resource,
|
|
4
|
+
import type { Resources, Resource, PolicyCheckBatchResult, GetAllowedActionsOptions } from "./types.js"
|
|
5
5
|
|
|
6
6
|
export class Policy {
|
|
7
7
|
private client: Client
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
2
|
import { Database } from '../../../src/lib/database/DatabaseClient.js'
|
|
3
3
|
import { Client } from '../../../src/client.js'
|
|
4
|
-
import type { DatabaseResponse, DatabaseSingleResponse } from '../../../src/lib/database/types.js'
|
|
4
|
+
import type { DatabaseResponse, DatabaseSingleResponse, EdgeResponse } from '../../../src/lib/database/types.js'
|
|
5
|
+
import type { TaruviResponse } from '../../../src/types.js'
|
|
5
6
|
|
|
6
7
|
// Mock the Client
|
|
7
8
|
const mockHttpClient = {
|
|
@@ -301,4 +302,186 @@ describe('Database', () => {
|
|
|
301
302
|
expect((result as any).status).toBe('success')
|
|
302
303
|
})
|
|
303
304
|
})
|
|
305
|
+
|
|
306
|
+
describe('graph traversal', () => {
|
|
307
|
+
it('include() sets include param', async () => {
|
|
308
|
+
mockHttpClient.get.mockResolvedValue([])
|
|
309
|
+
await new Database(mockClient).from('employees').get('1').include('descendants').execute()
|
|
310
|
+
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('include=descendants'))
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it('include() supports ancestors', async () => {
|
|
314
|
+
mockHttpClient.get.mockResolvedValue([])
|
|
315
|
+
await new Database(mockClient).from('employees').get('4').include('ancestors').execute()
|
|
316
|
+
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('include=ancestors'))
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it('include() supports both', async () => {
|
|
320
|
+
mockHttpClient.get.mockResolvedValue([])
|
|
321
|
+
await new Database(mockClient).from('employees').get('2').include('both').execute()
|
|
322
|
+
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('include=both'))
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
it('depth() sets depth param', async () => {
|
|
326
|
+
mockHttpClient.get.mockResolvedValue([])
|
|
327
|
+
await new Database(mockClient).from('employees').get('1').include('descendants').depth(3).execute()
|
|
328
|
+
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('depth=3'))
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
it('format() sets format param to tree', async () => {
|
|
332
|
+
mockHttpClient.get.mockResolvedValue([])
|
|
333
|
+
await new Database(mockClient).from('employees').get('1').format('tree').depth(3).execute()
|
|
334
|
+
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('format=tree'))
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
it('format() sets format param to graph', async () => {
|
|
338
|
+
mockHttpClient.get.mockResolvedValue([])
|
|
339
|
+
await new Database(mockClient).from('employees').format('graph').execute()
|
|
340
|
+
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('format=graph'))
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
it('types() sets relationship_type as repeated params', async () => {
|
|
344
|
+
mockHttpClient.get.mockResolvedValue([])
|
|
345
|
+
await new Database(mockClient).from('employees').format('graph').types(['manager', 'dotted_line']).execute()
|
|
346
|
+
const url = mockHttpClient.get.mock.calls[0][0]
|
|
347
|
+
expect(url).toContain('relationship_type=manager')
|
|
348
|
+
expect(url).toContain('relationship_type=dotted_line')
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
it('types() with single type', async () => {
|
|
352
|
+
mockHttpClient.get.mockResolvedValue([])
|
|
353
|
+
await new Database(mockClient).from('employees').format('graph').types(['manager']).execute()
|
|
354
|
+
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('relationship_type=manager'))
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
it('combines include, depth, and get', async () => {
|
|
358
|
+
mockHttpClient.get.mockResolvedValue([])
|
|
359
|
+
await new Database(mockClient).from('employees').get('1').include('descendants').depth(3).execute()
|
|
360
|
+
const url = mockHttpClient.get.mock.calls[0][0]
|
|
361
|
+
expect(url).toContain('/1/')
|
|
362
|
+
expect(url).toContain('include=descendants')
|
|
363
|
+
expect(url).toContain('depth=3')
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
it('combines format, types, and depth', async () => {
|
|
367
|
+
mockHttpClient.get.mockResolvedValue([])
|
|
368
|
+
await new Database(mockClient).from('employees').format('graph').types(['manager']).depth(2).execute()
|
|
369
|
+
const url = mockHttpClient.get.mock.calls[0][0]
|
|
370
|
+
expect(url).toContain('format=graph')
|
|
371
|
+
expect(url).toContain('relationship_type=manager')
|
|
372
|
+
expect(url).toContain('depth=2')
|
|
373
|
+
})
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
describe('graph response handling', () => {
|
|
377
|
+
it('returns descendants response', async () => {
|
|
378
|
+
const mockResponse = {
|
|
379
|
+
status: 'success',
|
|
380
|
+
data: {
|
|
381
|
+
data: { id: 1, name: 'Alice Chen', title: 'CEO' },
|
|
382
|
+
reports: [
|
|
383
|
+
{ id: 2, name: 'Bob Smith', _depth: 1, _relationship_type: 'manager' },
|
|
384
|
+
{ id: 3, name: 'Carol White', _depth: 1, _relationship_type: 'manager' }
|
|
385
|
+
]
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
mockHttpClient.get.mockResolvedValue(mockResponse)
|
|
389
|
+
const result = await new Database(mockClient).from('employees').get('1').include('descendants').execute()
|
|
390
|
+
expect((result as any).data.reports).toHaveLength(2)
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
it('returns tree format with nested children', async () => {
|
|
394
|
+
const mockResponse = {
|
|
395
|
+
status: 'success',
|
|
396
|
+
data: [{
|
|
397
|
+
id: 1, name: 'Alice Chen', _depth: 0,
|
|
398
|
+
children: [
|
|
399
|
+
{ id: 2, name: 'Bob Smith', _depth: 1, children: [] },
|
|
400
|
+
{ id: 3, name: 'Carol White', _depth: 1, children: [] }
|
|
401
|
+
]
|
|
402
|
+
}],
|
|
403
|
+
total: 3
|
|
404
|
+
}
|
|
405
|
+
mockHttpClient.get.mockResolvedValue(mockResponse)
|
|
406
|
+
const result = await new Database(mockClient).from('employees').get('1').format('tree').depth(2).execute()
|
|
407
|
+
expect((result as any).data[0].children).toHaveLength(2)
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
it('returns graph format with nodes and edges', async () => {
|
|
411
|
+
const mockResponse = {
|
|
412
|
+
status: 'success',
|
|
413
|
+
data: {
|
|
414
|
+
nodes: [{ id: 1, name: 'Alice Chen' }, { id: 2, name: 'Bob Smith' }],
|
|
415
|
+
edges: [{ id: 9, from_id: 2, to_id: 1, type: 'manager' }]
|
|
416
|
+
},
|
|
417
|
+
total: 2
|
|
418
|
+
}
|
|
419
|
+
mockHttpClient.get.mockResolvedValue(mockResponse)
|
|
420
|
+
const result = await new Database(mockClient).from('employees').format('graph').types(['manager']).execute()
|
|
421
|
+
expect((result as any).data.nodes).toHaveLength(2)
|
|
422
|
+
expect((result as any).data.edges).toHaveLength(1)
|
|
423
|
+
})
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
describe('edges()', () => {
|
|
427
|
+
it('targets _edges table for list', async () => {
|
|
428
|
+
mockHttpClient.get.mockResolvedValue({ edges: [], total: 0 })
|
|
429
|
+
await new Database(mockClient).from('employees').edges().execute()
|
|
430
|
+
expect(mockHttpClient.get).toHaveBeenCalledWith('api/apps/test-app/datatables/employees_edges/data/')
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
it('create() calls POST on edges route', async () => {
|
|
434
|
+
const edges = [
|
|
435
|
+
{ from_id: 5, to_id: 2, type: 'manager' },
|
|
436
|
+
{ from_id: 5, to_id: 3, type: 'dotted_line', metadata: { project: 'AI' } }
|
|
437
|
+
]
|
|
438
|
+
mockHttpClient.post.mockResolvedValue({ status: 'success', data: edges, total: 2 })
|
|
439
|
+
await new Database(mockClient).from('employees').edges().create(edges).execute()
|
|
440
|
+
expect(mockHttpClient.post).toHaveBeenCalledWith(
|
|
441
|
+
'api/apps/test-app/datatables/employees_edges/data/',
|
|
442
|
+
edges
|
|
443
|
+
)
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
it('update() calls PATCH with edge ID', async () => {
|
|
447
|
+
const edge = { from_id: 5, to_id: 3, type: 'dotted_line' }
|
|
448
|
+
mockHttpClient.patch.mockResolvedValue({ id: 9, ...edge })
|
|
449
|
+
await new Database(mockClient).from('employees').edges().get('9').update(edge).execute()
|
|
450
|
+
expect(mockHttpClient.patch).toHaveBeenCalledWith(
|
|
451
|
+
'api/apps/test-app/datatables/employees_edges/data/9/',
|
|
452
|
+
edge
|
|
453
|
+
)
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
it('delete() calls DELETE with edge_ids body', async () => {
|
|
457
|
+
mockHttpClient.delete.mockResolvedValue({ deleted: 2 })
|
|
458
|
+
await new Database(mockClient).from('employees').edges().delete([9, 10]).execute()
|
|
459
|
+
expect(mockHttpClient.delete).toHaveBeenCalledWith(
|
|
460
|
+
'api/apps/test-app/datatables/employees_edges/data/',
|
|
461
|
+
{ edge_ids: [9, 10] }
|
|
462
|
+
)
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
it('returns created edges matching EdgeResponse', async () => {
|
|
466
|
+
const mockResponse: TaruviResponse<EdgeResponse[]> = {
|
|
467
|
+
status: 'success',
|
|
468
|
+
message: 'Edges created successfully',
|
|
469
|
+
data: [{ id: 10, from_id: 5, to_id: 2, type: 'manager', metadata: {} }],
|
|
470
|
+
total: 1
|
|
471
|
+
}
|
|
472
|
+
mockHttpClient.post.mockResolvedValue(mockResponse)
|
|
473
|
+
const result = await new Database(mockClient).from('employees').edges().create([{ from_id: 5, to_id: 2, type: 'manager' }]).execute() as TaruviResponse<EdgeResponse[]>
|
|
474
|
+
expect(result.data).toHaveLength(1)
|
|
475
|
+
expect(result.data[0].id).toBe(10)
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
it('does not affect non-edge queries', async () => {
|
|
479
|
+
mockHttpClient.get.mockResolvedValue([])
|
|
480
|
+
const base = new Database(mockClient).from('employees')
|
|
481
|
+
await base.edges().execute()
|
|
482
|
+
await base.execute()
|
|
483
|
+
expect(mockHttpClient.get).toHaveBeenNthCalledWith(1, 'api/apps/test-app/datatables/employees_edges/data/')
|
|
484
|
+
expect(mockHttpClient.get).toHaveBeenNthCalledWith(2, 'api/apps/test-app/datatables/employees/data/')
|
|
485
|
+
})
|
|
486
|
+
})
|
|
304
487
|
})
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
2
|
import { Database } from '../../../src/lib/database/DatabaseClient.js'
|
|
3
3
|
import { Storage } from '../../../src/lib/storage/StorageClient.js'
|
|
4
|
-
import { Graph } from '../../../src/lib/graphs/GraphClient.js'
|
|
5
4
|
import { Client } from '../../../src/client.js'
|
|
6
5
|
|
|
7
6
|
const mockHttpClient = {
|
|
@@ -61,9 +60,9 @@ describe('Non-JSON / empty / malformed responses', () => {
|
|
|
61
60
|
await expect(new Database(mockClient).from('accounts').execute()).rejects.toThrow('Network Error')
|
|
62
61
|
})
|
|
63
62
|
|
|
64
|
-
it('propagates network error for
|
|
63
|
+
it('propagates network error for Database graph', async () => {
|
|
65
64
|
mockHttpClient.get.mockRejectedValue(new Error('ECONNREFUSED'))
|
|
66
|
-
await expect(new
|
|
65
|
+
await expect(new Database(mockClient).from('employees').execute()).rejects.toThrow('ECONNREFUSED')
|
|
67
66
|
})
|
|
68
67
|
|
|
69
68
|
it('propagates network error for Storage', async () => {
|
|
@@ -126,7 +125,7 @@ describe('Encoding edge cases', () => {
|
|
|
126
125
|
|
|
127
126
|
it('encodes graph relationship types with special chars', async () => {
|
|
128
127
|
mockHttpClient.get.mockResolvedValue([])
|
|
129
|
-
await new
|
|
128
|
+
await new Database(mockClient).from('employees').types(['reports_to']).execute()
|
|
130
129
|
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('relationship_type=reports_to'))
|
|
131
130
|
})
|
|
132
131
|
|
|
@@ -180,9 +179,9 @@ describe('Builder immutability', () => {
|
|
|
180
179
|
expect(mockHttpClient.get.mock.calls[0][0]).not.toContain('page=2')
|
|
181
180
|
})
|
|
182
181
|
|
|
183
|
-
it('
|
|
182
|
+
it('Database graph: chaining does not mutate original instance', async () => {
|
|
184
183
|
mockHttpClient.get.mockResolvedValue([])
|
|
185
|
-
const base = new
|
|
184
|
+
const base = new Database(mockClient).from('employees')
|
|
186
185
|
const descendants = base.get('1').include('descendants').depth(3)
|
|
187
186
|
const ancestors = base.get('4').include('ancestors')
|
|
188
187
|
|
|
@@ -201,9 +200,9 @@ describe('Builder immutability', () => {
|
|
|
201
200
|
expect(ancUrl).not.toContain('depth=3')
|
|
202
201
|
})
|
|
203
202
|
|
|
204
|
-
it('
|
|
203
|
+
it('Database graph: format does not leak between chains', async () => {
|
|
205
204
|
mockHttpClient.get.mockResolvedValue([])
|
|
206
|
-
const base = new
|
|
205
|
+
const base = new Database(mockClient).from('employees')
|
|
207
206
|
const tree = base.format('tree')
|
|
208
207
|
const graph = base.format('graph')
|
|
209
208
|
|
|
@@ -243,12 +242,12 @@ describe('Builder immutability', () => {
|
|
|
243
242
|
expect(mockHttpClient.get.mock.calls[1][0]).not.toContain('/123/')
|
|
244
243
|
})
|
|
245
244
|
|
|
246
|
-
it('
|
|
245
|
+
it('Database: edge operations do not affect traversal chain', async () => {
|
|
247
246
|
mockHttpClient.get.mockResolvedValue([])
|
|
248
247
|
mockHttpClient.post.mockResolvedValue({})
|
|
249
|
-
const base = new
|
|
248
|
+
const base = new Database(mockClient).from('employees')
|
|
250
249
|
const traversal = base.get('1').include('descendants')
|
|
251
|
-
const edge = base.create([{
|
|
250
|
+
const edge = base.edges().create([{ from_id: 1, to_id: 2, type: 'manager' }])
|
|
252
251
|
|
|
253
252
|
await traversal.execute()
|
|
254
253
|
await edge.execute()
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import type { Client } from "../../client.js"
|
|
2
|
-
import type { TaruviConfig } from "../../types.js"
|
|
3
|
-
import type { GraphInclude, GraphFormat, GraphQueryParams, GraphUrlParams, EdgeRequest, EdgeDeleteRequest } from "./types.js"
|
|
4
|
-
import { GraphRoutes, GraphEdgeRoutes, type GraphRouteKey } from "../../lib-internal/routes/GraphRoutes.js"
|
|
5
|
-
import { buildQueryString } from "../../utils/utils.js"
|
|
6
|
-
import { HttpMethod } from "../../lib-internal/http/types.js"
|
|
7
|
-
|
|
8
|
-
export class Graph<T = Record<string, unknown>> {
|
|
9
|
-
private client: Client
|
|
10
|
-
private config: TaruviConfig
|
|
11
|
-
private urlParams: GraphUrlParams
|
|
12
|
-
private queryParams: GraphQueryParams
|
|
13
|
-
private operation: HttpMethod | undefined
|
|
14
|
-
private body: object | undefined
|
|
15
|
-
private edgeRoute: string | undefined
|
|
16
|
-
|
|
17
|
-
constructor(client: Client, urlParams: GraphUrlParams = {}, queryParams: GraphQueryParams = {}, operation?: HttpMethod, body?: object, edgeRoute?: string) {
|
|
18
|
-
this.client = client
|
|
19
|
-
this.config = this.client.getConfig()
|
|
20
|
-
this.urlParams = urlParams
|
|
21
|
-
this.queryParams = queryParams
|
|
22
|
-
this.operation = operation
|
|
23
|
-
this.body = body
|
|
24
|
-
this.edgeRoute = edgeRoute
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
from<U = Record<string, unknown>>(dataTables: string): Graph<U> {
|
|
28
|
-
return new Graph<U>(this.client, { ...this.urlParams, dataTables }, { ...this.queryParams })
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
get(recordId: string): Graph<T> {
|
|
32
|
-
return new Graph<T>(this.client, { ...this.urlParams, recordId }, { ...this.queryParams })
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
include(direction: GraphInclude): Graph<T> {
|
|
36
|
-
return new Graph<T>(this.client, { ...this.urlParams }, { ...this.queryParams, include: direction })
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
depth(n: number): Graph<T> {
|
|
40
|
-
return new Graph<T>(this.client, { ...this.urlParams }, { ...this.queryParams, depth: n })
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
format(fmt: GraphFormat): Graph<T> {
|
|
44
|
-
return new Graph<T>(this.client, { ...this.urlParams }, { ...this.queryParams, format: fmt })
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
types(types: string[]): Graph<T> {
|
|
48
|
-
return new Graph<T>(this.client, { ...this.urlParams }, { ...this.queryParams, relationship_type: types })
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
list(): Graph<T> {
|
|
52
|
-
const route = GraphEdgeRoutes.baseUrl(this.config.appSlug) + GraphEdgeRoutes.edges(this.urlParams.dataTables!) + "/"
|
|
53
|
-
return new Graph<T>(this.client, { ...this.urlParams }, {}, HttpMethod.GET, undefined, route)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
create(edges: EdgeRequest[]): Graph<T> {
|
|
57
|
-
const route = GraphEdgeRoutes.baseUrl(this.config.appSlug) + GraphEdgeRoutes.edges(this.urlParams.dataTables!) + "/"
|
|
58
|
-
return new Graph<T>(this.client, { ...this.urlParams }, {}, HttpMethod.POST, edges as unknown as object, route)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
update(edgeId: string, edge: EdgeRequest): Graph<T> {
|
|
62
|
-
const route = GraphEdgeRoutes.baseUrl(this.config.appSlug) + GraphEdgeRoutes.edges(this.urlParams.dataTables!) + GraphEdgeRoutes.edgeId(edgeId) + "/"
|
|
63
|
-
return new Graph<T>(this.client, { ...this.urlParams }, {}, HttpMethod.PATCH, edge, route)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
delete(edgeIds: number[]): Graph<T> {
|
|
67
|
-
const route = GraphEdgeRoutes.baseUrl(this.config.appSlug) + GraphEdgeRoutes.edges(this.urlParams.dataTables!) + "/"
|
|
68
|
-
const body: EdgeDeleteRequest = { edge_ids: edgeIds }
|
|
69
|
-
return new Graph<T>(this.client, { ...this.urlParams }, {}, HttpMethod.DELETE, body, route)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
private buildRoute(): string {
|
|
73
|
-
if (this.edgeRoute) return this.edgeRoute
|
|
74
|
-
|
|
75
|
-
return (
|
|
76
|
-
GraphRoutes.baseUrl(this.config.appSlug) +
|
|
77
|
-
(Object.keys(this.urlParams) as GraphRouteKey[]).reduce((acc, key) => {
|
|
78
|
-
const value = this.urlParams[key]
|
|
79
|
-
const routeBuilder = GraphRoutes[key]
|
|
80
|
-
if (value && routeBuilder) {
|
|
81
|
-
acc += routeBuilder(value)
|
|
82
|
-
}
|
|
83
|
-
return acc
|
|
84
|
-
}, "") +
|
|
85
|
-
"/" +
|
|
86
|
-
buildQueryString(this.queryParams as Record<string, unknown>)
|
|
87
|
-
)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async execute(): Promise<T | T[]> {
|
|
91
|
-
const url = this.buildRoute()
|
|
92
|
-
const operation = this.operation || HttpMethod.GET
|
|
93
|
-
|
|
94
|
-
switch (operation) {
|
|
95
|
-
case HttpMethod.POST:
|
|
96
|
-
return await this.client.httpClient.post(url, this.body)
|
|
97
|
-
case HttpMethod.PATCH:
|
|
98
|
-
return await this.client.httpClient.patch(url, this.body)
|
|
99
|
-
case HttpMethod.DELETE:
|
|
100
|
-
return await this.client.httpClient.delete(url, this.body)
|
|
101
|
-
case HttpMethod.GET:
|
|
102
|
-
default:
|
|
103
|
-
return await this.client.httpClient.get(url)
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
package/src/lib/graphs/types.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
export type GraphInclude = 'descendants' | 'ancestors' | 'both'
|
|
2
|
-
export type GraphFormat = 'tree' | 'graph'
|
|
3
|
-
|
|
4
|
-
export interface GraphQueryParams {
|
|
5
|
-
include?: GraphInclude
|
|
6
|
-
depth?: number
|
|
7
|
-
format?: GraphFormat
|
|
8
|
-
relationship_type?: string[]
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface GraphUrlParams {
|
|
12
|
-
dataTables?: string
|
|
13
|
-
recordId?: string
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface EdgeRequest {
|
|
17
|
-
from: number | string
|
|
18
|
-
to: number | string
|
|
19
|
-
type: string
|
|
20
|
-
metadata?: Record<string, unknown>
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface EdgeResponse {
|
|
24
|
-
id: number
|
|
25
|
-
from: number | string
|
|
26
|
-
to: number | string
|
|
27
|
-
type: string
|
|
28
|
-
metadata?: Record<string, unknown>
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface EdgeDeleteRequest {
|
|
32
|
-
edge_ids: number[]
|
|
33
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export const GraphRoutes = {
|
|
2
|
-
baseUrl: (appSlug: string) => `api/apps/${appSlug}`,
|
|
3
|
-
dataTables: (tableName: string): string => `/datatables/${tableName}/data`,
|
|
4
|
-
recordId: (recordId: string): string => `/${recordId}`
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export const GraphEdgeRoutes = {
|
|
8
|
-
baseUrl: (appSlug: string) => `api/apps/${appSlug}`,
|
|
9
|
-
edges: (tableName: string): string => `/datatables/${tableName}_edges/data`,
|
|
10
|
-
edgeId: (edgeId: string): string => `/${edgeId}`
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
type AllRouteKeys = keyof typeof GraphRoutes
|
|
14
|
-
export type GraphRouteKey = Exclude<AllRouteKeys, 'baseUrl'>
|
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
-
import { Graph } from '../../../src/lib/graphs/GraphClient.js'
|
|
3
|
-
import { Client } from '../../../src/client.js'
|
|
4
|
-
import type { EdgeResponse } from '../../../src/lib/graphs/types.js'
|
|
5
|
-
import type { TaruviResponse } from '../../../src/types.js'
|
|
6
|
-
|
|
7
|
-
const mockHttpClient = {
|
|
8
|
-
get: vi.fn(),
|
|
9
|
-
post: vi.fn(),
|
|
10
|
-
patch: vi.fn(),
|
|
11
|
-
delete: vi.fn()
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const mockClient = {
|
|
15
|
-
getConfig: () => ({ apiKey: 'test-key', appSlug: 'test-app', apiUrl: 'https://api.test.com' }),
|
|
16
|
-
httpClient: mockHttpClient
|
|
17
|
-
} as unknown as Client
|
|
18
|
-
|
|
19
|
-
describe('Graph', () => {
|
|
20
|
-
beforeEach(() => {
|
|
21
|
-
vi.clearAllMocks()
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
describe('from()', () => {
|
|
25
|
-
it('returns new Graph instance', () => {
|
|
26
|
-
const graph = new Graph(mockClient)
|
|
27
|
-
expect(graph.from('employees')).toBeInstanceOf(Graph)
|
|
28
|
-
})
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
describe('traversal query params', () => {
|
|
32
|
-
it('include() sets include param', async () => {
|
|
33
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
34
|
-
await new Graph(mockClient).from('employees').get('1').include('descendants').execute()
|
|
35
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('include=descendants'))
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
it('include() supports ancestors', async () => {
|
|
39
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
40
|
-
await new Graph(mockClient).from('employees').get('4').include('ancestors').execute()
|
|
41
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('include=ancestors'))
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('include() supports both', async () => {
|
|
45
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
46
|
-
await new Graph(mockClient).from('employees').get('2').include('both').execute()
|
|
47
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('include=both'))
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
it('depth() sets depth param', async () => {
|
|
51
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
52
|
-
await new Graph(mockClient).from('employees').get('1').include('descendants').depth(3).execute()
|
|
53
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('depth=3'))
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
it('format() sets format param to tree', async () => {
|
|
57
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
58
|
-
await new Graph(mockClient).from('employees').get('1').format('tree').depth(3).execute()
|
|
59
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('format=tree'))
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
it('format() sets format param to graph', async () => {
|
|
63
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
64
|
-
await new Graph(mockClient).from('employees').format('graph').execute()
|
|
65
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('format=graph'))
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
it('types() sets relationship_type as repeated params', async () => {
|
|
69
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
70
|
-
await new Graph(mockClient).from('employees').format('graph').types(['manager', 'dotted_line']).execute()
|
|
71
|
-
const url = mockHttpClient.get.mock.calls[0][0]
|
|
72
|
-
expect(url).toContain('relationship_type=manager')
|
|
73
|
-
expect(url).toContain('relationship_type=dotted_line')
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
it('types() with single type', async () => {
|
|
77
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
78
|
-
await new Graph(mockClient).from('employees').format('graph').types(['manager']).execute()
|
|
79
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('relationship_type=manager'))
|
|
80
|
-
})
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
describe('chaining', () => {
|
|
84
|
-
it('combines include, depth, and get', async () => {
|
|
85
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
86
|
-
await new Graph(mockClient).from('employees').get('1').include('descendants').depth(3).execute()
|
|
87
|
-
const url = mockHttpClient.get.mock.calls[0][0]
|
|
88
|
-
expect(url).toContain('/1/')
|
|
89
|
-
expect(url).toContain('include=descendants')
|
|
90
|
-
expect(url).toContain('depth=3')
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
it('combines format, types, and depth', async () => {
|
|
94
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
95
|
-
await new Graph(mockClient).from('employees').format('graph').types(['manager']).depth(2).execute()
|
|
96
|
-
const url = mockHttpClient.get.mock.calls[0][0]
|
|
97
|
-
expect(url).toContain('format=graph')
|
|
98
|
-
expect(url).toContain('relationship_type=manager')
|
|
99
|
-
expect(url).toContain('depth=2')
|
|
100
|
-
})
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
describe('URL building', () => {
|
|
104
|
-
it('builds correct base URL for data queries', async () => {
|
|
105
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
106
|
-
await new Graph(mockClient).from('employees').execute()
|
|
107
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith('api/apps/test-app/datatables/employees/data/')
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
it('builds correct URL with record ID', async () => {
|
|
111
|
-
mockHttpClient.get.mockResolvedValue({})
|
|
112
|
-
await new Graph(mockClient).from('employees').get('1').execute()
|
|
113
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith('api/apps/test-app/datatables/employees/data/1/')
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
it('appends query string', async () => {
|
|
117
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
118
|
-
await new Graph(mockClient).from('employees').get('1').include('descendants').depth(2).execute()
|
|
119
|
-
const url = mockHttpClient.get.mock.calls[0][0]
|
|
120
|
-
expect(url).toContain('api/apps/test-app/datatables/employees/data/1/')
|
|
121
|
-
expect(url).toContain('?')
|
|
122
|
-
})
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
describe('edge CRUD', () => {
|
|
126
|
-
it('list() calls GET on edges route', async () => {
|
|
127
|
-
mockHttpClient.get.mockResolvedValue({ edges: [], total: 0 })
|
|
128
|
-
await new Graph(mockClient).from('employees').list().execute()
|
|
129
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith('api/apps/test-app/datatables/employees_edges/data/')
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
it('create() calls POST with array of edges', async () => {
|
|
133
|
-
const edges = [
|
|
134
|
-
{ from: 5, to: 2, type: 'manager' },
|
|
135
|
-
{ from: 5, to: 3, type: 'dotted_line', metadata: { project: 'AI' } }
|
|
136
|
-
]
|
|
137
|
-
mockHttpClient.post.mockResolvedValue({ status: 'success', data: edges, total: 2 })
|
|
138
|
-
await new Graph(mockClient).from('employees').create(edges).execute()
|
|
139
|
-
expect(mockHttpClient.post).toHaveBeenCalledWith(
|
|
140
|
-
'api/apps/test-app/datatables/employees_edges/data/',
|
|
141
|
-
edges
|
|
142
|
-
)
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
it('create() with metadata', async () => {
|
|
146
|
-
const edges = [{ from: 5, to: 3, type: 'dotted_line', metadata: { percentage: 30 } }]
|
|
147
|
-
mockHttpClient.post.mockResolvedValue({ status: 'success', data: edges, total: 1 })
|
|
148
|
-
await new Graph(mockClient).from('employees').create(edges).execute()
|
|
149
|
-
expect(mockHttpClient.post).toHaveBeenCalledWith(
|
|
150
|
-
'api/apps/test-app/datatables/employees_edges/data/',
|
|
151
|
-
edges
|
|
152
|
-
)
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
it('update() calls PATCH with edge ID in URL', async () => {
|
|
156
|
-
const edge = { from: 5, to: 3, type: 'dotted_line' }
|
|
157
|
-
mockHttpClient.patch.mockResolvedValue({ id: 9, ...edge })
|
|
158
|
-
await new Graph(mockClient).from('employees').update('9', edge).execute()
|
|
159
|
-
expect(mockHttpClient.patch).toHaveBeenCalledWith(
|
|
160
|
-
'api/apps/test-app/datatables/employees_edges/data/9/',
|
|
161
|
-
edge
|
|
162
|
-
)
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
it('delete() calls DELETE with edge_ids object', async () => {
|
|
166
|
-
mockHttpClient.delete.mockResolvedValue({ deleted: 2 })
|
|
167
|
-
await new Graph(mockClient).from('employees').delete([9, 10]).execute()
|
|
168
|
-
expect(mockHttpClient.delete).toHaveBeenCalledWith(
|
|
169
|
-
'api/apps/test-app/datatables/employees_edges/data/',
|
|
170
|
-
{ edge_ids: [9, 10] }
|
|
171
|
-
)
|
|
172
|
-
})
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
describe('execute()', () => {
|
|
176
|
-
it('defaults to GET for traversal queries', async () => {
|
|
177
|
-
mockHttpClient.get.mockResolvedValue([])
|
|
178
|
-
await new Graph(mockClient).from('employees').execute()
|
|
179
|
-
expect(mockHttpClient.get).toHaveBeenCalled()
|
|
180
|
-
expect(mockHttpClient.post).not.toHaveBeenCalled()
|
|
181
|
-
})
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
describe('response handling', () => {
|
|
185
|
-
it('returns descendants response with base record and related records', async () => {
|
|
186
|
-
const mockResponse = {
|
|
187
|
-
status: 'success',
|
|
188
|
-
message: 'Data retrieved successfully',
|
|
189
|
-
data: {
|
|
190
|
-
data: { id: 1, name: 'Alice Chen', title: 'CEO' },
|
|
191
|
-
reports: [
|
|
192
|
-
{ id: 2, name: 'Bob Smith', _depth: 1, _relationship_type: 'manager' },
|
|
193
|
-
{ id: 3, name: 'Carol White', _depth: 1, _relationship_type: 'manager' }
|
|
194
|
-
]
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
mockHttpClient.get.mockResolvedValue(mockResponse)
|
|
198
|
-
const result = await new Graph(mockClient).from('employees').get('1').include('descendants').execute()
|
|
199
|
-
expect(result).toEqual(mockResponse)
|
|
200
|
-
expect((result as any).data.data.id).toBe(1)
|
|
201
|
-
expect((result as any).data.reports).toHaveLength(2)
|
|
202
|
-
expect((result as any).data.reports[0]._depth).toBe(1)
|
|
203
|
-
expect((result as any).data.reports[0]._relationship_type).toBe('manager')
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
it('returns ancestors response with manager chain', async () => {
|
|
207
|
-
const mockResponse = {
|
|
208
|
-
status: 'success',
|
|
209
|
-
message: 'Data retrieved successfully',
|
|
210
|
-
data: {
|
|
211
|
-
data: { id: 4, name: 'David Lee' },
|
|
212
|
-
manager: [
|
|
213
|
-
{ id: 2, name: 'Bob Smith', _depth: 1, _relationship_type: 'manager' },
|
|
214
|
-
{ id: 1, name: 'Alice Chen', _depth: 2, _relationship_type: 'manager' }
|
|
215
|
-
]
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
mockHttpClient.get.mockResolvedValue(mockResponse)
|
|
219
|
-
const result = await new Graph(mockClient).from('employees').get('4').include('ancestors').execute()
|
|
220
|
-
expect((result as any).data.manager).toHaveLength(2)
|
|
221
|
-
expect((result as any).data.manager[1]._depth).toBe(2)
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
it('returns both directions response', async () => {
|
|
225
|
-
const mockResponse = {
|
|
226
|
-
status: 'success',
|
|
227
|
-
message: 'Data retrieved successfully',
|
|
228
|
-
data: {
|
|
229
|
-
data: { id: 2, name: 'Bob Smith' },
|
|
230
|
-
manager: [{ id: 1, name: 'Alice Chen', _depth: 1, _relationship_type: 'manager' }],
|
|
231
|
-
reports: [{ id: 4, name: 'David Lee', _depth: 1, _relationship_type: 'manager' }]
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
mockHttpClient.get.mockResolvedValue(mockResponse)
|
|
235
|
-
const result = await new Graph(mockClient).from('employees').get('2').include('both').depth(1).execute()
|
|
236
|
-
expect((result as any).data.manager).toHaveLength(1)
|
|
237
|
-
expect((result as any).data.reports).toHaveLength(1)
|
|
238
|
-
})
|
|
239
|
-
|
|
240
|
-
it('returns tree format with nested children', async () => {
|
|
241
|
-
const mockResponse = {
|
|
242
|
-
status: 'success',
|
|
243
|
-
message: 'Tree data retrieved successfully',
|
|
244
|
-
data: [{
|
|
245
|
-
id: 1,
|
|
246
|
-
name: 'Alice Chen',
|
|
247
|
-
_depth: 0,
|
|
248
|
-
children: [
|
|
249
|
-
{ id: 2, name: 'Bob Smith', _depth: 1, _relationship_type: 'manager', children: [] },
|
|
250
|
-
{ id: 3, name: 'Carol White', _depth: 1, _relationship_type: 'manager', children: [] }
|
|
251
|
-
]
|
|
252
|
-
}],
|
|
253
|
-
total: 3
|
|
254
|
-
}
|
|
255
|
-
mockHttpClient.get.mockResolvedValue(mockResponse)
|
|
256
|
-
const result = await new Graph(mockClient).from('employees').get('1').format('tree').depth(2).execute()
|
|
257
|
-
expect((result as any).data[0].children).toHaveLength(2)
|
|
258
|
-
expect((result as any).data[0].children[0]._relationship_type).toBe('manager')
|
|
259
|
-
expect((result as any).total).toBe(3)
|
|
260
|
-
})
|
|
261
|
-
|
|
262
|
-
it('returns graph format with nodes and edges', async () => {
|
|
263
|
-
const mockResponse = {
|
|
264
|
-
status: 'success',
|
|
265
|
-
message: 'Graph data retrieved successfully',
|
|
266
|
-
data: {
|
|
267
|
-
nodes: [
|
|
268
|
-
{ id: 1, name: 'Alice Chen', title: 'CEO' },
|
|
269
|
-
{ id: 2, name: 'Bob Smith', title: 'VP Engineering' }
|
|
270
|
-
],
|
|
271
|
-
edges: [
|
|
272
|
-
{ id: 9, from: 2, to: 1, type: 'manager', metadata: { primary: true } }
|
|
273
|
-
]
|
|
274
|
-
},
|
|
275
|
-
total: 2
|
|
276
|
-
}
|
|
277
|
-
mockHttpClient.get.mockResolvedValue(mockResponse)
|
|
278
|
-
const result = await new Graph(mockClient).from('employees').format('graph').types(['manager']).execute()
|
|
279
|
-
expect((result as any).data.nodes).toHaveLength(2)
|
|
280
|
-
expect((result as any).data.edges).toHaveLength(1)
|
|
281
|
-
expect((result as any).data.edges[0].type).toBe('manager')
|
|
282
|
-
})
|
|
283
|
-
|
|
284
|
-
it('returns edge list response', async () => {
|
|
285
|
-
const mockResponse = {
|
|
286
|
-
edges: [
|
|
287
|
-
{ id: 1, from: 5, to: 2, type: 'manager', metadata: {} },
|
|
288
|
-
{ id: 2, from: 5, to: 3, type: 'dotted_line', metadata: { percentage: 30 } }
|
|
289
|
-
],
|
|
290
|
-
total: 2
|
|
291
|
-
}
|
|
292
|
-
mockHttpClient.get.mockResolvedValue(mockResponse)
|
|
293
|
-
const result = await new Graph(mockClient).from('employees').list().execute()
|
|
294
|
-
expect((result as any).edges).toHaveLength(2)
|
|
295
|
-
expect((result as any).edges[0].type).toBe('manager')
|
|
296
|
-
expect((result as any).total).toBe(2)
|
|
297
|
-
})
|
|
298
|
-
|
|
299
|
-
it('returns created edges response matching TaruviResponse<EdgeResponse[]>', async () => {
|
|
300
|
-
const mockResponse: TaruviResponse<EdgeResponse[]> = {
|
|
301
|
-
status: 'success',
|
|
302
|
-
message: 'Edges created successfully',
|
|
303
|
-
data: [
|
|
304
|
-
{ id: 10, from: 5, to: 2, type: 'manager', metadata: {} }
|
|
305
|
-
],
|
|
306
|
-
total: 1
|
|
307
|
-
}
|
|
308
|
-
mockHttpClient.post.mockResolvedValue(mockResponse)
|
|
309
|
-
const result = await new Graph(mockClient).from('employees').create([{ from: 5, to: 2, type: 'manager' }]).execute() as TaruviResponse<EdgeResponse[]>
|
|
310
|
-
expect(result.data).toHaveLength(1)
|
|
311
|
-
expect(result.data[0].id).toBe(10)
|
|
312
|
-
})
|
|
313
|
-
|
|
314
|
-
it('returns updated edge matching EdgeResponse type', async () => {
|
|
315
|
-
const mockResponse: EdgeResponse = { id: 9, from: 5, to: 3, type: 'dotted_line', metadata: {} }
|
|
316
|
-
mockHttpClient.patch.mockResolvedValue(mockResponse)
|
|
317
|
-
const result = await new Graph(mockClient).from('employees').update('9', { from: 5, to: 3, type: 'dotted_line' }).execute() as EdgeResponse
|
|
318
|
-
expect(result.id).toBe(9)
|
|
319
|
-
expect(result.type).toBe('dotted_line')
|
|
320
|
-
})
|
|
321
|
-
|
|
322
|
-
it('returns delete count response', async () => {
|
|
323
|
-
const mockResponse = { deleted: 3 }
|
|
324
|
-
mockHttpClient.delete.mockResolvedValue(mockResponse)
|
|
325
|
-
const result = await new Graph(mockClient).from('employees').delete([1, 2, 3]).execute()
|
|
326
|
-
expect((result as any).deleted).toBe(3)
|
|
327
|
-
})
|
|
328
|
-
})
|
|
329
|
-
})
|