@api-client/core 0.12.7 → 0.12.10

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 (62) hide show
  1. package/build/src/browser.d.ts +3 -2
  2. package/build/src/browser.d.ts.map +1 -1
  3. package/build/src/browser.js +2 -1
  4. package/build/src/browser.js.map +1 -1
  5. package/build/src/index.d.ts +3 -2
  6. package/build/src/index.d.ts.map +1 -1
  7. package/build/src/index.js +2 -1
  8. package/build/src/index.js.map +1 -1
  9. package/build/src/mocking/lib/User.d.ts.map +1 -1
  10. package/build/src/mocking/lib/User.js +1 -0
  11. package/build/src/mocking/lib/User.js.map +1 -1
  12. package/build/src/models/kinds.d.ts +2 -0
  13. package/build/src/models/kinds.d.ts.map +1 -1
  14. package/build/src/models/kinds.js +2 -0
  15. package/build/src/models/kinds.js.map +1 -1
  16. package/build/src/models/store/Invitation.d.ts +146 -0
  17. package/build/src/models/store/Invitation.d.ts.map +1 -0
  18. package/build/src/models/store/Invitation.js +186 -0
  19. package/build/src/models/store/Invitation.js.map +1 -0
  20. package/build/src/models/store/Organization.d.ts +4 -4
  21. package/build/src/models/store/Organization.d.ts.map +1 -1
  22. package/build/src/models/store/Organization.js +9 -9
  23. package/build/src/models/store/Organization.js.map +1 -1
  24. package/build/src/models/store/User.d.ts +14 -0
  25. package/build/src/models/store/User.d.ts.map +1 -1
  26. package/build/src/models/store/User.js.map +1 -1
  27. package/build/src/runtime/store/HttpWeb.js +5 -5
  28. package/build/src/runtime/store/HttpWeb.js.map +1 -1
  29. package/build/src/runtime/store/OrganizationsSdk.d.ts +77 -2
  30. package/build/src/runtime/store/OrganizationsSdk.d.ts.map +1 -1
  31. package/build/src/runtime/store/OrganizationsSdk.js +278 -3
  32. package/build/src/runtime/store/OrganizationsSdk.js.map +1 -1
  33. package/build/src/runtime/store/RouteBuilder.d.ts +15 -2
  34. package/build/src/runtime/store/RouteBuilder.d.ts.map +1 -1
  35. package/build/src/runtime/store/RouteBuilder.js +33 -6
  36. package/build/src/runtime/store/RouteBuilder.js.map +1 -1
  37. package/build/src/runtime/store/Sdk.d.ts +1 -1
  38. package/build/src/runtime/store/Sdk.d.ts.map +1 -1
  39. package/build/src/runtime/store/Sdk.js +1 -1
  40. package/build/src/runtime/store/Sdk.js.map +1 -1
  41. package/build/src/runtime/store/SdkBase.d.ts +2 -2
  42. package/build/src/runtime/store/SdkBase.d.ts.map +1 -1
  43. package/build/src/runtime/store/SdkBase.js +2 -2
  44. package/build/src/runtime/store/SdkBase.js.map +1 -1
  45. package/build/src/runtime/store/UsersSdk.d.ts +2 -0
  46. package/build/src/runtime/store/UsersSdk.d.ts.map +1 -1
  47. package/build/src/runtime/store/UsersSdk.js +6 -0
  48. package/build/src/runtime/store/UsersSdk.js.map +1 -1
  49. package/eslint.config.js +6 -5
  50. package/package.json +23 -5
  51. package/src/mocking/lib/User.ts +1 -0
  52. package/src/models/kinds.ts +2 -0
  53. package/src/models/store/Invitation.ts +287 -0
  54. package/src/models/store/Organization.ts +11 -12
  55. package/src/models/store/User.ts +16 -0
  56. package/src/runtime/store/HttpWeb.ts +5 -5
  57. package/src/runtime/store/OrganizationsSdk.ts +293 -5
  58. package/src/runtime/store/RouteBuilder.ts +42 -8
  59. package/src/runtime/store/Sdk.ts +1 -1
  60. package/src/runtime/store/SdkBase.ts +2 -2
  61. package/src/runtime/store/UsersSdk.ts +6 -0
  62. package/tests/unit/models/store/Invitation.spec.ts +223 -0
