@prorigo/protrak-forge 0.3.5 → 0.4.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.
@@ -72,7 +72,7 @@ These are **Protrak-specific gotchas you cannot catch by reading the code**. Alw
72
72
 
73
73
  ### Understanding the workspace:
74
74
  - `list_protrak_schema_elements` (optionally with `kind`: `type`, `attribute`, `lifecycle`,
75
- `relation_type`) catalogs what's defined.
75
+ `relation_type`, `rule`) catalogs what's defined.
76
76
  - `read_protrak_schema_element` (with `kind` and `name`) returns the full JSON of any one
77
77
  element — for `kind='type'` you get the resolved schema with attributes, lifecycle, and
78
78
  relations hydrated.
@@ -149,6 +149,77 @@ Each call creates `NotificationTemplates/<name>.json` (metadata) and `Notificati
149
149
  Valid `target` values: `PromoteNotification`, `AccountNotification`, `WorkflowNotification`,
150
150
  `WorkflowTaskNotification`.
151
151
 
152
+ ## How to write Rules
153
+
154
+ Rules express attribute-filter logic used by display conditions, conditional formatting,
155
+ dependent picklists, and Access Policy. They are file-based: `Rules/<Name>.json`.
156
+
157
+ ### Writing a new rule:
158
+ 1. Call `get_protrak_rule_context(type_name?)` — returns the supported-conditions-per-
159
+ attribute-type table, value-less / range conditions, dynamic-value formats
160
+ (`@Instance.X`, `@User.X`, `$StartOfToday(±Nd)`), date mnemonics, basic-attribute map,
161
+ existing rule names, and canonical examples. Pass `type_name` to include that type's
162
+ resolved attribute schema (rules are not type-bound, but most rules reference one type).
163
+ 2. Translate the natural-language intent into `attribute_filter_groups[]`. Each group has
164
+ an `operator` joining it to the **next** group; each condition has an `operator` joining
165
+ it to the **next** condition in the same group. The final group / condition uses `None`.
166
+ 3. Instance-context `attribute_name` must exist in `Attributes/` or be a basic attribute
167
+ (`Name`, `State`, `Created`, `Creator`, `Modifier`, `Lifecycle`, ...). User-context names
168
+ refer to user-profile attributes (not workspace-validated). For `UserRoles`, omit
169
+ `attribute_name`.
170
+ 4. Call `generate_protrak_rule(name, attribute_filter_groups, error_message?)`. Pass
171
+ `error_message` **only** when the rule will be selected as a Type's Access Policy —
172
+ it appears in the access-denied response.
173
+
174
+ ### Attaching a rule to a layout / type / picklist:
175
+ Call `attach_protrak_rule(usage, rule_name, ...)`. One polymorphic tool covers five shapes:
176
+
177
+ - `usage='display_condition'`, `target_kind='layout_template'`, `target_name`, `widget_id?`
178
+ — appends to the widget's `displayConditions: string[]`.
179
+ - `usage='display_condition'`, `target_kind='form'`, `target_name`, `attribute_name`,
180
+ `container_id?` — sets the **singular** `displayCondition` on the form field.
181
+ - `usage='format_condition'`, `target_name=<ViewLayout>`, `attribute_name`, `section_name?`,
182
+ `widget_name?`, `style`, `order_index?` — appends `{ rule:{name}, orderIndex, style }`
183
+ to the ViewLayout field's `formatConditions`.
184
+ - `usage='conditional_formatting'`, `target_kind='type_widget'|'form'`, `target_name`,
185
+ `attribute_name`, `style`, `order_index?` — appends `{ ruleName, orderIndex, style }`
186
+ to the column/field's `conditionalFormatting`.
187
+ - `usage='access_policy'`, `type_name` — sets `accessPolicyRule` on `Types/<name>.json`.
188
+ - `usage='dependent_picklist'`, `attribute_name=<PicklistAttr>`, `option_name` — sets the
189
+ `rule` field on that picklist option.
190
+
191
+ Style is structured: `{ background_color?: '#RRGGBB', fore_color?: '#RRGGBB',
192
+ font_style?: ('Bold'|'Italic'|'Underline'|'Strikethrough')[] }`. The tool serializes it
193
+ to the comma-terminated CSS-string the SPA evaluator expects (`backgroundColor`, `color`,
194
+ `fontWeight`, `fontStyle`, `textDecorationLine`). Both `Underline` and `Strikethrough`
195
+ collapse into a single `textDecorationLine` token.
196
+
197
+ ```
198
+ get_protrak_rule_context(type_name='ProjectAllocation')
199
+ # ... agent picks attributes, condition, dynamic values ...
200
+ generate_protrak_rule(
201
+ name='OverdueAllocation',
202
+ attribute_filter_groups=[{
203
+ operator: 'None',
204
+ conditions: [{
205
+ attribute_context: 'Instance',
206
+ attribute_name: 'AllocationEndDate',
207
+ condition: 'LessThanOrEqual',
208
+ first_value: { value_type: 'Static', value: '$StartOfToday()' },
209
+ operator: 'None',
210
+ }],
211
+ }],
212
+ )
213
+ attach_protrak_rule(
214
+ usage='conditional_formatting',
215
+ rule_name='OverdueAllocation',
216
+ target_kind='type_widget',
217
+ target_name='ProjectActiveAllocationsRelationWidget',
218
+ attribute_name='AllocationEndDate',
219
+ style={ background_color: '#ee8bb6' },
220
+ )
221
+ ```
222
+
152
223
  ## Project structure
153
224
  - `Types/` — entity type JSON definitions
154
225
  - `Attributes/` — attribute JSON definitions
@@ -158,3 +229,6 @@ Valid `target` values: `PromoteNotification`, `AccountNotification`, `WorkflowNo
158
229
  - `RelationTypes/` — relation definitions between types
