@prorigo/protrak-forge 0.3.4 → 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.
- package/README.md +5 -0
- package/bin/protrak-forge.js +9495 -13424
- package/data/CLAUDE.md.template +102 -1
- package/data/patterns/index.md +0 -11
- package/package.json +1 -1
- package/data/patterns/attribute-copy.md +0 -89
- package/data/patterns/batch-update.md +0 -72
- package/data/patterns/reference-navigation.md +0 -76
- package/data/patterns/report.md +0 -73
- package/data/patterns/scheduler.md +0 -77
package/data/CLAUDE.md.template
CHANGED
|
@@ -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.
|
|
@@ -124,6 +124,102 @@ These are **Protrak-specific gotchas you cannot catch by reading the code**. Alw
|
|
|
124
124
|
- Return `ProgramResult { IsSuccess = false, Message = "..." }` to block operations in
|
|
125
125
|
Pre-triggers (style rule).
|
|
126
126
|
|
|
127
|
+
## How to write notification templates
|
|
128
|
+
|
|
129
|
+
Notification templates define the email/push content sent by Protrak. They use **Razor syntax**
|
|
130
|
+
(`@Model.*`), not `{{}}` placeholders.
|
|
131
|
+
|
|
132
|
+
### Writing a new notification template:
|
|
133
|
+
1. Call `get_protrak_notification_context(type_name?)` — returns existing templates and the full
|
|
134
|
+
`@Model` variable reference for all target types.
|
|
135
|
+
2. Write the Razor HTML body using `@Model.*` variables.
|
|
136
|
+
3. Call `generate_protrak_notification_template(name='<Title> Body', target='PromoteNotification', email_html='<Razor HTML>')`.
|
|
137
|
+
4. Call again for the subject: `generate_protrak_notification_template(name='<Title> Subject', target='PromoteNotification', email_html='<subject line>')`.
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
get_protrak_notification_context(type_name='Invoice')
|
|
141
|
+
# ... agent writes the Razor HTML ...
|
|
142
|
+
generate_protrak_notification_template(name='Invoice Sent Body', target='PromoteNotification', email_html='<p>Hi @Model.AttributeValues["Manager"].UserValues[0].UserName,...</p>')
|
|
143
|
+
generate_protrak_notification_template(name='Invoice Sent Subject', target='PromoteNotification', email_html='Invoice @Model.InstanceName has been sent')
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Each call creates `NotificationTemplates/<name>.json` (metadata) and `NotificationTemplates/<name>.html`
|
|
147
|
+
(email body). `hashCode` is left empty — Protrak recomputes it on import.
|
|
148
|
+
|
|
149
|
+
Valid `target` values: `PromoteNotification`, `AccountNotification`, `WorkflowNotification`,
|
|
150
|
+
`WorkflowTaskNotification`.
|
|
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
|
+
|
|
127
223
|
## Project structure
|
|
128
224
|
- `Types/` — entity type JSON definitions
|
|
129
225
|
- `Attributes/` — attribute JSON definitions
|
|
@@ -131,3 +227,8 @@ These are **Protrak-specific gotchas you cannot catch by reading the code**. Alw
|
|
|
131
227
|
- `Programs/` — C# program source + JSON metadata (paired files; use
|
|
132
228
|
`generate_protrak_program` to keep them in sync)
|
|
133
229
|
- `RelationTypes/` — relation definitions between types
|
|
230
|
+
- `NotificationTemplates/` — notification template files (paired `.json` + `.html`; use
|
|
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.
|
package/data/patterns/index.md
CHANGED
|
@@ -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,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,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).
|
package/data/patterns/report.md
DELETED
|
@@ -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
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
# Scheduler Program Pattern
|
|
2
|
-
|
|
3
|
-
## When to Use
|
|
4
|
-
Use this pattern for background jobs that run on a schedule (cron). Common uses:
|
|
5
|
-
- Send daily reminder notifications
|
|
6
|
-
- Auto-close overdue instances
|
|
7
|
-
- Compute/recalculate aggregate values from related instances (see [Aggregate Related Instances](./aggregate-related-instances.md) for the canonical roll-up shape)
|
|
8
|
-
- Sync data with external systems
|
|
9
|
-
|
|
10
|
-
## Program Types
|
|
11
|
-
Applies to: `Scheduler`
|
|
12
|
-
|
|
13
|
-
## Services Used
|
|
14
|
-
- `IInstanceService` — to query and update instances
|
|
15
|
-
- `INotificationService` — to send scheduled notifications
|
|
16
|
-
- `IQueryBuilderService` — to filter instances
|
|
17
|
-
|
|
18
|
-
## Code Example
|
|
19
|
-
|
|
20
|
-
```csharp
|
|
21
|
-
using Prorigo.Protrak.API.Contracts;
|
|
22
|
-
using System;
|
|
23
|
-
using System.Linq;
|
|
24
|
-
using System.Threading.Tasks;
|
|
25
|
-
|
|
26
|
-
namespace MyProject.Customization
|
|
27
|
-
{
|
|
28
|
-
public class SendDailyOverdueReminders : ISchedulerProgramAsync
|
|
29
|
-
{
|
|
30
|
-
public IInstanceService InstanceService { get; set; }
|
|
31
|
-
public INotificationService NotificationService { get; set; }
|
|
32
|
-
public IQueryBuilderService QueryBuilderService { get; set; }
|
|
33
|
-
|
|
34
|
-
public async Task RunAsync()
|
|
35
|
-
{
|
|
36
|
-
var today = DateTime.UtcNow.Date;
|
|
37
|
-
var filter = QueryBuilderService.CreateAttributeFilterExpression(
|
|
38
|
-
"DueDate", today.ToString("yyyy-MM-dd"), FilterOperator.LessThanOrEqual);
|
|
39
|
-
|
|
40
|
-
var query = new InstanceQuery
|
|
41
|
-
{
|
|
42
|
-
TypeName = "WorkOrder",
|
|
43
|
-
Filter = filter,
|
|
44
|
-
StateName = "Open",
|
|
45
|
-
Take = 500
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
var overdueItems = await InstanceService.GetInstancesAsync(query);
|
|
49
|
-
if (overdueItems?.Data == null || overdueItems.Data.Length == 0)
|
|
50
|
-
return;
|
|
51
|
-
|
|
52
|
-
foreach (var item in overdueItems.Data)
|
|
53
|
-
{
|
|
54
|
-
var assignee = item.Attributes?
|
|
55
|
-
.FirstOrDefault(a => a.Name == "AssignedEngineer")?.Value?.ToString();
|
|
56
|
-
|
|
57
|
-
if (string.IsNullOrEmpty(assignee)) continue;
|
|
58
|
-
|
|
59
|
-
await NotificationService.SendNotificationAsync(
|
|
60
|
-
new[] { assignee },
|
|
61
|
-
"Overdue Work Order",
|
|
62
|
-
$"Work Order {item.TrackingId} is overdue. Please take action.",
|
|
63
|
-
item.Id
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
## Key Rules
|
|
72
|
-
- The `Run()` / `RunAsync()` method takes no parameters
|
|
73
|
-
- Use `ISchedulerProgramAsync` for async operations
|
|
74
|
-
- Avoid long-running loops — use `Take` to limit batch sizes
|
|
75
|
-
- Log or notify on errors so failures are visible
|
|
76
|
-
- Consider idempotency: what happens if the scheduler runs twice?
|
|
77
|
-
- When the scheduler walks parent → related instances and reads attributes off each child, use `RelatedQueryBuilder.Select(...)` in a single `GetRelatedInstancesAsync` call. **Do not** fan out to `GetInstanceAsync` per related id — that's an N+1 anti-pattern. See [Aggregate Related Instances](./aggregate-related-instances.md).
|