@api-client/core 0.14.2 → 0.14.4

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 (86) hide show
  1. package/build/src/browser.d.ts +1 -1
  2. package/build/src/browser.d.ts.map +1 -1
  3. package/build/src/browser.js +1 -0
  4. package/build/src/browser.js.map +1 -1
  5. package/build/src/events/BaseEvents.d.ts +4 -0
  6. package/build/src/events/BaseEvents.d.ts.map +1 -1
  7. package/build/src/events/BaseEvents.js.map +1 -1
  8. package/build/src/index.d.ts +2 -1
  9. package/build/src/index.d.ts.map +1 -1
  10. package/build/src/index.js +2 -0
  11. package/build/src/index.js.map +1 -1
  12. package/build/src/modeling/ApiFile.d.ts +23 -0
  13. package/build/src/modeling/ApiFile.d.ts.map +1 -0
  14. package/build/src/modeling/ApiFile.js +44 -0
  15. package/build/src/modeling/ApiFile.js.map +1 -0
  16. package/build/src/modeling/ApiModel.d.ts +159 -0
  17. package/build/src/modeling/ApiModel.d.ts.map +1 -0
  18. package/build/src/modeling/ApiModel.js +237 -0
  19. package/build/src/modeling/ApiModel.js.map +1 -0
  20. package/build/src/modeling/DataDomain.d.ts +1 -1
  21. package/build/src/modeling/DataDomain.d.ts.map +1 -1
  22. package/build/src/modeling/DataDomain.js +1 -3
  23. package/build/src/modeling/DataDomain.js.map +1 -1
  24. package/build/src/modeling/DomainEntity.js +1 -1
  25. package/build/src/modeling/DomainEntity.js.map +1 -1
  26. package/build/src/modeling/DomainFile.d.ts +1 -2
  27. package/build/src/modeling/DomainFile.d.ts.map +1 -1
  28. package/build/src/modeling/DomainFile.js +3 -41
  29. package/build/src/modeling/DomainFile.js.map +1 -1
  30. package/build/src/modeling/Semantics.d.ts +55 -8
  31. package/build/src/modeling/Semantics.d.ts.map +1 -1
  32. package/build/src/modeling/Semantics.js +62 -8
  33. package/build/src/modeling/Semantics.js.map +1 -1
  34. package/build/src/modeling/amf/ShapeGenerator.d.ts.map +1 -1
  35. package/build/src/modeling/amf/ShapeGenerator.js.map +1 -1
  36. package/build/src/modeling/types.d.ts +491 -0
  37. package/build/src/modeling/types.d.ts.map +1 -1
  38. package/build/src/modeling/types.js.map +1 -1
  39. package/build/src/models/kinds.d.ts +3 -0
  40. package/build/src/models/kinds.d.ts.map +1 -1
  41. package/build/src/models/kinds.js +3 -0
  42. package/build/src/models/kinds.js.map +1 -1
  43. package/build/src/models/store/File.d.ts +19 -2
  44. package/build/src/models/store/File.d.ts.map +1 -1
  45. package/build/src/models/store/File.js +100 -13
  46. package/build/src/models/store/File.js.map +1 -1
  47. package/build/src/models/store/Group.d.ts +76 -2
  48. package/build/src/models/store/Group.d.ts.map +1 -1
  49. package/build/src/models/store/Group.js +84 -1
  50. package/build/src/models/store/Group.js.map +1 -1
  51. package/build/src/sdk/GroupsSdk.d.ts +41 -0
  52. package/build/src/sdk/GroupsSdk.d.ts.map +1 -0
  53. package/build/src/sdk/GroupsSdk.js +135 -0
  54. package/build/src/sdk/GroupsSdk.js.map +1 -0
  55. package/build/src/sdk/RouteBuilder.d.ts +2 -0
  56. package/build/src/sdk/RouteBuilder.d.ts.map +1 -1
  57. package/build/src/sdk/RouteBuilder.js +6 -0
  58. package/build/src/sdk/RouteBuilder.js.map +1 -1
  59. package/build/src/sdk/Sdk.d.ts +2 -0
  60. package/build/src/sdk/Sdk.d.ts.map +1 -1
  61. package/build/src/sdk/Sdk.js +5 -0
  62. package/build/src/sdk/Sdk.js.map +1 -1
  63. package/build/tsconfig.tsbuildinfo +1 -1
  64. package/package.json +2 -3
  65. package/src/events/BaseEvents.ts +4 -0
  66. package/src/modeling/ApiFile.ts +53 -0
  67. package/src/modeling/ApiModel.ts +327 -0
  68. package/src/modeling/DataDomain.ts +1 -1
  69. package/src/modeling/DomainEntity.ts +1 -1
  70. package/src/modeling/DomainFile.ts +3 -40
  71. package/src/modeling/Semantics.ts +63 -8
  72. package/src/modeling/amf/ShapeGenerator.ts +1 -1
  73. package/src/modeling/types.ts +545 -0
  74. package/src/models/kinds.ts +3 -0
  75. package/src/models/store/File.ts +100 -13
  76. package/src/models/store/Group.ts +148 -2
  77. package/src/sdk/GroupsSdk.ts +150 -0
  78. package/src/sdk/RouteBuilder.ts +8 -0
  79. package/src/sdk/Sdk.ts +6 -0
  80. package/tests/unit/modeling/api_model.spec.ts +291 -0
  81. package/tests/unit/modeling/domain_entity.spec.ts +15 -15
  82. package/tests/unit/modeling/domain_file.spec.ts +1 -11
  83. package/tests/unit/modeling/domain_model_entities.spec.ts +2 -2
  84. package/tests/unit/modeling/semantics.spec.ts +8 -11
  85. package/tests/unit/models/File/constructor.spec.ts +3 -2
  86. package/tests/unit/models/File/shortcutTo.spec.ts +1 -1
