@bluestep-systems/bspecs 0.10.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 +129 -0
- package/cli.js +74 -0
- package/package.json +30 -0
- package/src/prompts.js +74 -0
- package/src/scaffold.js +152 -0
- package/src/sync.js +123 -0
- package/src/utils.js +95 -0
- package/templates/claude/agents/b6p-code-review.md +81 -0
- package/templates/claude/agents/b6p-commenter.md +59 -0
- package/templates/claude/agents/b6p-task-implementer.md +77 -0
- package/templates/claude/hooks/block-generated-files.sh +16 -0
- package/templates/claude/hooks/block-tsc.sh +16 -0
- package/templates/claude/hooks/prettier-on-save.sh +21 -0
- package/templates/claude/instructions/b6p-platform.md.template +185 -0
- package/templates/claude/instructions/bsjs-development.md.template +430 -0
- package/templates/claude/instructions/conventions/always-snapshot.md.template +25 -0
- package/templates/claude/instructions/conventions/blueiq-no-ai-branding.md.template +11 -0
- package/templates/claude/instructions/conventions/date-format.md.template +27 -0
- package/templates/claude/instructions/conventions/endpoint-approach.md.template +9 -0
- package/templates/claude/instructions/conventions/formula-patterns.md.template +71 -0
- package/templates/claude/instructions/conventions/no-global-dollar.md.template +9 -0
- package/templates/claude/instructions/conventions/push-inner-draft.md.template +21 -0
- package/templates/claude/instructions/conventions/separate-files.md.template +17 -0
- package/templates/claude/instructions/conventions/single-script.md.template +28 -0
- package/templates/claude/instructions/conventions/snapshot-integrity.md.template +23 -0
- package/templates/claude/instructions/conventions/top-level-const-tdz.md.template +33 -0
- package/templates/claude/instructions/conventions/ts-in-template-literal.md.template +48 -0
- package/templates/claude/instructions/conventions/tsc-rootdir.md.template +17 -0
- package/templates/claude/instructions/gotchas/common-gotchas.md.template +91 -0
- package/templates/claude/instructions/gotchas/fetched-resource-code.md.template +9 -0
- package/templates/claude/instructions/index.md.template +82 -0
- package/templates/claude/instructions/reference/api-patterns.md.template +487 -0
- package/templates/claude/instructions/reference/blueiq-credit-integration-playbook.md.template +31 -0
- package/templates/claude/instructions/reference/chronounit-months.md.template +37 -0
- package/templates/claude/instructions/reference/code-patterns.md.template +265 -0
- package/templates/claude/instructions/reference/component-library.md.template +217 -0
- package/templates/claude/instructions/reference/crm-dashboard-inspo.md.template +17 -0
- package/templates/claude/instructions/reference/csv-parsing.md.template +18 -0
- package/templates/claude/instructions/reference/dashboard-design-system.md.template +38 -0
- package/templates/claude/instructions/reference/datetime-field-write.md.template +27 -0
- package/templates/claude/instructions/reference/design-system.md.template +150 -0
- package/templates/claude/instructions/reference/dpn-dashboard-framework.md.template +29 -0
- package/templates/claude/instructions/reference/endpoint-method-call.md.template +10 -0
- package/templates/claude/instructions/reference/endpoint-no-delete-method.md.template +9 -0
- package/templates/claude/instructions/reference/endpoint-output-channel.md.template +23 -0
- package/templates/claude/instructions/reference/endpoint-urls.md.template +15 -0
- package/templates/claude/instructions/reference/entry-delete.md.template +40 -0
- package/templates/claude/instructions/reference/file-execution.md.template +113 -0
- package/templates/claude/instructions/reference/http-requester.md.template +37 -0
- package/templates/claude/instructions/reference/id-full-vs-short.md.template +15 -0
- package/templates/claude/instructions/reference/internal-loopback-fetch.md.template +24 -0
- package/templates/claude/instructions/reference/localdate-parse.md.template +16 -0
- package/templates/claude/instructions/reference/merge-report-memo-json.md.template +25 -0
- package/templates/claude/instructions/reference/merge-report-static-index.md.template +29 -0
- package/templates/claude/instructions/reference/merge-report-urls.md.template +67 -0
- package/templates/claude/instructions/reference/multi-entry-in-multi-entry.md.template +21 -0
- package/templates/claude/instructions/reference/named-controls-submit.md.template +11 -0
- package/templates/claude/instructions/reference/new-entry-id.md.template +30 -0
- package/templates/claude/instructions/reference/relationship-field-set.md.template +37 -0
- package/templates/claude/instructions/reference/send-message-abort.md.template +37 -0
- package/templates/claude/instructions/reference/session-cookie-forwarding.md.template +31 -0
- package/templates/claude/instructions/reference/singleselect-null-copy.md.template +21 -0
- package/templates/claude/instructions/reference/staff-query-permission-gating.md.template +27 -0
- package/templates/claude/instructions/reference/timefield-vs-datetimefield.md.template +13 -0
- package/templates/claude/instructions/reference/user-zone-id.md.template +16 -0
- package/templates/claude/settings.json.template +46 -0
- package/templates/claude/skills/b6p-audit/SKILL.md +82 -0
- package/templates/claude/skills/b6p-pull/SKILL.md +123 -0
- package/templates/claude/skills/b6p-push/SKILL.md +70 -0
- package/templates/claude/skills/bug-fix/SKILL.md +28 -0
- package/templates/claude/skills/spec-create/SKILL.md +60 -0
- package/templates/claude/skills/spec-execute/SKILL.md +51 -0
- package/templates/claude/skills/spec-status/SKILL.md +20 -0
- package/templates/claude/skills/task-comment/SKILL.md +96 -0
- package/templates/claude/spec-templates/design.template.md +36 -0
- package/templates/claude/spec-templates/requirements.template.md +26 -0
- package/templates/claude/spec-templates/tasks.template.md +37 -0
- package/templates/module/README.md.template +46 -0
- package/templates/root/.gitignore.template +14 -0
- package/templates/root/.prettierrc.template +8 -0
- package/templates/root/CLAUDE.md.template +157 -0
- package/templates/root/README.md.template +58 -0
- package/templates/root/package.json.template +15 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
# API Patterns
|
|
2
|
+
|
|
3
|
+
> **Note**: for the complete API reference, see `declarations/B.d.ts`. This file contains usage patterns and examples, not API documentation.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
|
|
7
|
+
- [Working with Java Optionals](#working-with-java-optionals)
|
|
8
|
+
- [Field Types and Access Patterns](#field-types-and-access-patterns)
|
|
9
|
+
- [mergeTag() Method](#mergetag-method)
|
|
10
|
+
- [Safe Field Value Extraction](#safe-field-value-extraction)
|
|
11
|
+
- [Record and Form Access](#record-and-form-access)
|
|
12
|
+
- [Multi-Entry Form (MEF) Entries](#multi-entry-form-mef-entries)
|
|
13
|
+
- [Query Access Patterns](#query-access-patterns)
|
|
14
|
+
- [Java Collections](#java-collections)
|
|
15
|
+
- [Writing Date and DateTime Fields](#writing-date-and-datetime-fields)
|
|
16
|
+
- [Committing Writes and Formula Triggers](#committing-writes-and-formula-triggers)
|
|
17
|
+
- [Endpoint (Maestro) Request/Response](#endpoint-maestro-requestresponse)
|
|
18
|
+
- [Merge Report Client-Side Integration](#merge-report-client-side-integration)
|
|
19
|
+
- [Base64 and Byte Arrays](#base64-and-byte-arrays)
|
|
20
|
+
- [User and Session Access](#user-and-session-access)
|
|
21
|
+
- [Type Safety](#type-safety)
|
|
22
|
+
|
|
23
|
+
## Working with Java Optionals
|
|
24
|
+
|
|
25
|
+
⚠️ BlueStep uses Java optionals, NOT standard JavaScript undefined/null checks.
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
// Get value with default
|
|
29
|
+
const value = field.opt().orElse('default value');
|
|
30
|
+
|
|
31
|
+
// Transform and unwrap
|
|
32
|
+
const transformed = field.opt().map(v => v.toUpperCase()).orElse('');
|
|
33
|
+
|
|
34
|
+
// Check if present
|
|
35
|
+
if (field.opt().isPresent()) {
|
|
36
|
+
const value = field.opt().get();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Chain operations
|
|
40
|
+
const result = field.opt().map(v => v.trim()).filter(v => v.length > 0).orElse('empty');
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Common mistakes:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// ❌ Wrong - JavaScript optional chaining doesn't work with Java optionals
|
|
47
|
+
const value = record.forms.someForm?.fields.someField?.val();
|
|
48
|
+
// ✅ Correct - use Java optional methods
|
|
49
|
+
const value = record.forms.someForm.fields.someField.opt().orElse('');
|
|
50
|
+
|
|
51
|
+
// ❌ Wrong - direct value access may throw
|
|
52
|
+
if (field.val()) { ... }
|
|
53
|
+
// ✅ Correct - check with the optional first
|
|
54
|
+
if (field.opt().isPresent()) { const value = field.opt().get(); }
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Field Types and Access Patterns
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// Text / Memo (MemoField behaves like a text field)
|
|
61
|
+
const text = textField.opt().orElse('');
|
|
62
|
+
const trimmed = textField.opt().map(v => v.trim()).orElse('');
|
|
63
|
+
|
|
64
|
+
// Number
|
|
65
|
+
const num = numberField.opt().orElse(0);
|
|
66
|
+
|
|
67
|
+
// Boolean
|
|
68
|
+
const bool = boolField.opt().orElse(false);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Date/Time Fields (reading)
|
|
72
|
+
|
|
73
|
+
Date/Time fields return Java `LocalDateTime`, `LocalDate`, or `LocalTime` objects.
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
const dateTime = dateTimeField.opt().orElse(null);
|
|
77
|
+
if (dateTime) {
|
|
78
|
+
const year = dateTime.getYear();
|
|
79
|
+
const month = dateTime.getMonthValue();
|
|
80
|
+
const day = dateTime.getDayOfMonth();
|
|
81
|
+
const isoString = dateTime.toString(); // ISO string (common pattern)
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
(For *writing* date/datetime fields, see [Writing Date and DateTime Fields](#writing-date-and-datetime-fields).)
|
|
86
|
+
|
|
87
|
+
### Document Fields
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
const doc = docField.filename() ? { filename: docField.filename(), url: docField.permUrl() } : null;
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Signature Fields
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
const sig = sigField.optTimeStamp().map(ts => ({
|
|
97
|
+
user: sigField.optUser().map(u => u.fullName()).orElse(null),
|
|
98
|
+
dateTime: ts
|
|
99
|
+
})).orElse(null);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Single-Select Fields
|
|
103
|
+
|
|
104
|
+
`.val()` returns the `OptionItem` object, not a string, and `.selectedName()` does NOT exist. To get the display label, use `optSelected().map(o => o.displayName())`.
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
// ✅ Correct
|
|
108
|
+
const qType: string = fields.questionType.optSelected().map((o: any) => o.displayName()).orElse('');
|
|
109
|
+
|
|
110
|
+
// ❌ Wrong - selectedName() does not exist on SingleSelectField
|
|
111
|
+
fields.questionType.selectedName();
|
|
112
|
+
// ❌ Wrong - String(o) returns the Java object toString, not the label
|
|
113
|
+
fields.questionType.opt().map((o: any) => String(o)).orElse('');
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Multi-Select Fields
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
// Array of selected option names — .selectedNames() returns EList<string>; use .toArray()
|
|
120
|
+
const selectedNames = multiSelectField.selectedNames().toArray();
|
|
121
|
+
const count = multiSelectField.selectedCount();
|
|
122
|
+
|
|
123
|
+
// Selected OptionItem objects (for display name + export value)
|
|
124
|
+
multiSelectField.selected().forEach((item: Bluestep.Relate.OptionItem) => {
|
|
125
|
+
const name = item.displayName();
|
|
126
|
+
const exportValue = item.exportValue();
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
⚠️ Use `.toArray()` to convert an `EList` to a real JavaScript array before JSON serialization or array operations.
|
|
131
|
+
|
|
132
|
+
## mergeTag() Method
|
|
133
|
+
|
|
134
|
+
`mergeTag()` is available on all Field objects and generates the HTML for form-field components. It is used with the `fieldF` and `bsHorizFieldCol2F` components to produce properly formatted fields with labels, validation images, and inputs.
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
field.mergeTag(options?: string): string
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Option codes
|
|
141
|
+
|
|
142
|
+
| Option Code | Description |
|
|
143
|
+
|------------|-------------|
|
|
144
|
+
| `""` (empty) | View-only field (read-only display) |
|
|
145
|
+
| `"L"` | Label only |
|
|
146
|
+
| `"H"` | Hint/tooltip |
|
|
147
|
+
| `"F"` | Editable field (input element) |
|
|
148
|
+
| `"I"` | Validation image (must be used with `"F"`) |
|
|
149
|
+
|
|
150
|
+
⚠️ The validation-image code `"I"` should only be used in combination with the editable-field code `"F"`. Codes can be combined in one string to generate multiple tags at once (e.g. `"FI"`, `"LF"`, `"LFI"`).
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
const input = field.mergeTag("F"); // editable field
|
|
154
|
+
const label = field.mergeTag("L"); // label
|
|
155
|
+
const hint = field.mergeTag("H"); // hint
|
|
156
|
+
const viewOnly = field.mergeTag(""); // view-only
|
|
157
|
+
const complete = field.mergeTag("LFI"); // label + field + validation image
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Using with `fieldF` / `bsHorizFieldCol2F`
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
import { fieldF, bsHorizFormCol2F, bsHorizFieldCol2F } from 'genericComponents';
|
|
164
|
+
|
|
165
|
+
const firstNameField = record.forms.userInfo.fields.firstName;
|
|
166
|
+
|
|
167
|
+
// Separate calls
|
|
168
|
+
const fieldHtml = fieldF({
|
|
169
|
+
label: firstNameField.mergeTag("L"),
|
|
170
|
+
validationImg: firstNameField.mergeTag("I"),
|
|
171
|
+
field: firstNameField.mergeTag("F")
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// More efficient: generate field + validation together with "FI"
|
|
175
|
+
const fieldHtml2 = fieldF({
|
|
176
|
+
label: firstNameField.mergeTag("L"),
|
|
177
|
+
validationImg: '',
|
|
178
|
+
field: firstNameField.mergeTag("FI")
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Best practices: always use `mergeTag()` with `fieldF`/`bsHorizFieldCol2F` (they integrate with BlueStep's validation system); combine codes (`"FI"`/`"IF"`) rather than separate calls; never use `"I"` without `"F"`.
|
|
183
|
+
|
|
184
|
+
## Safe Field Value Extraction
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
function getFieldValue(field: any, fieldType: string): any {
|
|
188
|
+
switch (fieldType) {
|
|
189
|
+
case 'Text Field':
|
|
190
|
+
case 'Text Area Field':
|
|
191
|
+
case 'Memo Field':
|
|
192
|
+
return field.opt().orElse(null);
|
|
193
|
+
case 'Number Field':
|
|
194
|
+
return field.opt().orElse(null);
|
|
195
|
+
case 'Boolean Field':
|
|
196
|
+
return field.opt().orElse(false);
|
|
197
|
+
case 'Date/Time Field':
|
|
198
|
+
return field.opt().orElse(null); // Java date object
|
|
199
|
+
case 'Document Field':
|
|
200
|
+
return field.filename() ? { filename: field.filename(), url: field.permUrl() } : null;
|
|
201
|
+
case 'Signature Field':
|
|
202
|
+
return field.optTimeStamp().map(ts => ({
|
|
203
|
+
user: field.optUser().map(u => u.fullName()).orElse(null),
|
|
204
|
+
dateTime: ts
|
|
205
|
+
})).orElse(null);
|
|
206
|
+
case 'Multiple Select List Field':
|
|
207
|
+
return (field as Bluestep.Relate.MultiSelectField).selectedNames().length > 0
|
|
208
|
+
? (field as Bluestep.Relate.MultiSelectField).selectedNames().toArray()
|
|
209
|
+
: [];
|
|
210
|
+
default:
|
|
211
|
+
return field.opt().orElse(null);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Record and Form Access
|
|
217
|
+
|
|
218
|
+
Access forms and fields by direct property (the FID string). `byFID['...']` is unnecessary for runtime field/form access and should not be used (it appears only inside `require()` in the legacy `imports.ts` registration — see [Query Access Patterns](#query-access-patterns)).
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
// ✅ Correct - direct property access
|
|
222
|
+
entry.fields.title.val()
|
|
223
|
+
client.forms.medicalTasks
|
|
224
|
+
const value = record.forms.formName.fields.fieldName.opt().orElse('default');
|
|
225
|
+
|
|
226
|
+
// ❌ Wrong - byFID is unnecessary at runtime
|
|
227
|
+
entry.fields.byFID['title'].val()
|
|
228
|
+
client.forms.byFID['medicalTasks']
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Linking to a record — `summaryUrl()`
|
|
232
|
+
|
|
233
|
+
There is no universal `/records/{id}` route. Use `record.summaryUrl()` for the relative URL to any record's page.
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
const url = client.summaryUrl(); // e.g. "/rop/connect/..."
|
|
237
|
+
// ❌ Wrong - no such universal route
|
|
238
|
+
const url = `/rop/records/${clientId}`;
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Record lookup by ID — use the `sysId` field, not `id().shortId()`
|
|
242
|
+
|
|
243
|
+
`client.id().shortId()` returns the internal relate record ID, which does NOT resolve with `optById`. Use the `sysId` field from the `name` form.
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
// ✅ Correct
|
|
247
|
+
const clientId = client.forms.name.fields.sysId.val();
|
|
248
|
+
// In a CURRENT_RECORD merge report:
|
|
249
|
+
const clientId = (name as any).fields.sysId.val();
|
|
250
|
+
|
|
251
|
+
// ❌ Wrong - shortId doesn't match what optById expects
|
|
252
|
+
const clientId = client.id().shortId();
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
<!-- CONFLICT: client-ID field key — 01-Platform-Reference states the field key is `sysId` (not `systemID`/`systemId`) and that `id().shortId()` does NOT resolve with optById; Brandon's bluestep-knowledge merge-report examples read `name.fields.systemID.val()`. This may be form/org-specific (the field key depends on the form's schema). Needs human confirmation of the canonical key. -->
|
|
256
|
+
|
|
257
|
+
## Multi-Entry Form (MEF) Entries
|
|
258
|
+
|
|
259
|
+
`mefReports` does NOT exist at runtime. Create entries on the form, and find them by iterating the form directly.
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
// Create a new entry — newEntry() is on the form
|
|
263
|
+
const entry = client.forms.medicalTasks.newEntry();
|
|
264
|
+
entry.fields.title.val('Task title');
|
|
265
|
+
entry.fields.status.val(false);
|
|
266
|
+
// ❌ Wrong - client.forms.medicalTasks.mefReports.newEntry();
|
|
267
|
+
|
|
268
|
+
// Find a specific entry by ID — iterate the pre-loaded entries
|
|
269
|
+
let foundEntry: any = null;
|
|
270
|
+
for (const e of client.forms.medicalTasks) {
|
|
271
|
+
if (e.id().shortId() === entryId) { foundEntry = e; break; }
|
|
272
|
+
}
|
|
273
|
+
// ❌ Wrong - mefReports is undefined at runtime
|
|
274
|
+
// client.forms.medicalTasks.mefReports.allEntries.query().optById(entryId);
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
- **Entry IDs:** use `entry.id().shortId()` to get the string ID for serialization or comparison — it matches when iterating.
|
|
278
|
+
- **Entry creation date:** `entry.created()` returns a Java `Instant`. Convert to compare against a `LocalDate`:
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
const createdDate = B.time.LocalDate.ofInstant(entry.created(), B.time.ZoneId.systemDefault());
|
|
282
|
+
if (createdDate.isAfter(someLocalDate)) return; // e.g. exclude targets created after a note's service date
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Iterating single vs multi-entry forms
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
const isMultiEntry = formMetaData.isMultiEntry();
|
|
289
|
+
if (isMultiEntry) {
|
|
290
|
+
record.forms.multiEntryForm.entries().forEach((entry: any) => {
|
|
291
|
+
const value = entry.fields.fieldName.opt().orElse('');
|
|
292
|
+
});
|
|
293
|
+
} else {
|
|
294
|
+
const value = record.forms.singleEntryForm.fields.fieldName.opt().orElse('');
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Query Access Patterns
|
|
299
|
+
|
|
300
|
+
Which queries, forms, and fields a script can use are defined by the component's **form-import config on the platform**, regenerated into `declarations/index.d.ts` on `b6p pull`. A configured query is available in `app.ts` as a **bare top-level variable** named after the query FID — directly iterable, no `.query()` call:
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
// app.ts — the query is a top-level variable (configured on the platform, generated into declarations)
|
|
304
|
+
const unit = topLevelUnit[0];
|
|
305
|
+
const unitName = unit.forms.unitInformation.fields.unitName.opt().orElse('');
|
|
306
|
+
topLevelUnit.forEach((record: Record_topLevelUnit) => { /* ... */ });
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
To add a query/field, or make a form writable, update the form-import config **on the platform** and `b6p pull` — do not hand-edit `declarations/index.d.ts`. A configured query's top-level variable carries the writable transaction context, so writes go through it directly (writable access is set per form in the import config, not chained in code).
|
|
310
|
+
|
|
311
|
+
For an ad-hoc query that is *not* in the import config, run it explicitly. This is a fresh **read-only** execution — use it for reads only; writes against it fail:
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
B.queries.byFID['staffQuery'].query().forEach((record: Bluestep.Relate.Record) => {
|
|
315
|
+
console.log(record.forms.nameForm.fields.firstName.opt().orElse(''));
|
|
316
|
+
});
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
<details>
|
|
320
|
+
<summary>Legacy: <code>objects/imports.ts</code> registration (older modules only)</summary>
|
|
321
|
+
|
|
322
|
+
Older modules registered queries by hand in `objects/imports.ts` with `.require()`, which produced the same bare top-level variable and selected fields / writable context in code. **This file is not updated on pull and is not hand-written in current modules** — the platform form-import config replaces it. You may still encounter it:
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
// objects/imports.ts (legacy)
|
|
326
|
+
{
|
|
327
|
+
const forms = B.queries.byFID['topLevelUnit'].require().forms;
|
|
328
|
+
forms.byFID['unitInformation'].require({fields: ['unitName']});
|
|
329
|
+
// writable was an option INSIDE require(), never a chained .writable():
|
|
330
|
+
forms.byFID['medicalTasks'].mefReports.allEntries.require({fields: ['title','status'], writable: true});
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
</details>
|
|
335
|
+
|
|
336
|
+
## Java Collections
|
|
337
|
+
|
|
338
|
+
Query results and other Java collections do NOT have JavaScript array methods (`.filter()`, `.map()`). Use `.forEach()` and build an array.
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
// ❌ Wrong
|
|
342
|
+
const filtered = query.query().filter(r => condition);
|
|
343
|
+
// ✅ Correct
|
|
344
|
+
const results = [];
|
|
345
|
+
query.query().forEach((record: any) => { if (condition) results.push(record); });
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## Writing Date and DateTime Fields
|
|
349
|
+
|
|
350
|
+
Use `B.time`, never `Java.Time` — `Java.Time` is a TypeScript type namespace only and does not exist at runtime.
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
// DateField — accepts B.time.LocalDate, B.time.Instant, or millis (number)
|
|
354
|
+
entry.fields.dueDate.dateVal(B.time.LocalDate.parse('2025-04-01'));
|
|
355
|
+
// ❌ Wrong - Java.Time is undefined at runtime
|
|
356
|
+
entry.fields.dueDate.dateVal(Java.Time.LocalDate.parse('2025-04-01'));
|
|
357
|
+
|
|
358
|
+
// DateTimeField — expects a ZonedDateTime, NOT epoch millis
|
|
359
|
+
entry.fields.completedAt.val(B.time.ZonedDateTime.now());
|
|
360
|
+
// ❌ Wrong - toEpochMilli() returns a Long → ClassCastException: Long cannot be cast to ZonedDateTime
|
|
361
|
+
entry.fields.completedAt.val(B.time.Instant.now().toEpochMilli());
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
(For setting search values against date fields in MEF queries, that format differs — see [date-format](../conventions/date-format.md). For the `.val()` vs `.dateTimeVal()` overload trap, see [datetime-field-write](datetime-field-write.md).)
|
|
365
|
+
|
|
366
|
+
## Committing Writes and Formula Triggers
|
|
367
|
+
|
|
368
|
+
- **Form-attached formulas have NO `B.commit()`.** There `B` is `Bluestep.Relate.CurrentRecordB`, which does not extend `IsCommitable`; field writes auto-flush when the form save completes. (Cross-record writes need the target form configured writable in the platform form-import config — legacy modules declared `writable: true` in `objects/imports.ts`.)
|
|
369
|
+
- **Endpoints, on-demand formulas, and scheduled formulas** — `B` IS `IsCommitable`; call `B.commit()` after writes.
|
|
370
|
+
- **Gate on create vs edit** in a form-attached formula (`cur` is a `FormEntry`):
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
if (cur.entry().justCreated()) { /* first-save path */ }
|
|
374
|
+
if (!cur.entry().justCreated() && cur.entry().triggerFormulas()) { /* edit-only path */ }
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
## Endpoint (Maestro) Request/Response
|
|
378
|
+
|
|
379
|
+
In endpoint (Maestro) scripts, the HTTP request/response are on `B.net`. `B.request`, `B.response`, and `B.out` do NOT exist in this context (`B.out` is a merge-report construct).
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
const { request, response } = B.net;
|
|
383
|
+
const action = request.parameter('action'); // string or null
|
|
384
|
+
const id = request.optParameter('id').orElse(''); // Java Optional
|
|
385
|
+
const body = JSON.parse(request.content() || '{}'); // request body as string
|
|
386
|
+
response.contentType('application/json; charset=UTF-8');
|
|
387
|
+
response.out(JSON.stringify({ success: true }));
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
(For the full request/response method surface and an action-based router, see [code-patterns](code-patterns.md). For the no-`delete`-method endpoint rule and output channel, see [endpoint-method-call](endpoint-method-call.md) and [endpoint-output-channel](endpoint-output-channel.md).)
|
|
391
|
+
|
|
392
|
+
## Merge Report Client-Side Integration
|
|
393
|
+
|
|
394
|
+
### Intercepting the save — wrap `window.submitForm`
|
|
395
|
+
|
|
396
|
+
BlueStep's save button is `<a href="javascript:submitForm('Relate','commit')">`, which calls `submitForm()` → `form.submit()`. **`form.submit()` does NOT fire `addEventListener('submit', ...)`.** Wrap `window.submitForm` to intercept the save:
|
|
397
|
+
|
|
398
|
+
```javascript
|
|
399
|
+
var _orig = window.submitForm;
|
|
400
|
+
window.submitForm = function() {
|
|
401
|
+
// your logic here
|
|
402
|
+
if (_orig) return _orig.apply(this, arguments);
|
|
403
|
+
};
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Disable widget inputs before the POST
|
|
407
|
+
|
|
408
|
+
If a merge report renders inputs with `name` attributes, BlueStep tries to save them as form fields and any unknown `name` causes "There was a problem storing the data." Disable widget inputs inside the override before calling through:
|
|
409
|
+
|
|
410
|
+
```javascript
|
|
411
|
+
var _orig = window.submitForm;
|
|
412
|
+
window.submitForm = function() {
|
|
413
|
+
document.querySelectorAll('input[name^="tt-"], input[data-target-id], textarea[data-target-id]')
|
|
414
|
+
.forEach(function(el) { el.disabled = true; });
|
|
415
|
+
if (_orig) return _orig.apply(this, arguments);
|
|
416
|
+
};
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
(See also [named-controls-submit](named-controls-submit.md).)
|
|
420
|
+
|
|
421
|
+
### Script timing — native fields below the widget
|
|
422
|
+
|
|
423
|
+
A merge report's `<script>` is injected mid-page. Native BlueStep fields that render **after** the widget are not yet in the DOM when the script runs — defer those with `DOMContentLoaded`. Fields rendered **before** the script (inside the widget's own HTML) are accessible immediately.
|
|
424
|
+
|
|
425
|
+
```javascript
|
|
426
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
427
|
+
var field = document.querySelector('[data-fid="treatmentTargetJSON"]');
|
|
428
|
+
if (field) field.closest('tr').style.display = 'none';
|
|
429
|
+
});
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
(For passing per-row server data without a `DOMContentLoaded` race, see the queue/lazy-init pattern in [code-patterns](code-patterns.md).)
|
|
433
|
+
|
|
434
|
+
## Base64 and Byte Arrays
|
|
435
|
+
|
|
436
|
+
The base64 helpers take `Java.ByteArray`, not `string`. Convert with the I/O round-trip:
|
|
437
|
+
|
|
438
|
+
```typescript
|
|
439
|
+
const bytes = B.io.toByteArray(B.io.toInputStream(myString, "UTF-8"));
|
|
440
|
+
|
|
441
|
+
// Standard base64 — exposed on B
|
|
442
|
+
const standard = B.toBase64(bytes);
|
|
443
|
+
// URL-safe base64 — ONLY on B.text (RFC 4648 §5, e.g. Gmail messages/send raw field)
|
|
444
|
+
const encoded = B.text.toBaseUrl64(bytes);
|
|
445
|
+
// ❌ Wrong - B.toBaseUrl64 does not exist
|
|
446
|
+
const wrong = B.toBaseUrl64(bytes);
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
`B.io.toInputStream(input, charset?)` and `B.io.toByteArray(inputStream)` are the canonical pair — don't reach for `Java.type('java.lang.String')...getBytes(...)`.
|
|
450
|
+
|
|
451
|
+
## User and Session Access
|
|
452
|
+
|
|
453
|
+
```typescript
|
|
454
|
+
// B.optUser — Java Optional (preferred, safe)
|
|
455
|
+
const fullName = B.optUser.map(u => u.fullName()).orElse('Anonymous');
|
|
456
|
+
const isSuper = B.optUser.map(u => u.isGlobalSuper()).orElse(false);
|
|
457
|
+
const email = B.optUser.map(u => u.email()).orElse('');
|
|
458
|
+
if (B.optUser.isPresent()) {
|
|
459
|
+
const user = B.optUser.get();
|
|
460
|
+
// user.firstName(), user.fullName(), user.email(), user.userName()
|
|
461
|
+
// user.unit(), user.record(), user.entry()
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// B.user — direct access, returns User|null (equivalent to B.optUser.orElse(null)); prefer B.optUser
|
|
465
|
+
const user = B.user;
|
|
466
|
+
if (user) { const name = user.fullName(); }
|
|
467
|
+
|
|
468
|
+
// Layout detection
|
|
469
|
+
if (B.isLayout("MANAGE")) { /* Manage-specific behavior */ }
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
⚠️ `B.optUser` is a Java Optional — never serialize it directly into a template literal (it produces `"function () { [native code] }"`). Always resolve with `.map(...).orElse(...)` first.
|
|
473
|
+
|
|
474
|
+
## Type Safety
|
|
475
|
+
|
|
476
|
+
BlueStep APIs often return `any`. Add safety with casts/annotations and type guards.
|
|
477
|
+
|
|
478
|
+
```typescript
|
|
479
|
+
const value: string = field.opt().orElse('');
|
|
480
|
+
|
|
481
|
+
function isRecord(obj: any): obj is Bluestep.Relate.Record {
|
|
482
|
+
return obj && typeof obj.recordId === 'function';
|
|
483
|
+
}
|
|
484
|
+
if (isRecord(someObj)) { const id = someObj.recordId(); }
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
(For broader TypeScript guidance, see the [BsJs development overview](../bsjs-development.md).)
|
package/templates/claude/instructions/reference/blueiq-credit-integration-playbook.md.template
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Step-by-step playbook for wiring any new OpenAI/AI feature on summitridge into the BlueIQ credit gate + ledger — do this every time we build an AI feature
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
**Whenever we build a new AI feature on summitridge, it MUST be wired into the BlueIQ credit system** (gate + ledger at `/b/aiCredits`, file 1471659 — see blueiq credit system for the endpoint spec). This is the repeatable recipe. Two integration patterns depending on context.
|
|
6
|
+
|
|
7
|
+
## The contract (both patterns produce the same ledger row)
|
|
8
|
+
Per OpenAI "call" (define the granularity sensibly — e.g. one chat turn that fans out to many internal calls = ONE log with summed usage):
|
|
9
|
+
- **check** before spending → `{ok, allowed, used, limit, remaining, period}`. Proceed ONLY when `ok===true && allowed===true`. **Fail-closed.**
|
|
10
|
+
- **log** after the response → row fields: `function` (Text), `user` (Text), `model` (SingleSelect by export value), `promptTokens`, `cachedInputTokens`, `completionTokens`, `audioInputTokens`, `audioOutputTokens`, `audioSeconds`, `cost`, `credits` (all Number). Endpoint prices from its PRICING table; `credits = cost × 1000` (exact decimal, no rounding).
|
|
11
|
+
- **Token mapping:** `promptTokens` = TOTAL input (cached nested inside); pass `cachedInputTokens` too so cached is priced cheaply. Chat models: audio fields = 0. The model id passed MUST exist in `/b/aiCredits` PRICING or `log` hard-errors.
|
|
12
|
+
|
|
13
|
+
## Pattern A — user-context consumer (endpoint or merge report with a session)
|
|
14
|
+
Used by `/b/aiAudio` (1471299), the Shift-Note Assistant (1471399), the Patient Record Chatbot (1470299/1470459). **Copy the helpers verbatim from `/b/aiAudio` (1471299) `draft/scripts/app.ts`:** `creditFetch`, `currentUserName`, `CreditCheck` interface, `checkCredits`, `logCredits`.
|
|
15
|
+
- Loopback is **internal `B.net.fetch("/b/aiCredits", {method:"POST", credentials:true, followRedirects:false, enableErrorStream:true, ...})`** — NOT external httpRequester (can't hairpin to the org's own host → `this.in is null`). `credentials:true` carries the caller's session so the login-required endpoint doesn't 403. See [internal loopback fetch](internal-loopback-fetch.md).
|
|
16
|
+
- Gate BEFORE the spend; on `!allowed` return distinct flags: `creditLimitReached:true` (out_of_credits) vs `creditCheckFailed:true` (gate_unreachable/gate_error). `checkCredits` returns a structured `reason` so the UI tells the true story, not a false "out of credits".
|
|
17
|
+
- Log AFTER (best-effort, never block the user's result; return a `creditLogWarning` on failure).
|
|
18
|
+
- Client UX: on `creditLimitReached` show a styled banner + disable input; on `creditCheckFailed` show a softer banner but allow retry.
|
|
19
|
+
|
|
20
|
+
## Pattern B — scheduled / system-wide formula (NO session)
|
|
21
|
+
Used by the Daily Milieu Summary morning formula (1471599). A scheduled `CurrentRecordB` formula has **no user session and no inbound request**, so it CANNOT authenticate a loopback (would 403). Instead **write the `openAILogs` entry directly**:
|
|
22
|
+
- Prereq: add an `openAILogs` reference path to the formula in the BlueStep UI (multi-entry, writable) — it lives on the Facility/`community` record alongside `openAIIntegration`. Then `openAILogs.newEntry()` works.
|
|
23
|
+
- Price inline (mirror the relevant model's row from `/b/aiCredits` PRICING; comment it as a mirror). gpt-4o = in 2.50 / cached 1.25 / out 10.00 per 1M; `textIn = promptTokens − cached`; `credits = cost × 1000`.
|
|
24
|
+
- Set fields directly: `timestamp.val(B.time.ZonedDateTime.now())` (DateTimeField — use `.val()`, not `.dateTimeVal()`), `function`, `user:"System (scheduled)"`, `model` via `field.optionsByExport()[modelId]`, token fields, `cost`, `credits`, then `B.commit()`. Wrap best-effort (try/catch) AFTER the feature's own commit so logging can't lose the real output. No gate (background job — log only).
|
|
25
|
+
|
|
26
|
+
## Always also
|
|
27
|
+
- **API key from the form**, never hardcoded: read `openAIIntegration.fields.apiKey` (SecretField) via a memoized `getOpenAiKey()` (endpoints use `community[0].forms.openAIIntegration...`; CurrentRecord formulas access `openAIIntegration` directly).
|
|
28
|
+
- **No "AI" in front-facing text** — brand is always "BlueIQ". The ledger `function` label is admin-facing and feature-named (e.g. "Shift Note Recorder", "Patient Record Chatbot", "Daily Milieu Summary", optionally "– {sub-context}"). See [blueiq no ai branding](../conventions/blueiq-no-ai-branding.md).
|
|
29
|
+
- Build (`tsc --rootDir . --skipLibCheck`) and snapshot every touched component.
|
|
30
|
+
|
|
31
|
+
Related: blueiq credit system, [internal loopback fetch](internal-loopback-fetch.md), [blueiq no ai branding](../conventions/blueiq-no-ai-branding.md), ai audio, [datetime field write](datetime-field-write.md), [singleselect null copy](singleselect-null-copy.md).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "ChronoUnit.MONTHS.between counts complete elapsed months (day-aware), NOT calendar-month boundaries — use (year-year)*12 + (month-month) when porting SQL DATEDIFF(MONTH) semantics"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
`B.time.ChronoUnit.MONTHS.between(a, b)` and SQL `DATEDIFF(MONTH, a, b)`
|
|
6
|
+
compute *different* numbers whenever `day(b) < day(a)`:
|
|
7
|
+
|
|
8
|
+
- **ChronoUnit.MONTHS.between** — counts *complete elapsed* months, day-aware.
|
|
9
|
+
`between(2024-06-07, 2026-05-05)` = 22 (because 23 complete months would
|
|
10
|
+
end on 2026-05-07).
|
|
11
|
+
- **SQL DATEDIFF(MONTH)** — counts calendar-month boundary crossings, day-blind.
|
|
12
|
+
Same input = 23 (`(2026-2024)*12 + (5-6)`).
|
|
13
|
+
|
|
14
|
+
For "treatment-month ordinal" / "Nth-month bucket" math (where month 1
|
|
15
|
+
starts on the admit date and month 2 begins on the next same-day-of-month),
|
|
16
|
+
you want the SQL semantic. Implement with a plain calendar diff:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
const calMonths = (Number(note.getYear()) - Number(admit.getYear())) * 12
|
|
20
|
+
+ (Number(note.getMonthValue()) - Number(admit.getMonthValue()));
|
|
21
|
+
const adj = note.getDayOfMonth() >= admit.getDayOfMonth() ? calMonths : (calMonths - 1);
|
|
22
|
+
const treatmentMonth = adj + 1; // 1-indexed
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`ChronoUnit.WEEKS / DAYS / YEARS .between` have the same elapsed-count
|
|
26
|
+
semantics. DAYS happens to match SQL DATEDIFF(DAY) because both ignore
|
|
27
|
+
sub-day resolution, but WEEKS and YEARS will drift the same way MONTHS
|
|
28
|
+
does. When porting SQL date math, port the SQL formula faithfully — don't
|
|
29
|
+
substitute ChronoUnit conveniences.
|
|
30
|
+
|
|
31
|
+
**Discovered:** RISE Maestro v1 (alpineacademy/1515139) shipped with
|
|
32
|
+
`ChronoUnit.MONTHS.between` and was off-by-one on `monthsSinceAdmit` for
|
|
33
|
+
12 of 53 overlapping rows in the verification diff. See
|
|
34
|
+
treatment targets sibling project — also does treatment-month
|
|
35
|
+
math.
|
|
36
|
+
|
|
37
|
+
Related: [localdate parse](localdate-parse.md) (LocalDate.parse — use B.time).
|