159
230
  - `NotificationTemplates/` — notification template files (paired `.json` + `.html`; use
160
231
  `generate_protrak_notification_template` to keep them in sync)
232
+ - `Rules/` — business-rule JSON definitions (attribute filter groups). Use
233
+ `generate_protrak_rule` to author and `attach_protrak_rule` to wire into layouts /
234
+ type access policies / picklist dependencies.
@@ -37,7 +37,6 @@ instance.SetPicklistAttributeValue("Status", "Active");
37
37
  | [Create and Connect Child Instances](./create-and-connect-pattern.md) | Create related instances and link them | PostCreate, PostConnect |
38
38
  | [Cascade Promote](./cascade-promote-pattern.md) | Propagate a lifecycle transition to related instances; aggregate-all-approvers check | PromoteActionCommand, PostCreate, Scheduler |
39
39
  | [Query and Filter](./query-filter-pattern.md) | Query instances using InstanceQuery and RelatedInstanceQuery | All program types |
40
- | [Aggregate Related Instances](./aggregate-related-instances.md) | Parent → related fan-out + roll-up; uses `RelatedQueryBuilder.Select(...)` to avoid N+1 GetInstance calls | Scheduler, PostCreate, PromoteActionCommand |
41
40
  | [Invoke Common Program](./invoke-common-program.md) | Call reusable logic encapsulated in a Common Program | All program types |
42
41
  | [Manage Instance Access](./manage-instance-access-pattern.md) | Grant or revoke per-instance user access; create users; manage roles | PostConnect, PostCreate, PromoteActionCommand |
43
42
  | [Delete Related Data](./delete-related-data-pattern.md) | Delete or unlink child instances before parent deletion | PreDelete |
@@ -94,16 +93,6 @@ Scheduler
94
93
  → [Cascade Promote] — promote when all approvers are done
