@taruvi/sdk 1.3.3 → 1.3.4-beta.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 (249) hide show
  1. package/.claude/settings.local.json +19 -0
  2. package/.kiro/settings/lsp.json +198 -0
  3. package/MODULE_NAMING_CHANGES.md +81 -0
  4. package/PARAMETER_NAMING_CHANGES.md +106 -0
  5. package/README.md +4 -4
  6. package/USAGE_EXAMPLE.md +86 -0
  7. package/package.json +10 -9
  8. package/{dist/client.js → src/client.ts} +59 -39
  9. package/src/index.ts +54 -0
  10. package/src/lib/analytics/AnalyticsClient.ts +24 -0
  11. package/src/lib/analytics/types.ts +8 -0
  12. package/src/lib/app/AppClient.ts +54 -0
  13. package/src/lib/app/types.ts +50 -0
  14. package/{dist/lib/auth/AuthClient.js → src/lib/auth/AuthClient.ts} +106 -70
  15. package/src/lib/auth/types.ts +111 -0
  16. package/src/lib/database/DatabaseClient.ts +148 -0
  17. package/src/lib/database/types.ts +46 -0
  18. package/src/lib/functions/FunctionsClient.ts +27 -0
  19. package/src/lib/functions/types.ts +25 -0
  20. package/src/lib/graphs/GraphClient.ts +106 -0
  21. package/src/lib/graphs/types.ts +33 -0
  22. package/src/lib/policy/PolicyClient.ts +79 -0
  23. package/src/lib/policy/types.ts +40 -0
  24. package/src/lib/secrets/SecretsClient.ts +75 -0
  25. package/src/lib/secrets/types.ts +59 -0
  26. package/src/lib/settings/SettingsClient.ts +14 -0
  27. package/src/lib/settings/types.ts +9 -0
  28. package/src/lib/storage/StorageClient.ts +123 -0
  29. package/src/lib/storage/types.ts +86 -0
  30. package/src/lib/users/UserClient.ts +55 -0
  31. package/src/lib/users/types.ts +104 -0
  32. package/src/lib-internal/errors/ErrorClient.ts +102 -0
  33. package/src/lib-internal/errors/index.ts +3 -0
  34. package/src/lib-internal/errors/types.ts +28 -0
  35. package/src/lib-internal/http/HttpClient.ts +129 -0
  36. package/{dist/lib-internal/http/types.js → src/lib-internal/http/types.ts} +3 -2
  37. package/src/lib-internal/routes/AnalyticsRoutes.ts +3 -0
  38. package/src/lib-internal/routes/AppRoutes.ts +9 -0
  39. package/src/lib-internal/routes/AuthRoutes.ts +0 -0
  40. package/src/lib-internal/routes/DatabaseRoutes.ts +9 -0
  41. package/src/lib-internal/routes/FunctionRoutes.ts +3 -0
  42. package/src/lib-internal/routes/GraphRoutes.ts +14 -0
  43. package/src/lib-internal/routes/PolicyRoutes.ts +4 -0
  44. package/src/lib-internal/routes/SecretsRoutes.ts +5 -0
  45. package/{dist/lib-internal/routes/SettingsRoutes.js → src/lib-internal/routes/SettingsRoutes.ts} +1 -2
  46. package/src/lib-internal/routes/StorageRoutes.ts +15 -0
  47. package/src/lib-internal/routes/UserRoutes.ts +11 -0
  48. package/src/lib-internal/routes/index.ts +0 -0
  49. package/{dist/lib-internal/token/TokenClient.js → src/lib-internal/token/TokenClient.ts} +144 -99
  50. package/src/lib-internal/token/types.ts +0 -0
  51. package/src/types.ts +90 -0
  52. package/{dist/utils/enums.js → src/utils/enums.ts} +10 -4
  53. package/src/utils/utils.ts +37 -0
  54. package/tests/fixtures/mockClient.ts +19 -0
  55. package/tests/mocks/db.json +1 -0
  56. package/tests/unit/analytics/AnalyticsClient.test.ts +84 -0
  57. package/tests/unit/app/AppClient.test.ts +114 -0
  58. package/tests/unit/auth/AuthClient.test.ts +131 -0
  59. package/tests/unit/client/Client.test.ts +70 -0
  60. package/tests/unit/database/DatabaseClient.test.ts +304 -0
  61. package/tests/unit/edge-cases/robustness.test.ts +259 -0
  62. package/tests/unit/errors/errors.test.ts +209 -0
  63. package/tests/unit/functions/FunctionsClient.test.ts +99 -0
  64. package/tests/unit/graphs/GraphClient.test.ts +329 -0
  65. package/tests/unit/policy/PolicyClient.test.ts +184 -0
  66. package/tests/unit/secrets/SecretsClient.test.ts +146 -0
  67. package/tests/unit/settings/SettingsClient.test.ts +50 -0
  68. package/tests/unit/storage/StorageClient.test.ts +251 -0
  69. package/tests/unit/users/UserClient.test.ts +150 -0
  70. package/tsconfig.json +43 -0
  71. package/vitest.config.ts +7 -0
  72. package/dist/client.d.ts +0 -29
  73. package/dist/client.d.ts.map +0 -1
  74. package/dist/client.js.map +0 -1
  75. package/dist/index.d.ts +0 -25
  76. package/dist/index.d.ts.map +0 -1
  77. package/dist/index.js +0 -15
  78. package/dist/index.js.map +0 -1
  79. package/dist/lib/Analytics/AnalyticsClient.d.ts +0 -9
  80. package/dist/lib/Analytics/AnalyticsClient.d.ts.map +0 -1
  81. package/dist/lib/Analytics/AnalyticsClient.js +0 -17
  82. package/dist/lib/Analytics/AnalyticsClient.js.map +0 -1
  83. package/dist/lib/Analytics/types.d.ts +0 -7
  84. package/dist/lib/Analytics/types.d.ts.map +0 -1
  85. package/dist/lib/Analytics/types.js +0 -2
  86. package/dist/lib/Analytics/types.js.map +0 -1
  87. package/dist/lib/App/AppClient.d.ts +0 -15
  88. package/dist/lib/App/AppClient.d.ts.map +0 -1
  89. package/dist/lib/App/AppClient.js +0 -41
  90. package/dist/lib/App/AppClient.js.map +0 -1
  91. package/dist/lib/App/types.d.ts +0 -36
  92. package/dist/lib/App/types.d.ts.map +0 -1
  93. package/dist/lib/App/types.js +0 -2
  94. package/dist/lib/App/types.js.map +0 -1
  95. package/dist/lib/Database/DatabaseClient.d.ts +0 -28
  96. package/dist/lib/Database/DatabaseClient.d.ts.map +0 -1
  97. package/dist/lib/Database/DatabaseClient.js +0 -116
  98. package/dist/lib/Database/DatabaseClient.js.map +0 -1
  99. package/dist/lib/Database/types.d.ts +0 -24
  100. package/dist/lib/Database/types.d.ts.map +0 -1
  101. package/dist/lib/Database/types.js +0 -2
  102. package/dist/lib/Database/types.js.map +0 -1
  103. package/dist/lib/Function/FunctionsClient.d.ts +0 -9
  104. package/dist/lib/Function/FunctionsClient.d.ts.map +0 -1
  105. package/dist/lib/Function/FunctionsClient.js +0 -20
  106. package/dist/lib/Function/FunctionsClient.js.map +0 -1
  107. package/dist/lib/Function/types.d.ts +0 -16
  108. package/dist/lib/Function/types.d.ts.map +0 -1
  109. package/dist/lib/Function/types.js +0 -2
  110. package/dist/lib/Function/types.js.map +0 -1
  111. package/dist/lib/Graphs/GraphClient.d.ts +0 -26
  112. package/dist/lib/Graphs/GraphClient.d.ts.map +0 -1
  113. package/dist/lib/Graphs/GraphClient.js +0 -86
  114. package/dist/lib/Graphs/GraphClient.js.map +0 -1
  115. package/dist/lib/Graphs/types.d.ts +0 -23
  116. package/dist/lib/Graphs/types.d.ts.map +0 -1
  117. package/dist/lib/Graphs/types.js +0 -2
  118. package/dist/lib/Graphs/types.js.map +0 -1
  119. package/dist/lib/Policy/PolicyClient.d.ts +0 -9
  120. package/dist/lib/Policy/PolicyClient.d.ts.map +0 -1
  121. package/dist/lib/Policy/PolicyClient.js +0 -24
  122. package/dist/lib/Policy/PolicyClient.js.map +0 -1
  123. package/dist/lib/Policy/types.d.ts +0 -22
  124. package/dist/lib/Policy/types.d.ts.map +0 -1
  125. package/dist/lib/Policy/types.js +0 -2
  126. package/dist/lib/Policy/types.js.map +0 -1
  127. package/dist/lib/Secrets/SecretsClient.d.ts +0 -14
  128. package/dist/lib/Secrets/SecretsClient.d.ts.map +0 -1
  129. package/dist/lib/Secrets/SecretsClient.js +0 -32
  130. package/dist/lib/Secrets/SecretsClient.js.map +0 -1
  131. package/dist/lib/Secrets/types.d.ts +0 -13
  132. package/dist/lib/Secrets/types.d.ts.map +0 -1
  133. package/dist/lib/Secrets/types.js +0 -2
  134. package/dist/lib/Secrets/types.js.map +0 -1
  135. package/dist/lib/Settings/SettingsClient.d.ts +0 -7
  136. package/dist/lib/Settings/SettingsClient.d.ts.map +0 -1
  137. package/dist/lib/Settings/SettingsClient.js +0 -11
  138. package/dist/lib/Settings/SettingsClient.js.map +0 -1
  139. package/dist/lib/Settings/types.d.ts +0 -4
  140. package/dist/lib/Settings/types.d.ts.map +0 -1
  141. package/dist/lib/Settings/types.js +0 -2
  142. package/dist/lib/Settings/types.js.map +0 -1
  143. package/dist/lib/Storage/StorageClient.d.ts +0 -26
  144. package/dist/lib/Storage/StorageClient.d.ts.map +0 -1
  145. package/dist/lib/Storage/StorageClient.js +0 -78
  146. package/dist/lib/Storage/StorageClient.js.map +0 -1
  147. package/dist/lib/Storage/types.d.ts +0 -35
  148. package/dist/lib/Storage/types.d.ts.map +0 -1
  149. package/dist/lib/Storage/types.js +0 -2
  150. package/dist/lib/Storage/types.js.map +0 -1
  151. package/dist/lib/auth/AuthClient.d.ts +0 -66
  152. package/dist/lib/auth/AuthClient.d.ts.map +0 -1
  153. package/dist/lib/auth/AuthClient.js.map +0 -1
  154. package/dist/lib/auth/types.d.ts +0 -9
  155. package/dist/lib/auth/types.d.ts.map +0 -1
  156. package/dist/lib/auth/types.js +0 -2
  157. package/dist/lib/auth/types.js.map +0 -1
  158. package/dist/lib/user/UserClient.d.ts +0 -16
  159. package/dist/lib/user/UserClient.d.ts.map +0 -1
  160. package/dist/lib/user/UserClient.js +0 -41
  161. package/dist/lib/user/UserClient.js.map +0 -1
  162. package/dist/lib/user/types.d.ts +0 -100
  163. package/dist/lib/user/types.d.ts.map +0 -1
  164. package/dist/lib/user/types.js +0 -2
  165. package/dist/lib/user/types.js.map +0 -1
  166. package/dist/lib-internal/errors/ErrorClient.d.ts +0 -4
  167. package/dist/lib-internal/errors/ErrorClient.d.ts.map +0 -1
  168. package/dist/lib-internal/errors/ErrorClient.js +0 -6
  169. package/dist/lib-internal/errors/ErrorClient.js.map +0 -1
  170. package/dist/lib-internal/errors/index.d.ts +0 -8
  171. package/dist/lib-internal/errors/index.d.ts.map +0 -1
  172. package/dist/lib-internal/errors/index.js +0 -23
  173. package/dist/lib-internal/errors/index.js.map +0 -1
  174. package/dist/lib-internal/errors/types.d.ts +0 -83
  175. package/dist/lib-internal/errors/types.d.ts.map +0 -1
  176. package/dist/lib-internal/errors/types.js +0 -65
  177. package/dist/lib-internal/errors/types.js.map +0 -1
  178. package/dist/lib-internal/http/HttpClient.d.ts +0 -22
  179. package/dist/lib-internal/http/HttpClient.d.ts.map +0 -1
  180. package/dist/lib-internal/http/HttpClient.js +0 -65
  181. package/dist/lib-internal/http/HttpClient.js.map +0 -1
  182. package/dist/lib-internal/http/types.d.ts +0 -12
  183. package/dist/lib-internal/http/types.d.ts.map +0 -1
  184. package/dist/lib-internal/http/types.js.map +0 -1
  185. package/dist/lib-internal/routes/AnalyticsRoutes.d.ts +0 -4
  186. package/dist/lib-internal/routes/AnalyticsRoutes.d.ts.map +0 -1
  187. package/dist/lib-internal/routes/AnalyticsRoutes.js +0 -4
  188. package/dist/lib-internal/routes/AnalyticsRoutes.js.map +0 -1
  189. package/dist/lib-internal/routes/AppRoutes.d.ts +0 -10
  190. package/dist/lib-internal/routes/AppRoutes.d.ts.map +0 -1
  191. package/dist/lib-internal/routes/AppRoutes.js +0 -6
  192. package/dist/lib-internal/routes/AppRoutes.js.map +0 -1
  193. package/dist/lib-internal/routes/AuthRoutes.d.ts +0 -2
  194. package/dist/lib-internal/routes/AuthRoutes.d.ts.map +0 -1
  195. package/dist/lib-internal/routes/AuthRoutes.js +0 -2
  196. package/dist/lib-internal/routes/AuthRoutes.js.map +0 -1
  197. package/dist/lib-internal/routes/DatabaseRoutes.d.ts +0 -10
  198. package/dist/lib-internal/routes/DatabaseRoutes.d.ts.map +0 -1
  199. package/dist/lib-internal/routes/DatabaseRoutes.js +0 -6
  200. package/dist/lib-internal/routes/DatabaseRoutes.js.map +0 -1
  201. package/dist/lib-internal/routes/FunctionRoutes.d.ts +0 -4
  202. package/dist/lib-internal/routes/FunctionRoutes.d.ts.map +0 -1
  203. package/dist/lib-internal/routes/FunctionRoutes.js +0 -4
  204. package/dist/lib-internal/routes/FunctionRoutes.js.map +0 -1
  205. package/dist/lib-internal/routes/GraphRoutes.d.ts +0 -14
  206. package/dist/lib-internal/routes/GraphRoutes.d.ts.map +0 -1
  207. package/dist/lib-internal/routes/GraphRoutes.js +0 -11
  208. package/dist/lib-internal/routes/GraphRoutes.js.map +0 -1
  209. package/dist/lib-internal/routes/PolicyRoutes.d.ts +0 -5
  210. package/dist/lib-internal/routes/PolicyRoutes.d.ts.map +0 -1
  211. package/dist/lib-internal/routes/PolicyRoutes.js +0 -5
  212. package/dist/lib-internal/routes/PolicyRoutes.js.map +0 -1
  213. package/dist/lib-internal/routes/SecretsRoutes.d.ts +0 -6
  214. package/dist/lib-internal/routes/SecretsRoutes.d.ts.map +0 -1
  215. package/dist/lib-internal/routes/SecretsRoutes.js +0 -6
  216. package/dist/lib-internal/routes/SecretsRoutes.js.map +0 -1
  217. package/dist/lib-internal/routes/SettingsRoutes.d.ts +0 -4
  218. package/dist/lib-internal/routes/SettingsRoutes.d.ts.map +0 -1
  219. package/dist/lib-internal/routes/SettingsRoutes.js.map +0 -1
  220. package/dist/lib-internal/routes/StorageRoutes.d.ts +0 -11
  221. package/dist/lib-internal/routes/StorageRoutes.d.ts.map +0 -1
  222. package/dist/lib-internal/routes/StorageRoutes.js +0 -8
  223. package/dist/lib-internal/routes/StorageRoutes.js.map +0 -1
  224. package/dist/lib-internal/routes/UserRoutes.d.ts +0 -11
  225. package/dist/lib-internal/routes/UserRoutes.d.ts.map +0 -1
  226. package/dist/lib-internal/routes/UserRoutes.js +0 -11
  227. package/dist/lib-internal/routes/UserRoutes.js.map +0 -1
  228. package/dist/lib-internal/routes/index.d.ts +0 -2
  229. package/dist/lib-internal/routes/index.d.ts.map +0 -1
  230. package/dist/lib-internal/routes/index.js +0 -2
  231. package/dist/lib-internal/routes/index.js.map +0 -1
  232. package/dist/lib-internal/token/TokenClient.d.ts +0 -71
  233. package/dist/lib-internal/token/TokenClient.d.ts.map +0 -1
  234. package/dist/lib-internal/token/TokenClient.js.map +0 -1
  235. package/dist/lib-internal/token/types.d.ts +0 -2
  236. package/dist/lib-internal/token/types.d.ts.map +0 -1
  237. package/dist/lib-internal/token/types.js +0 -2
  238. package/dist/lib-internal/token/types.js.map +0 -1
  239. package/dist/types.d.ts +0 -49
  240. package/dist/types.d.ts.map +0 -1
  241. package/dist/types.js +0 -2
  242. package/dist/types.js.map +0 -1
  243. package/dist/utils/enums.d.ts +0 -20
  244. package/dist/utils/enums.d.ts.map +0 -1
  245. package/dist/utils/enums.js.map +0 -1
  246. package/dist/utils/utils.d.ts +0 -5
  247. package/dist/utils/utils.d.ts.map +0 -1
  248. package/dist/utils/utils.js +0 -32
  249. package/dist/utils/utils.js.map +0 -1