@@ -7,9 +7,12 @@ import {
7
7
  SdkOptions,
8
8
  } from './SdkBase.js'
9
9
  import { RouteBuilder } from './RouteBuilder.js'
10
- import { ContextListResult } from '../../events/BaseEvents.js'
11
- import { IOrganization } from '../../models/store/Organization.js'
10
+ import type { ContextListOptions, ContextListResult } from '../../events/BaseEvents.js'
11
+ import type { IOrganization, UserOrganizationGrantType } from '../../models/store/Organization.js'
12
+ import type { InvitationSchema } from '../../models/store/Invitation.js'
12
13
  import { SdkError } from './Errors.js'
14
+ import type { PatchInfo } from '../../patch/types.js'
15
+ import type { IUser } from '../../models/store/User.js'
13
16
 
14
17
  export class OrganizationsSdk extends SdkBase {
15
18
  /**
@@ -37,10 +40,10 @@ export class OrganizationsSdk extends SdkBase {
37
40
  try {
38
41
  data = JSON.parse(result.body)
39
42
  } catch {
40
- throw new Error(`${E_PREFIX}${E_INVALID_JSON}.`)
43
+ throw new Error(`${E_PREFIX}${E_INVALID_JSON}`)
41
44
  }
42
45
  if (!Array.isArray(data.items)) {
43
- throw new Error(`${E_PREFIX}${E_RESPONSE_UNKNOWN}.`)
46
+ throw new Error(`${E_PREFIX}${E_RESPONSE_UNKNOWN}`)
44
47
  }
45
48
  return data
46
49
  }
@@ -70,11 +73,296 @@ export class OrganizationsSdk extends SdkBase {
70
73
  try {
71
74
  data = JSON.parse(result.body)
72
75
  } catch {
73
- throw new Error(`${E_PREFIX}${E_INVALID_JSON}.`)
76
+ throw new Error(`${E_PREFIX}${E_INVALID_JSON}`)
77
+ }
78
+ if (!data.kind) {
79
+ throw new Error(`${E_PREFIX}${E_RESPONSE_UNKNOWN}`)
80
+ }
81
+ return data
82
+ }
83
+
84
+ /**
85
+ * Lists all invitations for a given organization.
86
+ * @param oid The organization ID.
87
+ * @param request The request options.
88
+ * @returns A promise that resolves to a list of invitations.
89
+ */
90
+ async listInvitations(oid: string, request: SdkOptions = {}): Promise<ContextListResult<InvitationSchema>> {
91
+ const { token } = request
92
+ const url = this.sdk.getUrl(RouteBuilder.invitations(oid))
93
+ const result = await this.sdk.http.get(url.toString(), { token })
94
+ this.inspectCommonStatusCodes(result)
95
+ const E_PREFIX = 'Unable to list organization invitations. '
96
+ if (result.status !== 200) {
97
+ this.logInvalidResponse(result)
98
+ let e = this.createGenericSdkError(result.body)
99
+ if (!e) {
100
+ e = new SdkError(`${E_PREFIX}${E_RESPONSE_STATUS}${result.status}`, result.status)
101
+ e.response = result.body
102
+ }
103
+ throw e
104
+ }
105
+ if (!result.body) {
106
+ throw new Error(`${E_PREFIX}${E_RESPONSE_NO_VALUE}`)
107
+ }
108
+ let data: ContextListResult<InvitationSchema>
109
+ try {
110
+ data = JSON.parse(result.body)
111
+ } catch {
112
+ throw new Error(`${E_PREFIX}${E_INVALID_JSON}`)
113
+ }
114
+ if (!Array.isArray(data.items)) {
115
+ throw new Error(`${E_PREFIX}${E_RESPONSE_UNKNOWN}`)
116
+ }
117
+ return data
118
+ }
119
+
120
+ /**
121
+ * Creates an invitation for a user to join an organization.
122
+ * @param oid The organization ID.
123
+ * @param email The email address of the user to invite.
124
+ * @param grant_type The type of grant for the user.
125
+ * @param name The name of the user (optional).
126
+ * @param request The request options.
127
+ * @returns A promise that resolves to the created invitation.
128
+ */
129
+ async createInvitation(
130
+ oid: string,
131
+ email: string,
132
+ grant_type: UserOrganizationGrantType,
133
+ name?: string,
134
+ request: SdkOptions = {}
135
+ ): Promise<InvitationSchema> {
136
+ const url = this.sdk.getUrl(RouteBuilder.invitations(oid))
137
+ const body = {
138
+ email,
139
+ name,
140
+ grant_type,
141
+ }
142
+ const result = await this.sdk.http.post(url.toString(), {
143
+ body: JSON.stringify(body),
144
+ token: request.token,
145
+ headers: {
146
+ 'Content-Type': 'application/json',
147
+ },
148
+ })
149
+ this.inspectCommonStatusCodes(result)
150
+ const E_PREFIX = 'Unable to create an invitation. '
151
+ if (result.status !== 200) {
152
+ throw new Error(`${E_PREFIX}Invalid response status: ${result.status}`)
153
+ }
154
+ if (!result.body) {
155
+ throw new Error(`${E_PREFIX}Response has no value.`)
156
+ }
157
+ let data: InvitationSchema
158
+ try {
159
+ data = JSON.parse(result.body)
160
+ } catch {
161
+ throw new Error(`${E_PREFIX}${E_INVALID_JSON}`)
74
162
  }
75
163
  if (!data.kind) {
164
+ throw new Error(`${E_PREFIX}${E_RESPONSE_UNKNOWN}`)
165
+ }
166
+ return data
167
+ }
168
+
169
+ /**
170
+ * Finds an invitation by its token.
171
+ * @param oid The organization ID.
172
+ * @param token The invitation token.
173
+ * @param request The request options.
174
+ * @returns A promise that resolves to the found invitation.
175
+ */
176
+ async findInvitationByToken(oid: string, token: string, request: SdkOptions = {}): Promise<InvitationSchema> {
177
+ const url = this.sdk.getUrl(RouteBuilder.findInvitation(oid))
178
+ url.searchParams.append('token', token)
179
+ const result = await this.sdk.http.get(url.toString(), request)
180
+ this.inspectCommonStatusCodes(result)
181
+ const E_PREFIX = 'Unable to find invitation by token. '
182
+ if (result.status !== 200) {
183
+ throw new Error(`${E_PREFIX}Invalid response status: ${result.status}`)
184
+ }
185
+ if (!result.body) {
186
+ throw new Error(`${E_PREFIX}Response has no value.`)
187
+ }
188
+ let data: InvitationSchema
189
+ try {
190
+ data = JSON.parse(result.body)
191
+ } catch {
192
+ throw new Error(`${E_PREFIX}${E_INVALID_JSON}`)
193
+ }
194
+ if (!data.kind) {
195
+ throw new Error(`${E_PREFIX}${E_RESPONSE_UNKNOWN}`)
196
+ }
197
+ return data
198
+ }
199
+
200
+ /**
201
+ * Declines an invitation.
202
+ * @param oid The organization ID.
203
+ * @param id The invitation ID.
204
+ * @param request The request options.
205
+ * @returns A promise that resolves when the invitation is declined.
206
+ */
207
+ async declineInvitation(oid: string, id: string, request: SdkOptions = {}): Promise<InvitationSchema> {
208
+ const url = this.sdk.getUrl(RouteBuilder.declineInvitation(oid, id))
209
+ const result = await this.sdk.http.post(url.toString(), request)
210
+ this.inspectCommonStatusCodes(result)
211
+ const E_PREFIX = 'Unable to decline invitation. '
212
+ if (result.status !== 200) {
213
+ throw new Error(`${E_PREFIX}Invalid response status: ${result.status}`)
214
+ }
215
+ if (!result.body) {
216
+ throw new Error(`${E_PREFIX}Response has no value.`)
217
+ }
218
+ let data: InvitationSchema
219
+ try {
220
+ data = JSON.parse(result.body)
221
+ } catch {
222
+ throw new Error(`${E_PREFIX}${E_INVALID_JSON}`)
223
+ }
224
+ if (!data.kind) {
225
+ throw new Error(`${E_PREFIX}${E_RESPONSE_UNKNOWN}`)
226
+ }
227
+ return data
228
+ }
229
+
230
+ /**
231
+ * Soft-deletes an invitation.
232
+ * @param oid The organization ID.
233
+ * @param id The invitation ID.
234
+ * @param request The request options.
235
+ * @returns A promise that resolves to the deleted invitation.
236
+ */
237
+ async deleteInvitation(oid: string, id: string, request: SdkOptions = {}): Promise<InvitationSchema> {
238
+ const url = this.sdk.getUrl(RouteBuilder.invitation(oid, id))
239
+ const result = await this.sdk.http.delete(url.toString(), request)
240
+ this.inspectCommonStatusCodes(result)
241
+ const E_PREFIX = 'Unable to delete invitation. '
242
+ if (result.status !== 200) {
243
+ throw new Error(`${E_PREFIX}Invalid response status: ${result.status}`)
244
+ }
245
+ if (!result.body) {
246
+ throw new Error(`${E_PREFIX}Response has no value.`)
247
+ }
248
+ let data: InvitationSchema
249
+ try {
250
+ data = JSON.parse(result.body)
251
+ } catch {
252
+ throw new Error(`${E_PREFIX}${E_INVALID_JSON}`)
253
+ }
254
+ if (!data.kind) {
255
+ throw new Error(`${E_PREFIX}${E_RESPONSE_UNKNOWN}`)
256
+ }
257
+ return data
258
+ }
259
+
260
+ /**
261
+ * Patches an invitation. The server performs the patch validation.
262
+ * The API only allows to patch some of the invitation properties:
263
+ * - name
264
+ * - expires_at
265
+ * - grant_type
266
+ * @param oid The organization ID.
267
+ * @param id The invitation ID.
268
+ * @param info The patch information.
269
+ * @param request The request options.
270
+ * @returns A promise that resolves to the patched invitation.
271
+ */
272
+ async patchInvitation(oid: string, id: string, info: PatchInfo, request: SdkOptions = {}): Promise<InvitationSchema> {
273
+ const url = this.sdk.getUrl(RouteBuilder.invitation(oid, id))
274
+ const result = await this.sdk.http.patch(url.toString(), {
275
+ body: JSON.stringify(info),
276
+ token: request.token,
277
+ headers: {
278
+ 'Content-Type': 'application/json',
279
+ },
280
+ })
281
+ this.inspectCommonStatusCodes(result)
282
+ const E_PREFIX = 'Unable to update invitation. '
283
+ if (result.status !== 200) {
284
+ throw new Error(`${E_PREFIX}Invalid response status: ${result.status}`)
285
+ }
286
+ if (!result.body) {
287
+ throw new Error(`${E_PREFIX}Response has no value.`)
288
+ }
289
+ let data: InvitationSchema
290
+ try {
291
+ data = JSON.parse(result.body)
292
+ } catch {
293
+ throw new Error(`${E_PREFIX}${E_INVALID_JSON}`)
294
+ }
295
+ if (!data.kind) {
296
+ throw new Error(`${E_PREFIX}${E_RESPONSE_UNKNOWN}`)
297
+ }
298
+ return data
299
+ }
300
+
301
+ /**
302
+ * Lists users in the organization.
303
+ *
304
+ * @param oid The key of the organization we want to read the user from.
305
+ * @param options Optional query options.
306
+ * @param request Optional request options.
307
+ */
308
+ async listUsers(
309
+ oid: string,
310
+ options?: ContextListOptions,
311
+ request: SdkOptions = {}
312
+ ): Promise<ContextListResult<IUser>> {
313
+ const { token } = request
314
+ const url = this.sdk.getUrl(RouteBuilder.organizationUsers(oid))
315
+ this.sdk.appendListOptions(url, options)
316
+ const result = await this.sdk.http.get(url.toString(), { token })
317
+ this.inspectCommonStatusCodes(result)
318
+ const E_PREFIX = 'Unable to list projects. '
319
+ if (result.status !== 200) {
320
+ this.logInvalidResponse(result)
321
+ throw new Error(`${E_PREFIX}${E_RESPONSE_STATUS}${result.status}`)
322
+ }
323
+ if (!result.body) {
324
+ throw new Error(`${E_PREFIX}${E_RESPONSE_NO_VALUE}`)
325
+ }
326
+ let data: ContextListResult<IUser>
327
+ try {
328
+ data = JSON.parse(result.body)
329
+ } catch {
330
+ throw new Error(`${E_PREFIX}${E_INVALID_JSON}.`)
331
+ }
332
+ if (!Array.isArray(data.items)) {
76
333
  throw new Error(`${E_PREFIX}${E_RESPONSE_UNKNOWN}.`)
77
334
  }
78
335
  return data
79
336
  }
337
+
338
+ /**
339
+ * Gets a user by its key in the organization.
340
+ *
341
+ * @param oid The key of the organization parent organization.
342
+ * @param key The user key.
343
+ * @param request Optional request options.
344
+ * @returns The user object
345
+ * @deprecated Use `organizations.readUser()` instead.
346
+ */
347
+ async getUser(oid: string, key: string, request: SdkOptions = {}): Promise<IUser> {
348
+ const { token } = request
349
+ const url = this.sdk.getUrl(RouteBuilder.organizationUser(oid, key))
350
+ const result = await this.sdk.http.get(url.toString(), { token })
351
+ this.inspectCommonStatusCodes(result)
352
+ const E_PREFIX = 'Unable to read the user info. '
353
+ if (result.status !== 200) {
354
+ this.logInvalidResponse(result)
355
+ throw new Error(`${E_PREFIX}${E_RESPONSE_STATUS}${result.status}`)
356
+ }
357
+ if (!result.body) {
358
+ throw new Error(`${E_PREFIX}${E_RESPONSE_NO_VALUE}`)
359
+ }
360
+ let data: IUser
361
+ try {
362
+ data = JSON.parse(result.body)
363
+ } catch {
364
+ throw new Error(`${E_PREFIX}${E_INVALID_JSON}.`)
365
+ }
366
+ return data
367
+ }
80
368
  }