95
94
  ```
96
95
 
97
- ### Scheduler: Roll up an aggregate from related instances onto the parent
98
-
99
- ```
100
- Scheduler
101
- → [Query and Filter] — pick the parent set (e.g. all Active Projects)
102
- → [Aggregate Related Instances] — for each parent, fetch related rows with
103
- RelatedQueryBuilder.Select(...) in a SINGLE call, sum/count/aggregate, and
104
- write back via InstanceBuilder.ForUpdate + UpdateInstanceAsync.
105
- ```
106
-
107
96
  ---
108
97
 
109
98
  ## Service Injection
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prorigo/protrak-forge",
3
- "version": "0.3.5",
3
+ "version": "0.4.0",
4
4
  "description": "Protrak domain context for coding agents — MCP server for GitHub Copilot, Claude, and Cursor",
5
5
  "bin": {
6
6
  "protrak-forge": "./bin/protrak-forge.js"
@@ -1,89 +0,0 @@
1
- # Attribute Copy / Auto-populate Pattern
2
-
3
- ## When to Use
4
- Use this pattern to automatically copy or compute attribute values when an instance is created or updated. Common uses:
5
- - Copy a field from a parent/related instance
6
- - Auto-fill a date field (e.g. start date = today)
7
- - Compute a derived value from other attributes
8
- - Propagate changes from parent to children
9
-
10
- ## Program Types
11
- Applies to: `PostCreate`, `PostUpdate`, `PreCreate`, `PreUpdate`
12
-
13
- ## Services Used
14
- - `IInstanceService` — to fetch and update instances
15
- - `IQueryBuilderService` — to filter related instances
16
-
17
- ## Code Example: Copy on Create
18
-
19
- ```csharp
20
- using Prorigo.Protrak.API.Contracts;
21
- using System;
22
- using System.Threading.Tasks;
23
-
24
- namespace MyProject.Customization
25
- {
26
- public class AutoPopulateStartDate : IPostCreateTriggerProgramAsync
27
- {
28
- public IInstanceService InstanceService { get; set; }
29
-
30
- public async Task RunAsync(Guid instanceId)
31
- {
32
- var instance = await InstanceService.GetInstanceAsync(
33
- instanceId, new[] { "StartDate" });
34
-
35
- if (instance == null) return;
36
-
37
- instance.Attributes = new[]
38
- {
39
- new Attribute { Name = "StartDate", Value = DateTime.UtcNow.ToString("o") }
40
- };
41
-
42
- await InstanceService.UpdateInstanceAsync(instance, null);
43
- }
44
- }
45
- }
46
- ```
47
-
48
- ## Code Example: Copy from Related Instance
49
-
50
- ```csharp
51
- public class CopyCustomerRegionToProject : IPostCreateTriggerProgramAsync
52
- {
53
- public IInstanceService InstanceService { get; set; }
54
-
55
- public async Task RunAsync(Guid instanceId)
56
- {
57
- var project = await InstanceService.GetInstanceAsync(
58
- instanceId, new[] { "Customer", "Region" });
59
-
60
- if (project == null) return;
61
-
62
- // Get related customer ID from reference attribute
63
- var customerIds = project.Attributes?
64
- .FirstOrDefault(a => a.Name == "Customer")?.ReferenceValues;
65
-
66
- if (customerIds == null || customerIds.Length == 0) return;
67
-
68
- var customer = await InstanceService.GetInstanceAsync(
69
- customerIds[0], new[] { "Region" });
70
-
71
- var region = customer?.Attributes?
72
- .FirstOrDefault(a => a.Name == "Region")?.Value?.ToString();
73
-
74
- if (string.IsNullOrEmpty(region)) return;
75
-
76
- project.Attributes = new[]
77
- {
78
- new Attribute { Name = "Region", Value = region }
79
- };
80
- await InstanceService.UpdateInstanceAsync(project, null);
81
- }
82
- }
83
- ```
84
-
85
- ## Key Rules
86
- - Use `PostCreate`/`PostUpdate` (not `PreCreate`) when you need the instance ID to perform updates
87
- - Null-check `ReferenceValues` before accessing related instances
88
- - Pass `null` for `lastModified` in `UpdateInstanceAsync` to skip optimistic concurrency check
89
- - Only set the attributes you want to change in `instance.Attributes`
@@ -1,72 +0,0 @@
1
- # Batch Update Pattern
2
-
3
- ## When to Use
4
- Use this pattern to update multiple related instances efficiently in parallel. Common uses:
5
- - Update all child instances when a parent changes
6
- - Recalculate a field across all instances of a type
7
- - Cascade attribute changes
8
-
9
- ## Program Types
10
- Applies to: `PostUpdate`, `PromoteActionCommand`, `Scheduler`
11
-
12
- ## Services Used
13
- - `IInstanceService` — to fetch and update instances in bulk
14
- - `IQueryBuilderService` — to filter instances for batch processing
15
-
16
- ## Code Example
17
-
18
- ```csharp
19
- using Prorigo.Protrak.API.Contracts;
20
- using System;
21
- using System.Linq;
22
- using System.Threading.Tasks;
23
-
24
- namespace MyProject.Customization
25
- {
26
- public class UpdateAllTaskPrioritiesOnProjectChange : IPostUpdateTriggerProgramAsync
27
- {
28
- public IInstanceService InstanceService { get; set; }
29
- public IQueryBuilderService QueryBuilderService { get; set; }
30
-
31
- public async Task<ProgramResult> RunAsync(Guid instanceId)
32
- {
33
- var project = await InstanceService.GetInstanceAsync(
34
- instanceId, new[] { "Priority" });
35
-
36
- var priority = project?.Attributes?
37
- .FirstOrDefault(a => a.Name == "Priority")?.Value?.ToString();
38
-
39
- if (string.IsNullOrEmpty(priority))
40
- return new ProgramResult { IsSuccess = true };
41
-
42
- // Find all tasks for this project
43
- var filter = QueryBuilderService.CreateRelatedInstanceFilterExpression(
44
- "Project", instanceId);
45
- var query = new InstanceQuery { TypeName = "Task", Filter = filter, Take = 500 };
46
- var tasks = await InstanceService.GetInstancesAsync(query);
47
-
48
- if (tasks?.Data == null || tasks.Data.Length == 0)
49
- return new ProgramResult { IsSuccess = true };
50
-
51
- // Update all tasks in parallel
52
- var updates = tasks.Data.Select(async task =>
53
- {
54
- task.Attributes = new[]
55
- {
56
- new Attribute { Name = "Priority", Value = priority }
57
- };
58
- await InstanceService.UpdateInstanceAsync(task, null);
59
- });
60
-
61
- await Task.WhenAll(updates);
62
- return new ProgramResult { IsSuccess = true };
63
- }
64
- }
65
- }
66
- ```
67
-
68
- ## Key Rules
69
- - Use `Task.WhenAll` for parallel updates — much faster than sequential `await` in a loop
70
- - Limit batch size with `Take` (max 500 recommended)
71
- - Pass `null` for `lastModified` in `UpdateInstanceAsync` to skip concurrency check
72
- - Only include changed attributes in `instance.Attributes` to avoid overwriting other fields
@@ -1,191 +0,0 @@
1
- # Notification Template Reference
2
-
3
- ## Overview
4
-
5
- Protrak notification templates define the content of notifications (email + push) triggered by
6
- platform events. Templates use **Razor markup syntax** (`@Model.*`) and are stored as paired files:
7
-
8
- - `NotificationTemplates/<title>.json` — metadata (title, target, push message, state)
9
- - `NotificationTemplates/<title>.html` — HTML email body (Razor template)
10
-
11
- Two templates are needed for a complete email notification: one for the **subject** (title ending
12
- in `Subject`) and one for the **body** (title ending in `Body`). Push-only templates need only
13
- the `.json` with a `messageText` value.
14
-
15
- ## Target Types
16
-
17
- | Target | Trigger event | Usage |
18
- |---|---|---|
19
- | `PromoteNotification` | Lifecycle promote action | Link in a lifecycle's "Send Notification" command |
20
- | `AccountNotification` | User creation / password reset | Link in Tenant Settings → Notifications |
21
- | `WorkflowNotification` | Workflow notification activity | Link in Workflow → Notification Activity |
22
- | `WorkflowTaskNotification` | Workflow input/checklist task creation | Link in Workflow → Input/Checklist Task Activity |
23
-
24
- ## @Model Variables — PromoteNotification
25
-
26
- Available when `target` is `PromoteNotification`. These reflect the promoted instance.
27
-
28
- ```
29
- @Model.InstanceName — Display name of the instance
30
- @Model.InstanceUrl — Deep link URL to the instance
31
- @Model.InstanceId — GUID of the instance
32
- @Model.InstanceTrackingId — Tracking / reference number
33
- @Model.PreviousStateName — Lifecycle state before the promote
34
- @Model.CurrentStateName — Lifecycle state after the promote
35
- @Model.ModifiedDate — Date/time of the promote (UTC)
36
- @Model.ModifiedBy — Name of the user who triggered it
37
- @Model.CreatedBy — Name of the user who created the instance
38
- @Model.Remarks — Remarks entered on the promote action
39
- @Model.AttributeValues["<AttributeName>"] — Attribute accessor (see below)
40
- @Model.TenantInfo.TenantName
41
- @Model.TenantInfo.LogoUrl
42
- @Model.TenantInfo.DateTimeFormat.DateFormat
43
- @Model.TenantInfo.DateTimeFormat.TimeFormat
44
- @Model.TenantInfo.DateTimeFormat.TimeZone
45
- @Model.TenantInfo.Locale.Name
46
- @Model.TenantInfo.Locale.DisplayName
47
- @Model.ApplicationUrl
48
- ```
49
-
50
- ## @Model Variables — AccountNotification
51
-
52
- Available when `target` is `AccountNotification`. These reflect the Protrak user account.
53
-
54
- ```
55
- @Model.UserName
56
- @Model.UserFullName
57
- @Model.UserEmail
58
- @Model.UserPassword
59
- @Model.CompanyName
60
- @Model.CompanyEmail
61
- @Model.CompanyDomain
62
- @Model.CompanyLogoUrl
63
- @Model.UserRoles[index]
64
- @Model.UserAttributes["<AttributeName>"]
65
- @Model.MFAVerificationCode
66
- @Model.MFAVerificationCodeValidity
67
- ```
68
-
69
- ## @Model Variables — WorkflowNotification
70
-
71
- Available when `target` is `WorkflowNotification`. These reflect the instance attached to the workflow.
72
-
73
- ```
74
- @Model.InstanceName
75
- @Model.InstanceUrl
76
- @Model.InstanceId
77
- @Model.InstanceTrackingId
78
- @Model.ApplicationUrl
79
- @Model.StateName
80
- @Model.ModifiedDate
81
- @Model.ModifiedBy
82
- @Model.CreatedBy
83
- @Model.AttributeValues["<AttributeName>"]
84
- ```
85
-
86
- ## @Model Variables — WorkflowTaskNotification
87
-
88
- Available when `target` is `WorkflowTaskNotification`. These reflect the workflow task.
89
-
90
- ```
91
- @Model.InstanceName
92
- @Model.InstanceUrl
93
- @Model.InstanceId
94
- @Model.InstanceTrackingId
95
- @Model.ApplicationUrl
96
- @Model.StateName
97
- @Model.ModifiedDate
98
- @Model.ModifiedBy
99
- @Model.CreatedBy
100
- @Model.AttributeValues["<AttributeName>"]
101
- @Model.WorkflowTask.Id
102
- @Model.WorkflowTask.Name
103
- @Model.WorkflowTask.Description
104
- @Model.WorkflowTask.Comments
105
- @Model.WorkflowTask.Type
106
- @Model.WorkflowTask.WorkflowInstanceId
107
- @Model.WorkflowTask.PausedWorkflowInstActivityId
108
- @Model.WorkflowTask.TypeInstId
109
- @Model.WorkflowTask.Status
110
- @Model.WorkflowTask.AssignedUserId
111
- @Model.WorkflowTask.AssignedRoleId
112
- @Model.WorkflowTask.Created
113
- @Model.WorkflowTask.Due
114
- @Model.WorkflowTask.Completed
115
- @Model.WorkflowTask.CompletedBy
116
- @Model.WorkflowTask.ChecklistItems[index].Id
117
- @Model.WorkflowTask.ChecklistItems[index].Name
118
- @Model.WorkflowTask.ChecklistItems[index].IsMandatory
119
- @Model.WorkflowTask.ChecklistItems[index].IsCompleted
120
- ```
121
-
122
- ## Attribute Value Accessors
123
-
124
- The `@Model.AttributeValues["<AttributeName>"]` accessor returns an object with type-specific
125
- properties. Access the correct property based on the attribute type:
126
-
127
- ```
128
- .TextValue — Text attributes
129
- .NumericValue — Numeric attributes
130
- .DateValue — Date attributes (stored as UTC DateTime)
131
- .ArrayValue[0] — Picklist (single) — string value
132
- .UserValues[0].UserName — User attributes (first user)
133
- .UserValues[0].UserEmail
134
- .UserValues[0].UserId
135
- .ReferenceValue[0].Name — Reference attributes (first linked instance)
136
- .ReferenceValue[0].Id
137
- ```
138
-
139
- ## Razor Syntax Examples
140
-
141
- ### Simple substitution
142
- ```html
143
- <p>Hi @Model.AttributeValues["Employee"].UserValues[0].UserName,</p>
144
- <p>Your <a href='@Model.InstanceUrl'>@Model.InstanceName</a> has been approved.</p>
145
- ```
146
-
147
- ### Conditional content
148
- ```html
149
- @if (Model.AttributeValues["Country"].ArrayValue[0] == "India")
150
- {
151
- <span>Tax rate: @Model.AttributeValues["GSTRate"].NumericValue%</span>
152
- }
153
- else
154
- {
155
- <span>No tax applicable.</span>
156
- }
157
- ```
158
-
159
- ### Null-safe access
160
- ```html
161
- @(Model.AttributeValues.ContainsKey("Title")
162
- ? Model.AttributeValues["Title"]?.TextValue ?? "N/A"
163
- : "N/A")
164
- ```
165
-
166
- ## File Format
167
-
168
- ### `<title>.json`
169
- ```json
170
- {
171
- "title": "Invoice Sent Body",
172
- "target": "PromoteNotification",
173
- "emailText": null,
174
- "hashCode": "",
175
- "messageText": null,
176
- "templateState": "Published"
177
- }
178
- ```
179
-
180
- - `emailText` is always `null` in file-based templates — the email body lives in the `.html` file.
181
- - `hashCode` is left empty; Protrak recomputes it on import (same pattern as `programHashCode`).
182
- - `messageText` is the plain-text push notification content (Razor syntax supported).
183
- - `templateState`: `"Published"` or `"InProgress"`.
184
-
185
- ### `<title>.html`
186
- ```html
187
- <p>Hi @Model.AttributeValues["Manager"].UserValues[0].UserName,</p>
188
- <p>Invoice <strong>@Model.InstanceName</strong> has been sent to the client.</p>
189
- <p><a href='@Model.InstanceUrl'>View invoice</a></p>
190
- <p><i>This is a system generated mail, please do not reply.</i></p>
191
- ```
@@ -1,76 +0,0 @@
1
- # Reference Navigation Pattern
2
-
3
- ## When to Use
4
- Use this pattern to navigate from one instance to a related instance via a reference attribute. Common uses:
5
- - Get the parent project of a task
6
- - Get the customer linked to an order
7
- - Follow a chain of relations (task → project → customer)
8
-
9
- ## Program Types
10
- Applies to: `PreCreate`, `PostCreate`, `PreUpdate`, `PostUpdate`, `PromoteActionCommand`
11
-
12
- ## Services Used
13
- - `IInstanceService` — to fetch related instances by GUID
14
-
15
- ## Code Example
16
-
17
- ```csharp
18
- using Prorigo.Protrak.API.Contracts;
19
- using System;
20
- using System.Linq;
21
- using System.Threading.Tasks;
22
-
23
- namespace MyProject.Customization
24
- {
25
- public class ValidateEquipmentStatus : IPreCreateTriggerProgramAsync
26
- {
27
- public IInstanceService InstanceService { get; set; }
28
-
29
- public async Task<ProgramResult> RunAsync(Instance instance)
30
- {
31
- // Navigate: Process → Equipment (via reference attribute)
32
- var equipmentIds = instance.Attributes?
33
- .FirstOrDefault(a => a.Name == "Equipment")?.ReferenceValues;
34
-
35
- if (equipmentIds == null || equipmentIds.Length == 0)
36
- return new ProgramResult { IsSuccess = false, Message = "Equipment is required." };
37
-
38
- var equipment = await InstanceService.GetInstanceAsync(
39
- equipmentIds[0], new[] { "Status", "AvailableFrom" });
40
-
41
- if (equipment == null)
42
- return new ProgramResult { IsSuccess = false, Message = "Equipment not found." };
43
-
44
- var status = equipment.Attributes?
45
- .FirstOrDefault(a => a.Name == "Status")?.Value?.ToString();
46
-
47
- if (status != "Available")
48
- {
49
- return new ProgramResult
50
- {
51
- IsSuccess = false,
52
- Message = $"Equipment is not available (current status: {status})."
53
- };
54
- }
55
-
56
- return new ProgramResult { IsSuccess = true };
57
- }
58
- }
59
- }
60
- ```
61
-
62
- ## Key Rules
63
- - Reference attributes expose related instance IDs via `ReferenceValues` (array of Guids)
64
- - Always null-check `ReferenceValues` and verify length before accessing index 0
65
- - Fetch only the attributes you need in the `GetInstanceAsync` attributes parameter
66
- - For multi-valued references, iterate or use `ReferenceValues[0]` for the first
67
-
68
- ## When NOT to use this pattern (anti-pattern callout)
69
-
70
- This pattern is for **navigating to a single related instance via a Reference attribute**. It is **not** the right approach when you have many related instances and want to read attributes from each.
71
-
72
- > ❌ **DO NOT** call `GetRelatedInstancesAsync` to get a list of related ids and then call `GetInstanceAsync` once per id to fetch their attributes. That's an N+1 round-trip — one extra DB hit per related instance.
73
- >
74
- > ✅ **DO** use `RelatedQueryBuilder.Select(attr1, attr2, ...)` so the related instances come back **with attributes populated in a single call**. Read the values off the `RelatedInstance` directly with `GetDateAttributeValue`, `GetTextAttributeValue`, etc.
75
-
76
- For collection-level fan-out (sums, counts, rollups), see [Aggregate Related Instances](./aggregate-related-instances.md).
@@ -1,73 +0,0 @@
1
- # Report Program Pattern
2
-
3
- ## When to Use
4
- Use this pattern to generate dynamic reports from Protrak data. Report programs provide both the data and the configuration (columns, charts) for the report UI.
5
-
6
- ## Program Types
7
- Applies to: `Report`
8
-
9
- ## Services Used
10
- - `IInstanceService` — to query data for the report
11
- - `IQueryBuilderService` — to filter data
12
-
13
- ## Code Example
14
-
15
- ```csharp
16
- using Prorigo.Protrak.API.Contracts;
17
- using System;
18
- using System.Collections.Generic;
19
- using System.Linq;
20
- using System.Threading.Tasks;
21
-
22
- namespace MyProject.Customization
23
- {
24
- public class EquipmentUtilizationReport : IReportProgramAsync
25
- {
26
- public IInstanceService InstanceService { get; set; }
27
- public IQueryBuilderService QueryBuilderService { get; set; }
28
-
29
- public async Task<ReportData> GetReportDataAsync(ReportQuery query)
30
- {
31
- var instanceQuery = new InstanceQuery
32
- {
33
- TypeName = "Equipment",
34
- Take = 500
35
- };
36
-
37
- var equipment = await InstanceService.GetInstancesAsync(instanceQuery);
38
-
39
- var rows = equipment?.Data?.Select(e => new ReportRow
40
- {
41
- Values = new Dictionary<string, object>
42
- {
43
- ["Name"] = e.Attributes?.FirstOrDefault(a => a.Name == "Name")?.Value ?? "",
44
- ["Status"] = e.Attributes?.FirstOrDefault(a => a.Name == "Status")?.Value ?? "",
45
- ["UtilizationHours"] = e.Attributes?.FirstOrDefault(a => a.Name == "UtilizationHours")?.Value ?? 0,
46
- }
47
- }).ToList() ?? new List<ReportRow>();
48
-
49
- return new ReportData { Rows = rows };
50
- }
51
-
52
- public async Task<ReportConfig> GetReportConfigAsync()
53
- {
54
- return new ReportConfig
55
- {
56
- Title = "Equipment Utilization",
57
- Columns = new[]
58
- {
59
- new ReportColumn { Name = "Name", DisplayName = "Equipment Name" },
60
- new ReportColumn { Name = "Status", DisplayName = "Status" },
61
- new ReportColumn { Name = "UtilizationHours", DisplayName = "Hours Used" },
62
- }
63
- };
64
- }
65
- }
66
- }
67
- ```
68
-
69
- ## Key Rules
70
- - Implement both `GetReportDataAsync` and `GetReportConfigAsync`
71
- - `ReportQuery` may contain filter parameters set by the user in the UI
72
- - Return an empty `ReportData` (not null) if no data matches
73
- - Keep queries efficient — reports run on demand, avoid slow full-table scans