@api-client/core 0.18.51 → 0.18.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@api-client/core",
3
3
  "description": "The API Client's core client library. Works in NodeJS and in a ES enabled browser.",
4
- "version": "0.18.51",
4
+ "version": "0.18.53",
5
5
  "license": "Apache-2.0",
6
6
  "exports": {
7
7
  "./browser.js": {
@@ -26,7 +26,7 @@ export type IShapeUnion =
26
26
 
27
27
  export interface IApiAssociationShape {
28
28
  /**
29
- * This is custom property not available in AMF and used with data associations.
29
+ * This is a custom property that not available in the AMF library and is used with data associations.
30
30
  *
31
31
  * Whether the target entity should be embedded under the property name.
32
32
  * When false, this association is just an information that one entity depend on another.
@@ -71,6 +71,12 @@ export enum SemanticType {
71
71
  * we may add more automation related to the `title` property in the future.
72
72
  */
73
73
  Title = 'Semantic#Title',
74
+ /**
75
+ * A semantic that describes a name of a person or pet. This semantic is used with
76
+ * the `PublicUniqueName` to determine which field is responsible for the slug generation. However,
77
+ * we may add more automation related to the `name` property in the future.
78
+ */
79
+ Name = 'Semantic#Name',
74
80
  /**
75
81
  * Designates a Data Property as the `role` of a user within the system.
76
82
  * This is used to define the user's permissions and access levels.
@@ -314,9 +320,13 @@ export enum SemanticOperation {
314
320
  Update = 'Update',
315
321
  Delete = 'Delete',
316
322
  /**
317
- * Special operation for list/query operations
323
+ * Special operation for list operations
318
324
  */
319
325
  List = 'List',
326
+ /**
327
+ * Special operation for search operations
328
+ */
329
+ Search = 'Search',
320
330
  }
321
331
 
322
332
  /**
@@ -814,6 +824,19 @@ export const DataSemantics: Record<SemanticType, DataSemantic> = {
814
824
  operations: [],
815
825
  },
816
826
  },
827
+ [SemanticType.Name]: {
828
+ id: SemanticType.Name,
829
+ displayName: 'Name',
830
+ scope: SemanticScope.Property,
831
+ description: 'A person or pet name',
832
+ category: SemanticCategory.Content,
833
+ applicableDataTypes: ['string'],
834
+ hasConfig: false,
835
+ runtime: {
836
+ timing: SemanticTiming.None,
837
+ operations: [],
838
+ },
839
+ },
817
840
  [SemanticType.Description]: {
818
841
  id: SemanticType.Description,
819
842
  displayName: 'Description',
@@ -15,6 +15,8 @@ const orderedRoles: PermissionRole[] = ['reader', 'commenter', 'writer', 'owner'
15
15
  * A predefined set of rules that can be used to determine the source of the permission.
16
16
  * - `direct_user_grant`: The permission is granted directly to the user.
17
17
  * - `creator_default_owner`: The permission is granted to the creator of the item as the default owner.
18
+ * - `parent_owner_editor_rule`: The object is placed in a folder and the owner of the folder
19
+ * has automatically granted role to the object.
18
20
  */
19
21
  export type PermissionSourceRule = 'direct_user_grant' | 'creator_default_owner' | 'parent_owner_editor_rule'
20
22
 
@@ -338,8 +340,61 @@ export class Permission {
338
340
  * Link to the `Permission.hasRole(minimumLevel, currentRole)`.
339
341
  * @see {@link Permission.hasRole}
340
342
  */
341
- hasRole(minimumLevel: PermissionRole, currentRole: PermissionRole): boolean {
342
- return Permission.hasRole(minimumLevel, currentRole)
343
+ hasRole(minimumLevel: PermissionRole): boolean {
344
+ return Permission.hasRole(minimumLevel, this.role)
345
+ }
346
+
347
+ /**
348
+ * Checks if the user has the required role in the list of permissions.
349
+ * The permissions list must be filtered for the user, user's organization, and user's groups.
350
+ *
351
+ * @param minimumLevel The minimum requested role
352
+ * @param permissions The list of permissions to check.
353
+ * @returns True if the user has the required role.
354
+ */
355
+ static hasRoleIn(minimumLevel: PermissionRole, permissions: IPermission[]): boolean {
356
+ if (!permissions || !permissions.length) {
357
+ return false
358
+ }
359
+ // 1. Sort/Group permissions by depth (ascending).
360
+ // We want to process the closest permissions first.
361
+ const sorted = [...permissions].sort((a, b) => a.depth - b.depth)
362
+ const closestDepth = sorted[0].depth
363
+ const closestPermissions = sorted.filter((p) => p.depth === closestDepth)
364
+
365
+ // 2. Specificity: User > Group > Organization
366
+ let effectivePermission: IPermission | undefined
367
+
368
+ // Check for user permissions
369
+ const userPermissions = closestPermissions.filter((p) => p.type === 'user')
370
+ if (userPermissions.length) {
371
+ // Pick highest role if multiple user permissions exist at same depth (unlikely but safe)
372
+ effectivePermission = userPermissions.sort((a, b) => {
373
+ return orderedRoles.indexOf(b.role) - orderedRoles.indexOf(a.role)
374
+ })[0]
375
+ } else {
376
+ // Check for group permissions
377
+ const groupPermissions = closestPermissions.filter((p) => p.type === 'group')
378
+ if (groupPermissions.length) {
379
+ effectivePermission = groupPermissions.sort((a, b) => {
380
+ return orderedRoles.indexOf(b.role) - orderedRoles.indexOf(a.role)
381
+ })[0]
382
+ } else {
383
+ // Check for organization permissions
384
+ const orgPermissions = closestPermissions.filter((p) => p.type === 'organization')
385
+ if (orgPermissions.length) {
386
+ effectivePermission = orgPermissions.sort((a, b) => {
387
+ return orderedRoles.indexOf(b.role) - orderedRoles.indexOf(a.role)
388
+ })[0]
389
+ }
390
+ }
391
+ }
392
+
393
+ if (!effectivePermission) {
394
+ return false
395
+ }
396
+
397
+ return Permission.hasRole(minimumLevel, effectivePermission.role)
343
398
  }
344
399
 
345
400
  toJSON(): IPermission {
@@ -14,7 +14,7 @@ export class MD4 {
14
14
  }
15
15
 
16
16
  static core(x: number[], len: number): number[] {
17
- x[len >> 5] |= 0x80 << len % 32
17
+ x[len >> 5] |= 0x80 << (len % 32)
18
18
  x[(((len + 64) >>> 9) << 4) + 14] = len
19
19
 
20
20
  let a = 1732584193
@@ -117,7 +117,7 @@ export class MD4 {
117
117
  const bin: number[] = []
118
118
  const mask = (1 << MD4.chrsz) - 1
119
119
  for (let i = 0; i < str.length * MD4.chrsz; i += MD4.chrsz) {
120
- bin[i >> 5] |= (str.charCodeAt(i / MD4.chrsz) & mask) << i % 32
120
+ bin[i >> 5] |= (str.charCodeAt(i / MD4.chrsz) & mask) << (i % 32)
121
121
  }
122
122
  return bin
123
123
  }
@@ -126,7 +126,7 @@ export class MD4 {
126
126
  let str = ''
127
127
  const mask = (1 << MD4.chrsz) - 1
128
128
  for (let i = 0; i < bin.length * 32; i += MD4.chrsz) {
129
- str += String.fromCharCode((bin[i >> 5] >>> i % 32) & mask)
129
+ str += String.fromCharCode((bin[i >> 5] >>> (i % 32)) & mask)
130
130
  }
131
131
  return str
132
132
  }
@@ -0,0 +1,278 @@
1
+ import { test } from '@japa/runner'
2
+ import { IPermission, Permission, PermissionRole, PermissionType } from '../../../../src/models/store/Permission.js'
3
+
4
+ test.group('Permission.hasRoleIn()', () => {
5
+ const createPerm = (role: PermissionRole, depth: number, type: PermissionType = 'user', id = 'id'): IPermission => ({
6
+ kind: 'Core#Permission',
7
+ key: 'key',
8
+ role,
9
+ depth,
10
+ type,
11
+ granteeId: id,
12
+ itemId: 'item',
13
+ addingUser: 'user',
14
+ sourceRule: 'direct_user_grant',
15
+ })
16
+
17
+ test('returns false for empty permissions', ({ assert }) => {
18
+ assert.isFalse(Permission.hasRoleIn('reader', []))
19
+ })
20
+
21
+ test('returns true when user has direct role meeting requirement', ({ assert }) => {
22
+ const perms = [createPerm('writer', 0)]
23
+ assert.isTrue(Permission.hasRoleIn('reader', perms))
24
+ assert.isTrue(Permission.hasRoleIn('writer', perms))
25
+ })
26
+
27
+ test('returns false when user does not meet requirement', ({ assert }) => {
28
+ const perms = [createPerm('reader', 0)]
29
+ assert.isFalse(Permission.hasRoleIn('writer', perms))
30
+ })
31
+
32
+ test('prioritizes closer depth (depth 0 overrides depth 1)', ({ assert }) => {
33
+ // Depth 0: Reader (Direct), Depth 1: Owner (Inherited)
34
+ // Expectation: Reader wins (Specificity of depth) -> returns false for Writer check
35
+ const perms = [createPerm('reader', 0), createPerm('owner', 1)]
36
+ assert.isFalse(Permission.hasRoleIn('writer', perms))
37
+ assert.isTrue(Permission.hasRoleIn('reader', perms))
38
+ })
39
+
40
+ test('prioritizes closer depth (depth 1 overrides depth 2)', ({ assert }) => {
41
+ // Depth 1: Writer, Depth 2: Reader
42
+ // Expectation: Writer wins
43
+ const perms = [createPerm('writer', 1), createPerm('reader', 2)]
44
+ assert.isTrue(Permission.hasRoleIn('writer', perms))
45
+ })
46
+
47
+ test('prioritizes user over group at same depth', ({ assert }) => {
48
+ // Depth 0: User=Reader, Group=Owner
49
+ // Expectation: User wins -> Reader
50
+ const perms = [createPerm('reader', 0, 'user'), createPerm('owner', 0, 'group')]
51
+ assert.isFalse(Permission.hasRoleIn('writer', perms))
52
+ })
53
+
54
+ test('prioritizes group over organization at same depth', ({ assert }) => {
55
+ // Depth 0: Group=Reader, Org=Owner
56
+ // Expectation: Group wins -> Reader
57
+ const perms = [createPerm('reader', 0, 'group'), createPerm('owner', 0, 'organization')]
58
+ assert.isFalse(Permission.hasRoleIn('writer', perms))
59
+ })
60
+
61
+ test('uses highest role when multiple groups at same depth', ({ assert }) => {
62
+ // Depth 0: GroupA=Reader, GroupB=Writer
63
+ // Expectation: Writer wins (Union)
64
+ const perms = [createPerm('reader', 0, 'group', 'A'), createPerm('writer', 0, 'group', 'B')]
65
+ assert.isTrue(Permission.hasRoleIn('writer', perms))
66
+ })
67
+
68
+ test('uses highest role when multiple users at same depth (theoretical)', ({ assert }) => {
69
+ // Depth 0: User=Reader, User=Writer
70
+ // Expectation: Writer wins
71
+ const perms = [createPerm('reader', 0, 'user'), createPerm('writer', 0, 'user')]
72
+ assert.isTrue(Permission.hasRoleIn('writer', perms))
73
+ })
74
+
75
+ test('ignores deeper permissions even if they are more specific types', ({ assert }) => {
76
+ // Depth 0: Org=Reader
77
+ // Depth 1: User=Owner
78
+ // Expectation: Depth 0 (Org=Reader) wins because it's closer.
79
+ // User assumption: "closest to the item directly set... should be considered first"
80
+ const perms = [createPerm('reader', 0, 'organization'), createPerm('owner', 1, 'user')]
81
+ assert.isFalse(Permission.hasRoleIn('writer', perms))
82
+ })
83
+ })
84
+
85
+ test.group('Permission.constructor()', () => {
86
+ test('creates a default permission when no input', ({ assert }) => {
87
+ const p = new Permission()
88
+ assert.equal(p.kind, 'Core#Permission')
89
+ assert.typeOf(p.key, 'string')
90
+ assert.equal(p.role, 'reader')
91
+ assert.equal(p.type, 'user')
92
+ assert.equal(p.depth, 0)
93
+ })
94
+
95
+ test('creates from string JSON', ({ assert }) => {
96
+ const json = JSON.stringify({
97
+ kind: 'Core#Permission',
98
+ key: 'key',
99
+ role: 'writer',
100
+ type: 'group',
101
+ granteeId: 'group-id',
102
+ itemId: 'item-id',
103
+ addingUser: 'adder',
104
+ })
105
+ const p = new Permission(json)
106
+ assert.equal(p.role, 'writer')
107
+ assert.equal(p.type, 'group')
108
+ })
109
+
110
+ test('creates from object', ({ assert }) => {
111
+ const init: IPermission = {
112
+ kind: 'Core#Permission',
113
+ key: 'key',
114
+ role: 'owner',
115
+ type: 'user',
116
+ granteeId: 'user-id',
117
+ itemId: 'item-id',
118
+ addingUser: 'adder',
119
+ depth: 0,
120
+ sourceRule: 'direct_user_grant',
121
+ }
122
+ const p = new Permission(init)
123
+ assert.equal(p.role, 'owner')
124
+ assert.equal(p.granteeId, 'user-id')
125
+ })
126
+
127
+ test('throws when input is not a permission', ({ assert }) => {
128
+ assert.throws(() => new Permission({ kind: 'Wrong' } as unknown as IPermission), /Not a permission/)
129
+ })
130
+
131
+ test('restores optional fields', ({ assert }) => {
132
+ const init: IPermission = {
133
+ kind: 'Core#Permission',
134
+ key: 'key',
135
+ role: 'owner',
136
+ type: 'user',
137
+ granteeId: 'user-id',
138
+ itemId: 'item-id',
139
+ addingUser: 'adder',
140
+ displayName: 'Name',
141
+ expirationTime: 12345,
142
+ depth: 0,
143
+ sourceRule: 'direct_user_grant',
144
+ }
145
+ const p = new Permission(init)
146
+ assert.equal(p.displayName, 'Name')
147
+ assert.equal(p.expirationTime, 12345)
148
+ })
149
+ })
150
+
151
+ test.group('Permission static factories', () => {
152
+ test('fromUserRole creates valid permission', ({ assert }) => {
153
+ const p = Permission.fromUserRole('writer', 'item', 'user', 'adder')
154
+ assert.equal(p.type, 'user')
155
+ assert.equal(p.role, 'writer')
156
+ assert.equal(p.itemId, 'item')
157
+ assert.equal(p.granteeId, 'user')
158
+ assert.equal(p.addingUser, 'adder')
159
+ assert.typeOf(p.key, 'string')
160
+ })
161
+
162
+ test('fromGroupRole creates valid permission', ({ assert }) => {
163
+ const p = Permission.fromGroupRole('commenter', 'item', 'group', 'adder')
164
+ assert.equal(p.type, 'group')
165
+ assert.equal(p.role, 'commenter')
166
+ assert.equal(p.itemId, 'item')
167
+ assert.equal(p.granteeId, 'group')
168
+ })
169
+
170
+ test('fromOrganizationRole creates valid permission', ({ assert }) => {
171
+ const p = Permission.fromOrganizationRole('reader', 'item', 'org', 'adder')
172
+ assert.equal(p.type, 'organization')
173
+ assert.equal(p.role, 'reader')
174
+ assert.equal(p.itemId, 'item')
175
+ assert.equal(p.granteeId, 'org')
176
+ })
177
+
178
+ test('fromValues creates permission with new key', ({ assert }) => {
179
+ const base = {
180
+ role: 'owner',
181
+ type: 'user',
182
+ granteeId: 'u',
183
+ itemId: 'i',
184
+ addingUser: 'a',
185
+ depth: 0,
186
+ sourceRule: 'direct_user_grant',
187
+ } as unknown as IPermission as unknown as IPermission
188
+ const p = Permission.fromValues(base)
189
+ assert.equal(p.role, 'owner')
190
+ // fromValues generates a new key
191
+ assert.typeOf(p.key, 'string')
192
+ })
193
+ })
194
+
195
+ test.group('Permission.isPermission()', () => {
196
+ test('returns true for valid permission interface', ({ assert }) => {
197
+ const p = { kind: 'Core#Permission' }
198
+ assert.isTrue(Permission.isPermission(p))
199
+ })
200
+
201
+ test('returns false for null', ({ assert }) => {
202
+ assert.isFalse(Permission.isPermission(null))
203
+ })
204
+
205
+ test('returns false for wrong kind', ({ assert }) => {
206
+ assert.isFalse(Permission.isPermission({ kind: 'Wrong' }))
207
+ })
208
+ })
209
+
210
+ test.group('Permission.hasRole()', () => {
211
+ test('returns false when current role is missing', ({ assert }) => {
212
+ assert.isFalse(Permission.hasRole('reader', undefined))
213
+ })
214
+
215
+ test('owner satisfies all', ({ assert }) => {
216
+ assert.isTrue(Permission.hasRole('owner', 'owner'))
217
+ assert.isTrue(Permission.hasRole('writer', 'owner'))
218
+ assert.isTrue(Permission.hasRole('commenter', 'owner'))
219
+ assert.isTrue(Permission.hasRole('reader', 'owner'))
220
+ })
221
+
222
+ test('writer satisfies lower', ({ assert }) => {
223
+ assert.isFalse(Permission.hasRole('owner', 'writer'))
224
+ assert.isTrue(Permission.hasRole('writer', 'writer'))
225
+ assert.isTrue(Permission.hasRole('commenter', 'writer'))
226
+ assert.isTrue(Permission.hasRole('reader', 'writer'))
227
+ })
228
+
229
+ test('reader only satisfies reader', ({ assert }) => {
230
+ assert.isFalse(Permission.hasRole('writer', 'reader'))
231
+ assert.isTrue(Permission.hasRole('reader', 'reader'))
232
+ })
233
+
234
+ test('instance method calls static', ({ assert }) => {
235
+ const p = Permission.fromUserRole('writer', 'item', 'user', 'adder')
236
+ assert.isTrue(p.hasRole('reader'))
237
+ })
238
+ })
239
+
240
+ test.group('Permission.isHigherRole()', () => {
241
+ test('owner > writer', ({ assert }) => {
242
+ assert.isTrue(Permission.isHigherRole('owner', 'writer'))
243
+ })
244
+
245
+ test('writer > reader', ({ assert }) => {
246
+ assert.isTrue(Permission.isHigherRole('writer', 'reader'))
247
+ })
248
+
249
+ test('reader !> writer', ({ assert }) => {
250
+ assert.isFalse(Permission.isHigherRole('reader', 'writer'))
251
+ })
252
+
253
+ test('same role returns false', ({ assert }) => {
254
+ assert.isFalse(Permission.isHigherRole('owner', 'owner'))
255
+ })
256
+ })
257
+
258
+ test.group('Permission.toJSON()', () => {
259
+ test('returns plain object with all fields', ({ assert }) => {
260
+ const p = Permission.fromUserRole('owner', 'item', 'user', 'adder')
261
+ p.displayName = 'User Name'
262
+ p.expirationTime = 1000
263
+
264
+ const json = p.toJSON()
265
+ assert.equal(json.kind, 'Core#Permission')
266
+ assert.equal(json.role, 'owner')
267
+ assert.equal(json.displayName, 'User Name')
268
+ assert.equal(json.expirationTime, 1000)
269
+ assert.typeOf(json.key, 'string')
270
+ })
271
+
272
+ test('omits optional fields if undefined', ({ assert }) => {
273
+ const p = Permission.fromUserRole('owner', 'item', 'user', 'adder')
274
+ const json = p.toJSON()
275
+ assert.isUndefined(json.displayName)
276
+ assert.isUndefined(json.expirationTime)
277
+ })
278
+ })