@chill-sharp/ts-client 1.1.12 → 1.1.13

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/src/client.ts CHANGED
@@ -1,1809 +1,1809 @@
1
- /*
2
- * ChillSharp is a lightweight .NET library that sits on top of Entity Framework Core
3
- * and turns an existing data model into a fully working REST API with almost no setup.
4
- * Copyright (C) 2025 Andrea Piovesan
5
- *
6
- * This program is free software: you can redistribute it and/or modify
7
- * it under the terms of the GNU Affero General Public License as published by
8
- * the Free Software Foundation, either version 3 of the License, or
9
- * (at your option) any later version.
10
- *
11
- * This program is distributed in the hope that it will be useful,
12
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
- * GNU Affero General Public License for more details.
15
- *
16
- * You should have received a copy of the GNU Affero General Public License
17
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
18
- */
19
-
20
- import {
21
- HubConnection,
22
- HubConnectionBuilder,
23
- HubConnectionState
24
- } from "@microsoft/signalr";
25
- import { ChillSharpClientError } from "./errors.js";
26
- import { CHILL_SHARP_TS_CLIENT_VERSION } from "./version.js";
27
-
28
- export const API_BASE_PATH = "api/";
29
-
30
- export type JsonPrimitive = string | number | boolean | null;
31
- export type JsonValue = JsonPrimitive | JsonObject | JsonValue[];
32
- export interface JsonObject {
33
- [key: string]: JsonValue;
34
- }
35
-
36
- export interface GetTextRequest extends JsonObject {
37
- labelGuid: string;
38
- cultureName: string;
39
- primaryCultureName: string;
40
- primaryDefaultText: string;
41
- secondaryCultureName: string;
42
- secondaryDefaultText: string;
43
- }
44
-
45
- export interface GetTextResponse extends JsonObject {
46
- labelGuid: string;
47
- cultureName: string;
48
- value: string;
49
- }
50
-
51
-
52
- export const ChillDtoPropertyType = {
53
- Unknown: 0,
54
- Guid: 1,
55
- Integer: 10,
56
- Decimal: 20,
57
- Date: 30,
58
- Time: 40,
59
- DateTime: 50,
60
- Duration: 60,
61
- Boolean: 70,
62
- String: 80,
63
- Text: 81,
64
- Json: 99,
65
- ChillEntity: 1000,
66
- ChillEntityCollection: 1010,
67
- ChillQuery: 1100
68
- } as const;
69
-
70
- export type ChillDtoPropertyType = (typeof ChillDtoPropertyType)[keyof typeof ChillDtoPropertyType];
71
-
72
- export interface ChillDtoPropertySchema extends JsonObject {
73
- name: string;
74
- displayName: string;
75
- propertyType: ChillDtoPropertyType;
76
- simplePropertyType: string;
77
- referenceChillType: string | null;
78
- referenceChillTypeQuery: string | null;
79
- mcpDescription: string;
80
- isNullable: boolean | null;
81
- isReadOnly: boolean | null;
82
- minLength: number | null;
83
- maxLength: number | null;
84
- integerMinValue: number | null;
85
- integerMaxValue: number | null;
86
- decimalMinValue: number | null;
87
- decimalMaxValue: number | null;
88
- decimalPlaces: number | null;
89
- precision: number | null;
90
- scale: number | null;
91
- dateFormat: string;
92
- customFormat: string;
93
- regexPattern: string;
94
- enumValues: string[];
95
- lookupQueryValues: string | null;
96
- metadata: Record<string, string>;
97
- }
98
-
99
- export interface ChillDtoSchemaRelationLabel extends JsonObject {
100
- labelGuid: string | null;
101
- primaryDefaultText: string;
102
- secondaryDefaultText: string;
103
- }
104
-
105
- export interface ChillDtoSchemaRelation extends JsonObject {
106
- chillType: string;
107
- chillQuery: string;
108
- fixedValues: Record<string, string>;
109
- fixedQueryValues: Record<string, string>;
110
- relationLabel: ChillDtoSchemaRelationLabel;
111
- }
112
-
113
- export interface ChillDtoSchema extends JsonObject {
114
- chillType: string;
115
- chillViewCode: string;
116
- displayName: string;
117
- handleAttachments: boolean;
118
- enableMCP: boolean;
119
- mcpDescription: string;
120
- metadata: Record<string, string>;
121
- queryRelatedChillType: string | null;
122
- properties: ChillDtoPropertySchema[];
123
- relations: ChillDtoSchemaRelation[];
124
- }
125
-
126
- export interface ChillDtoSchemaListItem extends JsonObject {
127
- name: string;
128
- chillType: string;
129
- type: string;
130
- relatedChillType: string | null;
131
- }
132
-
133
- export interface ChillDtoEntityOptions extends JsonObject {
134
- chillType: string;
135
- checksumEnabled: boolean;
136
- handleAttachments: boolean;
137
- labelFormatString: string | null;
138
- shortLabelFormatString: string | null;
139
- fullTextContentFormatString: string | null;
140
- enableMCP: boolean;
141
- mcpDescription: string | null;
142
- changeLogEnabled: boolean;
143
- }
144
-
145
- export interface ChillOrdering extends JsonObject {
146
- propertyName: string;
147
- direction: string;
148
- }
149
-
150
- export interface ChillPagination extends JsonObject {
151
- pageSize: number;
152
- pageNumber: number;
153
- }
154
-
155
- export interface ChillDtoProperty extends JsonObject {
156
- name: string;
157
- }
158
-
159
- export interface ChillDtoEntity extends JsonObject {
160
- guid: string;
161
- position: number;
162
- chillType: string;
163
- label: string | null;
164
- shortLabel: string | null;
165
- properties: Record<string, JsonValue>;
166
- }
167
-
168
- export interface ChillDtoQuery extends JsonObject {
169
- chillType: string;
170
- properties: Record<string, JsonValue>;
171
- resultProperties: ChillDtoProperty[] | null;
172
- pagination: ChillPagination | null;
173
- ordering: ChillOrdering | null;
174
- lightweightRequired: boolean | null;
175
- results: ChillDtoEntity[];
176
- }
177
-
178
- export interface ChillDtoMenuItem extends JsonObject {
179
- guid: string;
180
- positionNo: number;
181
- title: string;
182
- description: string | null;
183
- parent: ChillDtoMenuItem | null;
184
- componentName: string;
185
- componentConfigurationJson: string | null;
186
- menuHierarchy: string;
187
- }
188
-
189
- export interface ChillValidationError extends JsonObject {
190
- fieldName: string | null;
191
- message: string | null;
192
- }
193
-
194
- export interface AuthUserListItem extends JsonObject {
195
- guid: string;
196
- externalId: string;
197
- userName: string;
198
- displayName: string;
199
- displayCultureName: string;
200
- displayTimeZone: string;
201
- displayDateFormat: string;
202
- displayNumberFormat: string;
203
- preferredTheme: string;
204
- isActive: boolean;
205
- canManagePermissions: boolean;
206
- canManageSchema: boolean;
207
- menuHierarchy: string;
208
- }
209
-
210
- export interface AuthRoleListItem extends JsonObject {
211
- guid: string;
212
- name: string;
213
- description: string;
214
- isActive: boolean;
215
- menuHierarchy: string;
216
- }
217
-
218
- export interface AuthTokenResponse extends JsonObject {
219
- accessToken: string;
220
- accessTokenIssuedUtc: string;
221
- accessTokenExpiresUtc: string;
222
- refreshToken: string;
223
- refreshTokenExpiresUtc: string;
224
- userId: string;
225
- userName: string;
226
- }
227
-
228
- /** Display preferences resolved for the current authenticated user. */
229
- export interface ChillUserPreferences extends JsonObject {
230
- displayCultureName: string;
231
- displayTimeZone: string;
232
- displayDateFormat: string;
233
- displayNumberFormat: string;
234
- preferredTheme: string;
235
- }
236
-
237
- export interface RegisterAuthIdentityRequest extends JsonObject {
238
- userName: string;
239
- email: string | null;
240
- password: string;
241
- displayName: string;
242
- displayCultureName: string;
243
- createChillAuthUser: boolean;
244
- }
245
-
246
- export interface LoginAuthIdentityRequest extends JsonObject {
247
- userNameOrEmail: string;
248
- password: string;
249
- }
250
-
251
- export interface RefreshAuthTokenRequest extends JsonObject {
252
- refreshToken: string;
253
- }
254
-
255
- export interface ChangePasswordRequest extends JsonObject {
256
- currentPassword: string;
257
- newPassword: string;
258
- }
259
-
260
- export interface ChangePasswordResponse extends JsonObject {
261
- succeeded: boolean;
262
- }
263
-
264
- export interface RequestPasswordResetRequest extends JsonObject {
265
- userNameOrEmail: string;
266
- }
267
-
268
- export interface PasswordResetTokenResponse extends JsonObject {
269
- isAccepted: boolean;
270
- userId: string | null;
271
- resetToken: string | null;
272
- }
273
-
274
- export interface ResetPasswordRequest extends JsonObject {
275
- userId: string;
276
- resetToken: string;
277
- newPassword: string;
278
- }
279
-
280
- export interface ResetPasswordResponse extends JsonObject {
281
- succeeded: boolean;
282
- }
283
-
284
- export const PermissionEffect = {
285
- Allow: 1,
286
- Deny: 2
287
- } as const;
288
-
289
- export type PermissionEffect = (typeof PermissionEffect)[keyof typeof PermissionEffect];
290
-
291
- export const PermissionAction = {
292
- FullControl: 0,
293
- Query: 1,
294
- Create: 2,
295
- Update: 3,
296
- Delete: 4,
297
- See: 5,
298
- Modify: 6
299
- } as const;
300
-
301
- export type PermissionAction = (typeof PermissionAction)[keyof typeof PermissionAction];
302
-
303
- export const PermissionScope = {
304
- Module: 1,
305
- Entity: 2,
306
- Property: 3
307
- } as const;
308
-
309
- export type PermissionScope = (typeof PermissionScope)[keyof typeof PermissionScope];
310
-
311
- export interface AuthPermissionRule extends JsonObject {
312
- guid: string;
313
- userGuid: string | null;
314
- roleGuid: string | null;
315
- effect: PermissionEffect;
316
- action: PermissionAction;
317
- scope: PermissionScope;
318
- module: string;
319
- entityName: string | null;
320
- propertyName: string | null;
321
- appliesToAllProperties: boolean;
322
- description: string;
323
- createdUtc: string;
324
- }
325
-
326
- export interface AuthRolePermissions extends AuthRoleListItem {
327
- permissions: AuthPermissionRule[];
328
- }
329
-
330
- export interface GetAuthPermissionsResponse extends JsonObject {
331
- user: AuthUserListItem | null;
332
- permissions: AuthPermissionRule[];
333
- roles: AuthRolePermissions[];
334
- }
335
-
336
- export interface AuthUserDetailsResponse extends AuthUserListItem {
337
- roles: AuthRoleListItem[];
338
- permissions: AuthPermissionRule[];
339
- }
340
-
341
- export interface AuthRoleDetailsResponse extends AuthRoleListItem {
342
- users: AuthUserListItem[];
343
- permissions: AuthPermissionRule[];
344
- }
345
-
346
- export interface AuthPermissionRuleItem extends JsonObject {
347
- guid: string | null;
348
- effect: PermissionEffect;
349
- action: PermissionAction;
350
- scope: PermissionScope;
351
- module: string;
352
- entityName: string | null;
353
- propertyName: string | null;
354
- appliesToAllProperties: boolean;
355
- description: string;
356
- }
357
-
358
- export interface SetAuthUserRequest extends JsonObject {
359
- guid: string | null;
360
- externalId: string;
361
- userName: string;
362
- displayName: string;
363
- displayCultureName: string;
364
- displayTimeZone: string;
365
- displayDateFormat: string;
366
- displayNumberFormat: string;
367
- preferredTheme: string;
368
- isActive: boolean;
369
- canManagePermissions: boolean;
370
- canManageSchema: boolean;
371
- menuHierarchy: string;
372
- roleGuids: string[];
373
- permissions: AuthPermissionRuleItem[];
374
- }
375
-
376
- export interface CreateAuthUserRequest extends JsonObject {
377
- externalId: string;
378
- email: string;
379
- userName: string;
380
- displayName: string;
381
- displayCultureName: string;
382
- displayTimeZone: string;
383
- displayDateFormat: string;
384
- displayNumberFormat: string;
385
- preferredTheme: string;
386
- isActive: boolean;
387
- canManagePermissions: boolean;
388
- canManageSchema: boolean;
389
- menuHierarchy: string;
390
- }
391
-
392
- export interface UpdateAuthUserRequest extends JsonObject {
393
- externalId: string;
394
- userName: string;
395
- displayName: string;
396
- displayCultureName: string;
397
- displayTimeZone: string;
398
- displayDateFormat: string;
399
- displayNumberFormat: string;
400
- preferredTheme: string;
401
- isActive: boolean;
402
- canManagePermissions: boolean;
403
- canManageSchema: boolean;
404
- menuHierarchy: string;
405
- }
406
-
407
- export interface SetAuthRoleRequest extends JsonObject {
408
- guid: string | null;
409
- name: string;
410
- description: string;
411
- isActive: boolean;
412
- menuHierarchy: string;
413
- userGuids: string[];
414
- permissions: AuthPermissionRuleItem[];
415
- }
416
-
417
- export interface CreateAuthRoleRequest extends JsonObject {
418
- name: string;
419
- description: string;
420
- isActive: boolean;
421
- menuHierarchy: string;
422
- }
423
-
424
- export interface UpdateAuthRoleRequest extends CreateAuthRoleRequest {}
425
-
426
- export interface CreateAuthPermissionRuleRequest extends JsonObject {
427
- userGuid: string | null;
428
- roleGuid: string | null;
429
- effect: PermissionEffect;
430
- action: PermissionAction;
431
- scope: PermissionScope;
432
- module: string;
433
- entityName: string | null;
434
- propertyName: string | null;
435
- appliesToAllProperties: boolean;
436
- description: string;
437
- }
438
-
439
- export interface UpdateAuthPermissionRuleRequest extends CreateAuthPermissionRuleRequest {}
440
-
441
- export interface ChillSharpClientOptions {
442
- accessToken?: string;
443
- username?: string;
444
- password?: string;
445
- cultureName?: string;
446
- apiBasePath?: string;
447
- fetchImpl?: typeof fetch;
448
- signalRWithCredentials?: boolean;
449
- }
450
-
451
- export interface ChillAttachmentUploadFile {
452
- fileName: string;
453
- content: Blob | ArrayBuffer | Uint8Array | string;
454
- contentType?: string;
455
- }
456
-
457
- export interface ChillAttachmentUploadOptions {
458
- title?: string | null;
459
- description?: string | null;
460
- isPublic?: boolean;
461
- }
462
-
463
- export type ChillEntityChangeAction = "CREATED" | "UPDATED" | "DELETED";
464
-
465
- export interface ChillEntityChangeNotification extends JsonObject {
466
- chillType: string;
467
- guid: string;
468
- action: ChillEntityChangeAction;
469
- }
470
-
471
- export type ChillEntityChangeCallback = (
472
- changes: ChillEntityChangeNotification[]
473
- ) => void | Promise<void>;
474
-
475
- export interface ChillEntityChangeSubscription {
476
- chillType: string;
477
- guid: string | null;
478
- unsubscribe(): Promise<void>;
479
- }
480
-
481
- interface TokenState {
482
- accessToken: string | null;
483
- accessTokenIssuedUtc: Date | null;
484
- accessTokenExpiresUtc: Date | null;
485
- refreshToken: string | null;
486
- refreshTokenExpiresUtc: Date | null;
487
- }
488
-
489
- interface LocalEntityChangeSubscription {
490
- id: string;
491
- chillType: string;
492
- guid: string | null;
493
- callback: ChillEntityChangeCallback;
494
- }
495
-
496
- export class ChillSharpClient {
497
- static readonly API_BASE_PATH = API_BASE_PATH;
498
- private static readonly attachmentEntityChillType = "ChillSharp.Attachment.Model.Attachment";
499
- private static readonly attachmentQueryChillType = "ChillSharp.Attachment.Query.AttachmentQuery";
500
- private readonly baseUrl: string;
501
- private readonly fetchImpl: typeof fetch;
502
- private cultureName: string | null;
503
- private readonly signalRWithCredentials: boolean;
504
-
505
- private username: string | null;
506
- private password: string | null;
507
- private refreshPromise: Promise<AuthTokenResponse> | null = null;
508
- private tokenState: TokenState;
509
- private notificationConnection: HubConnection | null = null;
510
- private readonly entityChangeSubscriptions = new Map<string, LocalEntityChangeSubscription>();
511
- private readonly entityChangeRegistrationCounts = new Map<string, number>();
512
- private entityChangeSubscriptionSequence = 0;
513
-
514
- constructor(baseUrl: string, options: ChillSharpClientOptions = {}) {
515
- this.baseUrl = this.normalizeBaseUrl(baseUrl, options.apiBasePath);
516
- this.fetchImpl = options.fetchImpl ?? fetch;
517
- this.username = this.normalizeOptionalValue(options.username);
518
- this.password = this.normalizeOptionalValue(options.password);
519
- this.cultureName = this.normalizeOptionalValue(options.cultureName);
520
- this.signalRWithCredentials = options.signalRWithCredentials ?? true;
521
- this.tokenState = {
522
- accessToken: this.normalizeOptionalValue(options.accessToken),
523
- accessTokenIssuedUtc: null,
524
- accessTokenExpiresUtc: null,
525
- refreshToken: null,
526
- refreshTokenExpiresUtc: null
527
- };
528
- }
529
-
530
- query(dtoQuery: JsonObject): Promise<JsonObject> {
531
- return this.sendJson<JsonObject>("POST", this.buildChillUrl("query"), dtoQuery);
532
- }
533
-
534
- lookup(dtoQuery: JsonObject): Promise<JsonObject> {
535
- return this.sendJson<JsonObject>("POST", this.buildChillUrl("lookup"), dtoQuery);
536
- }
537
-
538
- find(dtoEntity: JsonObject): Promise<JsonObject | null> {
539
- return this.sendJson<JsonObject | null>("POST", this.buildChillUrl("find"), dtoEntity);
540
- }
541
-
542
- create(dtoEntity: JsonObject): Promise<JsonObject> {
543
- return this.sendJson<JsonObject>("POST", this.buildChillUrl("create"), dtoEntity);
544
- }
545
-
546
- update(dtoEntity: JsonObject): Promise<JsonObject> {
547
- return this.sendJson<JsonObject>("POST", this.buildChillUrl("update"), dtoEntity);
548
- }
549
-
550
- async delete(dtoEntity: JsonObject): Promise<void> {
551
- await this.sendJson("POST", this.buildChillUrl("delete"), dtoEntity, false);
552
- }
553
-
554
- autocomplete(dto: JsonObject): Promise<JsonObject> {
555
- return this.sendJson<JsonObject>("POST", this.buildChillUrl("autocomplete"), dto);
556
- }
557
-
558
- validate(dto: JsonObject): Promise<ChillValidationError[]> {
559
- return this.sendJson<ChillValidationError[]>("POST", this.buildChillUrl("validate"), dto);
560
- }
561
-
562
- chunk(operations: JsonObject[]): Promise<JsonObject[]> {
563
- return this.sendJson<JsonObject[]>("POST", this.buildChillUrl("chunk"), operations);
564
- }
565
-
566
- uploadAttachment(
567
- targetEntity: JsonObject,
568
- file: ChillAttachmentUploadFile,
569
- options: ChillAttachmentUploadOptions = {}
570
- ): Promise<JsonObject[]> {
571
- return this.uploadAttachments(targetEntity, [file], options);
572
- }
573
-
574
- async uploadAttachments(
575
- targetEntity: JsonObject,
576
- files: ChillAttachmentUploadFile[],
577
- options: ChillAttachmentUploadOptions = {}
578
- ): Promise<JsonObject[]> {
579
- const target = this.getAttachmentTargetInfo(targetEntity);
580
- if (!Array.isArray(files) || files.length === 0) {
581
- throw new Error("files is required.");
582
- }
583
-
584
- const form = new FormData();
585
- form.append("attachToChillType", target.chillType);
586
- form.append("attachToGuid", target.guid);
587
-
588
- const normalizedTitle = this.normalizeOptionalValue(options.title ?? undefined);
589
- if (normalizedTitle) {
590
- form.append("title", normalizedTitle);
591
- }
592
-
593
- const normalizedDescription = this.normalizeOptionalValue(options.description ?? undefined);
594
- if (normalizedDescription) {
595
- form.append("description", normalizedDescription);
596
- }
597
-
598
- form.append("public", options.isPublic ? "true" : "false");
599
-
600
- for (const file of files) {
601
- form.append(
602
- "file",
603
- this.toAttachmentBlob(file),
604
- this.normalizeRequiredValue(file.fileName, "file.fileName")
605
- );
606
- }
607
-
608
- return this.sendJson<JsonObject[]>(
609
- "POST",
610
- this.buildAttachmentUrl("attachment/upload"),
611
- form,
612
- true,
613
- false,
614
- false
615
- );
616
- }
617
-
618
- async getAttachments(targetEntity: JsonObject): Promise<JsonObject[]> {
619
- const target = this.getAttachmentTargetInfo(targetEntity);
620
- const response = await this.query({
621
- chillType: ChillSharpClient.attachmentQueryChillType,
622
- properties: {
623
- attachToChillType: target.chillType,
624
- attachToGuid: target.guid
625
- }
626
- });
627
-
628
- const results = this.readValue(response, "results");
629
- return Array.isArray(results)
630
- ? results.filter((item): item is JsonObject => !!item && typeof item === "object" && !Array.isArray(item))
631
- : [];
632
- }
633
-
634
- downloadAttachment(attachmentOrGuid: JsonObject | string): Promise<Blob> {
635
- const attachmentGuid = typeof attachmentOrGuid === "string"
636
- ? this.normalizeRequiredValue(attachmentOrGuid, "attachmentGuid")
637
- : this.getAttachmentGuid(attachmentOrGuid);
638
-
639
- return this.sendBlob(
640
- "GET",
641
- this.buildAttachmentUrl(`attachment/download?guid=${encodeURIComponent(attachmentGuid)}`),
642
- this.canUseAuthentication() ? false : true
643
- );
644
- }
645
-
646
- version(): string {
647
- return CHILL_SHARP_TS_CLIENT_VERSION;
648
- }
649
-
650
- /** Updates the default culture used by calls that do not provide one explicitly. */
651
- setCultureName(cultureName?: string | null): void {
652
- this.cultureName = this.normalizeOptionalValue(cultureName);
653
- }
654
-
655
- test(): Promise<string> {
656
- return this.sendText("GET", this.buildApiUrl("test"), true);
657
- }
658
-
659
- getSchema(chillType: string, chillViewCode: string, cultureName?: string, update = false): Promise<ChillDtoSchema | null> {
660
- const encodedType = encodeURIComponent(this.normalizeRequiredValue(chillType, "chillType"));
661
- const encodedView = encodeURIComponent(this.normalizeRequiredValue(chillViewCode, "chillViewCode"));
662
- const effectiveCultureName = this.normalizeOptionalValue(cultureName) ?? this.cultureName;
663
-
664
- let relativeUrl = `get-schema?chillType=${encodedType}&chillViewCode=${encodedView}`;
665
- if (effectiveCultureName) {
666
- relativeUrl += `&cultureName=${encodeURIComponent(effectiveCultureName)}`;
667
- }
668
- if (update) {
669
- relativeUrl += "&update=true";
670
- }
671
-
672
- return this.sendJson<ChillDtoSchema | null>("GET", this.buildSchemaUrl(relativeUrl));
673
- }
674
-
675
- getSchemaList(cultureName?: string): Promise<ChillDtoSchemaListItem[]> {
676
- const effectiveCultureName = this.normalizeOptionalValue(cultureName) ?? this.cultureName;
677
- let relativeUrl = "get-schema-list";
678
- if (effectiveCultureName) {
679
- relativeUrl += `?cultureName=${encodeURIComponent(effectiveCultureName)}`;
680
- }
681
-
682
- return this.sendJson<ChillDtoSchemaListItem[]>("GET", this.buildSchemaUrl(relativeUrl));
683
- }
684
-
685
- setSchema(schema: ChillDtoSchema): Promise<ChillDtoSchema | null> {
686
- return this.sendJson<ChillDtoSchema | null>("POST", this.buildSchemaUrl("set-schema"), schema);
687
- }
688
-
689
- getEntityOptions(chillType: string): Promise<ChillDtoEntityOptions> {
690
- const encodedType = encodeURIComponent(this.normalizeRequiredValue(chillType, "chillType"));
691
- return this.sendJson<ChillDtoEntityOptions>("GET", this.buildSchemaUrl(`get-entity-options?chillType=${encodedType}`));
692
- }
693
-
694
- setEntityOptions(entityOptions: ChillDtoEntityOptions): Promise<ChillDtoEntityOptions> {
695
- return this.sendJson<ChillDtoEntityOptions>("POST", this.buildSchemaUrl("set-entity-options"), entityOptions);
696
- }
697
-
698
- getMenu(parentGuid?: string | null): Promise<ChillDtoMenuItem[]> {
699
- const normalizedParentGuid = this.normalizeQueryValue(parentGuid);
700
- const suffix = normalizedParentGuid === null ? "" : `?parentGuid=${encodeURIComponent(normalizedParentGuid)}`;
701
- return this.sendJson<ChillDtoMenuItem[]>("GET", this.buildSchemaUrl(`get-menu${suffix}`));
702
- }
703
-
704
- setMenu(menuItem: ChillDtoMenuItem): Promise<ChillDtoMenuItem> {
705
- return this.sendJson<ChillDtoMenuItem>("POST", this.buildSchemaUrl("set-menu"), menuItem);
706
- }
707
-
708
-
709
- async deleteMenu(menuItemGuid: string): Promise<void> {
710
- const normalizedMenuItemGuid = this.normalizeRequiredValue(menuItemGuid, "menuItemGuid");
711
- await this.sendJson("DELETE", this.buildSchemaUrl(`delete-menu?menuItemGuid=${encodeURIComponent(normalizedMenuItemGuid)}`), undefined, false);
712
- }
713
- getText(request: GetTextRequest): Promise<GetTextResponse | null> {
714
- return this.sendJson<GetTextResponse | null>("POST", this.buildI18nUrl("get-text"), this.prepareGetTextRequest(request), true, true);
715
- }
716
-
717
- getTexts(requests: GetTextRequest[]): Promise<Array<GetTextResponse | null>> {
718
- if (!Array.isArray(requests)) {
719
- throw new Error("requests is required.");
720
- }
721
-
722
- return this.sendJson<Array<GetTextResponse | null>>(
723
- "POST",
724
- this.buildI18nUrl("get-multiple-text"),
725
- requests.map((request) => this.prepareGetTextRequest(request))
726
- );
727
- }
728
-
729
- setText(payload: JsonObject): Promise<GetTextResponse> {
730
- return this.sendJson<GetTextResponse>("PUT", this.buildI18nUrl("set-text"), payload);
731
- }
732
-
733
- async subscribeToEntityChanges(
734
- chillType: string,
735
- callback: ChillEntityChangeCallback,
736
- guid?: string | null
737
- ): Promise<ChillEntityChangeSubscription> {
738
- if (typeof callback !== "function") {
739
- throw new Error("callback is required.");
740
- }
741
-
742
- const normalizedChillType = this.normalizeRequiredValue(chillType, "chillType");
743
- const normalizedGuid = this.normalizeOptionalValue(guid);
744
- const connection = await this.ensureNotificationConnection();
745
- const registrationKey = this.buildEntityChangeRegistrationKey(normalizedChillType, normalizedGuid);
746
-
747
- const registrationCount = this.entityChangeRegistrationCounts.get(registrationKey) ?? 0;
748
- if (registrationCount === 0) {
749
- await connection.invoke("Register", normalizedChillType, normalizedGuid);
750
- }
751
- this.entityChangeRegistrationCounts.set(registrationKey, registrationCount + 1);
752
-
753
- const subscriptionId = `entity-change-${++this.entityChangeSubscriptionSequence}`;
754
- this.entityChangeSubscriptions.set(subscriptionId, {
755
- id: subscriptionId,
756
- chillType: normalizedChillType,
757
- guid: normalizedGuid,
758
- callback
759
- });
760
-
761
- return {
762
- chillType: normalizedChillType,
763
- guid: normalizedGuid,
764
- unsubscribe: async () => {
765
- await this.unsubscribeFromEntityChanges(subscriptionId);
766
- }
767
- };
768
- }
769
-
770
- async disconnectEntityChanges(): Promise<void> {
771
- this.entityChangeSubscriptions.clear();
772
- this.entityChangeRegistrationCounts.clear();
773
-
774
- if (!this.notificationConnection) {
775
- return;
776
- }
777
-
778
- const connection = this.notificationConnection;
779
- this.notificationConnection = null;
780
- await connection.stop();
781
- }
782
-
783
- async registerAuthAccount(payload: RegisterAuthIdentityRequest): Promise<AuthTokenResponse> {
784
- const response = await this.sendAuthJson<AuthTokenResponse>("POST", "register", payload, true, true);
785
- this.applyAuthToken(response, true);
786
- return response;
787
- }
788
-
789
- async loginAuthAccount(payload: LoginAuthIdentityRequest): Promise<AuthTokenResponse> {
790
- const response = await this.sendAuthJson<AuthTokenResponse>("POST", "login", payload, true, true);
791
- this.applyAuthToken(response, true);
792
- return response;
793
- }
794
-
795
- refreshAuthAccount(): Promise<AuthTokenResponse> {
796
- return this.getAuthTokenIfNecessary(true);
797
- }
798
-
799
- async logoutAuthAccount(): Promise<void> {
800
- await this.sendAuthJson("POST", "logout", undefined, false);
801
- this.clearAuthToken();
802
- }
803
-
804
- changeAuthPassword(payload: ChangePasswordRequest): Promise<ChangePasswordResponse> {
805
- return this.sendAuthJson<ChangePasswordResponse>("POST", "change-password", payload);
806
- }
807
-
808
- requestAuthPasswordReset(payload: RequestPasswordResetRequest): Promise<PasswordResetTokenResponse> {
809
- return this.sendAuthJson<PasswordResetTokenResponse>("POST", "request-password-reset", payload, true, true);
810
- }
811
-
812
- resetAuthPassword(payload: ResetPasswordRequest): Promise<ResetPasswordResponse> {
813
- return this.sendAuthJson<ResetPasswordResponse>("POST", "reset-password", payload, true, true);
814
- }
815
-
816
- getAuthPermissions(): Promise<GetAuthPermissionsResponse> {
817
- return this.sendAuthJson<GetAuthPermissionsResponse>("GET", "get-permissions");
818
- }
819
-
820
- getCurrentUserPreferences(): Promise<ChillUserPreferences> {
821
- return this.sendAuthJson<ChillUserPreferences>("GET", "current-user-preferences");
822
- }
823
-
824
- getAuthUserList(): Promise<AuthUserListItem[]> {
825
- return this.sendAuthJson<AuthUserListItem[]>("GET", "get-user-list");
826
- }
827
-
828
- async getAuthUser(userGuid: string): Promise<AuthUserDetailsResponse> {
829
- const normalizedUserGuid = this.normalizeRequiredValue(userGuid, "userGuid");
830
- const [user, roles, permissions] = await Promise.all([
831
- this.sendAuthJson<AuthUserListItem>("GET", `users/${encodeURIComponent(normalizedUserGuid)}`),
832
- this.getAuthUserRoles(normalizedUserGuid),
833
- this.getAuthPermissionRules(normalizedUserGuid, null)
834
- ]);
835
-
836
- return {
837
- ...user,
838
- roles,
839
- permissions
840
- };
841
- }
842
-
843
- async setAuthUser(payload: SetAuthUserRequest): Promise<AuthUserDetailsResponse> {
844
- const userGuid = this.normalizeOptionalValue(payload.guid);
845
- const basePayload = {
846
- externalId: payload.externalId,
847
- userName: payload.userName,
848
- displayName: payload.displayName,
849
- displayCultureName: payload.displayCultureName,
850
- displayTimeZone: payload.displayTimeZone,
851
- displayDateFormat: payload.displayDateFormat,
852
- displayNumberFormat: payload.displayNumberFormat,
853
- preferredTheme: payload.preferredTheme,
854
- isActive: payload.isActive,
855
- canManagePermissions: payload.canManagePermissions,
856
- canManageSchema: payload.canManageSchema,
857
- menuHierarchy: payload.menuHierarchy
858
- };
859
-
860
- const user = userGuid
861
- ? await this.updateAuthUser(userGuid, basePayload)
862
- : await this.createAuthUser({
863
- ...basePayload,
864
- email: "",
865
- externalId: payload.externalId
866
- });
867
-
868
- if (!user) {
869
- throw new ChillSharpClientError("Auth user was not found after setAuthUser execution.");
870
- }
871
-
872
- await this.syncUserRoles(user.guid, payload.roleGuids);
873
- await this.syncUserPermissions(user.guid, payload.permissions);
874
- return this.getAuthUser(user.guid);
875
- }
876
-
877
- getAuthRoleList(): Promise<AuthRoleListItem[]> {
878
- return this.sendAuthJson<AuthRoleListItem[]>("GET", "get-role-list");
879
- }
880
-
881
- getAuthModuleList(): Promise<string[]> {
882
- return this.sendAuthJson<string[]>("GET", "get-module-list");
883
- }
884
-
885
- getAuthEntityList(module?: string | null): Promise<string[]> {
886
- const normalizedModule = this.normalizeQueryValue(module);
887
- const suffix = normalizedModule === null ? "" : `?module=${encodeURIComponent(normalizedModule)}`;
888
- return this.sendAuthJson<string[]>("GET", `get-entity-list${suffix}`);
889
- }
890
-
891
- getAuthQueryList(module?: string | null): Promise<string[]> {
892
- const normalizedModule = this.normalizeQueryValue(module);
893
- const suffix = normalizedModule === null ? "" : `?module=${encodeURIComponent(normalizedModule)}`;
894
- return this.sendAuthJson<string[]>("GET", `get-query-list${suffix}`);
895
- }
896
-
897
- getAuthModuleEntityList(module?: string | null): Promise<string[]> {
898
- return this.getAuthEntityList(module);
899
- }
900
-
901
-
902
- getAuthPropertyList(chillType: string): Promise<string[]> {
903
- const normalizedChillType = this.normalizeRequiredValue(chillType, "chillType");
904
- return this.sendAuthJson<string[]>("GET", `get-property-list?chillType=${encodeURIComponent(normalizedChillType)}`);
905
- }
906
-
907
- async getAuthRole(roleGuid: string): Promise<AuthRoleDetailsResponse> {
908
- const normalizedRoleGuid = this.normalizeRequiredValue(roleGuid, "roleGuid");
909
- const [role, permissions, users] = await Promise.all([
910
- this.sendAuthJson<AuthRoleListItem>("GET", `roles/${encodeURIComponent(normalizedRoleGuid)}`),
911
- this.getAuthPermissionRules(null, normalizedRoleGuid),
912
- this.getUsersAssignedToRole(normalizedRoleGuid)
913
- ]);
914
-
915
- return {
916
- ...role,
917
- users,
918
- permissions
919
- };
920
- }
921
-
922
- async setAuthRole(payload: SetAuthRoleRequest): Promise<AuthRoleDetailsResponse> {
923
- const roleGuid = this.normalizeOptionalValue(payload.guid);
924
- const basePayload = {
925
- name: payload.name,
926
- description: payload.description,
927
- isActive: payload.isActive,
928
- menuHierarchy: payload.menuHierarchy
929
- };
930
-
931
- const role = roleGuid
932
- ? await this.updateAuthRole(roleGuid, basePayload)
933
- : await this.createAuthRole(basePayload);
934
-
935
- if (!role) {
936
- throw new ChillSharpClientError("Auth role was not found after setAuthRole execution.");
937
- }
938
-
939
- await this.syncRoleUsers(role.guid, payload.userGuids);
940
- await this.syncRolePermissions(role.guid, payload.permissions);
941
- return this.getAuthRole(role.guid);
942
- }
943
-
944
- getAuthUsers(): Promise<AuthUserListItem[]> {
945
- return this.sendAuthJson<AuthUserListItem[]>("GET", "users");
946
- }
947
-
948
- createAuthUser(payload: CreateAuthUserRequest): Promise<AuthUserListItem> {
949
- return this.sendAuthJson<AuthUserListItem>("POST", "users", payload);
950
- }
951
-
952
- updateAuthUser(userGuid: string, payload: UpdateAuthUserRequest): Promise<AuthUserListItem | null> {
953
- const normalizedUserGuid = this.normalizeRequiredValue(userGuid, "userGuid");
954
- return this.sendAuthJson<AuthUserListItem | null>("PUT", `users/${encodeURIComponent(normalizedUserGuid)}`, payload);
955
- }
956
-
957
- async deleteAuthUser(userGuid: string): Promise<void> {
958
- const normalizedUserGuid = this.normalizeRequiredValue(userGuid, "userGuid");
959
- await this.sendAuthJson("DELETE", `users/${encodeURIComponent(normalizedUserGuid)}`, undefined, false);
960
- }
961
-
962
- getAuthUserRoles(userGuid: string): Promise<AuthRoleListItem[]> {
963
- const normalizedUserGuid = this.normalizeRequiredValue(userGuid, "userGuid");
964
- return this.sendAuthJson<AuthRoleListItem[]>("GET", `users/${encodeURIComponent(normalizedUserGuid)}/roles`);
965
- }
966
-
967
- async assignAuthRole(userGuid: string, roleGuid: string): Promise<void> {
968
- const normalizedUserGuid = this.normalizeRequiredValue(userGuid, "userGuid");
969
- const normalizedRoleGuid = this.normalizeRequiredValue(roleGuid, "roleGuid");
970
- await this.sendAuthJson("PUT", `users/${encodeURIComponent(normalizedUserGuid)}/roles/${encodeURIComponent(normalizedRoleGuid)}`, undefined, false);
971
- }
972
-
973
- async removeAuthRole(userGuid: string, roleGuid: string): Promise<void> {
974
- const normalizedUserGuid = this.normalizeRequiredValue(userGuid, "userGuid");
975
- const normalizedRoleGuid = this.normalizeRequiredValue(roleGuid, "roleGuid");
976
- await this.sendAuthJson("DELETE", `users/${encodeURIComponent(normalizedUserGuid)}/roles/${encodeURIComponent(normalizedRoleGuid)}`, undefined, false);
977
- }
978
-
979
- getAuthRoles(): Promise<AuthRoleListItem[]> {
980
- return this.sendAuthJson<AuthRoleListItem[]>("GET", "roles");
981
- }
982
-
983
- createAuthRole(payload: CreateAuthRoleRequest): Promise<AuthRoleListItem> {
984
- return this.sendAuthJson<AuthRoleListItem>("POST", "roles", payload);
985
- }
986
-
987
- updateAuthRole(roleGuid: string, payload: UpdateAuthRoleRequest): Promise<AuthRoleListItem | null> {
988
- const normalizedRoleGuid = this.normalizeRequiredValue(roleGuid, "roleGuid");
989
- return this.sendAuthJson<AuthRoleListItem | null>("PUT", `roles/${encodeURIComponent(normalizedRoleGuid)}`, payload);
990
- }
991
-
992
- async deleteAuthRole(roleGuid: string): Promise<void> {
993
- const normalizedRoleGuid = this.normalizeRequiredValue(roleGuid, "roleGuid");
994
- await this.sendAuthJson("DELETE", `roles/${encodeURIComponent(normalizedRoleGuid)}`, undefined, false);
995
- }
996
-
997
- getAuthPermissionRules(userGuid?: string | null, roleGuid?: string | null): Promise<AuthPermissionRule[]> {
998
- const queryParts: string[] = [];
999
- const normalizedUserGuid = this.normalizeOptionalValue(userGuid);
1000
- const normalizedRoleGuid = this.normalizeOptionalValue(roleGuid);
1001
- if (normalizedUserGuid) {
1002
- queryParts.push(`userGuid=${encodeURIComponent(normalizedUserGuid)}`);
1003
- }
1004
- if (normalizedRoleGuid) {
1005
- queryParts.push(`roleGuid=${encodeURIComponent(normalizedRoleGuid)}`);
1006
- }
1007
-
1008
- const suffix = queryParts.length === 0 ? "" : `?${queryParts.join("&")}`;
1009
- return this.sendAuthJson<AuthPermissionRule[]>("GET", `permissions${suffix}`);
1010
- }
1011
-
1012
- getAuthPermissionRule(ruleGuid: string): Promise<AuthPermissionRule | null> {
1013
- const normalizedRuleGuid = this.normalizeRequiredValue(ruleGuid, "ruleGuid");
1014
- return this.sendAuthJson<AuthPermissionRule | null>("GET", `permissions/${encodeURIComponent(normalizedRuleGuid)}`);
1015
- }
1016
-
1017
- createAuthPermissionRule(payload: CreateAuthPermissionRuleRequest): Promise<AuthPermissionRule> {
1018
- return this.sendAuthJson<AuthPermissionRule>("POST", "permissions", payload);
1019
- }
1020
-
1021
- updateAuthPermissionRule(ruleGuid: string, payload: UpdateAuthPermissionRuleRequest): Promise<AuthPermissionRule | null> {
1022
- const normalizedRuleGuid = this.normalizeRequiredValue(ruleGuid, "ruleGuid");
1023
- return this.sendAuthJson<AuthPermissionRule | null>("PUT", `permissions/${encodeURIComponent(normalizedRuleGuid)}`, payload);
1024
- }
1025
-
1026
- async deleteAuthPermissionRule(ruleGuid: string): Promise<void> {
1027
- const normalizedRuleGuid = this.normalizeRequiredValue(ruleGuid, "ruleGuid");
1028
- await this.sendAuthJson("DELETE", `permissions/${encodeURIComponent(normalizedRuleGuid)}`, undefined, false);
1029
- }
1030
-
1031
- private prepareGetTextRequest(request: GetTextRequest): GetTextRequest {
1032
- if (!request || typeof request !== "object") {
1033
- throw new Error("request is required.");
1034
- }
1035
-
1036
- const effectiveCultureName = this.normalizeOptionalValue(this.readString(request, "cultureName")) ?? this.cultureName;
1037
- if (!effectiveCultureName) {
1038
- throw new Error("cultureName is required.");
1039
- }
1040
-
1041
- return {
1042
- labelGuid: this.normalizeRequiredValue(this.readString(request, "labelGuid"), "labelGuid"),
1043
- cultureName: effectiveCultureName,
1044
- primaryCultureName: this.readString(request, "primaryCultureName") ?? "",
1045
- primaryDefaultText: this.readString(request, "primaryDefaultText") ?? "",
1046
- secondaryCultureName: this.readString(request, "secondaryCultureName") ?? "",
1047
- secondaryDefaultText: this.readString(request, "secondaryDefaultText") ?? ""
1048
- };
1049
- }
1050
-
1051
- private sendAuthJson<T extends JsonValue | null>(
1052
- method: string,
1053
- relativeUrl: string,
1054
- payload?: JsonValue,
1055
- expectResponseBody = true,
1056
- allowAnonymous = false
1057
- ): Promise<T> {
1058
- return this.sendJson<T>(method, this.buildAuthUrl(relativeUrl), payload, expectResponseBody, allowAnonymous);
1059
- }
1060
-
1061
- private async sendJson<T extends JsonValue | null>(
1062
- method: string,
1063
- url: string,
1064
- payload?: JsonValue | FormData,
1065
- expectResponseBody = true,
1066
- allowAnonymous = false,
1067
- allowRetry = true
1068
- ): Promise<T> {
1069
- const response = await this.sendRequest(method, url, payload, allowAnonymous, allowRetry);
1070
- if (!expectResponseBody) {
1071
- return null as T;
1072
- }
1073
-
1074
- const text = await response.text();
1075
- if (!text.trim()) {
1076
- return null as T;
1077
- }
1078
-
1079
- return JSON.parse(text) as T;
1080
- }
1081
-
1082
- private async sendText(
1083
- method: string,
1084
- url: string,
1085
- allowAnonymous = false,
1086
- allowRetry = true
1087
- ): Promise<string> {
1088
- const response = await this.sendRequest(method, url, undefined, allowAnonymous, allowRetry);
1089
- return await response.text();
1090
- }
1091
-
1092
- private async sendBlob(
1093
- method: string,
1094
- url: string,
1095
- allowAnonymous = false,
1096
- allowRetry = true
1097
- ): Promise<Blob> {
1098
- const response = await this.sendRequest(method, url, undefined, allowAnonymous, allowRetry);
1099
- return await response.blob();
1100
- }
1101
-
1102
- private async sendRequest(
1103
- method: string,
1104
- url: string,
1105
- payload?: JsonValue | FormData,
1106
- allowAnonymous = false,
1107
- allowRetry = true
1108
- ): Promise<Response> {
1109
- try {
1110
- if (!allowAnonymous && this.canUseAuthentication()) {
1111
- await this.getAuthTokenIfNecessary();
1112
- }
1113
-
1114
- const headers = new Headers();
1115
- if (!allowAnonymous && this.tokenState.accessToken) {
1116
- headers.set("Authorization", `Bearer ${this.tokenState.accessToken}`);
1117
- }
1118
- if (payload !== undefined && !this.isFormDataPayload(payload)) {
1119
- headers.set("Content-Type", "application/json");
1120
- }
1121
-
1122
- const response = await this.fetchImpl(url, {
1123
- method,
1124
- headers,
1125
- body: payload === undefined
1126
- ? undefined
1127
- : this.isFormDataPayload(payload)
1128
- ? payload
1129
- : JSON.stringify(payload)
1130
- });
1131
-
1132
- if ((response.status === 401 || response.status === 403) && !allowAnonymous && allowRetry && await this.tryRefreshAuthentication()) {
1133
- return this.sendRequest(method, url, payload, allowAnonymous, false);
1134
- }
1135
-
1136
- if (!response.ok) {
1137
- throw new ChillSharpClientError(
1138
- `HTTP ${response.status} calling ${method} ${url}`,
1139
- response.status,
1140
- await response.text()
1141
- );
1142
- }
1143
-
1144
- return response;
1145
- } catch (error) {
1146
- if (error instanceof ChillSharpClientError) {
1147
- throw error;
1148
- }
1149
-
1150
- throw new ChillSharpClientError(`Unexpected error executing ${method} ${url}`, undefined, undefined, error);
1151
- }
1152
- }
1153
-
1154
- private async getAuthTokenIfNecessary(forceRefresh = false): Promise<AuthTokenResponse> {
1155
- if (this.refreshPromise) {
1156
- return this.refreshPromise;
1157
- }
1158
-
1159
- this.refreshPromise = this.getAuthTokenIfNecessaryCore(forceRefresh);
1160
- try {
1161
- return await this.refreshPromise;
1162
- } finally {
1163
- this.refreshPromise = null;
1164
- }
1165
- }
1166
-
1167
- private async getAuthTokenIfNecessaryCore(forceRefresh: boolean): Promise<AuthTokenResponse> {
1168
- if (!forceRefresh && this.hasUsableAccessToken() && !this.shouldRefreshAccessToken()) {
1169
- return this.createCurrentTokenResponse();
1170
- }
1171
-
1172
- if (this.tokenState.refreshToken && (!forceRefresh || !this.password)) {
1173
- try {
1174
- const refreshed = await this.sendAuthJson<AuthTokenResponse>(
1175
- "POST",
1176
- "refresh",
1177
- { refreshToken: this.tokenState.refreshToken },
1178
- true,
1179
- true
1180
- );
1181
-
1182
- this.applyAuthToken(refreshed, true);
1183
- return refreshed;
1184
- } catch (error) {
1185
- if (!(error instanceof ChillSharpClientError)) {
1186
- throw error;
1187
- }
1188
-
1189
- this.tokenState.refreshToken = null;
1190
- this.tokenState.refreshTokenExpiresUtc = null;
1191
- }
1192
- }
1193
-
1194
- if (this.username && this.password) {
1195
- const token = await this.sendAuthJson<AuthTokenResponse>(
1196
- "POST",
1197
- "login",
1198
- {
1199
- userNameOrEmail: this.username,
1200
- password: this.password
1201
- },
1202
- true,
1203
- true
1204
- );
1205
-
1206
- this.applyAuthToken(token, true);
1207
- return token;
1208
- }
1209
-
1210
- if (this.hasUsableAccessToken()) {
1211
- return this.createCurrentTokenResponse();
1212
- }
1213
-
1214
- throw new ChillSharpClientError("No auth token is available and the client cannot obtain a new one.");
1215
- }
1216
-
1217
- private applyAuthToken(payload: JsonObject, forgetPassword: boolean): void {
1218
- this.tokenState.accessToken = this.readString(payload, "accessToken");
1219
- this.tokenState.accessTokenIssuedUtc = this.readDate(payload, "accessTokenIssuedUtc");
1220
- this.tokenState.accessTokenExpiresUtc = this.readDate(payload, "accessTokenExpiresUtc");
1221
- this.tokenState.refreshToken = this.readString(payload, "refreshToken");
1222
- this.tokenState.refreshTokenExpiresUtc = this.readDate(payload, "refreshTokenExpiresUtc");
1223
-
1224
- const userName = this.readString(payload, "userName");
1225
- if (userName) {
1226
- this.username = userName;
1227
- }
1228
-
1229
- if (forgetPassword) {
1230
- this.password = null;
1231
- }
1232
- }
1233
-
1234
- private clearAuthToken(): void {
1235
- this.tokenState.accessToken = null;
1236
- this.tokenState.accessTokenIssuedUtc = null;
1237
- this.tokenState.accessTokenExpiresUtc = null;
1238
- this.tokenState.refreshToken = null;
1239
- this.tokenState.refreshTokenExpiresUtc = null;
1240
- }
1241
-
1242
- private canUseAuthentication(): boolean {
1243
- return !!(this.tokenState.accessToken || this.tokenState.refreshToken || (this.username && this.password));
1244
- }
1245
-
1246
- private hasUsableAccessToken(): boolean {
1247
- if (!this.tokenState.accessToken) {
1248
- return false;
1249
- }
1250
-
1251
- if (!this.tokenState.accessTokenExpiresUtc) {
1252
- return true;
1253
- }
1254
-
1255
- return new Date() < this.tokenState.accessTokenExpiresUtc;
1256
- }
1257
-
1258
- private shouldRefreshAccessToken(): boolean {
1259
- const issued = this.tokenState.accessTokenIssuedUtc;
1260
- const expires = this.tokenState.accessTokenExpiresUtc;
1261
-
1262
- if (!issued || !expires) {
1263
- return false;
1264
- }
1265
-
1266
- if (expires <= issued) {
1267
- return true;
1268
- }
1269
-
1270
- const refreshThreshold = new Date(issued.getTime() + (expires.getTime() - issued.getTime()) * 0.75);
1271
- return new Date() >= refreshThreshold;
1272
- }
1273
-
1274
- private async tryRefreshAuthentication(): Promise<boolean> {
1275
- if (!this.tokenState.refreshToken && !this.password) {
1276
- return false;
1277
- }
1278
-
1279
- try {
1280
- await this.getAuthTokenIfNecessary(true);
1281
- return true;
1282
- } catch (error) {
1283
- if (error instanceof ChillSharpClientError) {
1284
- return false;
1285
- }
1286
-
1287
- throw error;
1288
- }
1289
- }
1290
-
1291
- private createCurrentTokenResponse(): AuthTokenResponse {
1292
- return {
1293
- accessToken: this.tokenState.accessToken ?? "",
1294
- accessTokenIssuedUtc: this.formatDate(this.tokenState.accessTokenIssuedUtc),
1295
- accessTokenExpiresUtc: this.formatDate(this.tokenState.accessTokenExpiresUtc),
1296
- refreshToken: this.tokenState.refreshToken ?? "",
1297
- refreshTokenExpiresUtc: this.formatDate(this.tokenState.refreshTokenExpiresUtc),
1298
- userId: "",
1299
- userName: this.username ?? ""
1300
- };
1301
- }
1302
-
1303
- private buildChillUrl(relativeUrl: string): string {
1304
- return `${this.baseUrl}/${relativeUrl.replace(/^\/+/, "")}`;
1305
- }
1306
-
1307
- private buildNotifyUrl(): string {
1308
- return `${this.getApiBaseUrl().replace(/\/$/, "")}/notify`;
1309
- }
1310
-
1311
- private buildApiUrl(relativeUrl: string): string {
1312
- return `${this.getApiBaseUrl().replace(/\/$/, "")}/${relativeUrl.replace(/^\/+/, "")}`;
1313
- }
1314
-
1315
- private buildAuthUrl(relativeUrl: string): string {
1316
- return `${this.getAuthBaseUrl().replace(/\/$/, "")}/${relativeUrl.replace(/^\/+/, "")}`;
1317
- }
1318
-
1319
- private buildSchemaUrl(relativeUrl: string): string {
1320
- return `${this.getSchemaBaseUrl().replace(/\/$/, "")}/${relativeUrl.replace(/^\/+/, "")}`;
1321
- }
1322
-
1323
- private buildI18nUrl(relativeUrl: string): string {
1324
- return `${this.getI18nBaseUrl().replace(/\/$/, "")}/${relativeUrl.replace(/^\/+/, "")}`;
1325
- }
1326
-
1327
- private buildAttachmentUrl(relativeUrl: string): string {
1328
- return `${this.getAttachmentBaseUrl().replace(/\/$/, "")}/${relativeUrl.replace(/^\/+/, "")}`;
1329
- }
1330
-
1331
- private getAuthBaseUrl(): string {
1332
- const suffix = "/chill";
1333
- if (this.baseUrl.toLowerCase().endsWith(suffix)) {
1334
- return `${this.baseUrl.slice(0, -suffix.length)}/chill-auth`;
1335
- }
1336
-
1337
- return `${this.baseUrl.replace(/\/$/, "")}-auth`;
1338
- }
1339
-
1340
- private getSchemaBaseUrl(): string {
1341
- const suffix = "/chill";
1342
- if (this.baseUrl.toLowerCase().endsWith(suffix)) {
1343
- return `${this.baseUrl.slice(0, -suffix.length)}/chill-schema`;
1344
- }
1345
-
1346
- return `${this.baseUrl.replace(/\/$/, "")}-schema`;
1347
- }
1348
-
1349
- private getI18nBaseUrl(): string {
1350
- const suffix = "/chill";
1351
- if (this.baseUrl.toLowerCase().endsWith(suffix)) {
1352
- return `${this.baseUrl.slice(0, -suffix.length)}/chill-i18n`;
1353
- }
1354
-
1355
- return `${this.baseUrl.replace(/\/$/, "")}-i18n`;
1356
- }
1357
-
1358
- private getAttachmentBaseUrl(): string {
1359
- const suffix = "/chill";
1360
- if (this.baseUrl.toLowerCase().endsWith(suffix)) {
1361
- return `${this.baseUrl.slice(0, -suffix.length)}/chill-attachment`;
1362
- }
1363
-
1364
- return `${this.baseUrl.replace(/\/$/, "")}-attachment`;
1365
- }
1366
-
1367
- private getApiBaseUrl(): string {
1368
- const suffix = "/chill";
1369
- if (this.baseUrl.toLowerCase().endsWith(suffix)) {
1370
- return this.baseUrl.slice(0, -suffix.length);
1371
- }
1372
-
1373
- return this.baseUrl.replace(/\/$/, "");
1374
- }
1375
-
1376
- private normalizeBaseUrl(baseUrl: string, apiBasePath?: string): string {
1377
- const normalized = this.normalizeRequiredValue(baseUrl, "baseUrl").replace(/\/+$/, "");
1378
- if (this.isKnownChillSharpEndpointBase(normalized)) {
1379
- return normalized;
1380
- }
1381
-
1382
- const normalizedApiBasePath = this.normalizeApiBasePath(apiBasePath);
1383
- if (!normalizedApiBasePath) {
1384
- return `${normalized}/chill`;
1385
- }
1386
-
1387
- if (this.endsWithPathSegment(normalized, normalizedApiBasePath)) {
1388
- return `${normalized}/chill`;
1389
- }
1390
-
1391
- return `${normalized}/${normalizedApiBasePath}/chill`;
1392
- }
1393
-
1394
- private normalizeApiBasePath(apiBasePath?: string): string {
1395
- const normalized = this.normalizeOptionalValue(apiBasePath) ?? API_BASE_PATH;
1396
- return normalized.replace(/^\/+|\/+$/g, "");
1397
- }
1398
-
1399
- private isKnownChillSharpEndpointBase(baseUrl: string): boolean {
1400
- const lowerBaseUrl = baseUrl.toLowerCase();
1401
- return lowerBaseUrl.endsWith("/chill") ||
1402
- lowerBaseUrl.endsWith("/chill-auth") ||
1403
- lowerBaseUrl.endsWith("/chill-schema") ||
1404
- lowerBaseUrl.endsWith("/chill-i18n") ||
1405
- lowerBaseUrl.endsWith("/chill-attachment");
1406
- }
1407
-
1408
- private endsWithPathSegment(value: string, segment: string): boolean {
1409
- return value.toLowerCase().endsWith(`/${segment.toLowerCase()}`);
1410
- }
1411
-
1412
- private normalizeRequiredValue(value: string | null | undefined, argumentName: string): string {
1413
- const normalized = this.normalizeOptionalValue(value);
1414
- if (!normalized) {
1415
- throw new Error(`${argumentName} is required.`);
1416
- }
1417
-
1418
- return normalized;
1419
- }
1420
-
1421
- private normalizeOptionalValue(value?: string | null): string | null {
1422
- const normalized = value?.trim();
1423
- return normalized ? normalized : null;
1424
- }
1425
-
1426
- private normalizeQueryValue(value?: string | null): string | null {
1427
- return value == null ? null : value.trim();
1428
- }
1429
-
1430
- private readString(payload: JsonObject, key: string): string | null {
1431
- const value = this.readValue(payload, key);
1432
- return typeof value === "string" && value.trim() ? value.trim() : null;
1433
- }
1434
-
1435
- private readDate(payload: JsonObject, key: string): Date | null {
1436
- return this.parseDate(this.readValue(payload, key));
1437
- }
1438
-
1439
- private readValue(payload: JsonObject, key: string): JsonValue | undefined {
1440
- if (key in payload) {
1441
- return payload[key];
1442
- }
1443
-
1444
- const pascalKey = key.length > 1
1445
- ? `${key[0].toUpperCase()}${key.slice(1)}`
1446
- : key.toUpperCase();
1447
-
1448
- if (pascalKey in payload) {
1449
- return payload[pascalKey];
1450
- }
1451
-
1452
- const matchedKey = Object.keys(payload).find((candidate) => candidate.toLowerCase() === key.toLowerCase());
1453
- return matchedKey ? payload[matchedKey] : undefined;
1454
- }
1455
-
1456
- private getAttachmentTargetInfo(targetEntity: JsonObject): { guid: string; chillType: string } {
1457
- const guid = this.readString(targetEntity, "guid");
1458
- if (!guid) {
1459
- throw new Error("targetEntity.guid is required.");
1460
- }
1461
-
1462
- const chillType = this.readString(targetEntity, "chillType");
1463
- if (!chillType) {
1464
- throw new Error("targetEntity.chillType is required.");
1465
- }
1466
-
1467
- return {
1468
- guid,
1469
- chillType
1470
- };
1471
- }
1472
-
1473
- private getAttachmentGuid(attachmentEntity: JsonObject): string {
1474
- const guid = this.readString(attachmentEntity, "guid");
1475
- if (!guid) {
1476
- throw new Error("attachmentEntity.guid is required.");
1477
- }
1478
-
1479
- const chillType = this.readString(attachmentEntity, "chillType");
1480
- if (chillType && chillType !== ChillSharpClient.attachmentEntityChillType) {
1481
- const normalizedChillType = chillType.split(".").pop() ?? chillType;
1482
- const normalizedAttachmentType = ChillSharpClient.attachmentEntityChillType.split(".").pop() ?? ChillSharpClient.attachmentEntityChillType;
1483
- if (normalizedChillType !== normalizedAttachmentType) {
1484
- throw new Error("attachmentEntity must point to an attachment.");
1485
- }
1486
- }
1487
-
1488
- return guid;
1489
- }
1490
-
1491
- private toAttachmentBlob(file: ChillAttachmentUploadFile): Blob {
1492
- if (!file || typeof file !== "object") {
1493
- throw new Error("file is required.");
1494
- }
1495
-
1496
- const contentType = this.normalizeOptionalValue(file.contentType) ?? "application/octet-stream";
1497
- if (file.content instanceof Blob) {
1498
- return file.content;
1499
- }
1500
-
1501
- if (typeof file.content === "string" || file.content instanceof ArrayBuffer) {
1502
- return new Blob([file.content], { type: contentType });
1503
- }
1504
-
1505
- if (file.content instanceof Uint8Array) {
1506
- const buffer = file.content.buffer.slice(
1507
- file.content.byteOffset,
1508
- file.content.byteOffset + file.content.byteLength
1509
- ) as ArrayBuffer;
1510
- return new Blob([buffer], { type: contentType });
1511
- }
1512
-
1513
- return new Blob([String(file.content)], { type: contentType });
1514
- }
1515
-
1516
- private isFormDataPayload(payload: JsonValue | FormData): payload is FormData {
1517
- return typeof FormData !== "undefined" && payload instanceof FormData;
1518
- }
1519
-
1520
- private parseDate(value: JsonValue | undefined): Date | null {
1521
- if (typeof value !== "string" || !value.trim()) {
1522
- return null;
1523
- }
1524
-
1525
- const parsed = new Date(value);
1526
- return Number.isNaN(parsed.getTime()) ? null : parsed;
1527
- }
1528
-
1529
- private formatDate(value: Date | null): string {
1530
- return value ? value.toISOString() : "";
1531
- }
1532
-
1533
- private async ensureNotificationConnection(): Promise<HubConnection> {
1534
- if (this.notificationConnection) {
1535
- if (this.notificationConnection.state === HubConnectionState.Disconnected) {
1536
- await this.notificationConnection.start();
1537
- }
1538
-
1539
- return this.notificationConnection;
1540
- }
1541
-
1542
- const connection = new HubConnectionBuilder()
1543
- .withUrl(this.buildNotifyUrl(), {
1544
- withCredentials: this.signalRWithCredentials,
1545
- accessTokenFactory: async () => {
1546
- if (this.canUseAuthentication()) {
1547
- await this.getAuthTokenIfNecessary();
1548
- }
1549
-
1550
- return this.tokenState.accessToken ?? "";
1551
- }
1552
- })
1553
- .withAutomaticReconnect()
1554
- .build();
1555
-
1556
- connection.on("EntitiesChanged", (payload: unknown) => {
1557
- void this.dispatchEntityChangeNotifications(payload);
1558
- });
1559
-
1560
- connection.onreconnected(async () => {
1561
- await this.reregisterEntityChangeSubscriptions();
1562
- });
1563
-
1564
- await connection.start();
1565
- this.notificationConnection = connection;
1566
- return connection;
1567
- }
1568
-
1569
- private async unsubscribeFromEntityChanges(subscriptionId: string): Promise<void> {
1570
- const subscription = this.entityChangeSubscriptions.get(subscriptionId);
1571
- if (!subscription) {
1572
- return;
1573
- }
1574
-
1575
- this.entityChangeSubscriptions.delete(subscriptionId);
1576
-
1577
- const registrationKey = this.buildEntityChangeRegistrationKey(subscription.chillType, subscription.guid);
1578
- const registrationCount = this.entityChangeRegistrationCounts.get(registrationKey) ?? 0;
1579
- if (registrationCount <= 1) {
1580
- this.entityChangeRegistrationCounts.delete(registrationKey);
1581
-
1582
- const connection = this.notificationConnection;
1583
- if (connection && connection.state === HubConnectionState.Connected) {
1584
- await connection.invoke("Unregister", subscription.chillType, subscription.guid);
1585
- }
1586
- } else {
1587
- this.entityChangeRegistrationCounts.set(registrationKey, registrationCount - 1);
1588
- }
1589
- }
1590
-
1591
- private async dispatchEntityChangeNotifications(payload: unknown): Promise<void> {
1592
- const notifications = this.normalizeEntityChangeNotifications(payload);
1593
- if (notifications.length === 0) {
1594
- return;
1595
- }
1596
-
1597
- for (const subscription of this.entityChangeSubscriptions.values()) {
1598
- const matchingChanges = notifications.filter((change) =>
1599
- change.chillType === subscription.chillType &&
1600
- (!subscription.guid || change.guid === subscription.guid)
1601
- );
1602
-
1603
- if (matchingChanges.length === 0) {
1604
- continue;
1605
- }
1606
-
1607
- await subscription.callback(matchingChanges);
1608
- }
1609
- }
1610
-
1611
- private normalizeEntityChangeNotifications(payload: unknown): ChillEntityChangeNotification[] {
1612
- if (!Array.isArray(payload)) {
1613
- return [];
1614
- }
1615
-
1616
- return payload
1617
- .filter((entry): entry is JsonObject => !!entry && typeof entry === "object" && !Array.isArray(entry))
1618
- .map((entry) => {
1619
- const chillType = this.readString(entry, "chillType");
1620
- const guid = this.readString(entry, "guid");
1621
- const action = this.readString(entry, "action");
1622
- if (!chillType || !guid || !this.isEntityChangeAction(action)) {
1623
- return null;
1624
- }
1625
-
1626
- return {
1627
- chillType,
1628
- guid,
1629
- action
1630
- } satisfies ChillEntityChangeNotification;
1631
- })
1632
- .filter((entry): entry is ChillEntityChangeNotification => entry !== null);
1633
- }
1634
-
1635
- private isEntityChangeAction(value: string | null): value is ChillEntityChangeAction {
1636
- return value === "CREATED" || value === "UPDATED" || value === "DELETED";
1637
- }
1638
-
1639
- private async reregisterEntityChangeSubscriptions(): Promise<void> {
1640
- const connection = this.notificationConnection;
1641
- if (!connection || connection.state !== HubConnectionState.Connected) {
1642
- return;
1643
- }
1644
-
1645
- for (const registrationKey of this.entityChangeRegistrationCounts.keys()) {
1646
- const separatorIndex = registrationKey.indexOf("|");
1647
- const chillType = separatorIndex >= 0 ? registrationKey.slice(0, separatorIndex) : registrationKey;
1648
- const guid = separatorIndex >= 0 ? registrationKey.slice(separatorIndex + 1) : "";
1649
- await connection.invoke("Register", chillType, guid || null);
1650
- }
1651
- }
1652
-
1653
- private buildEntityChangeRegistrationKey(chillType: string, guid: string | null): string {
1654
- return `${chillType}|${guid ?? ""}`;
1655
- }
1656
-
1657
- private async getUsersAssignedToRole(roleGuid: string): Promise<AuthUserListItem[]> {
1658
- const users = await this.getAuthUsers();
1659
- const matches = await Promise.all(
1660
- users.map(async (user) => {
1661
- const roles = await this.getAuthUserRoles(user.guid);
1662
- return roles.some((role) => role.guid === roleGuid) ? user : null;
1663
- })
1664
- );
1665
-
1666
- return matches.filter((user): user is AuthUserListItem => user !== null);
1667
- }
1668
-
1669
- private async syncUserRoles(userGuid: string, roleGuids: string[]): Promise<void> {
1670
- const desiredRoleGuids = new Set(roleGuids.map((roleGuid) => this.normalizeRequiredValue(roleGuid, "roleGuid")));
1671
- const currentRoles = await this.getAuthUserRoles(userGuid);
1672
- const currentRoleGuids = new Set(currentRoles.map((role) => role.guid));
1673
-
1674
- for (const roleGuid of desiredRoleGuids) {
1675
- if (!currentRoleGuids.has(roleGuid)) {
1676
- await this.assignAuthRole(userGuid, roleGuid);
1677
- }
1678
- }
1679
-
1680
- for (const role of currentRoles) {
1681
- if (!desiredRoleGuids.has(role.guid)) {
1682
- await this.removeAuthRole(userGuid, role.guid);
1683
- }
1684
- }
1685
- }
1686
-
1687
- private async syncUserPermissions(userGuid: string, permissions: AuthPermissionRuleItem[]): Promise<void> {
1688
- const currentRules = await this.getAuthPermissionRules(userGuid, null);
1689
- await this.syncPermissionRules(
1690
- currentRules,
1691
- permissions,
1692
- (permission) => ({
1693
- userGuid,
1694
- roleGuid: null,
1695
- effect: permission.effect,
1696
- action: permission.action,
1697
- scope: permission.scope,
1698
- module: permission.module,
1699
- entityName: permission.entityName,
1700
- propertyName: permission.propertyName,
1701
- appliesToAllProperties: permission.appliesToAllProperties,
1702
- description: permission.description
1703
- }),
1704
- (payload) => this.createAuthPermissionRule(payload),
1705
- (guid, payload) => this.updateAuthPermissionRule(guid, payload),
1706
- (guid) => this.deleteAuthPermissionRule(guid)
1707
- );
1708
- }
1709
-
1710
- private async syncRoleUsers(roleGuid: string, userGuids: string[]): Promise<void> {
1711
- const desiredUserGuids = new Set(userGuids.map((userGuid) => this.normalizeRequiredValue(userGuid, "userGuid")));
1712
- const currentUsers = await this.getUsersAssignedToRole(roleGuid);
1713
- const currentUserGuids = new Set(currentUsers.map((user) => user.guid));
1714
-
1715
- for (const userGuid of desiredUserGuids) {
1716
- if (!currentUserGuids.has(userGuid)) {
1717
- await this.assignAuthRole(userGuid, roleGuid);
1718
- }
1719
- }
1720
-
1721
- for (const user of currentUsers) {
1722
- if (!desiredUserGuids.has(user.guid)) {
1723
- await this.removeAuthRole(user.guid, roleGuid);
1724
- }
1725
- }
1726
- }
1727
-
1728
- private async syncRolePermissions(roleGuid: string, permissions: AuthPermissionRuleItem[]): Promise<void> {
1729
- const currentRules = await this.getAuthPermissionRules(null, roleGuid);
1730
- await this.syncPermissionRules(
1731
- currentRules,
1732
- permissions,
1733
- (permission) => ({
1734
- userGuid: null,
1735
- roleGuid,
1736
- effect: permission.effect,
1737
- action: permission.action,
1738
- scope: permission.scope,
1739
- module: permission.module,
1740
- entityName: permission.entityName,
1741
- propertyName: permission.propertyName,
1742
- appliesToAllProperties: permission.appliesToAllProperties,
1743
- description: permission.description
1744
- }),
1745
- (payload) => this.createAuthPermissionRule(payload),
1746
- (guid, payload) => this.updateAuthPermissionRule(guid, payload),
1747
- (guid) => this.deleteAuthPermissionRule(guid)
1748
- );
1749
- }
1750
-
1751
- private async syncPermissionRules(
1752
- currentRules: AuthPermissionRule[],
1753
- desiredRules: AuthPermissionRuleItem[],
1754
- toPayload: (permission: AuthPermissionRuleItem) => CreateAuthPermissionRuleRequest,
1755
- createRule: (payload: CreateAuthPermissionRuleRequest) => Promise<AuthPermissionRule>,
1756
- updateRule: (guid: string, payload: UpdateAuthPermissionRuleRequest) => Promise<AuthPermissionRule | null>,
1757
- deleteRule: (guid: string) => Promise<void>
1758
- ): Promise<void> {
1759
- const desiredByGuid = new Map<string, AuthPermissionRuleItem>();
1760
- const newRules: AuthPermissionRuleItem[] = [];
1761
-
1762
- for (const permission of desiredRules) {
1763
- const guid = this.normalizeOptionalValue(permission.guid);
1764
- if (guid) {
1765
- desiredByGuid.set(guid, permission);
1766
- } else {
1767
- newRules.push(permission);
1768
- }
1769
- }
1770
-
1771
- for (const currentRule of currentRules) {
1772
- const desiredRule = desiredByGuid.get(currentRule.guid);
1773
- if (!desiredRule) {
1774
- await deleteRule(currentRule.guid);
1775
- continue;
1776
- }
1777
-
1778
- await updateRule(currentRule.guid, toPayload(desiredRule));
1779
- desiredByGuid.delete(currentRule.guid);
1780
- }
1781
-
1782
- for (const desiredRule of desiredByGuid.values()) {
1783
- await createRule(toPayload(desiredRule));
1784
- }
1785
-
1786
- for (const desiredRule of newRules) {
1787
- await createRule(toPayload(desiredRule));
1788
- }
1789
- }
1790
- }
1791
-
1792
-
1793
-
1794
-
1795
-
1796
-
1797
-
1798
-
1799
-
1800
-
1801
-
1802
-
1803
-
1804
-
1805
-
1806
-
1807
-
1808
-
1809
-
1
+ /*
2
+ * ChillSharp is a lightweight .NET library that sits on top of Entity Framework Core
3
+ * and turns an existing data model into a fully working REST API with almost no setup.
4
+ * Copyright (C) 2025 Andrea Piovesan
5
+ *
6
+ * This program is free software: you can redistribute it and/or modify
7
+ * it under the terms of the GNU Affero General Public License as published by
8
+ * the Free Software Foundation, either version 3 of the License, or
9
+ * (at your option) any later version.
10
+ *
11
+ * This program is distributed in the hope that it will be useful,
12
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ * GNU Affero General Public License for more details.
15
+ *
16
+ * You should have received a copy of the GNU Affero General Public License
17
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
18
+ */
19
+
20
+ import {
21
+ HubConnection,
22
+ HubConnectionBuilder,
23
+ HubConnectionState
24
+ } from "@microsoft/signalr";
25
+ import { ChillSharpClientError } from "./errors.js";
26
+ import { CHILL_SHARP_TS_CLIENT_VERSION } from "./version.js";
27
+
28
+ export const API_BASE_PATH = "api/";
29
+
30
+ export type JsonPrimitive = string | number | boolean | null;
31
+ export type JsonValue = JsonPrimitive | JsonObject | JsonValue[];
32
+ export interface JsonObject {
33
+ [key: string]: JsonValue;
34
+ }
35
+
36
+ export interface GetTextRequest extends JsonObject {
37
+ labelGuid: string;
38
+ cultureName: string;
39
+ primaryCultureName: string;
40
+ primaryDefaultText: string;
41
+ secondaryCultureName: string;
42
+ secondaryDefaultText: string;
43
+ }
44
+
45
+ export interface GetTextResponse extends JsonObject {
46
+ labelGuid: string;
47
+ cultureName: string;
48
+ value: string;
49
+ }
50
+
51
+
52
+ export const ChillDtoPropertyType = {
53
+ Unknown: 0,
54
+ Guid: 1,
55
+ Integer: 10,
56
+ Decimal: 20,
57
+ Date: 30,
58
+ Time: 40,
59
+ DateTime: 50,
60
+ Duration: 60,
61
+ Boolean: 70,
62
+ String: 80,
63
+ Text: 81,
64
+ Json: 99,
65
+ ChillEntity: 1000,
66
+ ChillEntityCollection: 1010,
67
+ ChillQuery: 1100
68
+ } as const;
69
+
70
+ export type ChillDtoPropertyType = (typeof ChillDtoPropertyType)[keyof typeof ChillDtoPropertyType];
71
+
72
+ export interface ChillDtoPropertySchema extends JsonObject {
73
+ name: string;
74
+ displayName: string;
75
+ propertyType: ChillDtoPropertyType;
76
+ simplePropertyType: string;
77
+ referenceChillType: string | null;
78
+ referenceChillTypeQuery: string | null;
79
+ mcpDescription: string;
80
+ isNullable: boolean | null;
81
+ isReadOnly: boolean | null;
82
+ minLength: number | null;
83
+ maxLength: number | null;
84
+ integerMinValue: number | null;
85
+ integerMaxValue: number | null;
86
+ decimalMinValue: number | null;
87
+ decimalMaxValue: number | null;
88
+ decimalPlaces: number | null;
89
+ precision: number | null;
90
+ scale: number | null;
91
+ dateFormat: string;
92
+ customFormat: string;
93
+ regexPattern: string;
94
+ enumValues: string[];
95
+ lookupQueryValues: string | null;
96
+ metadata: Record<string, string>;
97
+ }
98
+
99
+ export interface ChillDtoSchemaRelationLabel extends JsonObject {
100
+ labelGuid: string | null;
101
+ primaryDefaultText: string;
102
+ secondaryDefaultText: string;
103
+ }
104
+
105
+ export interface ChillDtoSchemaRelation extends JsonObject {
106
+ chillType: string;
107
+ chillQuery: string;
108
+ fixedValues: Record<string, string>;
109
+ fixedQueryValues: Record<string, string>;
110
+ relationLabel: ChillDtoSchemaRelationLabel;
111
+ }
112
+
113
+ export interface ChillDtoSchema extends JsonObject {
114
+ chillType: string;
115
+ chillViewCode: string;
116
+ displayName: string;
117
+ handleAttachments: boolean;
118
+ enableMCP: boolean;
119
+ mcpDescription: string;
120
+ metadata: Record<string, string>;
121
+ queryRelatedChillType: string | null;
122
+ properties: ChillDtoPropertySchema[];
123
+ relations: ChillDtoSchemaRelation[];
124
+ }
125
+
126
+ export interface ChillDtoSchemaListItem extends JsonObject {
127
+ name: string;
128
+ chillType: string;
129
+ type: string;
130
+ relatedChillType: string | null;
131
+ }
132
+
133
+ export interface ChillDtoEntityOptions extends JsonObject {
134
+ chillType: string;
135
+ checksumEnabled: boolean;
136
+ handleAttachments: boolean;
137
+ labelFormatString: string | null;
138
+ shortLabelFormatString: string | null;
139
+ fullTextContentFormatString: string | null;
140
+ enableMCP: boolean;
141
+ mcpDescription: string | null;
142
+ changeLogEnabled: boolean;
143
+ }
144
+
145
+ export interface ChillOrdering extends JsonObject {
146
+ propertyName: string;
147
+ direction: string;
148
+ }
149
+
150
+ export interface ChillPagination extends JsonObject {
151
+ pageSize: number;
152
+ pageNumber: number;
153
+ }
154
+
155
+ export interface ChillDtoProperty extends JsonObject {
156
+ name: string;
157
+ }
158
+
159
+ export interface ChillDtoEntity extends JsonObject {
160
+ guid: string;
161
+ position: number;
162
+ chillType: string;
163
+ label: string | null;
164
+ shortLabel: string | null;
165
+ properties: Record<string, JsonValue>;
166
+ }
167
+
168
+ export interface ChillDtoQuery extends JsonObject {
169
+ chillType: string;
170
+ properties: Record<string, JsonValue>;
171
+ resultProperties: ChillDtoProperty[] | null;
172
+ pagination: ChillPagination | null;
173
+ ordering: ChillOrdering | null;
174
+ lightweightRequired: boolean | null;
175
+ results: ChillDtoEntity[];
176
+ }
177
+
178
+ export interface ChillDtoMenuItem extends JsonObject {
179
+ guid: string;
180
+ positionNo: number;
181
+ title: string;
182
+ description: string | null;
183
+ parent: ChillDtoMenuItem | null;
184
+ componentName: string;
185
+ componentConfigurationJson: string | null;
186
+ menuHierarchy: string;
187
+ }
188
+
189
+ export interface ChillValidationError extends JsonObject {
190
+ fieldName: string | null;
191
+ message: string | null;
192
+ }
193
+
194
+ export interface AuthUserListItem extends JsonObject {
195
+ guid: string;
196
+ externalId: string;
197
+ userName: string;
198
+ displayName: string;
199
+ displayCultureName: string;
200
+ displayTimeZone: string;
201
+ displayDateFormat: string;
202
+ displayNumberFormat: string;
203
+ preferredTheme: string;
204
+ isActive: boolean;
205
+ canManagePermissions: boolean;
206
+ canManageSchema: boolean;
207
+ menuHierarchy: string;
208
+ }
209
+
210
+ export interface AuthRoleListItem extends JsonObject {
211
+ guid: string;
212
+ name: string;
213
+ description: string;
214
+ isActive: boolean;
215
+ menuHierarchy: string;
216
+ }
217
+
218
+ export interface AuthTokenResponse extends JsonObject {
219
+ accessToken: string;
220
+ accessTokenIssuedUtc: string;
221
+ accessTokenExpiresUtc: string;
222
+ refreshToken: string;
223
+ refreshTokenExpiresUtc: string;
224
+ userId: string;
225
+ userName: string;
226
+ }
227
+
228
+ /** Display preferences resolved for the current authenticated user. */
229
+ export interface ChillUserPreferences extends JsonObject {
230
+ displayCultureName: string;
231
+ displayTimeZone: string;
232
+ displayDateFormat: string;
233
+ displayNumberFormat: string;
234
+ preferredTheme: string;
235
+ }
236
+
237
+ export interface RegisterAuthIdentityRequest extends JsonObject {
238
+ userName: string;
239
+ email: string | null;
240
+ password: string;
241
+ displayName: string;
242
+ displayCultureName: string;
243
+ createChillAuthUser: boolean;
244
+ }
245
+
246
+ export interface LoginAuthIdentityRequest extends JsonObject {
247
+ userNameOrEmail: string;
248
+ password: string;
249
+ }
250
+
251
+ export interface RefreshAuthTokenRequest extends JsonObject {
252
+ refreshToken: string;
253
+ }
254
+
255
+ export interface ChangePasswordRequest extends JsonObject {
256
+ currentPassword: string;
257
+ newPassword: string;
258
+ }
259
+
260
+ export interface ChangePasswordResponse extends JsonObject {
261
+ succeeded: boolean;
262
+ }
263
+
264
+ export interface RequestPasswordResetRequest extends JsonObject {
265
+ userNameOrEmail: string;
266
+ }
267
+
268
+ export interface PasswordResetTokenResponse extends JsonObject {
269
+ isAccepted: boolean;
270
+ userId: string | null;
271
+ resetToken: string | null;
272
+ }
273
+
274
+ export interface ResetPasswordRequest extends JsonObject {
275
+ userId: string;
276
+ resetToken: string;
277
+ newPassword: string;
278
+ }
279
+
280
+ export interface ResetPasswordResponse extends JsonObject {
281
+ succeeded: boolean;
282
+ }
283
+
284
+ export const PermissionEffect = {
285
+ Allow: 1,
286
+ Deny: 2
287
+ } as const;
288
+
289
+ export type PermissionEffect = (typeof PermissionEffect)[keyof typeof PermissionEffect];
290
+
291
+ export const PermissionAction = {
292
+ FullControl: 0,
293
+ Query: 1,
294
+ Create: 2,
295
+ Update: 3,
296
+ Delete: 4,
297
+ See: 5,
298
+ Modify: 6
299
+ } as const;
300
+
301
+ export type PermissionAction = (typeof PermissionAction)[keyof typeof PermissionAction];
302
+
303
+ export const PermissionScope = {
304
+ Module: 1,
305
+ Entity: 2,
306
+ Property: 3
307
+ } as const;
308
+
309
+ export type PermissionScope = (typeof PermissionScope)[keyof typeof PermissionScope];
310
+
311
+ export interface AuthPermissionRule extends JsonObject {
312
+ guid: string;
313
+ userGuid: string | null;
314
+ roleGuid: string | null;
315
+ effect: PermissionEffect;
316
+ action: PermissionAction;
317
+ scope: PermissionScope;
318
+ module: string;
319
+ entityName: string | null;
320
+ propertyName: string | null;
321
+ appliesToAllProperties: boolean;
322
+ description: string;
323
+ createdUtc: string;
324
+ }
325
+
326
+ export interface AuthRolePermissions extends AuthRoleListItem {
327
+ permissions: AuthPermissionRule[];
328
+ }
329
+
330
+ export interface GetAuthPermissionsResponse extends JsonObject {
331
+ user: AuthUserListItem | null;
332
+ permissions: AuthPermissionRule[];
333
+ roles: AuthRolePermissions[];
334
+ }
335
+
336
+ export interface AuthUserDetailsResponse extends AuthUserListItem {
337
+ roles: AuthRoleListItem[];
338
+ permissions: AuthPermissionRule[];
339
+ }
340
+
341
+ export interface AuthRoleDetailsResponse extends AuthRoleListItem {
342
+ users: AuthUserListItem[];
343
+ permissions: AuthPermissionRule[];
344
+ }
345
+
346
+ export interface AuthPermissionRuleItem extends JsonObject {
347
+ guid: string | null;
348
+ effect: PermissionEffect;
349
+ action: PermissionAction;
350
+ scope: PermissionScope;
351
+ module: string;
352
+ entityName: string | null;
353
+ propertyName: string | null;
354
+ appliesToAllProperties: boolean;
355
+ description: string;
356
+ }
357
+
358
+ export interface SetAuthUserRequest extends JsonObject {
359
+ guid: string | null;
360
+ externalId: string;
361
+ userName: string;
362
+ displayName: string;
363
+ displayCultureName: string;
364
+ displayTimeZone: string;
365
+ displayDateFormat: string;
366
+ displayNumberFormat: string;
367
+ preferredTheme: string;
368
+ isActive: boolean;
369
+ canManagePermissions: boolean;
370
+ canManageSchema: boolean;
371
+ menuHierarchy: string;
372
+ roleGuids: string[];
373
+ permissions: AuthPermissionRuleItem[];
374
+ }
375
+
376
+ export interface CreateAuthUserRequest extends JsonObject {
377
+ externalId: string;
378
+ email: string;
379
+ userName: string;
380
+ displayName: string;
381
+ displayCultureName: string;
382
+ displayTimeZone: string;
383
+ displayDateFormat: string;
384
+ displayNumberFormat: string;
385
+ preferredTheme: string;
386
+ isActive: boolean;
387
+ canManagePermissions: boolean;
388
+ canManageSchema: boolean;
389
+ menuHierarchy: string;
390
+ }
391
+
392
+ export interface UpdateAuthUserRequest extends JsonObject {
393
+ externalId: string;
394
+ userName: string;
395
+ displayName: string;
396
+ displayCultureName: string;
397
+ displayTimeZone: string;
398
+ displayDateFormat: string;
399
+ displayNumberFormat: string;
400
+ preferredTheme: string;
401
+ isActive: boolean;
402
+ canManagePermissions: boolean;
403
+ canManageSchema: boolean;
404
+ menuHierarchy: string;
405
+ }
406
+
407
+ export interface SetAuthRoleRequest extends JsonObject {
408
+ guid: string | null;
409
+ name: string;
410
+ description: string;
411
+ isActive: boolean;
412
+ menuHierarchy: string;
413
+ userGuids: string[];
414
+ permissions: AuthPermissionRuleItem[];
415
+ }
416
+
417
+ export interface CreateAuthRoleRequest extends JsonObject {
418
+ name: string;
419
+ description: string;
420
+ isActive: boolean;
421
+ menuHierarchy: string;
422
+ }
423
+
424
+ export interface UpdateAuthRoleRequest extends CreateAuthRoleRequest {}
425
+
426
+ export interface CreateAuthPermissionRuleRequest extends JsonObject {
427
+ userGuid: string | null;
428
+ roleGuid: string | null;
429
+ effect: PermissionEffect;
430
+ action: PermissionAction;
431
+ scope: PermissionScope;
432
+ module: string;
433
+ entityName: string | null;
434
+ propertyName: string | null;
435
+ appliesToAllProperties: boolean;
436
+ description: string;
437
+ }
438
+
439
+ export interface UpdateAuthPermissionRuleRequest extends CreateAuthPermissionRuleRequest {}
440
+
441
+ export interface ChillSharpClientOptions {
442
+ accessToken?: string;
443
+ username?: string;
444
+ password?: string;
445
+ cultureName?: string;
446
+ apiBasePath?: string;
447
+ fetchImpl?: typeof fetch;
448
+ signalRWithCredentials?: boolean;
449
+ }
450
+
451
+ export interface ChillAttachmentUploadFile {
452
+ fileName: string;
453
+ content: Blob | ArrayBuffer | Uint8Array | string;
454
+ contentType?: string;
455
+ }
456
+
457
+ export interface ChillAttachmentUploadOptions {
458
+ title?: string | null;
459
+ description?: string | null;
460
+ isPublic?: boolean;
461
+ }
462
+
463
+ export type ChillEntityChangeAction = "CREATED" | "UPDATED" | "DELETED";
464
+
465
+ export interface ChillEntityChangeNotification extends JsonObject {
466
+ chillType: string;
467
+ guid: string;
468
+ action: ChillEntityChangeAction;
469
+ }
470
+
471
+ export type ChillEntityChangeCallback = (
472
+ changes: ChillEntityChangeNotification[]
473
+ ) => void | Promise<void>;
474
+
475
+ export interface ChillEntityChangeSubscription {
476
+ chillType: string;
477
+ guid: string | null;
478
+ unsubscribe(): Promise<void>;
479
+ }
480
+
481
+ interface TokenState {
482
+ accessToken: string | null;
483
+ accessTokenIssuedUtc: Date | null;
484
+ accessTokenExpiresUtc: Date | null;
485
+ refreshToken: string | null;
486
+ refreshTokenExpiresUtc: Date | null;
487
+ }
488
+
489
+ interface LocalEntityChangeSubscription {
490
+ id: string;
491
+ chillType: string;
492
+ guid: string | null;
493
+ callback: ChillEntityChangeCallback;
494
+ }
495
+
496
+ export class ChillSharpClient {
497
+ static readonly API_BASE_PATH = API_BASE_PATH;
498
+ private static readonly attachmentEntityChillType = "ChillSharp.Attachment.Model.Attachment";
499
+ private static readonly attachmentQueryChillType = "ChillSharp.Attachment.Query.AttachmentQuery";
500
+ private readonly baseUrl: string;
501
+ private readonly fetchImpl: typeof fetch;
502
+ private cultureName: string | null;
503
+ private readonly signalRWithCredentials: boolean;
504
+
505
+ private username: string | null;
506
+ private password: string | null;
507
+ private refreshPromise: Promise<AuthTokenResponse> | null = null;
508
+ private tokenState: TokenState;
509
+ private notificationConnection: HubConnection | null = null;
510
+ private readonly entityChangeSubscriptions = new Map<string, LocalEntityChangeSubscription>();
511
+ private readonly entityChangeRegistrationCounts = new Map<string, number>();
512
+ private entityChangeSubscriptionSequence = 0;
513
+
514
+ constructor(baseUrl: string, options: ChillSharpClientOptions = {}) {
515
+ this.baseUrl = this.normalizeBaseUrl(baseUrl, options.apiBasePath);
516
+ this.fetchImpl = options.fetchImpl ?? fetch;
517
+ this.username = this.normalizeOptionalValue(options.username);
518
+ this.password = this.normalizeOptionalValue(options.password);
519
+ this.cultureName = this.normalizeOptionalValue(options.cultureName);
520
+ this.signalRWithCredentials = options.signalRWithCredentials ?? true;
521
+ this.tokenState = {
522
+ accessToken: this.normalizeOptionalValue(options.accessToken),
523
+ accessTokenIssuedUtc: null,
524
+ accessTokenExpiresUtc: null,
525
+ refreshToken: null,
526
+ refreshTokenExpiresUtc: null
527
+ };
528
+ }
529
+
530
+ query(dtoQuery: JsonObject): Promise<JsonObject> {
531
+ return this.sendJson<JsonObject>("POST", this.buildChillUrl("query"), dtoQuery);
532
+ }
533
+
534
+ lookup(dtoQuery: JsonObject): Promise<JsonObject> {
535
+ return this.sendJson<JsonObject>("POST", this.buildChillUrl("lookup"), dtoQuery);
536
+ }
537
+
538
+ find(dtoEntity: JsonObject): Promise<JsonObject | null> {
539
+ return this.sendJson<JsonObject | null>("POST", this.buildChillUrl("find"), dtoEntity);
540
+ }
541
+
542
+ create(dtoEntity: JsonObject): Promise<JsonObject> {
543
+ return this.sendJson<JsonObject>("POST", this.buildChillUrl("create"), dtoEntity);
544
+ }
545
+
546
+ update(dtoEntity: JsonObject): Promise<JsonObject> {
547
+ return this.sendJson<JsonObject>("POST", this.buildChillUrl("update"), dtoEntity);
548
+ }
549
+
550
+ async delete(dtoEntity: JsonObject): Promise<void> {
551
+ await this.sendJson("POST", this.buildChillUrl("delete"), dtoEntity, false);
552
+ }
553
+
554
+ autocomplete(dto: JsonObject): Promise<JsonObject> {
555
+ return this.sendJson<JsonObject>("POST", this.buildChillUrl("autocomplete"), dto);
556
+ }
557
+
558
+ validate(dto: JsonObject): Promise<ChillValidationError[]> {
559
+ return this.sendJson<ChillValidationError[]>("POST", this.buildChillUrl("validate"), dto);
560
+ }
561
+
562
+ chunk(operations: JsonObject[]): Promise<JsonObject[]> {
563
+ return this.sendJson<JsonObject[]>("POST", this.buildChillUrl("chunk"), operations);
564
+ }
565
+
566
+ uploadAttachment(
567
+ targetEntity: JsonObject,
568
+ file: ChillAttachmentUploadFile,
569
+ options: ChillAttachmentUploadOptions = {}
570
+ ): Promise<JsonObject[]> {
571
+ return this.uploadAttachments(targetEntity, [file], options);
572
+ }
573
+
574
+ async uploadAttachments(
575
+ targetEntity: JsonObject,
576
+ files: ChillAttachmentUploadFile[],
577
+ options: ChillAttachmentUploadOptions = {}
578
+ ): Promise<JsonObject[]> {
579
+ const target = this.getAttachmentTargetInfo(targetEntity);
580
+ if (!Array.isArray(files) || files.length === 0) {
581
+ throw new Error("files is required.");
582
+ }
583
+
584
+ const form = new FormData();
585
+ form.append("attachToChillType", target.chillType);
586
+ form.append("attachToGuid", target.guid);
587
+
588
+ const normalizedTitle = this.normalizeOptionalValue(options.title ?? undefined);
589
+ if (normalizedTitle) {
590
+ form.append("title", normalizedTitle);
591
+ }
592
+
593
+ const normalizedDescription = this.normalizeOptionalValue(options.description ?? undefined);
594
+ if (normalizedDescription) {
595
+ form.append("description", normalizedDescription);
596
+ }
597
+
598
+ form.append("public", options.isPublic ? "true" : "false");
599
+
600
+ for (const file of files) {
601
+ form.append(
602
+ "file",
603
+ this.toAttachmentBlob(file),
604
+ this.normalizeRequiredValue(file.fileName, "file.fileName")
605
+ );
606
+ }
607
+
608
+ return this.sendJson<JsonObject[]>(
609
+ "POST",
610
+ this.buildAttachmentUrl("attachment/upload"),
611
+ form,
612
+ true,
613
+ false,
614
+ false
615
+ );
616
+ }
617
+
618
+ async getAttachments(targetEntity: JsonObject): Promise<JsonObject[]> {
619
+ const target = this.getAttachmentTargetInfo(targetEntity);
620
+ const response = await this.query({
621
+ chillType: ChillSharpClient.attachmentQueryChillType,
622
+ properties: {
623
+ attachToChillType: target.chillType,
624
+ attachToGuid: target.guid
625
+ }
626
+ });
627
+
628
+ const results = this.readValue(response, "results");
629
+ return Array.isArray(results)
630
+ ? results.filter((item): item is JsonObject => !!item && typeof item === "object" && !Array.isArray(item))
631
+ : [];
632
+ }
633
+
634
+ downloadAttachment(attachmentOrGuid: JsonObject | string): Promise<Blob> {
635
+ const attachmentGuid = typeof attachmentOrGuid === "string"
636
+ ? this.normalizeRequiredValue(attachmentOrGuid, "attachmentGuid")
637
+ : this.getAttachmentGuid(attachmentOrGuid);
638
+
639
+ return this.sendBlob(
640
+ "GET",
641
+ this.buildAttachmentUrl(`attachment/download?guid=${encodeURIComponent(attachmentGuid)}`),
642
+ this.canUseAuthentication() ? false : true
643
+ );
644
+ }
645
+
646
+ version(): string {
647
+ return CHILL_SHARP_TS_CLIENT_VERSION;
648
+ }
649
+
650
+ /** Updates the default culture used by calls that do not provide one explicitly. */
651
+ setCultureName(cultureName?: string | null): void {
652
+ this.cultureName = this.normalizeOptionalValue(cultureName);
653
+ }
654
+
655
+ test(): Promise<string> {
656
+ return this.sendText("GET", this.buildApiUrl("test"), true);
657
+ }
658
+
659
+ getSchema(chillType: string, chillViewCode: string, cultureName?: string, update = false): Promise<ChillDtoSchema | null> {
660
+ const encodedType = encodeURIComponent(this.normalizeRequiredValue(chillType, "chillType"));
661
+ const encodedView = encodeURIComponent(this.normalizeRequiredValue(chillViewCode, "chillViewCode"));
662
+ const effectiveCultureName = this.normalizeOptionalValue(cultureName) ?? this.cultureName;
663
+
664
+ let relativeUrl = `get-schema?chillType=${encodedType}&chillViewCode=${encodedView}`;
665
+ if (effectiveCultureName) {
666
+ relativeUrl += `&cultureName=${encodeURIComponent(effectiveCultureName)}`;
667
+ }
668
+ if (update) {
669
+ relativeUrl += "&update=true";
670
+ }
671
+
672
+ return this.sendJson<ChillDtoSchema | null>("GET", this.buildSchemaUrl(relativeUrl));
673
+ }
674
+
675
+ getSchemaList(cultureName?: string): Promise<ChillDtoSchemaListItem[]> {
676
+ const effectiveCultureName = this.normalizeOptionalValue(cultureName) ?? this.cultureName;
677
+ let relativeUrl = "get-schema-list";
678
+ if (effectiveCultureName) {
679
+ relativeUrl += `?cultureName=${encodeURIComponent(effectiveCultureName)}`;
680
+ }
681
+
682
+ return this.sendJson<ChillDtoSchemaListItem[]>("GET", this.buildSchemaUrl(relativeUrl));
683
+ }
684
+
685
+ setSchema(schema: ChillDtoSchema): Promise<ChillDtoSchema | null> {
686
+ return this.sendJson<ChillDtoSchema | null>("POST", this.buildSchemaUrl("set-schema"), schema);
687
+ }
688
+
689
+ getEntityOptions(chillType: string): Promise<ChillDtoEntityOptions> {
690
+ const encodedType = encodeURIComponent(this.normalizeRequiredValue(chillType, "chillType"));
691
+ return this.sendJson<ChillDtoEntityOptions>("GET", this.buildSchemaUrl(`get-entity-options?chillType=${encodedType}`));
692
+ }
693
+
694
+ setEntityOptions(entityOptions: ChillDtoEntityOptions): Promise<ChillDtoEntityOptions> {
695
+ return this.sendJson<ChillDtoEntityOptions>("POST", this.buildSchemaUrl("set-entity-options"), entityOptions);
696
+ }
697
+
698
+ getMenu(parentGuid?: string | null): Promise<ChillDtoMenuItem[]> {
699
+ const normalizedParentGuid = this.normalizeQueryValue(parentGuid);
700
+ const suffix = normalizedParentGuid === null ? "" : `?parentGuid=${encodeURIComponent(normalizedParentGuid)}`;
701
+ return this.sendJson<ChillDtoMenuItem[]>("GET", this.buildSchemaUrl(`get-menu${suffix}`));
702
+ }
703
+
704
+ setMenu(menuItem: ChillDtoMenuItem): Promise<ChillDtoMenuItem> {
705
+ return this.sendJson<ChillDtoMenuItem>("POST", this.buildSchemaUrl("set-menu"), menuItem);
706
+ }
707
+
708
+
709
+ async deleteMenu(menuItemGuid: string): Promise<void> {
710
+ const normalizedMenuItemGuid = this.normalizeRequiredValue(menuItemGuid, "menuItemGuid");
711
+ await this.sendJson("DELETE", this.buildSchemaUrl(`delete-menu?menuItemGuid=${encodeURIComponent(normalizedMenuItemGuid)}`), undefined, false);
712
+ }
713
+ getText(request: GetTextRequest): Promise<GetTextResponse | null> {
714
+ return this.sendJson<GetTextResponse | null>("POST", this.buildI18nUrl("get-text"), this.prepareGetTextRequest(request), true, true);
715
+ }
716
+
717
+ getTexts(requests: GetTextRequest[]): Promise<Array<GetTextResponse | null>> {
718
+ if (!Array.isArray(requests)) {
719
+ throw new Error("requests is required.");
720
+ }
721
+
722
+ return this.sendJson<Array<GetTextResponse | null>>(
723
+ "POST",
724
+ this.buildI18nUrl("get-multiple-text"),
725
+ requests.map((request) => this.prepareGetTextRequest(request))
726
+ );
727
+ }
728
+
729
+ setText(payload: JsonObject): Promise<GetTextResponse> {
730
+ return this.sendJson<GetTextResponse>("PUT", this.buildI18nUrl("set-text"), payload);
731
+ }
732
+
733
+ async subscribeToEntityChanges(
734
+ chillType: string,
735
+ callback: ChillEntityChangeCallback,
736
+ guid?: string | null
737
+ ): Promise<ChillEntityChangeSubscription> {
738
+ if (typeof callback !== "function") {
739
+ throw new Error("callback is required.");
740
+ }
741
+
742
+ const normalizedChillType = this.normalizeRequiredValue(chillType, "chillType");
743
+ const normalizedGuid = this.normalizeOptionalValue(guid);
744
+ const connection = await this.ensureNotificationConnection();
745
+ const registrationKey = this.buildEntityChangeRegistrationKey(normalizedChillType, normalizedGuid);
746
+
747
+ const registrationCount = this.entityChangeRegistrationCounts.get(registrationKey) ?? 0;
748
+ if (registrationCount === 0) {
749
+ await connection.invoke("Register", normalizedChillType, normalizedGuid);
750
+ }
751
+ this.entityChangeRegistrationCounts.set(registrationKey, registrationCount + 1);
752
+
753
+ const subscriptionId = `entity-change-${++this.entityChangeSubscriptionSequence}`;
754
+ this.entityChangeSubscriptions.set(subscriptionId, {
755
+ id: subscriptionId,
756
+ chillType: normalizedChillType,
757
+ guid: normalizedGuid,
758
+ callback
759
+ });
760
+
761
+ return {
762
+ chillType: normalizedChillType,
763
+ guid: normalizedGuid,
764
+ unsubscribe: async () => {
765
+ await this.unsubscribeFromEntityChanges(subscriptionId);
766
+ }
767
+ };
768
+ }
769
+
770
+ async disconnectEntityChanges(): Promise<void> {
771
+ this.entityChangeSubscriptions.clear();
772
+ this.entityChangeRegistrationCounts.clear();
773
+
774
+ if (!this.notificationConnection) {
775
+ return;
776
+ }
777
+
778
+ const connection = this.notificationConnection;
779
+ this.notificationConnection = null;
780
+ await connection.stop();
781
+ }
782
+
783
+ async registerAuthAccount(payload: RegisterAuthIdentityRequest): Promise<AuthTokenResponse> {
784
+ const response = await this.sendAuthJson<AuthTokenResponse>("POST", "register", payload, true, true);
785
+ this.applyAuthToken(response, true);
786
+ return response;
787
+ }
788
+
789
+ async loginAuthAccount(payload: LoginAuthIdentityRequest): Promise<AuthTokenResponse> {
790
+ const response = await this.sendAuthJson<AuthTokenResponse>("POST", "login", payload, true, true);
791
+ this.applyAuthToken(response, true);
792
+ return response;
793
+ }
794
+
795
+ refreshAuthAccount(): Promise<AuthTokenResponse> {
796
+ return this.getAuthTokenIfNecessary(true);
797
+ }
798
+
799
+ async logoutAuthAccount(): Promise<void> {
800
+ await this.sendAuthJson("POST", "logout", undefined, false);
801
+ this.clearAuthToken();
802
+ }
803
+
804
+ changeAuthPassword(payload: ChangePasswordRequest): Promise<ChangePasswordResponse> {
805
+ return this.sendAuthJson<ChangePasswordResponse>("POST", "change-password", payload);
806
+ }
807
+
808
+ requestAuthPasswordReset(payload: RequestPasswordResetRequest): Promise<PasswordResetTokenResponse> {
809
+ return this.sendAuthJson<PasswordResetTokenResponse>("POST", "request-password-reset", payload, true, true);
810
+ }
811
+
812
+ resetAuthPassword(payload: ResetPasswordRequest): Promise<ResetPasswordResponse> {
813
+ return this.sendAuthJson<ResetPasswordResponse>("POST", "reset-password", payload, true, true);
814
+ }
815
+
816
+ getAuthPermissions(): Promise<GetAuthPermissionsResponse> {
817
+ return this.sendAuthJson<GetAuthPermissionsResponse>("GET", "get-permissions");
818
+ }
819
+
820
+ getCurrentUserPreferences(): Promise<ChillUserPreferences> {
821
+ return this.sendAuthJson<ChillUserPreferences>("GET", "current-user-preferences");
822
+ }
823
+
824
+ getAuthUserList(): Promise<AuthUserListItem[]> {
825
+ return this.sendAuthJson<AuthUserListItem[]>("GET", "get-user-list");
826
+ }
827
+
828
+ async getAuthUser(userGuid: string): Promise<AuthUserDetailsResponse> {
829
+ const normalizedUserGuid = this.normalizeRequiredValue(userGuid, "userGuid");
830
+ const [user, roles, permissions] = await Promise.all([
831
+ this.sendAuthJson<AuthUserListItem>("GET", `users/${encodeURIComponent(normalizedUserGuid)}`),
832
+ this.getAuthUserRoles(normalizedUserGuid),
833
+ this.getAuthPermissionRules(normalizedUserGuid, null)
834
+ ]);
835
+
836
+ return {
837
+ ...user,
838
+ roles,
839
+ permissions
840
+ };
841
+ }
842
+
843
+ async setAuthUser(payload: SetAuthUserRequest): Promise<AuthUserDetailsResponse> {
844
+ const userGuid = this.normalizeOptionalValue(payload.guid);
845
+ const basePayload = {
846
+ externalId: payload.externalId,
847
+ userName: payload.userName,
848
+ displayName: payload.displayName,
849
+ displayCultureName: payload.displayCultureName,
850
+ displayTimeZone: payload.displayTimeZone,
851
+ displayDateFormat: payload.displayDateFormat,
852
+ displayNumberFormat: payload.displayNumberFormat,
853
+ preferredTheme: payload.preferredTheme,
854
+ isActive: payload.isActive,
855
+ canManagePermissions: payload.canManagePermissions,
856
+ canManageSchema: payload.canManageSchema,
857
+ menuHierarchy: payload.menuHierarchy
858
+ };
859
+
860
+ const user = userGuid
861
+ ? await this.updateAuthUser(userGuid, basePayload)
862
+ : await this.createAuthUser({
863
+ ...basePayload,
864
+ email: "",
865
+ externalId: payload.externalId
866
+ });
867
+
868
+ if (!user) {
869
+ throw new ChillSharpClientError("Auth user was not found after setAuthUser execution.");
870
+ }
871
+
872
+ await this.syncUserRoles(user.guid, payload.roleGuids);
873
+ await this.syncUserPermissions(user.guid, payload.permissions);
874
+ return this.getAuthUser(user.guid);
875
+ }
876
+
877
+ getAuthRoleList(): Promise<AuthRoleListItem[]> {
878
+ return this.sendAuthJson<AuthRoleListItem[]>("GET", "get-role-list");
879
+ }
880
+
881
+ getAuthModuleList(): Promise<string[]> {
882
+ return this.sendAuthJson<string[]>("GET", "get-module-list");
883
+ }
884
+
885
+ getAuthEntityList(module?: string | null): Promise<string[]> {
886
+ const normalizedModule = this.normalizeQueryValue(module);
887
+ const suffix = normalizedModule === null ? "" : `?module=${encodeURIComponent(normalizedModule)}`;
888
+ return this.sendAuthJson<string[]>("GET", `get-entity-list${suffix}`);
889
+ }
890
+
891
+ getAuthQueryList(module?: string | null): Promise<string[]> {
892
+ const normalizedModule = this.normalizeQueryValue(module);
893
+ const suffix = normalizedModule === null ? "" : `?module=${encodeURIComponent(normalizedModule)}`;
894
+ return this.sendAuthJson<string[]>("GET", `get-query-list${suffix}`);
895
+ }
896
+
897
+ getAuthModuleEntityList(module?: string | null): Promise<string[]> {
898
+ return this.getAuthEntityList(module);
899
+ }
900
+
901
+
902
+ getAuthPropertyList(chillType: string): Promise<string[]> {
903
+ const normalizedChillType = this.normalizeRequiredValue(chillType, "chillType");
904
+ return this.sendAuthJson<string[]>("GET", `get-property-list?chillType=${encodeURIComponent(normalizedChillType)}`);
905
+ }
906
+
907
+ async getAuthRole(roleGuid: string): Promise<AuthRoleDetailsResponse> {
908
+ const normalizedRoleGuid = this.normalizeRequiredValue(roleGuid, "roleGuid");
909
+ const [role, permissions, users] = await Promise.all([
910
+ this.sendAuthJson<AuthRoleListItem>("GET", `roles/${encodeURIComponent(normalizedRoleGuid)}`),
911
+ this.getAuthPermissionRules(null, normalizedRoleGuid),
912
+ this.getUsersAssignedToRole(normalizedRoleGuid)
913
+ ]);
914
+
915
+ return {
916
+ ...role,
917
+ users,
918
+ permissions
919
+ };
920
+ }
921
+
922
+ async setAuthRole(payload: SetAuthRoleRequest): Promise<AuthRoleDetailsResponse> {
923
+ const roleGuid = this.normalizeOptionalValue(payload.guid);
924
+ const basePayload = {
925
+ name: payload.name,
926
+ description: payload.description,
927
+ isActive: payload.isActive,
928
+ menuHierarchy: payload.menuHierarchy
929
+ };
930
+
931
+ const role = roleGuid
932
+ ? await this.updateAuthRole(roleGuid, basePayload)
933
+ : await this.createAuthRole(basePayload);
934
+
935
+ if (!role) {
936
+ throw new ChillSharpClientError("Auth role was not found after setAuthRole execution.");
937
+ }
938
+
939
+ await this.syncRoleUsers(role.guid, payload.userGuids);
940
+ await this.syncRolePermissions(role.guid, payload.permissions);
941
+ return this.getAuthRole(role.guid);
942
+ }
943
+
944
+ getAuthUsers(): Promise<AuthUserListItem[]> {
945
+ return this.sendAuthJson<AuthUserListItem[]>("GET", "users");
946
+ }
947
+
948
+ createAuthUser(payload: CreateAuthUserRequest): Promise<AuthUserListItem> {
949
+ return this.sendAuthJson<AuthUserListItem>("POST", "users", payload);
950
+ }
951
+
952
+ updateAuthUser(userGuid: string, payload: UpdateAuthUserRequest): Promise<AuthUserListItem | null> {
953
+ const normalizedUserGuid = this.normalizeRequiredValue(userGuid, "userGuid");
954
+ return this.sendAuthJson<AuthUserListItem | null>("PUT", `users/${encodeURIComponent(normalizedUserGuid)}`, payload);
955
+ }
956
+
957
+ async deleteAuthUser(userGuid: string): Promise<void> {
958
+ const normalizedUserGuid = this.normalizeRequiredValue(userGuid, "userGuid");
959
+ await this.sendAuthJson("DELETE", `users/${encodeURIComponent(normalizedUserGuid)}`, undefined, false);
960
+ }
961
+
962
+ getAuthUserRoles(userGuid: string): Promise<AuthRoleListItem[]> {
963
+ const normalizedUserGuid = this.normalizeRequiredValue(userGuid, "userGuid");
964
+ return this.sendAuthJson<AuthRoleListItem[]>("GET", `users/${encodeURIComponent(normalizedUserGuid)}/roles`);
965
+ }
966
+
967
+ async assignAuthRole(userGuid: string, roleGuid: string): Promise<void> {
968
+ const normalizedUserGuid = this.normalizeRequiredValue(userGuid, "userGuid");
969
+ const normalizedRoleGuid = this.normalizeRequiredValue(roleGuid, "roleGuid");
970
+ await this.sendAuthJson("PUT", `users/${encodeURIComponent(normalizedUserGuid)}/roles/${encodeURIComponent(normalizedRoleGuid)}`, undefined, false);
971
+ }
972
+
973
+ async removeAuthRole(userGuid: string, roleGuid: string): Promise<void> {
974
+ const normalizedUserGuid = this.normalizeRequiredValue(userGuid, "userGuid");
975
+ const normalizedRoleGuid = this.normalizeRequiredValue(roleGuid, "roleGuid");
976
+ await this.sendAuthJson("DELETE", `users/${encodeURIComponent(normalizedUserGuid)}/roles/${encodeURIComponent(normalizedRoleGuid)}`, undefined, false);
977
+ }
978
+
979
+ getAuthRoles(): Promise<AuthRoleListItem[]> {
980
+ return this.sendAuthJson<AuthRoleListItem[]>("GET", "roles");
981
+ }
982
+
983
+ createAuthRole(payload: CreateAuthRoleRequest): Promise<AuthRoleListItem> {
984
+ return this.sendAuthJson<AuthRoleListItem>("POST", "roles", payload);
985
+ }
986
+
987
+ updateAuthRole(roleGuid: string, payload: UpdateAuthRoleRequest): Promise<AuthRoleListItem | null> {
988
+ const normalizedRoleGuid = this.normalizeRequiredValue(roleGuid, "roleGuid");
989
+ return this.sendAuthJson<AuthRoleListItem | null>("PUT", `roles/${encodeURIComponent(normalizedRoleGuid)}`, payload);
990
+ }
991
+
992
+ async deleteAuthRole(roleGuid: string): Promise<void> {
993
+ const normalizedRoleGuid = this.normalizeRequiredValue(roleGuid, "roleGuid");
994
+ await this.sendAuthJson("DELETE", `roles/${encodeURIComponent(normalizedRoleGuid)}`, undefined, false);
995
+ }
996
+
997
+ getAuthPermissionRules(userGuid?: string | null, roleGuid?: string | null): Promise<AuthPermissionRule[]> {
998
+ const queryParts: string[] = [];
999
+ const normalizedUserGuid = this.normalizeOptionalValue(userGuid);
1000
+ const normalizedRoleGuid = this.normalizeOptionalValue(roleGuid);
1001
+ if (normalizedUserGuid) {
1002
+ queryParts.push(`userGuid=${encodeURIComponent(normalizedUserGuid)}`);
1003
+ }
1004
+ if (normalizedRoleGuid) {
1005
+ queryParts.push(`roleGuid=${encodeURIComponent(normalizedRoleGuid)}`);
1006
+ }
1007
+
1008
+ const suffix = queryParts.length === 0 ? "" : `?${queryParts.join("&")}`;
1009
+ return this.sendAuthJson<AuthPermissionRule[]>("GET", `permissions${suffix}`);
1010
+ }
1011
+
1012
+ getAuthPermissionRule(ruleGuid: string): Promise<AuthPermissionRule | null> {
1013
+ const normalizedRuleGuid = this.normalizeRequiredValue(ruleGuid, "ruleGuid");
1014
+ return this.sendAuthJson<AuthPermissionRule | null>("GET", `permissions/${encodeURIComponent(normalizedRuleGuid)}`);
1015
+ }
1016
+
1017
+ createAuthPermissionRule(payload: CreateAuthPermissionRuleRequest): Promise<AuthPermissionRule> {
1018
+ return this.sendAuthJson<AuthPermissionRule>("POST", "permissions", payload);
1019
+ }
1020
+
1021
+ updateAuthPermissionRule(ruleGuid: string, payload: UpdateAuthPermissionRuleRequest): Promise<AuthPermissionRule | null> {
1022
+ const normalizedRuleGuid = this.normalizeRequiredValue(ruleGuid, "ruleGuid");
1023
+ return this.sendAuthJson<AuthPermissionRule | null>("PUT", `permissions/${encodeURIComponent(normalizedRuleGuid)}`, payload);
1024
+ }
1025
+
1026
+ async deleteAuthPermissionRule(ruleGuid: string): Promise<void> {
1027
+ const normalizedRuleGuid = this.normalizeRequiredValue(ruleGuid, "ruleGuid");
1028
+ await this.sendAuthJson("DELETE", `permissions/${encodeURIComponent(normalizedRuleGuid)}`, undefined, false);
1029
+ }
1030
+
1031
+ private prepareGetTextRequest(request: GetTextRequest): GetTextRequest {
1032
+ if (!request || typeof request !== "object") {
1033
+ throw new Error("request is required.");
1034
+ }
1035
+
1036
+ const effectiveCultureName = this.normalizeOptionalValue(this.readString(request, "cultureName")) ?? this.cultureName;
1037
+ if (!effectiveCultureName) {
1038
+ throw new Error("cultureName is required.");
1039
+ }
1040
+
1041
+ return {
1042
+ labelGuid: this.normalizeRequiredValue(this.readString(request, "labelGuid"), "labelGuid"),
1043
+ cultureName: effectiveCultureName,
1044
+ primaryCultureName: this.readString(request, "primaryCultureName") ?? "",
1045
+ primaryDefaultText: this.readString(request, "primaryDefaultText") ?? "",
1046
+ secondaryCultureName: this.readString(request, "secondaryCultureName") ?? "",
1047
+ secondaryDefaultText: this.readString(request, "secondaryDefaultText") ?? ""
1048
+ };
1049
+ }
1050
+
1051
+ private sendAuthJson<T extends JsonValue | null>(
1052
+ method: string,
1053
+ relativeUrl: string,
1054
+ payload?: JsonValue,
1055
+ expectResponseBody = true,
1056
+ allowAnonymous = false
1057
+ ): Promise<T> {
1058
+ return this.sendJson<T>(method, this.buildAuthUrl(relativeUrl), payload, expectResponseBody, allowAnonymous);
1059
+ }
1060
+
1061
+ private async sendJson<T extends JsonValue | null>(
1062
+ method: string,
1063
+ url: string,
1064
+ payload?: JsonValue | FormData,
1065
+ expectResponseBody = true,
1066
+ allowAnonymous = false,
1067
+ allowRetry = true
1068
+ ): Promise<T> {
1069
+ const response = await this.sendRequest(method, url, payload, allowAnonymous, allowRetry);
1070
+ if (!expectResponseBody) {
1071
+ return null as T;
1072
+ }
1073
+
1074
+ const text = await response.text();
1075
+ if (!text.trim()) {
1076
+ return null as T;
1077
+ }
1078
+
1079
+ return JSON.parse(text) as T;
1080
+ }
1081
+
1082
+ private async sendText(
1083
+ method: string,
1084
+ url: string,
1085
+ allowAnonymous = false,
1086
+ allowRetry = true
1087
+ ): Promise<string> {
1088
+ const response = await this.sendRequest(method, url, undefined, allowAnonymous, allowRetry);
1089
+ return await response.text();
1090
+ }
1091
+
1092
+ private async sendBlob(
1093
+ method: string,
1094
+ url: string,
1095
+ allowAnonymous = false,
1096
+ allowRetry = true
1097
+ ): Promise<Blob> {
1098
+ const response = await this.sendRequest(method, url, undefined, allowAnonymous, allowRetry);
1099
+ return await response.blob();
1100
+ }
1101
+
1102
+ private async sendRequest(
1103
+ method: string,
1104
+ url: string,
1105
+ payload?: JsonValue | FormData,
1106
+ allowAnonymous = false,
1107
+ allowRetry = true
1108
+ ): Promise<Response> {
1109
+ try {
1110
+ if (!allowAnonymous && this.canUseAuthentication()) {
1111
+ await this.getAuthTokenIfNecessary();
1112
+ }
1113
+
1114
+ const headers = new Headers();
1115
+ if (!allowAnonymous && this.tokenState.accessToken) {
1116
+ headers.set("Authorization", `Bearer ${this.tokenState.accessToken}`);
1117
+ }
1118
+ if (payload !== undefined && !this.isFormDataPayload(payload)) {
1119
+ headers.set("Content-Type", "application/json");
1120
+ }
1121
+
1122
+ const response = await this.fetchImpl(url, {
1123
+ method,
1124
+ headers,
1125
+ body: payload === undefined
1126
+ ? undefined
1127
+ : this.isFormDataPayload(payload)
1128
+ ? payload
1129
+ : JSON.stringify(payload)
1130
+ });
1131
+
1132
+ if ((response.status === 401 || response.status === 403) && !allowAnonymous && allowRetry && await this.tryRefreshAuthentication()) {
1133
+ return this.sendRequest(method, url, payload, allowAnonymous, false);
1134
+ }
1135
+
1136
+ if (!response.ok) {
1137
+ throw new ChillSharpClientError(
1138
+ `HTTP ${response.status} calling ${method} ${url}`,
1139
+ response.status,
1140
+ await response.text()
1141
+ );
1142
+ }
1143
+
1144
+ return response;
1145
+ } catch (error) {
1146
+ if (error instanceof ChillSharpClientError) {
1147
+ throw error;
1148
+ }
1149
+
1150
+ throw new ChillSharpClientError(`Unexpected error executing ${method} ${url}`, undefined, undefined, error);
1151
+ }
1152
+ }
1153
+
1154
+ private async getAuthTokenIfNecessary(forceRefresh = false): Promise<AuthTokenResponse> {
1155
+ if (this.refreshPromise) {
1156
+ return this.refreshPromise;
1157
+ }
1158
+
1159
+ this.refreshPromise = this.getAuthTokenIfNecessaryCore(forceRefresh);
1160
+ try {
1161
+ return await this.refreshPromise;
1162
+ } finally {
1163
+ this.refreshPromise = null;
1164
+ }
1165
+ }
1166
+
1167
+ private async getAuthTokenIfNecessaryCore(forceRefresh: boolean): Promise<AuthTokenResponse> {
1168
+ if (!forceRefresh && this.hasUsableAccessToken() && !this.shouldRefreshAccessToken()) {
1169
+ return this.createCurrentTokenResponse();
1170
+ }
1171
+
1172
+ if (this.tokenState.refreshToken && (!forceRefresh || !this.password)) {
1173
+ try {
1174
+ const refreshed = await this.sendAuthJson<AuthTokenResponse>(
1175
+ "POST",
1176
+ "refresh",
1177
+ { refreshToken: this.tokenState.refreshToken },
1178
+ true,
1179
+ true
1180
+ );
1181
+
1182
+ this.applyAuthToken(refreshed, true);
1183
+ return refreshed;
1184
+ } catch (error) {
1185
+ if (!(error instanceof ChillSharpClientError)) {
1186
+ throw error;
1187
+ }
1188
+
1189
+ this.tokenState.refreshToken = null;
1190
+ this.tokenState.refreshTokenExpiresUtc = null;
1191
+ }
1192
+ }
1193
+
1194
+ if (this.username && this.password) {
1195
+ const token = await this.sendAuthJson<AuthTokenResponse>(
1196
+ "POST",
1197
+ "login",
1198
+ {
1199
+ userNameOrEmail: this.username,
1200
+ password: this.password
1201
+ },
1202
+ true,
1203
+ true
1204
+ );
1205
+
1206
+ this.applyAuthToken(token, true);
1207
+ return token;
1208
+ }
1209
+
1210
+ if (this.hasUsableAccessToken()) {
1211
+ return this.createCurrentTokenResponse();
1212
+ }
1213
+
1214
+ throw new ChillSharpClientError("No auth token is available and the client cannot obtain a new one.");
1215
+ }
1216
+
1217
+ private applyAuthToken(payload: JsonObject, forgetPassword: boolean): void {
1218
+ this.tokenState.accessToken = this.readString(payload, "accessToken");
1219
+ this.tokenState.accessTokenIssuedUtc = this.readDate(payload, "accessTokenIssuedUtc");
1220
+ this.tokenState.accessTokenExpiresUtc = this.readDate(payload, "accessTokenExpiresUtc");
1221
+ this.tokenState.refreshToken = this.readString(payload, "refreshToken");
1222
+ this.tokenState.refreshTokenExpiresUtc = this.readDate(payload, "refreshTokenExpiresUtc");
1223
+
1224
+ const userName = this.readString(payload, "userName");
1225
+ if (userName) {
1226
+ this.username = userName;
1227
+ }
1228
+
1229
+ if (forgetPassword) {
1230
+ this.password = null;
1231
+ }
1232
+ }
1233
+
1234
+ private clearAuthToken(): void {
1235
+ this.tokenState.accessToken = null;
1236
+ this.tokenState.accessTokenIssuedUtc = null;
1237
+ this.tokenState.accessTokenExpiresUtc = null;
1238
+ this.tokenState.refreshToken = null;
1239
+ this.tokenState.refreshTokenExpiresUtc = null;
1240
+ }
1241
+
1242
+ private canUseAuthentication(): boolean {
1243
+ return !!(this.tokenState.accessToken || this.tokenState.refreshToken || (this.username && this.password));
1244
+ }
1245
+
1246
+ private hasUsableAccessToken(): boolean {
1247
+ if (!this.tokenState.accessToken) {
1248
+ return false;
1249
+ }
1250
+
1251
+ if (!this.tokenState.accessTokenExpiresUtc) {
1252
+ return true;
1253
+ }
1254
+
1255
+ return new Date() < this.tokenState.accessTokenExpiresUtc;
1256
+ }
1257
+
1258
+ private shouldRefreshAccessToken(): boolean {
1259
+ const issued = this.tokenState.accessTokenIssuedUtc;
1260
+ const expires = this.tokenState.accessTokenExpiresUtc;
1261
+
1262
+ if (!issued || !expires) {
1263
+ return false;
1264
+ }
1265
+
1266
+ if (expires <= issued) {
1267
+ return true;
1268
+ }
1269
+
1270
+ const refreshThreshold = new Date(issued.getTime() + (expires.getTime() - issued.getTime()) * 0.75);
1271
+ return new Date() >= refreshThreshold;
1272
+ }
1273
+
1274
+ private async tryRefreshAuthentication(): Promise<boolean> {
1275
+ if (!this.tokenState.refreshToken && !this.password) {
1276
+ return false;
1277
+ }
1278
+
1279
+ try {
1280
+ await this.getAuthTokenIfNecessary(true);
1281
+ return true;
1282
+ } catch (error) {
1283
+ if (error instanceof ChillSharpClientError) {
1284
+ return false;
1285
+ }
1286
+
1287
+ throw error;
1288
+ }
1289
+ }
1290
+
1291
+ private createCurrentTokenResponse(): AuthTokenResponse {
1292
+ return {
1293
+ accessToken: this.tokenState.accessToken ?? "",
1294
+ accessTokenIssuedUtc: this.formatDate(this.tokenState.accessTokenIssuedUtc),
1295
+ accessTokenExpiresUtc: this.formatDate(this.tokenState.accessTokenExpiresUtc),
1296
+ refreshToken: this.tokenState.refreshToken ?? "",
1297
+ refreshTokenExpiresUtc: this.formatDate(this.tokenState.refreshTokenExpiresUtc),
1298
+ userId: "",
1299
+ userName: this.username ?? ""
1300
+ };
1301
+ }
1302
+
1303
+ private buildChillUrl(relativeUrl: string): string {
1304
+ return `${this.baseUrl}/${relativeUrl.replace(/^\/+/, "")}`;
1305
+ }
1306
+
1307
+ private buildNotifyUrl(): string {
1308
+ return `${this.getApiBaseUrl().replace(/\/$/, "")}/notify`;
1309
+ }
1310
+
1311
+ private buildApiUrl(relativeUrl: string): string {
1312
+ return `${this.getApiBaseUrl().replace(/\/$/, "")}/${relativeUrl.replace(/^\/+/, "")}`;
1313
+ }
1314
+
1315
+ private buildAuthUrl(relativeUrl: string): string {
1316
+ return `${this.getAuthBaseUrl().replace(/\/$/, "")}/${relativeUrl.replace(/^\/+/, "")}`;
1317
+ }
1318
+
1319
+ private buildSchemaUrl(relativeUrl: string): string {
1320
+ return `${this.getSchemaBaseUrl().replace(/\/$/, "")}/${relativeUrl.replace(/^\/+/, "")}`;
1321
+ }
1322
+
1323
+ private buildI18nUrl(relativeUrl: string): string {
1324
+ return `${this.getI18nBaseUrl().replace(/\/$/, "")}/${relativeUrl.replace(/^\/+/, "")}`;
1325
+ }
1326
+
1327
+ private buildAttachmentUrl(relativeUrl: string): string {
1328
+ return `${this.getAttachmentBaseUrl().replace(/\/$/, "")}/${relativeUrl.replace(/^\/+/, "")}`;
1329
+ }
1330
+
1331
+ private getAuthBaseUrl(): string {
1332
+ const suffix = "/chill";
1333
+ if (this.baseUrl.toLowerCase().endsWith(suffix)) {
1334
+ return `${this.baseUrl.slice(0, -suffix.length)}/chill-auth`;
1335
+ }
1336
+
1337
+ return `${this.baseUrl.replace(/\/$/, "")}-auth`;
1338
+ }
1339
+
1340
+ private getSchemaBaseUrl(): string {
1341
+ const suffix = "/chill";
1342
+ if (this.baseUrl.toLowerCase().endsWith(suffix)) {
1343
+ return `${this.baseUrl.slice(0, -suffix.length)}/chill-schema`;
1344
+ }
1345
+
1346
+ return `${this.baseUrl.replace(/\/$/, "")}-schema`;
1347
+ }
1348
+
1349
+ private getI18nBaseUrl(): string {
1350
+ const suffix = "/chill";
1351
+ if (this.baseUrl.toLowerCase().endsWith(suffix)) {
1352
+ return `${this.baseUrl.slice(0, -suffix.length)}/chill-i18n`;
1353
+ }
1354
+
1355
+ return `${this.baseUrl.replace(/\/$/, "")}-i18n`;
1356
+ }
1357
+
1358
+ private getAttachmentBaseUrl(): string {
1359
+ const suffix = "/chill";
1360
+ if (this.baseUrl.toLowerCase().endsWith(suffix)) {
1361
+ return `${this.baseUrl.slice(0, -suffix.length)}/chill-attachment`;
1362
+ }
1363
+
1364
+ return `${this.baseUrl.replace(/\/$/, "")}-attachment`;
1365
+ }
1366
+
1367
+ private getApiBaseUrl(): string {
1368
+ const suffix = "/chill";
1369
+ if (this.baseUrl.toLowerCase().endsWith(suffix)) {
1370
+ return this.baseUrl.slice(0, -suffix.length);
1371
+ }
1372
+
1373
+ return this.baseUrl.replace(/\/$/, "");
1374
+ }
1375
+
1376
+ private normalizeBaseUrl(baseUrl: string, apiBasePath?: string): string {
1377
+ const normalized = this.normalizeRequiredValue(baseUrl, "baseUrl").replace(/\/+$/, "");
1378
+ if (this.isKnownChillSharpEndpointBase(normalized)) {
1379
+ return normalized;
1380
+ }
1381
+
1382
+ const normalizedApiBasePath = this.normalizeApiBasePath(apiBasePath);
1383
+ if (!normalizedApiBasePath) {
1384
+ return `${normalized}/chill`;
1385
+ }
1386
+
1387
+ if (this.endsWithPathSegment(normalized, normalizedApiBasePath)) {
1388
+ return `${normalized}/chill`;
1389
+ }
1390
+
1391
+ return `${normalized}/${normalizedApiBasePath}/chill`;
1392
+ }
1393
+
1394
+ private normalizeApiBasePath(apiBasePath?: string): string {
1395
+ const normalized = this.normalizeOptionalValue(apiBasePath) ?? API_BASE_PATH;
1396
+ return normalized.replace(/^\/+|\/+$/g, "");
1397
+ }
1398
+
1399
+ private isKnownChillSharpEndpointBase(baseUrl: string): boolean {
1400
+ const lowerBaseUrl = baseUrl.toLowerCase();
1401
+ return lowerBaseUrl.endsWith("/chill") ||
1402
+ lowerBaseUrl.endsWith("/chill-auth") ||
1403
+ lowerBaseUrl.endsWith("/chill-schema") ||
1404
+ lowerBaseUrl.endsWith("/chill-i18n") ||
1405
+ lowerBaseUrl.endsWith("/chill-attachment");
1406
+ }
1407
+
1408
+ private endsWithPathSegment(value: string, segment: string): boolean {
1409
+ return value.toLowerCase().endsWith(`/${segment.toLowerCase()}`);
1410
+ }
1411
+
1412
+ private normalizeRequiredValue(value: string | null | undefined, argumentName: string): string {
1413
+ const normalized = this.normalizeOptionalValue(value);
1414
+ if (!normalized) {
1415
+ throw new Error(`${argumentName} is required.`);
1416
+ }
1417
+
1418
+ return normalized;
1419
+ }
1420
+
1421
+ private normalizeOptionalValue(value?: string | null): string | null {
1422
+ const normalized = value?.trim();
1423
+ return normalized ? normalized : null;
1424
+ }
1425
+
1426
+ private normalizeQueryValue(value?: string | null): string | null {
1427
+ return value == null ? null : value.trim();
1428
+ }
1429
+
1430
+ private readString(payload: JsonObject, key: string): string | null {
1431
+ const value = this.readValue(payload, key);
1432
+ return typeof value === "string" && value.trim() ? value.trim() : null;
1433
+ }
1434
+
1435
+ private readDate(payload: JsonObject, key: string): Date | null {
1436
+ return this.parseDate(this.readValue(payload, key));
1437
+ }
1438
+
1439
+ private readValue(payload: JsonObject, key: string): JsonValue | undefined {
1440
+ if (key in payload) {
1441
+ return payload[key];
1442
+ }
1443
+
1444
+ const pascalKey = key.length > 1
1445
+ ? `${key[0].toUpperCase()}${key.slice(1)}`
1446
+ : key.toUpperCase();
1447
+
1448
+ if (pascalKey in payload) {
1449
+ return payload[pascalKey];
1450
+ }
1451
+
1452
+ const matchedKey = Object.keys(payload).find((candidate) => candidate.toLowerCase() === key.toLowerCase());
1453
+ return matchedKey ? payload[matchedKey] : undefined;
1454
+ }
1455
+
1456
+ private getAttachmentTargetInfo(targetEntity: JsonObject): { guid: string; chillType: string } {
1457
+ const guid = this.readString(targetEntity, "guid");
1458
+ if (!guid) {
1459
+ throw new Error("targetEntity.guid is required.");
1460
+ }
1461
+
1462
+ const chillType = this.readString(targetEntity, "chillType");
1463
+ if (!chillType) {
1464
+ throw new Error("targetEntity.chillType is required.");
1465
+ }
1466
+
1467
+ return {
1468
+ guid,
1469
+ chillType
1470
+ };
1471
+ }
1472
+
1473
+ private getAttachmentGuid(attachmentEntity: JsonObject): string {
1474
+ const guid = this.readString(attachmentEntity, "guid");
1475
+ if (!guid) {
1476
+ throw new Error("attachmentEntity.guid is required.");
1477
+ }
1478
+
1479
+ const chillType = this.readString(attachmentEntity, "chillType");
1480
+ if (chillType && chillType !== ChillSharpClient.attachmentEntityChillType) {
1481
+ const normalizedChillType = chillType.split(".").pop() ?? chillType;
1482
+ const normalizedAttachmentType = ChillSharpClient.attachmentEntityChillType.split(".").pop() ?? ChillSharpClient.attachmentEntityChillType;
1483
+ if (normalizedChillType !== normalizedAttachmentType) {
1484
+ throw new Error("attachmentEntity must point to an attachment.");
1485
+ }
1486
+ }
1487
+
1488
+ return guid;
1489
+ }
1490
+
1491
+ private toAttachmentBlob(file: ChillAttachmentUploadFile): Blob {
1492
+ if (!file || typeof file !== "object") {
1493
+ throw new Error("file is required.");
1494
+ }
1495
+
1496
+ const contentType = this.normalizeOptionalValue(file.contentType) ?? "application/octet-stream";
1497
+ if (file.content instanceof Blob) {
1498
+ return file.content;
1499
+ }
1500
+
1501
+ if (typeof file.content === "string" || file.content instanceof ArrayBuffer) {
1502
+ return new Blob([file.content], { type: contentType });
1503
+ }
1504
+
1505
+ if (file.content instanceof Uint8Array) {
1506
+ const buffer = file.content.buffer.slice(
1507
+ file.content.byteOffset,
1508
+ file.content.byteOffset + file.content.byteLength
1509
+ ) as ArrayBuffer;
1510
+ return new Blob([buffer], { type: contentType });
1511
+ }
1512
+
1513
+ return new Blob([String(file.content)], { type: contentType });
1514
+ }
1515
+
1516
+ private isFormDataPayload(payload: JsonValue | FormData): payload is FormData {
1517
+ return typeof FormData !== "undefined" && payload instanceof FormData;
1518
+ }
1519
+
1520
+ private parseDate(value: JsonValue | undefined): Date | null {
1521
+ if (typeof value !== "string" || !value.trim()) {
1522
+ return null;
1523
+ }
1524
+
1525
+ const parsed = new Date(value);
1526
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
1527
+ }
1528
+
1529
+ private formatDate(value: Date | null): string {
1530
+ return value ? value.toISOString() : "";
1531
+ }
1532
+
1533
+ private async ensureNotificationConnection(): Promise<HubConnection> {
1534
+ if (this.notificationConnection) {
1535
+ if (this.notificationConnection.state === HubConnectionState.Disconnected) {
1536
+ await this.notificationConnection.start();
1537
+ }
1538
+
1539
+ return this.notificationConnection;
1540
+ }
1541
+
1542
+ const connection = new HubConnectionBuilder()
1543
+ .withUrl(this.buildNotifyUrl(), {
1544
+ withCredentials: this.signalRWithCredentials,
1545
+ accessTokenFactory: async () => {
1546
+ if (this.canUseAuthentication()) {
1547
+ await this.getAuthTokenIfNecessary();
1548
+ }
1549
+
1550
+ return this.tokenState.accessToken ?? "";
1551
+ }
1552
+ })
1553
+ .withAutomaticReconnect()
1554
+ .build();
1555
+
1556
+ connection.on("EntitiesChanged", (payload: unknown) => {
1557
+ void this.dispatchEntityChangeNotifications(payload);
1558
+ });
1559
+
1560
+ connection.onreconnected(async () => {
1561
+ await this.reregisterEntityChangeSubscriptions();
1562
+ });
1563
+
1564
+ await connection.start();
1565
+ this.notificationConnection = connection;
1566
+ return connection;
1567
+ }
1568
+
1569
+ private async unsubscribeFromEntityChanges(subscriptionId: string): Promise<void> {
1570
+ const subscription = this.entityChangeSubscriptions.get(subscriptionId);
1571
+ if (!subscription) {
1572
+ return;
1573
+ }
1574
+
1575
+ this.entityChangeSubscriptions.delete(subscriptionId);
1576
+
1577
+ const registrationKey = this.buildEntityChangeRegistrationKey(subscription.chillType, subscription.guid);
1578
+ const registrationCount = this.entityChangeRegistrationCounts.get(registrationKey) ?? 0;
1579
+ if (registrationCount <= 1) {
1580
+ this.entityChangeRegistrationCounts.delete(registrationKey);
1581
+
1582
+ const connection = this.notificationConnection;
1583
+ if (connection && connection.state === HubConnectionState.Connected) {
1584
+ await connection.invoke("Unregister", subscription.chillType, subscription.guid);
1585
+ }
1586
+ } else {
1587
+ this.entityChangeRegistrationCounts.set(registrationKey, registrationCount - 1);
1588
+ }
1589
+ }
1590
+
1591
+ private async dispatchEntityChangeNotifications(payload: unknown): Promise<void> {
1592
+ const notifications = this.normalizeEntityChangeNotifications(payload);
1593
+ if (notifications.length === 0) {
1594
+ return;
1595
+ }
1596
+
1597
+ for (const subscription of this.entityChangeSubscriptions.values()) {
1598
+ const matchingChanges = notifications.filter((change) =>
1599
+ change.chillType === subscription.chillType &&
1600
+ (!subscription.guid || change.guid === subscription.guid)
1601
+ );
1602
+
1603
+ if (matchingChanges.length === 0) {
1604
+ continue;
1605
+ }
1606
+
1607
+ await subscription.callback(matchingChanges);
1608
+ }
1609
+ }
1610
+
1611
+ private normalizeEntityChangeNotifications(payload: unknown): ChillEntityChangeNotification[] {
1612
+ if (!Array.isArray(payload)) {
1613
+ return [];
1614
+ }
1615
+
1616
+ return payload
1617
+ .filter((entry): entry is JsonObject => !!entry && typeof entry === "object" && !Array.isArray(entry))
1618
+ .map((entry) => {
1619
+ const chillType = this.readString(entry, "chillType");
1620
+ const guid = this.readString(entry, "guid");
1621
+ const action = this.readString(entry, "action");
1622
+ if (!chillType || !guid || !this.isEntityChangeAction(action)) {
1623
+ return null;
1624
+ }
1625
+
1626
+ return {
1627
+ chillType,
1628
+ guid,
1629
+ action
1630
+ } satisfies ChillEntityChangeNotification;
1631
+ })
1632
+ .filter((entry): entry is ChillEntityChangeNotification => entry !== null);
1633
+ }
1634
+
1635
+ private isEntityChangeAction(value: string | null): value is ChillEntityChangeAction {
1636
+ return value === "CREATED" || value === "UPDATED" || value === "DELETED";
1637
+ }
1638
+
1639
+ private async reregisterEntityChangeSubscriptions(): Promise<void> {
1640
+ const connection = this.notificationConnection;
1641
+ if (!connection || connection.state !== HubConnectionState.Connected) {
1642
+ return;
1643
+ }
1644
+
1645
+ for (const registrationKey of this.entityChangeRegistrationCounts.keys()) {
1646
+ const separatorIndex = registrationKey.indexOf("|");
1647
+ const chillType = separatorIndex >= 0 ? registrationKey.slice(0, separatorIndex) : registrationKey;
1648
+ const guid = separatorIndex >= 0 ? registrationKey.slice(separatorIndex + 1) : "";
1649
+ await connection.invoke("Register", chillType, guid || null);
1650
+ }
1651
+ }
1652
+
1653
+ private buildEntityChangeRegistrationKey(chillType: string, guid: string | null): string {
1654
+ return `${chillType}|${guid ?? ""}`;
1655
+ }
1656
+
1657
+ private async getUsersAssignedToRole(roleGuid: string): Promise<AuthUserListItem[]> {
1658
+ const users = await this.getAuthUsers();
1659
+ const matches = await Promise.all(
1660
+ users.map(async (user) => {
1661
+ const roles = await this.getAuthUserRoles(user.guid);
1662
+ return roles.some((role) => role.guid === roleGuid) ? user : null;
1663
+ })
1664
+ );
1665
+
1666
+ return matches.filter((user): user is AuthUserListItem => user !== null);
1667
+ }
1668
+
1669
+ private async syncUserRoles(userGuid: string, roleGuids: string[]): Promise<void> {
1670
+ const desiredRoleGuids = new Set(roleGuids.map((roleGuid) => this.normalizeRequiredValue(roleGuid, "roleGuid")));
1671
+ const currentRoles = await this.getAuthUserRoles(userGuid);
1672
+ const currentRoleGuids = new Set(currentRoles.map((role) => role.guid));
1673
+
1674
+ for (const roleGuid of desiredRoleGuids) {
1675
+ if (!currentRoleGuids.has(roleGuid)) {
1676
+ await this.assignAuthRole(userGuid, roleGuid);
1677
+ }
1678
+ }
1679
+
1680
+ for (const role of currentRoles) {
1681
+ if (!desiredRoleGuids.has(role.guid)) {
1682
+ await this.removeAuthRole(userGuid, role.guid);
1683
+ }
1684
+ }
1685
+ }
1686
+
1687
+ private async syncUserPermissions(userGuid: string, permissions: AuthPermissionRuleItem[]): Promise<void> {
1688
+ const currentRules = await this.getAuthPermissionRules(userGuid, null);
1689
+ await this.syncPermissionRules(
1690
+ currentRules,
1691
+ permissions,
1692
+ (permission) => ({
1693
+ userGuid,
1694
+ roleGuid: null,
1695
+ effect: permission.effect,
1696
+ action: permission.action,
1697
+ scope: permission.scope,
1698
+ module: permission.module,
1699
+ entityName: permission.entityName,
1700
+ propertyName: permission.propertyName,
1701
+ appliesToAllProperties: permission.appliesToAllProperties,
1702
+ description: permission.description
1703
+ }),
1704
+ (payload) => this.createAuthPermissionRule(payload),
1705
+ (guid, payload) => this.updateAuthPermissionRule(guid, payload),
1706
+ (guid) => this.deleteAuthPermissionRule(guid)
1707
+ );
1708
+ }
1709
+
1710
+ private async syncRoleUsers(roleGuid: string, userGuids: string[]): Promise<void> {
1711
+ const desiredUserGuids = new Set(userGuids.map((userGuid) => this.normalizeRequiredValue(userGuid, "userGuid")));
1712
+ const currentUsers = await this.getUsersAssignedToRole(roleGuid);
1713
+ const currentUserGuids = new Set(currentUsers.map((user) => user.guid));
1714
+
1715
+ for (const userGuid of desiredUserGuids) {
1716
+ if (!currentUserGuids.has(userGuid)) {
1717
+ await this.assignAuthRole(userGuid, roleGuid);
1718
+ }
1719
+ }
1720
+
1721
+ for (const user of currentUsers) {
1722
+ if (!desiredUserGuids.has(user.guid)) {
1723
+ await this.removeAuthRole(user.guid, roleGuid);
1724
+ }
1725
+ }
1726
+ }
1727
+
1728
+ private async syncRolePermissions(roleGuid: string, permissions: AuthPermissionRuleItem[]): Promise<void> {
1729
+ const currentRules = await this.getAuthPermissionRules(null, roleGuid);
1730
+ await this.syncPermissionRules(
1731
+ currentRules,
1732
+ permissions,
1733
+ (permission) => ({
1734
+ userGuid: null,
1735
+ roleGuid,
1736
+ effect: permission.effect,
1737
+ action: permission.action,
1738
+ scope: permission.scope,
1739
+ module: permission.module,
1740
+ entityName: permission.entityName,
1741
+ propertyName: permission.propertyName,
1742
+ appliesToAllProperties: permission.appliesToAllProperties,
1743
+ description: permission.description
1744
+ }),
1745
+ (payload) => this.createAuthPermissionRule(payload),
1746
+ (guid, payload) => this.updateAuthPermissionRule(guid, payload),
1747
+ (guid) => this.deleteAuthPermissionRule(guid)
1748
+ );
1749
+ }
1750
+
1751
+ private async syncPermissionRules(
1752
+ currentRules: AuthPermissionRule[],
1753
+ desiredRules: AuthPermissionRuleItem[],
1754
+ toPayload: (permission: AuthPermissionRuleItem) => CreateAuthPermissionRuleRequest,
1755
+ createRule: (payload: CreateAuthPermissionRuleRequest) => Promise<AuthPermissionRule>,
1756
+ updateRule: (guid: string, payload: UpdateAuthPermissionRuleRequest) => Promise<AuthPermissionRule | null>,
1757
+ deleteRule: (guid: string) => Promise<void>
1758
+ ): Promise<void> {
1759
+ const desiredByGuid = new Map<string, AuthPermissionRuleItem>();
1760
+ const newRules: AuthPermissionRuleItem[] = [];
1761
+
1762
+ for (const permission of desiredRules) {
1763
+ const guid = this.normalizeOptionalValue(permission.guid);
1764
+ if (guid) {
1765
+ desiredByGuid.set(guid, permission);
1766
+ } else {
1767
+ newRules.push(permission);
1768
+ }
1769
+ }
1770
+
1771
+ for (const currentRule of currentRules) {
1772
+ const desiredRule = desiredByGuid.get(currentRule.guid);
1773
+ if (!desiredRule) {
1774
+ await deleteRule(currentRule.guid);
1775
+ continue;
1776
+ }
1777
+
1778
+ await updateRule(currentRule.guid, toPayload(desiredRule));
1779
+ desiredByGuid.delete(currentRule.guid);
1780
+ }
1781
+
1782
+ for (const desiredRule of desiredByGuid.values()) {
1783
+ await createRule(toPayload(desiredRule));
1784
+ }
1785
+
1786
+ for (const desiredRule of newRules) {
1787
+ await createRule(toPayload(desiredRule));
1788
+ }
1789
+ }
1790
+ }
1791
+
1792
+
1793
+
1794
+
1795
+
1796
+
1797
+
1798
+
1799
+
1800
+
1801
+
1802
+
1803
+
1804
+
1805
+
1806
+
1807
+
1808
+
1809
+