@@ -158,6 +158,48 @@ export class RouteBuilder {
158
158
  return '/v1/orgs'
159
159
  }
160
160
 
161
+ static organization(oid: string): string {
162
+ return `/v1/orgs/${oid}`
163
+ }
164
+
165
+ static organizationUsers(oid: string): string {
166
+ return `${this.organization(oid)}/users`
167
+ }
168
+
169
+ static organizationUser(oid: string, key: string): string {
170
+ return `${this.organization(oid)}/users/${key}`
171
+ }
172
+
173
+ /**
174
+ * @deprecated Use `organizationUsers` instead.
175
+ */
176
+ static users(oid: string): string {
177
+ return `${this.organization(oid)}/users`
178
+ }
179
+
180
+ /**
181
+ * @deprecated Use `organizationUser` instead.
182
+ */
183
+ static user(oid: string, key: string): string {
184
+ return `${this.organization(oid)}/users/${key}`
185
+ }
186
+
187
+ static invitations(oid: string): string {
188
+ return `${this.organization(oid)}/invitations`
189
+ }
190
+
191
+ static invitation(oid: string, id: string): string {
192
+ return `${this.invitations(oid)}/${id}`
193
+ }
194
+
195
+ static findInvitation(oid: string): string {
196
+ return `${this.invitations(oid)}/find`
197
+ }
198
+
199
+ static declineInvitation(oid: string, id: string): string {
200
+ return `${this.invitation(oid, id)}/decline`
201
+ }
202
+
161
203
  /**
162
204
  * @deprecated Not used anymore
163
205
  */
