@clivly/core 0.10.9 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/adapter.d.cts +44 -7
  2. package/dist/adapter.d.mts +44 -7
  3. package/dist/auth-adapter.d.cts +6 -7
  4. package/dist/auth-adapter.d.mts +6 -7
  5. package/dist/builder.d.cts +12 -12
  6. package/dist/builder.d.mts +12 -12
  7. package/dist/drizzle.d.cts +5 -6
  8. package/dist/drizzle.d.mts +5 -6
  9. package/dist/{entity-config-C_NDVfrL.mjs → entity-config-C0rtu-K_.mjs} +20 -2
  10. package/dist/{entity-config-J5KYJVTO.cjs → entity-config-C2sKAti2.cjs} +20 -2
  11. package/dist/{entity-config-ChHLozko.d.cts → entity-config-OhC4Otkz.d.cts} +4 -2
  12. package/dist/{entity-config-ChHLozko.d.mts → entity-config-OhC4Otkz.d.mts} +4 -2
  13. package/dist/entity-config.cjs +1 -1
  14. package/dist/entity-config.d.cts +1 -1
  15. package/dist/entity-config.d.mts +1 -1
  16. package/dist/entity-config.mjs +1 -1
  17. package/dist/entity-heuristics.cjs +43 -24
  18. package/dist/entity-heuristics.d.cts +21 -21
  19. package/dist/entity-heuristics.d.mts +21 -21
  20. package/dist/entity-heuristics.mjs +43 -24
  21. package/dist/env-file.d.cts +6 -7
  22. package/dist/env-file.d.mts +6 -7
  23. package/dist/index.cjs +83 -1
  24. package/dist/index.d.cts +89 -22
  25. package/dist/index.d.mts +89 -22
  26. package/dist/index.mjs +80 -2
  27. package/dist/integration-state.d.cts +30 -31
  28. package/dist/integration-state.d.mts +30 -31
  29. package/dist/mapping-form.d.cts +6 -7
  30. package/dist/mapping-form.d.mts +6 -7
  31. package/dist/mapping-score.d.cts +14 -15
  32. package/dist/mapping-score.d.mts +14 -15
  33. package/dist/mappings-config.cjs +2 -1
  34. package/dist/mappings-config.d.cts +8 -7
  35. package/dist/mappings-config.d.mts +8 -7
  36. package/dist/mappings-config.mjs +2 -1
  37. package/dist/sync-engine.d.cts +15 -16
  38. package/dist/sync-engine.d.mts +15 -16
  39. package/dist/types.d.cts +99 -33
  40. package/dist/types.d.mts +99 -33
  41. package/dist/view-compiler.d.cts +10 -11
  42. package/dist/view-compiler.d.mts +10 -11
  43. package/package.json +2 -2
@@ -1,4 +1,4 @@
1
- import { a as ClivlyEntitiesConfig } from "./entity-config-ChHLozko.cjs";
1
+ import { a as ClivlyEntitiesConfig } from "./entity-config-OhC4Otkz.cjs";
2
2
  import { CompileOptions, CompiledView } from "./view-compiler.cjs";
3
3
  //#region src/sync-engine.d.ts
4
4
  /**
@@ -19,7 +19,7 @@ import { CompileOptions, CompiledView } from "./view-compiler.cjs";
19
19
  * package provides the concrete store.
20
20
  */
21
21
  /** A row read from a compiled view (what Postgres returns). */