@@ -0,0 +1,184 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+ import { Policy } from '../../../src/lib/policy/PolicyClient.js'
3
+ import { Client } from '../../../src/client.js'
4
+
5
+ const mockHttpClient = {
6
+ post: vi.fn()
7
+ }
8
+
9
+ const mockClient = {
10
+ getConfig: () => ({ apiKey: 'test-key', appSlug: 'test-app', apiUrl: 'https://api.test.com' }),
11
+ httpClient: mockHttpClient
12
+ } as unknown as Client
13
+
14
+ describe('Policy', () => {
15
+ beforeEach(() => {
16
+ vi.clearAllMocks()
17
+ })
18
+
19
+ describe('checkResource()', () => {
20
+ it('checks permissions for single resource', async () => {
21
+ const response = { results: [{ actions: { read: 'EFFECT_ALLOW' } }] }
22
+ mockHttpClient.post.mockResolvedValue(response)
23
+
24
+ const policy = new Policy(mockClient)
25
+ const result = await policy.checkResource([
26
+ {
27
+ entityType: 'crm',
28
+ tableName: 'accounts',
29
+ recordId: 'record-123',
30
+ attributes: { owner_id: 'user-456' },
31
+ actions: ['read']
32
+ }
33
+ ])
34
+
35
+ expect(mockHttpClient.post).toHaveBeenCalledWith(
36
+ 'api/apps/test-app/check/resources',
37
+ {
38
+ resources: [{
39
+ resource: {
40
+ kind: 'crm:accounts',
41
+ id: 'record-123',
42
+ attr: { owner_id: 'user-456' }
43
+ },
44
+ actions: ['read']
45
+ }]
46
+ }
47
+ )
48
+ expect(result).toEqual(response)
49
+ })
50
+
51
+ it('checks permissions for multiple resources', async () => {
52
+ const response = {
53
+ results: [
54
+ { actions: { read: 'EFFECT_ALLOW', update: 'EFFECT_ALLOW' } },
55
+ { actions: { delete: 'EFFECT_DENY' } }
56
+ ]
57
+ }
58
+ mockHttpClient.post.mockResolvedValue(response)
59
+
60
+ const policy = new Policy(mockClient)
61
+ await policy.checkResource([
62
+ {
63
+ entityType: 'crm',
64
+ tableName: 'accounts',
65
+ recordId: 'acc-1',
66
+ attributes: {},
67
+ actions: ['read', 'update']
68
+ },
69
+ {
70
+ entityType: 'docs',
71
+ tableName: 'documents',
72
+ recordId: 'doc-1',
73
+ attributes: {},
74
+ actions: ['delete']
75
+ }
76
+ ])
77
+
78
+ expect(mockHttpClient.post).toHaveBeenCalledWith(
79
+ 'api/apps/test-app/check/resources',
80
+ {
81
+ resources: [
82
+ {
83
+ resource: { kind: 'crm:accounts', id: 'acc-1', attr: {} },
84
+ actions: ['read', 'update']
85
+ },
86
+ {
87
+ resource: { kind: 'docs:documents', id: 'doc-1', attr: {} },
88
+ actions: ['delete']
89
+ }
90
+ ]
91
+ }
92
+ )
93
+ })
94
+
95
+ it('handles empty attributes', async () => {
96
+ mockHttpClient.post.mockResolvedValue({ results: [] })
97
+
98
+ const policy = new Policy(mockClient)
99
+ await policy.checkResource([
100
+ {
101
+ entityType: 'crm',
102
+ tableName: 'contacts',
103
+ recordId: 'contact-1',
104
+ attributes: {},
105
+ actions: ['read']
106
+ }
107
+ ])
108
+
109
+ const body = mockHttpClient.post.mock.calls[0][1]
110
+ expect(body.resources[0].resource.attr).toEqual({})
111
+ })
112
+ })
113
+
114
+ describe('getAllowedActions()', () => {
115
+ it('returns allowed actions for resource', async () => {
116
+ const response = {
117
+ results: [{
118
+ actions: {
119
+ read: 'EFFECT_ALLOW',
120
+ write: 'EFFECT_ALLOW',
121
+ delete: 'EFFECT_DENY'
122
+ }
123
+ }]
124
+ }
125
+ mockHttpClient.post.mockResolvedValue(response)
126
+
127
+ const policy = new Policy(mockClient)
128
+ const result = await policy.getAllowedActions(
129
+ { kind: 'datatable:users', id: '123', attr: {} }
130
+ )
131
+
132
+ expect(result).toContain('read')
133
+ expect(result).toContain('write')
134
+ expect(result).not.toContain('delete')
135
+ })
136
+
137
+ it('uses default actions when not specified', async () => {
138
+ mockHttpClient.post.mockResolvedValue({ results: [{ actions: {} }] })
139
+
140
+ const policy = new Policy(mockClient)
141
+ await policy.getAllowedActions({ kind: 'test:table', id: '1', attr: {} })
142
+
143
+ const body = mockHttpClient.post.mock.calls[0][1]
144
+ expect(body.resources[0].actions).toEqual(['read', 'write', 'create', 'update', 'delete'])
145
+ })
146
+
147
+ it('uses custom actions when specified', async () => {
148
+ mockHttpClient.post.mockResolvedValue({ results: [{ actions: {} }] })
149
+
150
+ const policy = new Policy(mockClient)
151
+ await policy.getAllowedActions(
152
+ { kind: 'test:table', id: '1', attr: {} },
153
+ { actions: ['read', 'update'] }
154
+ )
155
+
156
+ const body = mockHttpClient.post.mock.calls[0][1]
157
+ expect(body.resources[0].actions).toEqual(['read', 'update'])
158
+ })
159
+
160
+ it('returns empty array when no results', async () => {
161
+ mockHttpClient.post.mockResolvedValue({ results: [] })
162
+
163
+ const policy = new Policy(mockClient)
164
+ const result = await policy.getAllowedActions(
165
+ { kind: 'test:table', id: '1', attr: {} }
166
+ )
167
+
168
+ expect(result).toEqual([])
169
+ })
170
+
171
+ it('includes principal when provided', async () => {
172
+ mockHttpClient.post.mockResolvedValue({ results: [{ actions: {} }] })
173
+
174
+ const policy = new Policy(mockClient)
175
+ await policy.getAllowedActions(
176
+ { kind: 'test:table', id: '1', attr: {} },
177
+ { principal: { id: 'user-1', roles: ['admin'], attr: {} } }
178
+ )
179
+
180
+ const body = mockHttpClient.post.mock.calls[0][1]
181
+ expect(body.principal).toEqual({ id: 'user-1', roles: ['admin'], attr: {} })
182
+ })
183
+ })
184
+ })
@@ -0,0 +1,146 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+ import { Secrets } from '../../../src/lib/secrets/SecretsClient.js'
3
+ import { Client } from '../../../src/client.js'
4
+ import type { SecretResponse, SecretsListResponse } from '../../../src/lib/secrets/types.js'
5
+
6
+ const mockHttpClient = {
7
+ get: vi.fn(),
8
+ put: vi.fn()
9
+ }
10
+
11
+ const mockClient = {
12
+ getConfig: () => ({ apiKey: 'test-key', appSlug: 'test-app', apiUrl: 'https://api.test.com' }),
13
+ httpClient: mockHttpClient
14
+ } as unknown as Client
15
+
16
+ describe('Secrets', () => {
17
+ beforeEach(() => {
18
+ vi.clearAllMocks()
19
+ })
20
+
21
+ describe('get()', () => {
22
+ it('gets secret by key', async () => {
23
+ const secretData = { key: 'MY_SECRET', value: 'secret-value' }
24
+ mockHttpClient.get.mockResolvedValue(secretData)
25
+
26
+ const secrets = new Secrets(mockClient)
27
+ const result = await secrets.get('MY_SECRET').execute()
28
+
29
+ expect(mockHttpClient.get).toHaveBeenCalledWith('api/secrets/MY_SECRET/')
30
+ expect(result).toEqual(secretData)
31
+ })
32
+
33
+ it('throws error when key is empty', () => {
34
+ const secrets = new Secrets(mockClient)
35
+ expect(() => secrets.get('')).toThrow('Secret key is required')
36
+ })
37
+
38
+ it('throws error when key is not a string', () => {
39
+ const secrets = new Secrets(mockClient)
40
+ expect(() => secrets.get(123 as any)).toThrow('Secret key is required')
41
+ })
42
+
43
+ it('includes app context in query params', async () => {
44
+ mockHttpClient.get.mockResolvedValue({})
45
+
46
+ const secrets = new Secrets(mockClient)
47
+ await secrets.get('MY_SECRET', { app: 'my-app' }).execute()
48
+
49
+ expect(mockHttpClient.get).toHaveBeenCalledWith(
50
+ expect.stringContaining('app=my-app')
51
+ )
52
+ })
53
+
54
+ it('includes tags in query params', async () => {
55
+ mockHttpClient.get.mockResolvedValue({})
56
+
57
+ const secrets = new Secrets(mockClient)
58
+ await secrets.get('MY_SECRET', { tags: ['prod', 'api'] }).execute()
59
+
60
+ expect(mockHttpClient.get).toHaveBeenCalledWith(
61
+ expect.stringContaining('tags=prod%2Capi')
62
+ )
63
+ })
64
+ })
65
+
66
+ describe('list()', () => {
67
+ it('lists secrets by keys', async () => {
68
+ const response = { MY_KEY: 'value1', OTHER_KEY: 'value2' }
69
+ mockHttpClient.get.mockResolvedValue(response)
70
+
71
+ const secrets = new Secrets(mockClient)
72
+ const result = await secrets.list(['MY_KEY', 'OTHER_KEY'])
73
+
74
+ expect(mockHttpClient.get).toHaveBeenCalledWith(
75
+ expect.stringContaining('keys=MY_KEY%2COTHER_KEY')
76
+ )
77
+ expect(result).toEqual(response)
78
+ })
79
+
80
+ it('includes app context', async () => {
81
+ mockHttpClient.get.mockResolvedValue({})
82
+
83
+ const secrets = new Secrets(mockClient)
84
+ await secrets.list(['KEY1'], { app: 'my-app' })
85
+
86
+ expect(mockHttpClient.get).toHaveBeenCalledWith(
87
+ expect.stringContaining('app=my-app')
88
+ )
89
+ })
90
+
91
+ it('includes metadata flag', async () => {
92
+ mockHttpClient.get.mockResolvedValue({})
93
+
94
+ const secrets = new Secrets(mockClient)
95
+ await secrets.list(['KEY1'], { includeMetadata: true })
96
+
97
+ expect(mockHttpClient.get).toHaveBeenCalledWith(
98
+ expect.stringContaining('include_metadata=true')
99
+ )
100
+ })
101
+ })
102
+
103
+ describe('execute()', () => {
104
+ it('executes GET request by default', async () => {
105
+ mockHttpClient.get.mockResolvedValue({ key: 'value' })
106
+
107
+ const secrets = new Secrets(mockClient)
108
+ await secrets.execute()
109
+
110
+ expect(mockHttpClient.get).toHaveBeenCalled()
111
+ })
112
+ })
113
+
114
+ describe('response handling', () => {
115
+ it('returns single secret matching SecretResponse type', async () => {
116
+ const mockResponse: SecretResponse = {
117
+ status: 'success',
118
+ message: 'Secret retrieved successfully',
119
+ data: { key: 'MY_SECRET', value: 'secret-value', tags: ['prod'], secret_type: 'string' }
120
+ }
121
+ mockHttpClient.get.mockResolvedValue(mockResponse)
122
+ const result = await new Secrets(mockClient).get('MY_SECRET').execute() as SecretResponse
123
+ expect(result.status).toBe('success')
124
+ expect(result.data.key).toBe('MY_SECRET')
125
+ expect(result.data.value).toBe('secret-value')
126
+ expect(result.data.tags).toContain('prod')
127
+ })
128
+
129
+ it('returns secrets list matching SecretsListResponse type', async () => {
130
+ const mockResponse: SecretsListResponse = {
131
+ status: 'success',
132
+ message: 'Retrieved 2 secret(s)',
133
+ data: [
134
+ { key: 'KEY1', value: 'val1' },
135
+ { key: 'KEY2', value: 'val2' }
136
+ ],
137
+ total: 2
138
+ }
139
+ mockHttpClient.get.mockResolvedValue(mockResponse)
140
+ const result = await new Secrets(mockClient).list(['KEY1', 'KEY2']) as SecretsListResponse
141
+ expect(result.data).toHaveLength(2)
142
+ expect(result.data[0].key).toBe('KEY1')
143
+ expect(result.total).toBe(2)
144
+ })
145
+ })
146
+ })
@@ -0,0 +1,50 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+ import { Settings } from '../../../src/lib/settings/SettingsClient.js'
3
+ import { Client } from '../../../src/client.js'
4
+
5
+ const mockHttpClient = {
6
+ get: vi.fn()
7
+ }
8
+
9
+ const mockClient = {
10
+ getConfig: () => ({ apiKey: 'test-key', appSlug: 'test-app', apiUrl: 'https://api.test.com' }),
11
+ httpClient: mockHttpClient
12
+ } as unknown as Client
13
+
14
+ describe('Settings', () => {
15
+ beforeEach(() => {
16
+ vi.clearAllMocks()
17
+ })
18
+
19
+ describe('get()', () => {
20
+ it('fetches site settings', async () => {
21
+ const settingsData = {
22
+ site_name: 'My Site',
23
+ frontend_url: 'https://mysite.com',
24
+ features: { analytics: true }
25
+ }
26
+ mockHttpClient.get.mockResolvedValue(settingsData)
27
+
28
+ const settings = new Settings(mockClient)
29
+ const result = await settings.get()
30
+
31
+ expect(mockHttpClient.get).toHaveBeenCalledWith('api/settings/metadata/')
32
+ expect(result).toEqual(settingsData)
33
+ })
34
+
35
+ it('supports typed response', async () => {
36
+ interface SiteSettings {
37
+ site_name: string
38
+ frontend_url: string
39
+ }
40
+ const settingsData = { site_name: 'Test', frontend_url: 'https://test.com' }
41
+ mockHttpClient.get.mockResolvedValue(settingsData)
42
+
43
+ const settings = new Settings(mockClient)
44
+ const result = await settings.get<SiteSettings>()
45
+
46
+ expect(result.site_name).toBe('Test')
47
+ expect(result.frontend_url).toBe('https://test.com')
48
+ })
49
+ })
50
+ })
@@ -0,0 +1,251 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+ import { Storage } from '../../../src/lib/storage/StorageClient.js'
3
+ import { Client } from '../../../src/client.js'
4
+ import type { StorageResponse, StorageListResponse, StorageDeleteBatchResponse } from '../../../src/lib/storage/types.js'
5
+
6
+ const mockHttpClient = {
7
+ get: vi.fn(),
8
+ post: vi.fn(),
9
+ put: vi.fn(),
10
+ delete: vi.fn()
11
+ }
12
+
13
+ const mockClient = {
14
+ getConfig: () => ({ apiKey: 'test-key', appSlug: 'test-app', apiUrl: 'https://api.test.com' }),
15
+ httpClient: mockHttpClient
16
+ } as unknown as Client
17
+
18
+ describe('Storage', () => {
19
+ beforeEach(() => {
20
+ vi.clearAllMocks()
21
+ })
22
+
23
+ describe('from()', () => {
24
+ it('returns new Storage instance with bucket name', () => {
25
+ const storage = new Storage(mockClient)
26
+ const result = storage.from('documents')
27
+ expect(result).toBeInstanceOf(Storage)
28
+ })
29
+ })
30
+
31
+ describe('filter()', () => {
32
+ it('applies search filter', async () => {
33
+ mockHttpClient.get.mockResolvedValue([])
34
+ await new Storage(mockClient).from('documents').filter({ search: 'invoice' }).execute()
35
+ expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('search=invoice'))
36
+ })
37
+
38
+ it('applies size filters', async () => {
39
+ mockHttpClient.get.mockResolvedValue([])
40
+ await new Storage(mockClient).from('documents').filter({ size__gte: 1024, size__lte: 10485760 }).execute()
41
+ const url = mockHttpClient.get.mock.calls[0][0]
42
+ expect(url).toContain('size__gte=1024')
43
+ expect(url).toContain('size__lte=10485760')
44
+ })
45
+
46
+ it('applies date filters', async () => {
47
+ mockHttpClient.get.mockResolvedValue([])
48
+ await new Storage(mockClient).from('documents').filter({ created_at__gte: '2024-01-01' }).execute()
49
+ expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('created_at__gte=2024-01-01'))
50
+ })
51
+
52
+ it('applies mimetype filter', async () => {
53
+ mockHttpClient.get.mockResolvedValue([])
54
+ await new Storage(mockClient).from('documents').filter({ mimetype: 'application/pdf' }).execute()
55
+ expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('mimetype=application%2Fpdf'))
56
+ })
57
+
58
+ it('applies visibility filter', async () => {
59
+ mockHttpClient.get.mockResolvedValue([])
60
+ await new Storage(mockClient).from('documents').filter({ visibility: 'public' }).execute()
61
+ expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('visibility=public'))
62
+ })
63
+
64
+ it('applies pagination filters', async () => {
65
+ mockHttpClient.get.mockResolvedValue([])
66
+ await new Storage(mockClient).from('documents').filter({ page: 1, page_size: 20 }).execute()
67
+ const url = mockHttpClient.get.mock.calls[0][0]
68
+ expect(url).toContain('page=1')
69
+ expect(url).toContain('page_size=20')
70
+ })
71
+
72
+ it('applies ordering filter', async () => {
73
+ mockHttpClient.get.mockResolvedValue([])
74
+ await new Storage(mockClient).from('documents').filter({ ordering: '-created_at' }).execute()
75
+ expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('ordering=-created_at'))
76
+ })
77
+ })
78
+
79
+ describe('download()', () => {
80
+ it('calls httpClient.get with encoded path', async () => {
81
+ mockHttpClient.get.mockResolvedValue(new Blob())
82
+ await new Storage(mockClient).from('documents').download('path/to/file.pdf').execute()
83
+ expect(mockHttpClient.get).toHaveBeenCalledWith(expect.stringContaining('path%2Fto%2Ffile.pdf'))
84
+ })
85
+ })
86
+
87
+ describe('upload()', () => {
88
+ it('calls httpClient.post with FormData', async () => {
89
+ const mockFile = new File(['content'], 'test.pdf', { type: 'application/pdf' })
90
+ mockHttpClient.post.mockResolvedValue({ uploaded_count: 1 })
91
+
92
+ await new Storage(mockClient).from('documents').upload({
93
+ files: [mockFile],
94
+ metadatas: [{ name: 'test' }],
95
+ paths: ['test.pdf']
96
+ }).execute()
97
+
98
+ expect(mockHttpClient.post).toHaveBeenCalledWith(
99
+ expect.stringContaining('/batch-upload/'),
100
+ expect.any(FormData)
101
+ )
102
+ })
103
+ })
104
+
105
+ describe('delete()', () => {
106
+ it('calls httpClient.post with paths array', async () => {
107
+ mockHttpClient.post.mockResolvedValue({ deleted_count: 2 })
108
+ await new Storage(mockClient).from('documents').delete(['file1.pdf', 'file2.pdf']).execute()
109
+ expect(mockHttpClient.post).toHaveBeenCalledWith(
110
+ expect.stringContaining('/batch-delete/'),
111
+ { paths: ['file1.pdf', 'file2.pdf'] }
112
+ )
113
+ })
114
+ })
115
+
116
+ describe('update()', () => {
117
+ it('calls httpClient.put with path and body', async () => {
118
+ const body = { visibility: 'public', metadata: { category: 'reports' } }
119
+ mockHttpClient.put.mockResolvedValue({ id: 1 })
120
+ await new Storage(mockClient).from('documents').update('file.pdf', body).execute()
121
+ expect(mockHttpClient.put).toHaveBeenCalledWith(
122
+ expect.stringContaining('file.pdf'),
123
+ body
124
+ )
125
+ })
126
+ })
127
+
128
+ describe('execute()', () => {
129
+ it('throws error without bucket name', async () => {
130
+ await expect(new Storage(mockClient).execute()).rejects.toThrow('Bucket is required')
131
+ })
132
+
133
+ it('calls httpClient.get for list query', async () => {
134
+ mockHttpClient.get.mockResolvedValue([])
135
+ await new Storage(mockClient).from('documents').execute()
136
+ expect(mockHttpClient.get).toHaveBeenCalled()
137
+ })
138
+ })
139
+
140
+ describe('URL building', () => {
141
+ it('builds correct base URL with app slug and bucket', async () => {
142
+ mockHttpClient.get.mockResolvedValue([])
143
+ await new Storage(mockClient).from('documents').execute()
144
+ expect(mockHttpClient.get).toHaveBeenCalledWith('api/apps/test-app/storage/buckets/documents/objects/')
145
+ })
146
+
147
+ it('builds correct URL for download with encoded path', async () => {
148
+ mockHttpClient.get.mockResolvedValue(new Blob())
149
+ await new Storage(mockClient).from('documents').download('folder/file name.pdf').execute()
150
+ expect(mockHttpClient.get).toHaveBeenCalledWith(
151
+ 'api/apps/test-app/storage/buckets/documents/objects/folder%2Ffile%20name.pdf/'
152
+ )
153
+ })
154
+
155
+ it('builds correct URL for batch upload', async () => {
156
+ const mockFile = new File([''], 'test.pdf')
157
+ mockHttpClient.post.mockResolvedValue({})
158
+ await new Storage(mockClient).from('documents').upload({
159
+ files: [mockFile],
160
+ metadatas: [{}],
161
+ paths: ['test.pdf']
162
+ }).execute()
163
+ expect(mockHttpClient.post).toHaveBeenCalledWith(
164
+ 'api/apps/test-app/storage/buckets/documents/objects/batch-upload/',
165
+ expect.any(FormData)
166
+ )
167
+ })
168
+
169
+ it('builds correct URL for batch delete', async () => {
170
+ mockHttpClient.post.mockResolvedValue({})
171
+ await new Storage(mockClient).from('documents').delete(['file.pdf']).execute()
172
+ expect(mockHttpClient.post).toHaveBeenCalledWith(
173
+ 'api/apps/test-app/storage/buckets/documents/objects/batch-delete/',
174
+ { paths: ['file.pdf'] }
175
+ )
176
+ })
177
+
178
+ it('appends query string with filters', async () => {
179
+ mockHttpClient.get.mockResolvedValue([])
180
+ await new Storage(mockClient).from('documents').filter({
181
+ search: 'test',
182
+ page: 1,
183
+ ordering: '-created_at'
184
+ }).execute()
185
+ const url = mockHttpClient.get.mock.calls[0][0]
186
+ expect(url).toContain('?')
187
+ expect(url).toContain('search=test')
188
+ expect(url).toContain('page=1')
189
+ expect(url).toContain('ordering=-created_at')
190
+ })
191
+ })
192
+
193
+ describe('response handling', () => {
194
+ it('returns list response matching StorageListResponse type', async () => {
195
+ const mockResponse: StorageListResponse = {
196
+ status: 'success',
197
+ message: 'Objects retrieved successfully',
198
+ data: [{ id: 1, file: 'doc.pdf', path: 'doc.pdf', size: 1024, mimetype: 'application/pdf', created_at: '2024-01-01', updated_at: '2024-01-01' }],
199
+ total: 1
200
+ }
201
+ mockHttpClient.get.mockResolvedValue(mockResponse)
202
+ const result = await new Storage(mockClient).from('documents').execute() as StorageListResponse
203
+ expect(result.status).toBe('success')
204
+ expect(result.data).toHaveLength(1)
205
+ expect(result.data[0].mimetype).toBe('application/pdf')
206
+ expect(result.total).toBe(1)
207
+ })
208
+
209
+ it('returns upload response matching StorageResponse type', async () => {
210
+ const mockResponse: StorageResponse = {
211
+ status: 'success',
212
+ message: 'Object created successfully',
213
+ data: { id: 1, file: 'doc.pdf', path: 'doc.pdf', size: 2048, mimetype: 'application/pdf', created_at: '2024-01-01', updated_at: '2024-01-01' }
214
+ }
215
+ mockHttpClient.post.mockResolvedValue(mockResponse)
216
+ const result = await new Storage(mockClient).from('documents').upload({ files: [], metadatas: [], paths: ['doc.pdf'] }).execute() as StorageResponse
217
+ expect(result.data.file).toBe('doc.pdf')
218
+ expect(result.data.size).toBe(2048)
219
+ })
220
+
221
+ it('returns download response as blob', async () => {
222
+ const mockBlob = new Blob(['content'])
223
+ mockHttpClient.get.mockResolvedValue(mockBlob)
224
+ const result = await new Storage(mockClient).from('documents').download('doc.pdf').execute()
225
+ expect(result).toBeInstanceOf(Blob)
226
+ })
227
+
228
+ it('returns delete response matching StorageDeleteBatchResponse type', async () => {
229
+ const mockResponse: StorageDeleteBatchResponse = {
230
+ status: 'success',
231
+ message: 'Objects deleted successfully',
232
+ data: { deleted_count: 1, failed: [] }
233
+ }
234
+ mockHttpClient.post.mockResolvedValue(mockResponse)
235
+ const result = await new Storage(mockClient).from('documents').delete(['doc.pdf']).execute() as StorageDeleteBatchResponse
236
+ expect(result.status).toBe('success')
237
+ expect(result.data.deleted_count).toBe(1)
238
+ })
239
+
240
+ it('returns update metadata response', async () => {
241
+ const mockResponse: StorageResponse = {
242
+ status: 'success',
243
+ message: 'Object metadata updated successfully',
244
+ data: { id: 1, file: 'doc.pdf', path: 'doc.pdf', size: 1024, mimetype: 'application/pdf', visibility: 'public', created_at: '2024-01-01', updated_at: '2024-01-01' }
245
+ }
246
+ mockHttpClient.put.mockResolvedValue(mockResponse)
247
+ const result = await new Storage(mockClient).from('documents').update('doc.pdf', { visibility: 'public' }).execute() as StorageResponse
248
+ expect(result.data.visibility).toBe('public')
249
+ })
250
+ })
251
+ })