@@ -1,21 +1,167 @@
1
+ import { GroupKind } from '../kinds.js'
2
+ import type { IDeletion } from './Deletion.js'
3
+ import { nanoid } from '../../nanoid.js'
4
+
5
+ /**
6
+ * @deprecated Use `GroupSchema` instead.
7
+ * This interface is kept for backward compatibility.
8
+ */
9
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
10
+ export interface IGroup extends GroupSchema {}
11
+
1
12
  /**
2
13
  * An object representing a user group.
3
14
  */
4
- export interface IGroup {
15
+ export interface GroupSchema {
16
+ kind: typeof GroupKind
5
17
  /**
6
18
  * The key of the group.
19
+ * If not provided, it will be generated automatically by the API
7
20
  */
8
21
  key: string
9
22
  /**
10
- * The name of the group
23
+ * The name of the group.
24
+ * This is a required field and must be unique within the organization.
25
+ * @required
11
26
  */
12
27
  name: string
28
+ /**
29
+ * The description of the group.
30
+ */
31
+ description?: string
13
32
  /**
14
33
  * The id of the user that created this group.
34
+ * This field is ignored by the API and is set automatically.
35
+ * @readonly
15
36
  */
16
37
  owner: string
38
+ /**
39
+ * The icon of the group, if any.
40
+ * An optional icon for the group, stored as a URL or path.
41
+ * Max 255 characters.
42
+ */
43
+ icon?: string
44
+ /**
45
+ * The color of the group, if any.
46
+ * An optional color for the group, stored as a hex code.
47
+ * Max 50 characters (including the #).
48
+ */
49
+ color?: string
50
+ /**
51
+ * The organization ID this group belongs to.
52
+ * This field is ignored by the API and is set automatically.
53
+ * @readonly
54
+ */
55
+ oid: string
17
56
  /**
18
57
  * The list of users in this group.
58
+ * This field is ignored by the API and is set automatically.
59
+ * @readonly
19
60
  */
20
61
  users: string[]
62
+ /**
63
+ * Describes when this group was created
64
+ * This field is ignored by the API and is set automatically.
65
+ * @readonly
66
+ */
67
+ createdAt: number
68
+ /**
69
+ * Describes when this group was updated
70
+ * This field is ignored by the API and is set automatically.
71
+ * @readonly
72
+ */
73
+ updatedAt: number
74
+ /**
75
+ * Optional deletion information if the group has been deleted.
76
+ * This field is ignored by the API and is set automatically.
77
+ * @readonly
78
+ */
79
+ deleteInfo?: IDeletion
80
+ }
81
+
82
+ export class Group implements GroupSchema {
83
+ kind: typeof GroupKind = GroupKind
84
+ key = ''
85
+ name = ''
86
+ description?: string
87
+ owner = ''
88
+ icon?: string
89
+ color?: string
90
+ oid = ''
91
+ users: string[] = []
92
+ createdAt = 0
93
+ updatedAt = 0
94
+ deleteInfo?: IDeletion
95
+
96
+ static createSchema(input: Partial<GroupSchema> = {}): GroupSchema {
97
+ const { key = nanoid() } = input
98
+ const result: GroupSchema = {
99
+ kind: GroupKind,
100
+ key,
101
+ name: input.name || 'Unnamed group',
102
+ oid: input.oid || '',
103
+ owner: input.owner || '',
104
+ users: input.users || [],
105
+ createdAt: input.createdAt || Date.now(),
106
+ updatedAt: input.updatedAt || Date.now(),
107
+ }
108
+ if (input.description) {
109
+ result.description = input.description
110
+ }
111
+ if (input.icon) {
112
+ result.icon = input.icon
113
+ }
114
+ if (input.color) {
115
+ result.color = input.color
116
+ }
117
+ if (input.deleteInfo) {
118
+ result.deleteInfo = input.deleteInfo
119
+ }
120
+ return result
121
+ }
122
+
123
+ static fromName(name: string, oid: string): Group {
124
+ return new Group({ name, oid })
125
+ }
126
+
127
+ constructor(state?: Partial<GroupSchema>) {
128
+ const init = Group.createSchema(state)
129
+ this.key = init.key
130
+ this.name = init.name
131
+ this.description = init.description
132
+ this.owner = init.owner
133
+ this.icon = init.icon
134
+ this.color = init.color
135
+ this.oid = init.oid
136
+ this.users = init.users
137
+ this.createdAt = init.createdAt
138
+ this.updatedAt = init.updatedAt
139
+ this.deleteInfo = init.deleteInfo
140
+ }
141
+
142
+ toJSON(): GroupSchema {
143
+ const result: GroupSchema = {
144
+ kind: this.kind,
145
+ key: GroupKind,
146
+ name: this.name,
147
+ oid: this.oid,
148
+ owner: this.owner,
149
+ users: [...this.users],
150
+ createdAt: this.createdAt,
151
+ updatedAt: this.updatedAt,
152
+ }
153
+ if (this.description) {
154
+ result.description = this.description
155
+ }
156
+ if (this.icon) {
157
+ result.icon = this.icon
158
+ }
159
+ if (this.color) {
160
+ result.color = this.color
161
+ }
162
+ if (this.deleteInfo) {
163
+ result.deleteInfo = { ...this.deleteInfo }
164
+ }
165
+ return result
166
+ }
21
167
  }
@@ -0,0 +1,150 @@
1
+ import { ContextListOptions, ContextListResult } from '../browser.js'
2
+ import { Exception } from '../exceptions/exception.js'
3
+ import type { GroupSchema } from '../models/store/Group.js'
4
+ import { RouteBuilder } from './RouteBuilder.js'
5
+ import {
6
+ E_INVALID_JSON,
7
+ E_RESPONSE_NO_VALUE,
8
+ E_RESPONSE_STATUS,
9
+ E_RESPONSE_UNKNOWN,
10
+ SdkBase,
11
+ type SdkOptions,
12
+ } from './SdkBase.js'
13
+
14
+ export class GroupsSdk extends SdkBase {
15
+ /**
16
+ * Creates a new group in the specified organization.
17
+ * @param oid The organization ID where the group will be created.
18
+ * @param info The information about the group to create. See the `GroupSchema` interface for required fields.
19
+ * @param request Optional SDK options, including authentication token.
20
+ * @returns A promise that resolves to the created group.
21
+ */
22
+ async create(oid: string, info: Partial<GroupSchema>, request: SdkOptions = {}): Promise<GroupSchema> {
23
+ const { token } = request
24
+ const url = this.sdk.getUrl(RouteBuilder.groups(oid))
25
+ const body = JSON.stringify(info)
26
+ const result = await this.sdk.http.post(url.toString(), {
27
+ token,
28
+ body,
29
+ headers: {
30
+ 'content-type': 'application/json',
31
+ },
32
+ })
33
+ this.inspectCommonStatusCodes(result)
34
+ const E_PREFIX = 'Unable to create a group. '
35
+ if (result.status !== 201) {
36
+ this.logInvalidResponse(result)
37
+ throw this.createApiError(`${E_PREFIX}${E_RESPONSE_STATUS}${result.status}`, result.body)
38
+ }
39
+
40
+ if (!result.body) {
41
+ throw new Exception(`${E_PREFIX}${E_RESPONSE_NO_VALUE}`, { code: 'E_RESPONSE_NO_VALUE', status: result.status })
42
+ }
43
+ let data: GroupSchema
44
+ try {
45
+ data = JSON.parse(result.body)
46
+ } catch {
47
+ throw new Exception(`${E_PREFIX}${E_INVALID_JSON}`, { code: 'E_INVALID_JSON', status: result.status })
48
+ }
49
+ if (!data.key) {
50
+ throw new Exception(`${E_PREFIX}${E_RESPONSE_UNKNOWN}`, { code: 'E_RESPONSE_UNKNOWN', status: result.status })
51
+ }
52
+ return data
53
+ }
54
+
55
+ /**
56
+ * Lists user groups in the specified organization.
57
+ * If you want to list specific user groups, you can use the `uid` option to filter by user ID.
58
+ * @param oid The organization ID to list groups from.
59
+ * @param options Optional parameters for filtering, sorting, and pagination.
60
+ * @param request Optional SDK options, including authentication token.
61
+ * @returns A promise that resolves to a list of groups in the organization.
62
+ */
63
+ async list(
64
+ oid: string,
65
+ options: ContextListOptions = {},
66
+ request: SdkOptions = {}
67
+ ): Promise<ContextListResult<GroupSchema>> {
68
+ const { token } = request
69
+ const url = this.sdk.getUrl(RouteBuilder.groups(oid))
70
+ this.sdk.appendListOptions(url, options)
71
+ const result = await this.sdk.http.get(url.toString(), { token })
72
+ this.inspectCommonStatusCodes(result)
73
+ const E_PREFIX = 'Unable to create a group. '
74
+ if (result.status !== 200) {
75
+ this.logInvalidResponse(result)
76
+ throw this.createApiError(E_PREFIX + E_RESPONSE_STATUS + result.status, result.body)
77
+ }
78
+ if (!result.body) {
79
+ throw new Exception(E_PREFIX + E_RESPONSE_NO_VALUE, { code: 'E_RESPONSE_NO_VALUE', status: result.status })
80
+ }
81
+ let data: ContextListResult<GroupSchema>
82
+ try {
83
+ data = JSON.parse(result.body)
84
+ } catch {
85
+ throw new Exception(E_PREFIX + E_INVALID_JSON, { code: 'E_INVALID_JSON', status: result.status })
86
+ }
87
+ return data
88
+ }
89
+
90
+ /**
91
+ * Updates an existing group in the specified organization.
92
+ * Note that this method support PUT requests only. Some fields are ignored by the API and are set automatically.
93
+ * @param oid The organization ID where the group exists.
94
+ * @param updated The updated information for the group. Must include the `key` of the group to update.
95
+ * @param request Optional SDK options, including authentication token.
96
+ * @returns A promise that resolves to the updated group.
97
+ */
98
+ async update(oid: string, updated: GroupSchema, request: SdkOptions = {}): Promise<GroupSchema> {
99
+ const { token } = request
100
+ const url = this.sdk.getUrl(RouteBuilder.group(oid, updated.key))
101
+ const body = JSON.stringify(updated)
102
+ const result = await this.sdk.http.put(url.toString(), {
103
+ token,
104
+ body,
105
+ headers: {
106
+ 'content-type': 'application/json',
107
+ },
108
+ })
109
+ this.inspectCommonStatusCodes(result)
110
+ const E_PREFIX = 'Unable to update a group. '
111
+ if (result.status !== 200) {
112
+ this.logInvalidResponse(result)
113
+ throw this.createApiError(`${E_PREFIX}${E_RESPONSE_STATUS}${result.status}`, result.body)
114
+ }
115
+
116
+ if (!result.body) {
117
+ throw new Exception(`${E_PREFIX}${E_RESPONSE_NO_VALUE}`, { code: 'E_RESPONSE_NO_VALUE', status: result.status })
118
+ }
119
+ let data: GroupSchema
120
+ try {
121
+ data = JSON.parse(result.body)
122
+ } catch {
123
+ throw new Exception(`${E_PREFIX}${E_INVALID_JSON}`, { code: 'E_INVALID_JSON', status: result.status })
124
+ }
125
+ if (!data.key) {
126
+ throw new Exception(`${E_PREFIX}${E_RESPONSE_UNKNOWN}`, { code: 'E_RESPONSE_UNKNOWN', status: result.status })
127
+ }
128
+ return data
129
+ }
130
+
131
+ /**
132
+ * Deletes a group in the specified organization.
133
+ * The groups support soft deletion, meaning that the group is not immediately removed from the database.
134
+ * @param oid The organization ID where the group exists.
135
+ * @param gid The group ID to delete.
136
+ * @param request Optional SDK options, including authentication token.
137
+ * @returns A promise that resolves when the group is deleted.
138
+ */
139
+ async delete(oid: string, gid: string, request: SdkOptions = {}): Promise<void> {
140
+ const { token } = request
141
+ const url = this.sdk.getUrl(RouteBuilder.group(oid, gid))
142
+ const result = await this.sdk.http.delete(url.toString(), { token })
143
+ this.inspectCommonStatusCodes(result)
144
+ const E_PREFIX = 'Unable to delete a group. '
145
+ if (result.status !== 204) {
146
+ this.logInvalidResponse(result)
147
+ throw this.createApiError(`${E_PREFIX}${E_RESPONSE_STATUS}${result.status}`, result.body)
148
+ }
149
+ }
150
+ }
@@ -287,4 +287,12 @@ export class RouteBuilder {
287
287
  static dataCatalogVersionUnpublish(entryId: string, version: string): string {
288
288
  return `${RouteBuilder.dataCatalogVersion(entryId, version)}/unpublish`
289
289
  }
290
+
291
+ static groups(oid: string): string {
292
+ return `/v1/orgs/${oid}/groups`
293
+ }
294
+
295
+ static group(oid: string, gid: string): string {
296
+ return `/v1/orgs/${oid}/groups/${gid}`
297
+ }
290
298
  }
package/src/sdk/Sdk.ts CHANGED
@@ -11,6 +11,7 @@ import { TrashSdk } from './TrashSdk.js'
11
11
  import { ProjectExecutionSdk } from './ProjectExecutionsSdk.js'
12
12
  import { OrganizationsSdk } from './OrganizationsSdk.js'
13
13
  import { DataCatalogSdk } from './DataCatalogSdk.js'
14
+ import { GroupsSdk } from './GroupsSdk.js'
14
15
 
15
16
  const baseUriSymbol = Symbol('baseUri')
16
17
 
@@ -70,6 +71,8 @@ export abstract class Sdk {
70
71
  organizations = new OrganizationsSdk(this)
71
72
 
72
73
  dataCatalog = new DataCatalogSdk(this)
74
+
75
+ groups = new GroupsSdk(this)
73
76
  /**
74
77
  * When set it limits log output to minimum.
75
78
  */
@@ -162,6 +165,9 @@ export abstract class Sdk {
162
165
  if (options.oid) {
163
166
  searchParams.set('oid', String(options.oid))
164
167
  }
168
+ if (options.uid) {
169
+ searchParams.set('uid', String(options.uid))
170
+ }
165
171
  if (options.filter) {
166
172
  const bytes = new TextEncoder().encode(JSON.stringify(options.filter))
167
173
  // To be used when Uint8Array.prototype.toBase64() is available.
@@ -0,0 +1,291 @@
1
+ import { test } from '@japa/runner'
2
+ import {
3
+ ApiModel,
4
+ ApiModelKind,
5
+ DataDomain,
6
+ type RolesBasedAccessControl,
7
+ type ApiModelSchema,
8
+ type ExposedEntity,
9
+ } from '../../../src/index.js'
10
+
11
+ test.group('ApiModel.createSchema()', () => {
12
+ test('creates a schema with default values', ({ assert }) => {
13
+ const schema = ApiModel.createSchema()
14
+ assert.equal(schema.kind, ApiModelKind)
15
+ assert.typeOf(schema.key, 'string')
16
+ assert.isNotEmpty(schema.key)
17
+ assert.deepInclude(schema.info, { name: 'Unnamed API' })
18
+ assert.deepEqual(schema.exposes, [])
19
+ assert.isUndefined(schema.userKey)
20
+ assert.isUndefined(schema.domain)
21
+ assert.isUndefined(schema.authentication)
22
+ assert.isUndefined(schema.authorization)
23
+ assert.isUndefined(schema.session)
24
+ assert.isUndefined(schema.accessRule)
25
+ assert.isUndefined(schema.rateLimiting)
26
+ })
27
+
28
+ test('creates a schema with provided values', ({ assert }) => {
29
+ const input: Partial<ApiModelSchema> = {
30
+ key: 'test-api',
31
+ info: { name: 'Test API', description: 'A test API' },
32
+ exposes: [{ key: 'entity1', actions: [] }],
33
+ userKey: 'user-entity',
34
+ domain: { key: 'domain1', version: '1.0.0' },
35
+ authentication: { strategy: 'UsernamePassword' },
36
+ authorization: { strategy: 'RBAC', roleKey: 'role' } as RolesBasedAccessControl,
37
+ session: { secret: 'secret', properties: ['email'] },
38
+ accessRule: [{ type: 'public' }],
39
+ rateLimiting: { rules: [] },
40
+ }
41
+ const schema = ApiModel.createSchema(input)
42
+
43
+ assert.equal(schema.kind, ApiModelKind)
44
+ assert.equal(schema.key, 'test-api')
45
+ assert.deepInclude(schema.info, { name: 'Test API', description: 'A test API' })
46
+ assert.deepEqual(schema.exposes, [{ key: 'entity1', actions: [] }])
47
+ assert.equal(schema.userKey, 'user-entity')
48
+ assert.deepEqual(schema.domain, { key: 'domain1', version: '1.0.0' })
49
+ assert.deepEqual(schema.authentication, { strategy: 'UsernamePassword' })
50
+ assert.deepEqual(schema.authorization, { strategy: 'RBAC', roleKey: 'role' })
51
+ assert.deepEqual(schema.session, { secret: 'secret', properties: ['email'] })
52
+ assert.deepEqual(schema.accessRule, [{ type: 'public' }])
53
+ assert.deepEqual(schema.rateLimiting, { rules: [] })
54
+ })
55
+
56
+ test('creates a schema with partial info', ({ assert }) => {
57
+ const schema = ApiModel.createSchema({ info: { name: 'Partial API' } })
58
+ assert.deepInclude(schema.info, { name: 'Partial API' })
59
+ })
60
+
61
+ test('creates a schema with empty info', ({ assert }) => {
62
+ const schema = ApiModel.createSchema({ info: {} })
63
+ assert.deepInclude(schema.info, { name: 'Unnamed API' })
64
+ })
65
+ })
66
+
67
+ test.group('ApiModel.constructor()', () => {
68
+ test('creates an instance with default values', ({ assert }) => {
69
+ const model = new ApiModel()
70
+ assert.equal(model.kind, ApiModelKind)
71
+ assert.typeOf(model.key, 'string')
72
+ assert.isNotEmpty(model.key)
73
+ assert.equal(model.info.name, 'Unnamed API')
74
+ assert.deepEqual(model.exposes, [])
75
+ assert.isUndefined(model.userKey)
76
+ assert.isUndefined(model.domain)
77
+ assert.isUndefined(model.authentication)
78
+ assert.isUndefined(model.authorization)
79
+ assert.isUndefined(model.session)
80
+ assert.isUndefined(model.accessRule)
81
+ assert.isUndefined(model.rateLimiting)
82
+ assert.isUndefined(model.dataDomain)
83
+ })
84
+
85
+ test('creates an instance with provided schema values', ({ assert }) => {
86
+ const schema: ApiModelSchema = {
87
+ kind: ApiModelKind,
88
+ key: 'test-api',
89
+ info: { name: 'Test API', description: 'A test API' },
90
+ exposes: [{ key: 'entity1', actions: [] }],
91
+ userKey: 'user-entity',
92
+ domain: { key: 'domain1', version: '1.0.0' },
93
+ authentication: { strategy: 'UsernamePassword' },
94
+ authorization: { strategy: 'RBAC', roleKey: 'role' } as RolesBasedAccessControl,
95
+ session: { secret: 'secret', properties: ['email'] },
96
+ accessRule: [{ type: 'public' }],
97
+ rateLimiting: { rules: [] },
98
+ }
99
+ const model = new ApiModel(schema)
100
+
101
+ assert.equal(model.key, 'test-api')
102
+ assert.equal(model.info.name, 'Test API')
103
+ assert.deepEqual(model.exposes, [{ key: 'entity1', actions: [] }])
104
+ assert.equal(model.userKey, 'user-entity')
105
+ assert.deepEqual(model.domain, { key: 'domain1', version: '1.0.0' })
106
+ assert.deepEqual(model.authentication, { strategy: 'UsernamePassword' })
107
+ assert.deepEqual(model.authorization, { strategy: 'RBAC', roleKey: 'role' })
108
+ assert.deepEqual(model.session, { secret: 'secret', properties: ['email'] })
109
+ assert.deepEqual(model.accessRule, [{ type: 'public' }])
110
+ assert.deepEqual(model.rateLimiting, { rules: [] })
111
+ assert.isUndefined(model.dataDomain)
112
+ })
113
+
114
+ test('creates an instance with a DataDomain', ({ assert }) => {
115
+ const domainSchema = DataDomain.createSchema({ key: 'my-domain' })
116
+ const model = new ApiModel({}, domainSchema)
117
+ assert.isDefined(model.dataDomain)
118
+ assert.instanceOf(model.dataDomain, DataDomain)
119
+ assert.equal(model.dataDomain!.key, 'my-domain')
120
+ })
121
+
122
+ test('notifies change when info is modified', async ({ assert }) => {
123
+ const model = new ApiModel()
124
+ let notified = false
125
+ model.addEventListener('change', () => {
126
+ notified = true
127
+ })
128
+ model.info.name = 'New Name'
129
+ await Promise.resolve() // Allow microtask to run
130
+ assert.isTrue(notified)
131
+ })
132
+ })
133
+
134
+ test.group('ApiModel.toJSON()', () => {
135
+ test('serializes default values', ({ assert }) => {
136
+ const model = new ApiModel()
137
+ const json = model.toJSON()
138
+
139
+ assert.equal(json.kind, ApiModelKind)
140
+ assert.equal(json.key, model.key)
141
+ assert.deepInclude(json.info, { name: 'Unnamed API' })
142
+ assert.deepEqual(json.exposes, [])
143
+ assert.isUndefined(json.userKey)
144
+ assert.isUndefined(json.domain)
145
+ assert.isUndefined(json.authentication)
146
+ assert.isUndefined(json.authorization)
147
+ assert.isUndefined(json.session)
148
+ assert.isUndefined(json.accessRule)
149
+ assert.isUndefined(json.rateLimiting)
150
+ })
151
+
152
+ test('serializes all provided values', ({ assert }) => {
153
+ const schema: ApiModelSchema = {
154
+ kind: ApiModelKind,
155
+ key: 'test-api',
156
+ info: { name: 'Test API', description: 'A test API' },
157
+ exposes: [{ key: 'entity1', actions: [] }],
158
+ userKey: 'user-entity',
159
+ domain: { key: 'domain1', version: '1.0.0' },
160
+ authentication: { strategy: 'UsernamePassword' },
161
+ authorization: { strategy: 'RBAC', roleKey: 'role' } as RolesBasedAccessControl,
162
+ session: { secret: 'secret', properties: ['email'] },
163
+ accessRule: [{ type: 'public' }],
164
+ rateLimiting: { rules: [] },
165
+ }
166
+ const model = new ApiModel(schema)
167
+ const json = model.toJSON()
168
+
169
+ assert.equal(json.key, 'test-api')
170
+ assert.deepInclude(json.info, { name: 'Test API', description: 'A test API' })
171
+ assert.deepEqual(json.exposes, [{ key: 'entity1', actions: [] }])
172
+ assert.equal(json.userKey, 'user-entity')
173
+ assert.deepEqual(json.domain, { key: 'domain1', version: '1.0.0' })
174
+ assert.deepEqual(json.authentication, { strategy: 'UsernamePassword' })
175
+ assert.deepEqual(json.authorization, { strategy: 'RBAC', roleKey: 'role' })
176
+ assert.deepEqual(json.session, { secret: 'secret', properties: ['email'] })
177
+ assert.deepEqual(json.accessRule, [{ type: 'public' }])
178
+ assert.deepEqual(json.rateLimiting, { rules: [] })
179
+ })
180
+ })
181
+
182
+ test.group('ApiModel.exposeEntity()', () => {
183
+ test('exposes a new entity', ({ assert }) => {
184
+ const model = new ApiModel()
185
+ const entityKey = 'new-entity'
186
+ const exposedEntity = model.exposeEntity(entityKey)
187
+
188
+ assert.isDefined(exposedEntity)
189
+ assert.equal(exposedEntity.key, entityKey)
190
+ assert.deepEqual(exposedEntity.actions, [])
191
+ assert.includeDeepMembers(model.exposes, [exposedEntity])
192
+ })
193
+
194
+ test('returns an existing entity if already exposed', ({ assert }) => {
195
+ const model = new ApiModel()
196
+ const entityKey = 'existing-entity'
197
+ const initialExposedEntity = model.exposeEntity(entityKey)
198
+ const retrievedExposedEntity = model.exposeEntity(entityKey)
199
+
200
+ assert.strictEqual(retrievedExposedEntity, initialExposedEntity)
201
+ assert.lengthOf(model.exposes, 1)
202
+ })
203
+
204
+ test('notifies change when a new entity is exposed', async ({ assert }) => {
205
+ const model = new ApiModel()
206
+ let notified = false
207
+ model.addEventListener('change', () => {
208
+ notified = true
209
+ })
210
+ model.exposeEntity('notify-entity')
211
+ await Promise.resolve() // Allow microtask to run
212
+ assert.isTrue(notified)
213
+ })
214
+
215
+ test('does not notify change if entity already exposed', async ({ assert }) => {
216
+ const model = new ApiModel()
217
+ model.exposeEntity('no-notify-entity') // First exposure
218
+ await Promise.resolve() // Allow microtask to run
219
+ let notified = false
220
+ model.addEventListener('change', () => {
221
+ notified = true
222
+ })
223
+ model.exposeEntity('no-notify-entity') // Second exposure
224
+ await Promise.resolve() // Allow microtask to run
225
+ assert.isFalse(notified)
226
+ })
227
+ })
228
+
229
+ test.group('ApiModel.removeEntity()', () => {
230
+ test('removes an existing entity', ({ assert }) => {
231
+ const model = new ApiModel()
232
+ const entityKey = 'entity-to-remove'
233
+ model.exposeEntity(entityKey)
234
+ assert.lengthOf(model.exposes, 1)
235
+
236
+ model.removeEntity(entityKey)
237
+ assert.lengthOf(model.exposes, 0)
238
+ })
239
+
240
+ test('does nothing if entity does not exist', ({ assert }) => {
241
+ const model = new ApiModel()
242
+ model.exposeEntity('existing-entity')
243
+ const initialExposes = [...model.exposes]
244
+
245
+ model.removeEntity('non-existing-entity')
246
+ assert.deepEqual(model.exposes, initialExposes)
247
+ })
248
+
249
+ test('notifies change when an entity is removed', async ({ assert }) => {
250
+ const model = new ApiModel()
251
+ const entityKey = 'notify-remove-entity'
252
+ model.exposeEntity(entityKey)
253
+
254
+ let notified = false
255
+ model.addEventListener('change', () => {
256
+ notified = true
257
+ })
258
+ model.removeEntity(entityKey)
259
+ await Promise.resolve() // Allow microtask to run
260
+ assert.isTrue(notified)
261
+ })
262
+
263
+ test('does not notify change if entity to remove does not exist', async ({ assert }) => {
264
+ const model = new ApiModel()
265
+ let notified = false
266
+ model.addEventListener('change', () => {
267
+ notified = true
268
+ })
269
+ model.removeEntity('no-notify-remove-entity')
270
+ await Promise.resolve() // Allow microtask to run
271
+ assert.isFalse(notified)
272
+ })
273
+ })
274
+
275
+ test.group('ApiModel.getExposedEntity()', () => {
276
+ test('returns an existing exposed entity', ({ assert }) => {
277
+ const model = new ApiModel()
278
+ const entityKey = 'get-entity'
279
+ const exposed: ExposedEntity = { key: entityKey, actions: [] }
280
+ model.exposes.push(exposed)
281
+
282
+ const retrievedEntity = model.getExposedEntity(entityKey)
283
+ assert.deepEqual(retrievedEntity, exposed)
284
+ })
285
+
286
+ test('returns undefined if entity is not exposed', ({ assert }) => {
287
+ const model = new ApiModel()
288
+ const retrievedEntity = model.getExposedEntity('non-exposed-entity')
289
+ assert.isUndefined(retrievedEntity)
290
+ })
291
+ })