@@ -191,14 +233,6 @@ export class RouteBuilder {
191
233
  return '/v1/users/me'
192
234
  }
193
235
 
194
- static users(oid: string): string {
195
- return `/v1/orgs/${oid}/users`
196
- }
197
-
198
- static user(oid: string, key: string): string {
199
- return `/v1/orgs/${oid}/users/${key}`
200
- }
201
-
202
236
  static dataCatalog(): string {
203
237
  return '/v1/datacatalog'
204
238
  }
@@ -67,7 +67,7 @@ export abstract class Sdk {
67
67
 
68
68
  projectExecution = new ProjectExecutionSdk(this)
69
69
 
70
- organization = new OrganizationsSdk(this)
70
+ organizations = new OrganizationsSdk(this)
71
71
 
72
72
  dataCatalog = new DataCatalogSdk(this)
73
73
  /**
@@ -33,10 +33,10 @@ export interface IStoreResponse {
33
33
  body?: string
34
34
  }
35
35
 
36
- export const E_INVALID_JSON = 'The response is not a valid JSON.'
36
+ export const E_INVALID_JSON = 'Invalid JSON response.'
37
37
  export const E_RESPONSE_NO_VALUE = 'The response has no value.'
38
38
  export const E_RESPONSE_STATUS = 'Invalid response status: '
39
- export const E_RESPONSE_UNKNOWN = 'The response has unknown format.'
39
+ export const E_RESPONSE_UNKNOWN = 'Unknown response from the server.'
40
40
  export const E_RESPONSE_LOCATION = 'The response has no "location" header.'
41
41
 
42
42
  export class SdkBase {
@@ -116,12 +116,15 @@ export class UsersSdk extends SdkBase {
116
116
  * @param organizationId The key of the organization we want to read the user from.
117
117
  * @param options Optional query options.
118
118
  * @param request Optional request options.
119
+ * @deprecated Use `organizations.listUsers()` instead.
119
120
  */
120
121
  async list(
121
122
  organizationId: string,
122
123
  options?: ContextListOptions,
123
124
  request: SdkOptions = {}
124
125
  ): Promise<ContextListResult<IUser>> {
126
+ // eslint-disable-next-line no-console
127
+ console.warn('The `users.list` method is deprecated. Use `organizations.listUsers()` instead.')
125
128
  const { token } = request
126
129
  const url = this.sdk.getUrl(RouteBuilder.users(organizationId))
127
130
  this.sdk.appendListOptions(url, options)
@@ -154,8 +157,11 @@ export class UsersSdk extends SdkBase {
154
157
  * @param key The user key.
155
158
  * @param request Optional request options.
156
159
  * @returns The user object
160
+ * @deprecated Use `organizations.getUser()` instead.
157
161
  */
158
162
  async read(organizationId: string, key: string, request: SdkOptions = {}): Promise<IUser> {
163
+ // eslint-disable-next-line no-console
164
+ console.warn('The `users.read` method is deprecated. Use `organizations.getUser()` instead.')
159
165
  const { token } = request
160
166
  const url = this.sdk.getUrl(RouteBuilder.user(organizationId, key))
161
167
  const result = await this.sdk.http.get(url.toString(), { token })
@@ -0,0 +1,223 @@
1
+ import { test } from '@japa/runner'
2
+ import { Invitation, InvitationKind, type InvitationSchema, type IDeletion } from '../../../../src/index.js'
3
+ import { OperationType } from '@api-client/json/patch/types.js'
4
+
5
+ const baseInput: Partial<InvitationSchema> = {
6
+ email: 'test@example.com',
7
+ oid: 'org-123',
8
+ grantType: 'viewer',
9
+ }
10
+
11
+ test.group('Invitation', (group) => {
12
+ group.tap((t) => t.tags(['@store', '@invitation']))
13
+
14
+ test('constructor initializes with minimal valid input', ({ assert }) => {
15
+ const invitation = new Invitation(baseInput)
16
+ assert.equal(invitation.kind, InvitationKind)
17
+ assert.isString(invitation.key)
18
+ assert.isNotEmpty(invitation.key)
19
+ assert.equal(invitation.email, baseInput.email)
20
+ assert.equal(invitation.oid, baseInput.oid)
21
+ assert.equal(invitation.grantType, baseInput.grantType)
22
+ assert.equal(invitation.uid, '') // default
23
+ assert.equal(invitation.token, '') // default
24
+ assert.equal(invitation.expiresAt, 0) // default
25
+ assert.equal(invitation.status, 'pending') // default
26
+ assert.isUndefined(invitation.name)
27
+ assert.isUndefined(invitation.respondedAt)
28
+ assert.isFalse(invitation.deleted)
29
+ assert.isUndefined(invitation.deletedInfo)
30
+ assert.equal(invitation.createdAt, 0) // default
31
+ assert.equal(invitation.updatedAt, 0) // default
32
+ })
33
+
34
+ test('constructor initializes with full input', ({ assert }) => {
35
+ const fullInput: InvitationSchema = {
36
+ kind: InvitationKind,
37
+ key: 'inv-key-123',
38
+ uid: 'user-abc',
39
+ oid: 'org-xyz',
40
+ email: 'full@example.com',
41
+ name: 'Test User',
42
+ token: 'secret-token',
43
+ expiresAt: Date.now() + 3600000,
44
+ respondedAt: Date.now(),
45
+ status: 'accepted',
46
+ grantType: 'editor',
47
+ deleted: true,
48
+ deletedInfo: { time: Date.now(), user: 'admin-user', byMe: false, name: 'Admin' },
49
+ createdAt: Date.now() - 7200000,
50
+ updatedAt: Date.now() - 3600000,
51
+ }
52
+ const invitation = new Invitation(fullInput)
53
+ assert.deepEqual(invitation.toJSON(), fullInput)
54
+ })
55
+
56
+ test('toJSON() serializes all properties correctly', ({ assert }) => {
57
+ const deletionInfo: IDeletion = { time: Date.now(), user: 'deleter-id', byMe: false, name: 'Deleter' }
58
+ const input: Partial<InvitationSchema> = {
59
+ ...baseInput,
60
+ key: 'inv-key-json',
61
+ uid: 'user-json',
62
+ name: 'JSON User',
63
+ token: 'json-token',
64
+ expiresAt: 1234567890,
65
+ respondedAt: 1234500000,
66
+ status: 'declined',
67
+ grantType: 'manager',
68
+ deleted: true,
69
+ deletedInfo: deletionInfo,
70
+ createdAt: 1230000000,
71
+ updatedAt: 1234000000,
72
+ }
73
+ const invitation = new Invitation(input)
74
+ const json = invitation.toJSON()
75
+
76
+ assert.deepEqual(json, {
77
+ kind: InvitationKind,
78
+ key: 'inv-key-json',
79
+ uid: 'user-json',
80
+ oid: baseInput.oid,
81
+ email: baseInput.email,
82
+ name: 'JSON User',
83
+ token: 'json-token',
84
+ expiresAt: 1234567890,
85
+ respondedAt: 1234500000,
86
+ status: 'declined',
87
+ grantType: 'manager',
88
+ deleted: true,
89
+ deletedInfo: deletionInfo,
90
+ createdAt: 1230000000,
91
+ updatedAt: 1234000000,
92
+ })
93
+ })
94
+
95
+ test('toJSON() omits optional fields when not set', ({ assert }) => {
96
+ const invitation = new Invitation(baseInput)
97
+ const json = invitation.toJSON()
98
+
99
+ assert.notProperty(json, 'name')
100
+ assert.notProperty(json, 'respondedAt')
101
+ assert.notProperty(json, 'deletedInfo') // deleted is false, so deletedInfo should not be present
102
+ })
103
+
104
+ test('getPatch() returns empty array when no changes', ({ assert }) => {
105
+ const invitation = new Invitation(baseInput)
106
+ assert.deepEqual(invitation.getPatch(), [])
107
+ })
108
+
109
+ test('setName() adds name if not present', ({ assert }) => {
110
+ const invitation = new Invitation(baseInput)
111
+ invitation.setName('New Name')
112
+ assert.deepEqual(invitation.getPatch(), [{ op: OperationType.add, path: '/name', value: 'New Name' }])
113
+ })
114
+
115
+ test('setName() replaces name if different', ({ assert }) => {
116
+ const invitation = new Invitation({ ...baseInput, name: 'Old Name' })
117
+ invitation.setName('New Name')
118
+ assert.deepEqual(invitation.getPatch(), [{ op: OperationType.replace, path: '/name', value: 'New Name' }])
119
+ })
120
+
121
+ test('setName() removes name if set to undefined', ({ assert }) => {
122
+ const invitation = new Invitation({ ...baseInput, name: 'Old Name' })
123
+ invitation.setName(undefined)
124
+ assert.deepEqual(invitation.getPatch(), [{ op: OperationType.remove, path: '/name' }])
125
+ })
126
+
127
+ test('setName() does nothing if name is the same', ({ assert }) => {
128
+ const invitation = new Invitation({ ...baseInput, name: 'Same Name' })
129
+ invitation.setName('Same Name')
130
+ assert.deepEqual(invitation.getPatch(), [])
131
+ })
132
+
133
+ test('setExpiresAt() replaces expiresAt if different', ({ assert }) => {
134
+ const invitation = new Invitation({ ...baseInput, expiresAt: 1000 })
135
+ invitation.setExpiresAt(2000)
136
+ assert.deepEqual(invitation.getPatch(), [{ op: OperationType.replace, path: '/expiresAt', value: 2000 }])
137
+ })
138
+
139
+ test('setExpiresAt() does nothing if expiresAt is the same', ({ assert }) => {
140
+ const invitation = new Invitation({ ...baseInput, expiresAt: 1000 })
141
+ invitation.setExpiresAt(1000)
142
+ assert.deepEqual(invitation.getPatch(), [])
143
+ })
144
+
145
+ test('setGrantType() replaces grantType if different', ({ assert }) => {
146
+ const invitation = new Invitation({ ...baseInput, grantType: 'viewer' })
147
+ invitation.setGrantType('editor')
148
+ assert.deepEqual(invitation.getPatch(), [{ op: OperationType.replace, path: '/grantType', value: 'editor' }])
149
+ })
150
+
151
+ test('setGrantType() does nothing if grantType is the same', ({ assert }) => {
152
+ const invitation = new Invitation({ ...baseInput, grantType: 'viewer' })
153
+ invitation.setGrantType('viewer')
154
+ assert.deepEqual(invitation.getPatch(), [])
155
+ })
156
+
157
+ test('builder getter initializes and returns builder', ({ assert }) => {
158
+ const invitation = new Invitation(baseInput)
159
+ const builder1 = invitation.builder
160
+ assert.isObject(builder1)
161
+ const builder2 = invitation.builder
162
+ assert.strictEqual(builder1, builder2) // Should return the same instance
163
+ })
164
+ })
165
+
166
+ test.group('Invitation.createSchema()', (schemaGroup) => {
167
+ schemaGroup.tap((t) => t.tags(['@store', '@invitation', '@static']))
168
+
169
+ test('throws if email is missing', ({ assert }) => {
170
+ assert.throws(
171
+ () => Invitation.createSchema({ oid: 'org-1', grantType: 'viewer' }),
172
+ 'Email is required to create an invitation schema.'
173
+ )
174
+ })
175
+
176
+ test('throws if oid is missing', ({ assert }) => {
177
+ assert.throws(
178
+ () => Invitation.createSchema({ email: 'test@example.com', grantType: 'viewer' }),
179
+ 'Organization ID is required to create an invitation schema.'
180
+ )
181
+ })
182
+
183
+ test('throws if grantType is missing', ({ assert }) => {
184
+ assert.throws(
185
+ () => Invitation.createSchema({ email: 'test@example.com', oid: 'org-1' }),
186
+ 'Grant type is required to create an invitation schema.'
187
+ )
188
+ })
189
+
190
+ test('creates schema with all default values for optional fields', ({ assert }) => {
191
+ const schema = Invitation.createSchema(baseInput)
192
+ assert.equal(schema.kind, InvitationKind)
193
+ assert.isString(schema.key)
194
+ assert.equal(schema.uid, '')
195
+ assert.equal(schema.oid, baseInput.oid)
196
+ assert.equal(schema.email, baseInput.email)
197
+ assert.equal(schema.grantType, baseInput.grantType)
198
+ assert.equal(schema.token, '')
199
+ assert.equal(schema.expiresAt, 0)
200
+ assert.equal(schema.status, 'pending')
201
+ assert.equal(schema.createdAt, 0)
202
+ assert.equal(schema.updatedAt, 0)
203
+ assert.isUndefined(schema.name)
204
+ assert.isUndefined(schema.respondedAt)
205
+ })
206
+
207
+ test('creates schema with provided optional values', ({ assert }) => {
208
+ const partial: Partial<InvitationSchema> = {
209
+ ...baseInput,
210
+ name: 'Provided Name',
211
+ token: 'provided-token',
212
+ expiresAt: 999,
213
+ respondedAt: 888,
214
+ status: 'accepted',
215
+ }
216
+ const schema = Invitation.createSchema(partial)
217
+ assert.equal(schema.name, 'Provided Name')
218
+ assert.equal(schema.token, 'provided-token')
219
+ assert.equal(schema.expiresAt, 999)
220
+ assert.equal(schema.respondedAt, 888)
221
+ assert.equal(schema.status, 'accepted')
222
+ })
223
+ })