22
- interface SourceRow {
22
+ export interface SourceRow {
23
23
  /** Host-side company key from `<relationship>_ref`; resolved before persist. */
24
24
  companyRef?: string | null;
25
25
  /** Mapped identity fields (clivly field → value). */
@@ -28,14 +28,14 @@ interface SourceRow {
28
28
  sourceRef: string;
29
29
  }
30
30
  /** An existing mirror row, as far as reconciliation needs to know. */
31
- interface MirrorRecord {
31
+ export interface MirrorRecord {
32
32
  archivedAt: Date | null;
33
33
  /** Host-side company key; only consulted in composite (join-table) mode. */
34
34
  companyRef: string | null;
35
35
  id: string;
36
36
  sourceRef: string;
37
37
  }
38
- type SyncAction = {
38
+ export type SyncAction = {
39
39
  kind: "create";
40
40
  sourceRef: string;
41
41
  fields: Record<string, string | null>;
@@ -58,7 +58,7 @@ type SyncAction = {
58
58
  sourceRef: string;
59
59
  };
60
60
  /** A persist action with the company ref resolved to a `crm_companies.id`. */
61
- type PersistAction = {
61
+ export type PersistAction = {
62
62
  kind: "create";
63
63
  sourceRef: string;
64
64
  fields: Record<string, string | null>;
@@ -99,11 +99,11 @@ type PersistAction = {
99
99
  * person in several companies materializes one mirror row per company and a
100
100
  * single membership leaving archives only that one row.
101
101
  */
102
- declare function reconcile(source: SourceRow[], mirror: MirrorRecord[], opts?: {
102
+ export declare function reconcile(source: SourceRow[], mirror: MirrorRecord[], opts?: {
103
103
  composite?: boolean;
104
104
  }): SyncAction[];
105
105
  /** DB I/O port. The concrete store lives in the SDK/drizzle package. */
106
- interface SyncStore {
106
+ export interface SyncStore {
107
107
  /** Current mirror rows for a concept + role (including archived). */
108
108
  listMirror(input: {
109
109
  concept: string;
@@ -129,7 +129,7 @@ interface SyncStore {
129
129
  /** Map host-side company refs → `crm_companies.id` (already-synced companies only). */
130
130
  resolveCompanyRefs(refs: string[]): Promise<Map<string, string>>;
131
131
  }
132
- interface EntitySyncResult {
132
+ export interface EntitySyncResult {
133
133
  archived: number;
134
134
  concept: string;
135
135
  created: number;
@@ -138,7 +138,7 @@ interface EntitySyncResult {
138
138
  role: string;
139
139
  updated: number;
140
140
  }
141
- interface SyncResult {
141
+ export interface SyncResult {
142
142
  entities: EntitySyncResult[];
143
143
  totals: {
144
144
  created: number;
@@ -151,12 +151,12 @@ interface SyncResult {
151
151
  * Run a full sync: compile the config's views, then reconcile + persist each,
152
152
  * companies first. Returns per-entity and total action counts.
153
153
  */
154
- declare function runSync(config: ClivlyEntitiesConfig, store: SyncStore, options?: CompileOptions): Promise<SyncResult>;
155
- interface PreviewWarning {
154
+ export declare function runSync(config: ClivlyEntitiesConfig, store: SyncStore, options?: CompileOptions): Promise<SyncResult>;
155
+ export interface PreviewWarning {
156
156
  field?: string;
157
157
  message: string;
158
158
  }
159
- interface PreviewResult {
159
+ export interface PreviewResult {
160
160
  rows: SourceRow[];
161
161
  sampledCount: number;
162
162
  warnings: PreviewWarning[];
@@ -167,7 +167,7 @@ interface PreviewResult {
167
167
  * a required field empty in ≥1 row → `K/N` form; an optional field empty in
168
168
  * ALL rows → all-empty form; zero source rows → a single table-level warning.
169
169
  */
170
- declare function computeSampleWarnings(rows: SourceRow[], requiredFields: string[], sourceTable: string): PreviewWarning[];
170
+ export declare function computeSampleWarnings(rows: SourceRow[], requiredFields: string[], sourceTable: string): PreviewWarning[];
171
171
  /**
172
172
  * Zero-write preview: read ≤`limit` rows through the compiled view (a pure
173
173
  * SELECT whose SQL already projects each row into its mapped fields) and
@@ -175,10 +175,9 @@ declare function computeSampleWarnings(rows: SourceRow[], requiredFields: string
175
175
  * so it can never mutate the mirror. The distinct, write-one-row sibling is
176
176
  * `runSync`'s sample mode (T13).
177
177
  */
178
- declare function previewSync(store: SyncStore, view: CompiledView, opts: {
178
+ export declare function previewSync(store: SyncStore, view: CompiledView, opts: {
179
179
  limit: number;
180
180
  requiredFields: string[];
181
181
  sourceTable: string;
182
182
  }): Promise<PreviewResult>;
183
- //#endregion
184
- export { EntitySyncResult, MirrorRecord, PersistAction, PreviewResult, PreviewWarning, SourceRow, SyncAction, SyncResult, SyncStore, computeSampleWarnings, previewSync, reconcile, runSync };
183
+ //#endregion
@@ -1,4 +1,4 @@
1
- import { a as ClivlyEntitiesConfig } from "./entity-config-ChHLozko.mjs";
1
+ import { a as ClivlyEntitiesConfig } from "./entity-config-OhC4Otkz.mjs";
2
2
  import { CompileOptions, CompiledView } from "./view-compiler.mjs";
3
3
  //#region src/sync-engine.d.ts
4
4
  /**
@@ -19,7 +19,7 @@ import { CompileOptions, CompiledView } from "./view-compiler.mjs";
19
19
  * package provides the concrete store.
20
20
  */
21
21
  /** A row read from a compiled view (what Postgres returns). */
22
- interface SourceRow {
22
+ export interface SourceRow {
23
23
  /** Host-side company key from `<relationship>_ref`; resolved before persist. */
24
24
  companyRef?: string | null;
25
25
  /** Mapped identity fields (clivly field → value). */
@@ -28,14 +28,14 @@ interface SourceRow {
28
28
  sourceRef: string;
29
29
  }
30
30
  /** An existing mirror row, as far as reconciliation needs to know. */
31
- interface MirrorRecord {
31
+ export interface MirrorRecord {
32
32
  archivedAt: Date | null;
33
33
  /** Host-side company key; only consulted in composite (join-table) mode. */
34
34
  companyRef: string | null;
35
35
  id: string;
36
36
  sourceRef: string;
37
37
  }
38
- type SyncAction = {
38
+ export type SyncAction = {
39
39
  kind: "create";
40
40
  sourceRef: string;
41
41
  fields: Record<string, string | null>;
@@ -58,7 +58,7 @@ type SyncAction = {
58
58
  sourceRef: string;
59
59
  };
60
60
  /** A persist action with the company ref resolved to a `crm_companies.id`. */
61
- type PersistAction = {
61
+ export type PersistAction = {
62
62
  kind: "create";
63
63
  sourceRef: string;
64
64
  fields: Record<string, string | null>;
@@ -99,11 +99,11 @@ type PersistAction = {
99
99
  * person in several companies materializes one mirror row per company and a
100
100
  * single membership leaving archives only that one row.
101
101
  */
102
- declare function reconcile(source: SourceRow[], mirror: MirrorRecord[], opts?: {
102
+ export declare function reconcile(source: SourceRow[], mirror: MirrorRecord[], opts?: {
103
103
  composite?: boolean;
104
104
  }): SyncAction[];
105
105
  /** DB I/O port. The concrete store lives in the SDK/drizzle package. */
106
- interface SyncStore {
106
+ export interface SyncStore {
107
107
  /** Current mirror rows for a concept + role (including archived). */
108
108
  listMirror(input: {
109
109
  concept: string;
@@ -129,7 +129,7 @@ interface SyncStore {
129
129
  /** Map host-side company refs → `crm_companies.id` (already-synced companies only). */
130
130
  resolveCompanyRefs(refs: string[]): Promise<Map<string, string>>;
131
131
  }
132
- interface EntitySyncResult {
132
+ export interface EntitySyncResult {
133
133
  archived: number;
134
134
  concept: string;
135
135
  created: number;
@@ -138,7 +138,7 @@ interface EntitySyncResult {
138
138
  role: string;
139
139
  updated: number;
140
140
  }
141
- interface SyncResult {
141
+ export interface SyncResult {
142
142
  entities: EntitySyncResult[];
143
143
  totals: {
144
144
  created: number;
@@ -151,12 +151,12 @@ interface SyncResult {
151
151
  * Run a full sync: compile the config's views, then reconcile + persist each,
152
152
  * companies first. Returns per-entity and total action counts.
153
153
  */
154
- declare function runSync(config: ClivlyEntitiesConfig, store: SyncStore, options?: CompileOptions): Promise<SyncResult>;
155
- interface PreviewWarning {
154
+ export declare function runSync(config: ClivlyEntitiesConfig, store: SyncStore, options?: CompileOptions): Promise<SyncResult>;
155
+ export interface PreviewWarning {
156
156
  field?: string;
157
157
  message: string;
158
158
  }
159
- interface PreviewResult {
159
+ export interface PreviewResult {
160
160
  rows: SourceRow[];
161
161
  sampledCount: number;
162
162
  warnings: PreviewWarning[];
@@ -167,7 +167,7 @@ interface PreviewResult {
167
167
  * a required field empty in ≥1 row → `K/N` form; an optional field empty in
168
168
  * ALL rows → all-empty form; zero source rows → a single table-level warning.
169
169
  */
170
- declare function computeSampleWarnings(rows: SourceRow[], requiredFields: string[], sourceTable: string): PreviewWarning[];
170
+ export declare function computeSampleWarnings(rows: SourceRow[], requiredFields: string[], sourceTable: string): PreviewWarning[];
171
171
  /**
172
172
  * Zero-write preview: read ≤`limit` rows through the compiled view (a pure
173
173
  * SELECT whose SQL already projects each row into its mapped fields) and
@@ -175,10 +175,9 @@ declare function computeSampleWarnings(rows: SourceRow[], requiredFields: string
175
175
  * so it can never mutate the mirror. The distinct, write-one-row sibling is
176
176
  * `runSync`'s sample mode (T13).
177
177
  */
178
- declare function previewSync(store: SyncStore, view: CompiledView, opts: {
178
+ export declare function previewSync(store: SyncStore, view: CompiledView, opts: {
179
179
  limit: number;
180
180
  requiredFields: string[];
181
181
  sourceTable: string;
182
182
  }): Promise<PreviewResult>;
183
- //#endregion
184
- export { EntitySyncResult, MirrorRecord, PersistAction, PreviewResult, PreviewWarning, SourceRow, SyncAction, SyncResult, SyncStore, computeSampleWarnings, previewSync, reconcile, runSync };
183
+ //#endregion
package/dist/types.d.cts CHANGED
@@ -1,14 +1,14 @@
1
1
  //#region src/types.d.ts
2
- type ContactStatus = "lead" | "active" | "customer" | "churned";
3
- type DealStage = string;
4
- type StageType = "open" | "won" | "lost";
5
- type PipelineKind = "standard" | "recurring";
6
- type ValueInterval = "one_time" | "monthly" | "annual";
2
+ export type ContactStatus = "lead" | "active" | "customer" | "churned";
3
+ export type DealStage = string;
4
+ export type StageType = "open" | "won" | "lost";
5
+ export type PipelineKind = "standard" | "recurring";
6
+ export type ValueInterval = "one_time" | "monthly" | "annual";
7
7
  /** Named once so SQL predicates and UI fallbacks cannot drift apart. */
8
- declare const OPEN_STAGE_TYPE: "open";
8
+ export declare const OPEN_STAGE_TYPE: "open";
9
9
  /** A closed deal is a historical fact: automation never reopens one. */
10
- declare function isClosedStageType(type: StageType): boolean;
11
- interface PipelineStageConfig {
10
+ export declare function isClosedStageType(type: StageType): boolean;
11
+ export interface PipelineStageConfig {
12
12
  color: string | null;
13
13
  id: string;
14
14
  key: DealStage;
@@ -17,7 +17,7 @@ interface PipelineStageConfig {
17
17
  type: StageType;
18
18
  valueFixed: number | null;
19
19
  }
20
- interface PipelineConfig {
20
+ export interface PipelineConfig {
21
21
  id: string;
22
22
  isDefault: boolean;
23
23
  kind: PipelineKind;
@@ -26,12 +26,12 @@ interface PipelineConfig {
26
26
  sourceMappingId: string | null;
27
27
  stages: PipelineStageConfig[];
28
28
  }
29
- type TaskStatus = "todo" | "in_progress" | "done" | "cancelled";
30
- type TaskPriority = "low" | "medium" | "high";
31
- type TaskEntityType = "contact" | "company" | "deal";
32
- type ActivityType = "note" | "email" | "task" | "deal" | "call";
33
- type CrmRole = "owner" | "admin" | "member" | "developer" | "support_agent";
34
- interface Contact {
29
+ export type TaskStatus = "todo" | "in_progress" | "done" | "cancelled";
30
+ export type TaskPriority = "low" | "medium" | "high";
31
+ export type TaskEntityType = "contact" | "company" | "custom" | "deal";
32
+ export type ActivityType = "note" | "email" | "task" | "deal" | "call";
33
+ export type CrmRole = "owner" | "admin" | "member" | "developer" | "support_agent";
34
+ export interface Contact {
35
35
  companyId: string | null;
36
36
  createdAt: Date;
37
37
  email: string;
@@ -39,19 +39,50 @@ interface Contact {
39
39
  lastActivityAt: Date | null;
40
40
  name: string;
41
41
  orgId: string;
42
+ phone: string | null;
43
+ /**
44
+ * The host row this contact mirrors, or null when it was created in the CRM.
45
+ *
46
+ * Carried on the domain type because it decides whether a user may edit the
47
+ * identity fields: sync rewrites them from the host view on every run, so an
48
+ * edit to a sourced row would be silently reverted. See `host-owned-fields`.
49
+ */
50
+ sourceRef: string | null;
42
51
  status: ContactStatus;
43
52
  title: string | null;
44
53
  }
45
- interface Company {
54
+ export interface Company {
55
+ /**
56
+ * How many non-archived contacts are attached to this company.
57
+ *
58
+ * Derived per read, never stored — distinct from {@link Company.size}, which
59
+ * is the mappable headcount of the business itself. A provider with 400 staff
60
+ * and 4 CRM contacts has `size: "400"` and `contactCount: 4`; conflating them
61
+ * would state a falsehood in the authoritative-looking direction.
62
+ */
63
+ contactCount: number;
46
64
  createdAt: Date;
47
65
  domain: string | null;
66
+ email: string | null;
48
67
  id: string;
49
68
  industry: string | null;
50
69
  name: string;
51
70
  orgId: string;
71
+ phone: string | null;
72
+ /**
73
+ * The one contact to talk to at this company, if chosen.
74
+ *
75
+ * Constrained to a contact of this same company, and cleared when that
76
+ * contact is archived — so a non-null value always names someone live and
77
+ * attached here. Unlike the other fields it is never host-owned: it is a CRM
78
+ * judgement, so it stays editable on synced rows.
79
+ */
80
+ primaryContactId: string | null;
52
81
  size: string | null;
82
+ /** See {@link Contact.sourceRef}. */
83
+ sourceRef: string | null;
53
84
  }
54
- interface Deal {
85
+ export interface Deal {
55
86
  closeDate: Date | null;
56
87
  contactId: string | null;
57
88
  createdAt: Date;
@@ -63,7 +94,7 @@ interface Deal {
63
94
  stage: DealStage;
64
95
  value: number;
65
96
  }
66
- interface Task {
97
+ export interface Task {
67
98
  archivedAt: Date | null;
68
99
  assignee: string | null;
69
100
  completedAt: Date | null;
@@ -80,7 +111,7 @@ interface Task {
80
111
  status: TaskStatus;
81
112
  title: string;
82
113
  }
83
- interface Note {
114
+ export interface Note {
84
115
  archivedAt: Date | null;
85
116
  author: string | null;
86
117
  body: string;
@@ -95,7 +126,7 @@ interface Note {
95
126
  title: string;
96
127
  updatedAt: Date;
97
128
  }
98
- interface Activity {
129
+ export interface Activity {
99
130
  actor: string | null;
100
131
  contactId: string | null;
101
132
  createdAt: Date;
@@ -104,27 +135,41 @@ interface Activity {
104
135
  summary: string;
105
136
  type: ActivityType;
106
137
  }
107
- interface CreateContactInput {
138
+ export interface CreateContactInput {
108
139
  companyId?: string;
109
140
  email: string;
110
141
  name: string;
142
+ phone?: string;
111
143
  status?: ContactStatus;
112
144
  title?: string;
113
145
  }
114
- interface UpdateContactInput {
146
+ export interface UpdateContactInput {
115
147
  companyId?: string | null;
116
148
  email?: string;
117
149
  name?: string;
150
+ phone?: string | null;
118
151
  status?: ContactStatus;
119
152
  title?: string | null;
120
153
  }
121
- interface CreateCompanyInput {
154
+ export interface CreateCompanyInput {
122
155
  domain?: string;
156
+ email?: string;
123
157
  industry?: string;
124
158
  name: string;
159
+ phone?: string;
125
160
  size?: string;
126
161
  }
127
- interface CreateDealInput {
162
+ export interface UpdateCompanyInput {
163
+ domain?: string | null;
164
+ email?: string | null;
165
+ industry?: string | null;
166
+ name?: string;
167
+ phone?: string | null;
168
+ /** Null clears the point of contact; see {@link Company.primaryContactId}. */
169
+ primaryContactId?: string | null;
170
+ size?: string | null;
171
+ }
172
+ export interface CreateDealInput {
128
173
  contactId?: string;
129
174
  name: string;
130
175
  owner?: string;
@@ -132,7 +177,7 @@ interface CreateDealInput {
132
177
  stage?: DealStage;
133
178
  value?: number;
134
179
  }
135
- interface UpdateDealInput {
180
+ export interface UpdateDealInput {
136
181
  closeDate?: Date | null;
137
182
  contactId?: string | null;
138
183
  name?: string;
@@ -140,7 +185,7 @@ interface UpdateDealInput {
140
185
  stage?: DealStage;
141
186
  value?: number;
142
187
  }
143
- interface CreateTaskInput {
188
+ export interface CreateTaskInput {
144
189
  assignee?: string;
145
190
  createdBy?: string;
146
191
  description?: string;
@@ -150,7 +195,7 @@ interface CreateTaskInput {
150
195
  priority?: TaskPriority;
151
196
  title: string;
152
197
  }
153
- interface UpdateTaskInput {
198
+ export interface UpdateTaskInput {
154
199
  assignee?: string | null;
155
200
  description?: string | null;
156
201
  dueAt?: Date | null;
@@ -159,20 +204,42 @@ interface UpdateTaskInput {
159
204
  priority?: TaskPriority;
160
205
  title?: string;
161
206
  }
162
- interface CreateActivityInput {
207
+ export interface CreateActivityInput {
163
208
  actor?: string;
164
209
  contactId?: string;
165
210
  summary: string;
166
211
  type: ActivityType;
167
212
  }
168
- type RecordSource = "all" | "synced";
169
- interface ContactFilters {
213
+ export type RecordSource = "all" | "synced";
214
+ export interface ContactFilters {
170
215
  companyId?: string;
216
+ /**
217
+ * Exclude archived rows. Defaults to false — the adapter returns everything
218
+ * — because most callers are lookup maps, not user-facing lists:
219
+ * `importContacts` builds its dedup set from this (hiding archived rows
220
+ * would resurrect a contact the user archived as a duplicate) and
221
+ * `deals.list` builds its contact-name map from it (an archived contact on
222
+ * a live deal would render blank).
223
+ *
224
+ * The user-facing `contacts.list` / `companies.list` routes opt in, because
225
+ * there `archiveContact` is the delete affordance and a row still visible
226
+ * after archiving reads as a failed delete.
227
+ *
228
+ * Independent of `source`: `source: "synced"` already excludes archived
229
+ * rows, since sync archives a row when it leaves the host view and such a
230
+ * row is no longer sync-provenanced.
231
+ */
232
+ excludeArchived?: boolean;
171
233
  search?: string;
172
234
  source?: RecordSource;
173
235
  status?: ContactStatus;
174
236
  }
175
- interface TaskFilters {
237
+ export interface CompanyFilters {
238
+ /** See {@link ContactFilters.excludeArchived}. */
239
+ excludeArchived?: boolean;
240
+ source?: RecordSource;
241
+ }
242
+ export interface TaskFilters {
176
243
  assignee?: string;
177
244
  entityId?: string;
178
245
  entityType?: TaskEntityType;
@@ -181,5 +248,4 @@ interface TaskFilters {
181
248
  priority?: TaskPriority;
182
249
  status?: TaskStatus;
183
250
  }
184
- //#endregion
185
- export { Activity, ActivityType, Company, Contact, ContactFilters, ContactStatus, CreateActivityInput, CreateCompanyInput, CreateContactInput, CreateDealInput, CreateTaskInput, CrmRole, Deal, DealStage, Note, OPEN_STAGE_TYPE, PipelineConfig, PipelineKind, PipelineStageConfig, RecordSource, StageType, Task, TaskEntityType, TaskFilters, TaskPriority, TaskStatus, UpdateContactInput, UpdateDealInput, UpdateTaskInput, ValueInterval, isClosedStageType };
251
+ //#endregion