ngx-t-forms-types 0.0.30 → 0.0.33
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/dist/interfaces/Form/formSubmissionHandleInterface.d.ts +6 -0
- package/dist/interfaces/FormBuilder/DefaultEelement.js +76 -0
- package/dist/interfaces/FormBuilder/DefaultInputConfigInterface.d.ts +13 -0
- package/dist/interfaces/FormBuilder/FormInputKeys.d.ts +6 -0
- package/dist/interfaces/FormBuilder/FormInputKeys.js +6 -0
- package/dist/interfaces/FormBuilder/inputConfig/ElementEditConfig.js +188 -3
- package/dist/interfaces/formInput/APIDataFetchingConfigurationInterface.d.ts +34 -0
- package/dist/interfaces/formInput/BasicFormInputInterface.d.ts +1 -0
- package/dist/interfaces/formInput/BasicFormInputInterface.js +1 -0
- package/dist/interfaces/formInput/IMscoaAccount.d.ts +19 -3
- package/dist/interfaces/formInput/ISelectInputInterface.d.ts +12 -0
- package/dist/interfaces/formInput/MultipleInterface.d.ts +10 -0
- package/dist/interfaces/formInput/WorkflowDocumentPicker.d.ts +2 -0
- package/dist/schemas/FormInputSchema.js +41 -2
- package/dist/schemas/MatOptionsSchema.js +7 -0
- package/dist/schemas/MscoaConfigSchema.js +1 -0
- package/dist/schemas/index.d.ts +2 -1
- package/dist/schemas/index.js +2 -1
- package/dist/skillet/authoring-rules.d.ts +41 -0
- package/dist/skillet/authoring-rules.js +61 -0
- package/dist/skillet/extra-members.d.ts +143 -0
- package/dist/skillet/extra-members.js +79 -0
- package/dist/skillet/form.d.ts +170 -0
- package/dist/skillet/form.js +139 -0
- package/dist/skillet/index.d.ts +43 -0
- package/dist/skillet/index.js +46 -0
- package/dist/skillet/input-members.d.ts +375 -0
- package/dist/skillet/input-members.js +186 -0
- package/dist/skillet/internal/coupling.d.ts +63 -0
- package/dist/skillet/internal/coupling.js +69 -0
- package/dist/skillet/internal/editor-guidance.d.ts +64 -0
- package/dist/skillet/internal/editor-guidance.js +291 -0
- package/dist/skillet/members/calculated-field.d.ts +155 -0
- package/dist/skillet/members/calculated-field.js +105 -0
- package/dist/skillet/members/conditional.d.ts +69 -0
- package/dist/skillet/members/conditional.js +16 -0
- package/dist/skillet/members/document-picker.d.ts +182 -0
- package/dist/skillet/members/document-picker.js +111 -0
- package/dist/skillet/members/mat-options.d.ts +187 -0
- package/dist/skillet/members/mat-options.js +92 -0
- package/dist/skillet/members/mscoa.d.ts +182 -0
- package/dist/skillet/members/mscoa.js +129 -0
- package/dist/skillet/members/pagination.d.ts +59 -0
- package/dist/skillet/members/pagination.js +16 -0
- package/dist/skillet/members/table.d.ts +178 -0
- package/dist/skillet/members/table.js +105 -0
- package/dist/skillet/members/validators.d.ts +197 -0
- package/dist/skillet/members/validators.js +74 -0
- package/dist/skillet/members/value.d.ts +154 -0
- package/dist/skillet/members/value.js +97 -0
- package/dist/skillet/shared-types.d.ts +57 -0
- package/dist/skillet/shared-types.js +69 -0
- package/dist/skillet/tests/house-rules.spec.d.ts +1 -0
- package/dist/skillet/tests/house-rules.spec.js +162 -0
- package/dist/skillet/tests/skillet-coupling.spec.d.ts +1 -0
- package/dist/skillet/tests/skillet-coupling.spec.js +191 -0
- package/dist/skillet/tests/variant-assembly.spec.d.ts +1 -0
- package/dist/skillet/tests/variant-assembly.spec.js +643 -0
- package/dist/skillet/variants.d.ts +138 -0
- package/dist/skillet/variants.js +536 -0
- package/dist/skillet/workflow-context.d.ts +95 -0
- package/dist/skillet/workflow-context.js +128 -0
- package/package.json +78 -60
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { s } from '@hashbrownai/core';
|
|
2
|
+
import type { AllFormInputPrimaryKeys } from '../../interfaces/FormBuilder/FormInputKeys.js';
|
|
3
|
+
import { type DocumentPickerFilterOperator, type DocumentPickerFilterValueType } from '../../interfaces/formInput/WorkflowDocumentPicker.js';
|
|
4
|
+
import { AssertNever } from '../internal/coupling.js';
|
|
5
|
+
import type { AvailableWorkflow } from './mat-options.js';
|
|
6
|
+
/**
|
|
7
|
+
* The `workflowPickerConfig` member: which workflow a document picker browses,
|
|
8
|
+
* and how that list is narrowed before the user is shown anything.
|
|
9
|
+
*
|
|
10
|
+
* ## Why this is a factory, and why it may decline to build a schema at all
|
|
11
|
+
*
|
|
12
|
+
* The one field an administrator must supply is `workflowId`, and it is a
|
|
13
|
+
* persisted id. A model asked for one emits a plausible hex string, and what
|
|
14
|
+
* comes back is a picker that renders, opens, and lists nothing — a failure
|
|
15
|
+
* that surfaces to an end user rather than to whoever generated the form.
|
|
16
|
+
*
|
|
17
|
+
* The builder never asks a human to type it either: the editor row for this
|
|
18
|
+
* property is a `WorkflowPicker`, which PICKS from the workflows the tenant
|
|
19
|
+
* actually has. So the model picks too. {@link createWorkflowPickerSchema}
|
|
20
|
+
* takes the workflows available in the caller's context and enumerates their
|
|
21
|
+
* ids, exactly as the `api` branch of `mat-options.ts` enumerates endpoint ids.
|
|
22
|
+
*
|
|
23
|
+
* With no workflows to enumerate, that reasoning goes one step further than it
|
|
24
|
+
* does there. In `mat-options` a missing endpoint list drops one BRANCH and the
|
|
25
|
+
* remaining sources still stand, so a schema is still worth offering. Here the
|
|
26
|
+
* unavailable field is the only required one, and every other member is scoped
|
|
27
|
+
* by it — a step id, a filter path, a document field all mean nothing until the
|
|
28
|
+
* workflow is known — so there is no reduced schema left to offer. The factory
|
|
29
|
+
* returns `null`, and the caller leaves `workflowPickerConfig` off the element
|
|
30
|
+
* rather than letting a model invent the one value that has to be real. That is
|
|
31
|
+
* also why this module exports no ready-made default schema: the no-workflows
|
|
32
|
+
* default is `null`, and an exported `null` reads as an oversight.
|
|
33
|
+
*
|
|
34
|
+
* ## Why this produces a draft
|
|
35
|
+
*
|
|
36
|
+
* `fromInputId` is stored as an input id — the runtime form group is keyed by
|
|
37
|
+
* input id, not by form-control name. A model authoring a form does not have
|
|
38
|
+
* those ids; it has just written the fields by NAME. So the draft carries the
|
|
39
|
+
* `formControlName`, the same trade `inputSourceId` makes in `mat-options.ts`,
|
|
40
|
+
* and the application resolves it against the form it was generated alongside
|
|
41
|
+
* before the config is stored. The member keeps its interface name so that
|
|
42
|
+
* resolution stays a substitution rather than a rename.
|
|
43
|
+
*
|
|
44
|
+
* The absent members are the second reason this is a draft: Skillet has no
|
|
45
|
+
* `optional()`, so every declared key is emitted and optionality is a `| null`
|
|
46
|
+
* branch. {@link WorkflowPickerConfigDraft} therefore describes what the model
|
|
47
|
+
* writes, not what `IWorkflowDocumentPickerConfig` stores, and stripping the
|
|
48
|
+
* nulls is part of the same expansion pass that resolves `fromInputId`.
|
|
49
|
+
*
|
|
50
|
+
* ## Deliberate omissions
|
|
51
|
+
*
|
|
52
|
+
* `primaryIdentifierKey` is `TreeNode[]` — nodes of the key tree the builder
|
|
53
|
+
* derives from a captured sample document. A model has no sample and therefore
|
|
54
|
+
* no tree to select from, and the behaviour it would be overriding (identify
|
|
55
|
+
* the transaction by `_id`) is already the right answer whenever it cannot.
|
|
56
|
+
*
|
|
57
|
+
* `sampleDocument` is captured from a real transaction when the workflow is
|
|
58
|
+
* chosen. It is evidence, not configuration; generated, it would be fiction
|
|
59
|
+
* that the key tree above is then built from.
|
|
60
|
+
*
|
|
61
|
+
* `availableSteps` is a cache of the chosen workflow's steps, refreshed by the
|
|
62
|
+
* builder whenever the workflow changes and never consulted at runtime — the
|
|
63
|
+
* step chooser always reads live steps. Generating it means inventing the very
|
|
64
|
+
* list the step filter is then checked against.
|
|
65
|
+
*
|
|
66
|
+
* A filter's `id` is stamped by the record-list editor the first time the
|
|
67
|
+
* filter is saved, and is the key that editor tracks a row by for edit and for
|
|
68
|
+
* delete. It is an identity, not a decision, so it is left to the editor that
|
|
69
|
+
* owns it.
|
|
70
|
+
*
|
|
71
|
+
* `value` is `unknown` on the interface and is narrowed here to a string,
|
|
72
|
+
* number or boolean — the three shapes `valueType` can actually coerce.
|
|
73
|
+
*
|
|
74
|
+
* ## Known gap: step ids
|
|
75
|
+
*
|
|
76
|
+
* `stepFilter.stepIds` is modelled, but the ids belong to the workflow and
|
|
77
|
+
* {@link AvailableWorkflow} carries only an id and a name, so nothing in this
|
|
78
|
+
* schema can enumerate them. The description says so plainly and tells the
|
|
79
|
+
* model to leave the step filter null unless the request itself names real step
|
|
80
|
+
* ids; an invented id narrows the picker to nothing, which is the same silent
|
|
81
|
+
* failure an invented `workflowId` produces. Widening `AvailableWorkflow` to
|
|
82
|
+
* carry its steps would close this properly, but that type is shared with
|
|
83
|
+
* `mat-options.ts` and is not changed from here.
|
|
84
|
+
*/
|
|
85
|
+
export interface WorkflowPickerSchemaOptions {
|
|
86
|
+
/**
|
|
87
|
+
* Workflows the model may point the picker at. Omit, or pass an empty list,
|
|
88
|
+
* and {@link createWorkflowPickerSchema} produces no schema at all — there is
|
|
89
|
+
* no useful subset of this config that does not name a workflow.
|
|
90
|
+
*/
|
|
91
|
+
workflows?: readonly AvailableWorkflow[];
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The two value sources, addressed by meaning.
|
|
95
|
+
*
|
|
96
|
+
* Each branch of the filter union carries DIFFERENT members, so these literals
|
|
97
|
+
* cannot be taken positionally out of `DOCUMENT_PICKER_FILTER_VALUE_SOURCES`:
|
|
98
|
+
* reordering that array would quietly file `value` and `valueType` under the
|
|
99
|
+
* input-bound source, and produce filters that validate and never match.
|
|
100
|
+
* {@link exhaustive} is this codebase's answer to restating members safely —
|
|
101
|
+
* the union it checks against is itself read off that array, so adding a third
|
|
102
|
+
* source fails to compile here until it is given a branch of its own.
|
|
103
|
+
*/
|
|
104
|
+
declare const FIXED_SOURCE: "fixed", INPUT_SOURCE: "input";
|
|
105
|
+
export interface WorkflowStepFilterDraft {
|
|
106
|
+
stepIds: string[];
|
|
107
|
+
}
|
|
108
|
+
/** A clause comparing a document field against a value fixed in the form. */
|
|
109
|
+
export interface FixedValueFilterDraft {
|
|
110
|
+
path: string;
|
|
111
|
+
op: DocumentPickerFilterOperator | null;
|
|
112
|
+
valueSource: typeof FIXED_SOURCE;
|
|
113
|
+
value: string | number | boolean;
|
|
114
|
+
valueType: DocumentPickerFilterValueType | null;
|
|
115
|
+
}
|
|
116
|
+
/** A clause comparing a document field against another field on this form. */
|
|
117
|
+
export interface InputValueFilterDraft {
|
|
118
|
+
path: string;
|
|
119
|
+
op: DocumentPickerFilterOperator | null;
|
|
120
|
+
valueSource: typeof INPUT_SOURCE;
|
|
121
|
+
/** A `formControlName`, which the application resolves to an input id. */
|
|
122
|
+
fromInputId: string;
|
|
123
|
+
omitWhenEmpty: boolean;
|
|
124
|
+
}
|
|
125
|
+
export type DocumentPickerFilterDraft = FixedValueFilterDraft | InputValueFilterDraft;
|
|
126
|
+
export interface WorkflowPickerConfigDraft {
|
|
127
|
+
workflowId: string;
|
|
128
|
+
stepFilter: WorkflowStepFilterDraft | null;
|
|
129
|
+
presetFilters: DocumentPickerFilterDraft[];
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Builds the `workflowPickerConfig` schema over the workflows the caller has.
|
|
133
|
+
*
|
|
134
|
+
* @param options the workflows the model may choose between. Empty — the
|
|
135
|
+
* default — yields `null` rather than a schema whose only required field
|
|
136
|
+
* would have to be invented.
|
|
137
|
+
* @returns the schema, or `null` when there is no workflow to point it at.
|
|
138
|
+
*/
|
|
139
|
+
export declare function createWorkflowPickerSchema(options?: WorkflowPickerSchemaOptions): s.ObjectType<{
|
|
140
|
+
workflowId: s.EnumType<string[]>;
|
|
141
|
+
stepFilter: import("@hashbrownai/core/src/schema/base").SchemaForUnion<{
|
|
142
|
+
stepIds: string[];
|
|
143
|
+
} | null>;
|
|
144
|
+
presetFilters: s.ArrayType<import("@hashbrownai/core/src/schema/base").SchemaForUnion<{
|
|
145
|
+
path: string;
|
|
146
|
+
op: "eq" | "ne" | "in" | "nin" | "gt" | "gte" | "lt" | "lte" | "regex" | "exists" | null;
|
|
147
|
+
valueSource: "fixed";
|
|
148
|
+
value: string | number | boolean;
|
|
149
|
+
valueType: "string" | "number" | "boolean" | null;
|
|
150
|
+
} | {
|
|
151
|
+
path: string;
|
|
152
|
+
op: "eq" | "ne" | "in" | "nin" | "gt" | "gte" | "lt" | "lte" | "regex" | "exists" | null;
|
|
153
|
+
valueSource: "input";
|
|
154
|
+
fromInputId: string;
|
|
155
|
+
omitWhenEmpty: boolean;
|
|
156
|
+
}>>;
|
|
157
|
+
}> | null;
|
|
158
|
+
/** The schema the factory builds, independent of which workflows it was given. */
|
|
159
|
+
type WorkflowPickerSchema = NonNullable<ReturnType<typeof createWorkflowPickerSchema>>;
|
|
160
|
+
/**
|
|
161
|
+
* The drafted node produces the draft it declares. Loosening a branch, or
|
|
162
|
+
* retyping a member of {@link WorkflowPickerConfigDraft}, surfaces here by key
|
|
163
|
+
* name — the guarantee `_NoDraftDrift` gives `matOptions` in
|
|
164
|
+
* `input-members.ts`, applied to the one member this module owns. What no
|
|
165
|
+
* assertion can check is the expansion from draft to stored config; that
|
|
166
|
+
* contract lives with the caller.
|
|
167
|
+
*/
|
|
168
|
+
export type _NoWorkflowPickerDraftDrift = AssertNever<[
|
|
169
|
+
s.Infer<WorkflowPickerSchema>
|
|
170
|
+
] extends [WorkflowPickerConfigDraft] ? never : AllFormInputPrimaryKeys.WorkflowPickerConfig>;
|
|
171
|
+
/**
|
|
172
|
+
* Members of a generated `workflowPickerConfig` the application must resolve.
|
|
173
|
+
*
|
|
174
|
+
* Only one, and it is not a Joi requirement — `presetFilters[].id` is optional
|
|
175
|
+
* there, so an unexpanded filter still validates. It is listed because it would
|
|
176
|
+
* still be WRONG: `fromInputId` is keyed by input id, and a draft carries a
|
|
177
|
+
* formControlName in that slot. A filter left unresolved passes validation and
|
|
178
|
+
* then matches nothing, which is the failure mode this whole layer is most
|
|
179
|
+
* concerned with.
|
|
180
|
+
*/
|
|
181
|
+
export declare const WORKFLOW_PICKER_DRAFT_COMPLETION: readonly ["workflowPickerConfig.presetFilters[].fromInputId (resolved from formControlName)"];
|
|
182
|
+
export {};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { s } from '@hashbrownai/core';
|
|
2
|
+
import { DOCUMENT_PICKER_FILTER_OPERATORS, DOCUMENT_PICKER_FILTER_VALUE_TYPES, } from '../../interfaces/formInput/WorkflowDocumentPicker.js';
|
|
3
|
+
import { exhaustive, optional } from '../internal/coupling.js';
|
|
4
|
+
import { describe, describeIn } from '../internal/editor-guidance.js';
|
|
5
|
+
// --- the value-source discriminator -----------------------------------------
|
|
6
|
+
/**
|
|
7
|
+
* The two value sources, addressed by meaning.
|
|
8
|
+
*
|
|
9
|
+
* Each branch of the filter union carries DIFFERENT members, so these literals
|
|
10
|
+
* cannot be taken positionally out of `DOCUMENT_PICKER_FILTER_VALUE_SOURCES`:
|
|
11
|
+
* reordering that array would quietly file `value` and `valueType` under the
|
|
12
|
+
* input-bound source, and produce filters that validate and never match.
|
|
13
|
+
* {@link exhaustive} is this codebase's answer to restating members safely —
|
|
14
|
+
* the union it checks against is itself read off that array, so adding a third
|
|
15
|
+
* source fails to compile here until it is given a branch of its own.
|
|
16
|
+
*/
|
|
17
|
+
const [FIXED_SOURCE, INPUT_SOURCE] = exhaustive()([
|
|
18
|
+
'fixed',
|
|
19
|
+
'input',
|
|
20
|
+
]);
|
|
21
|
+
// --- schema -----------------------------------------------------------------
|
|
22
|
+
/**
|
|
23
|
+
* The `deepBind` of the row that opens the preset-filter editor. The filter
|
|
24
|
+
* members below bind relative to the record that editor edits, so their
|
|
25
|
+
* guidance is looked up within it rather than at the top level — see
|
|
26
|
+
* `internal/editor-guidance.ts` for why the two indexes are kept apart.
|
|
27
|
+
*/
|
|
28
|
+
const FILTER_EDITOR = ['workflowPickerConfig', 'presetFilters'];
|
|
29
|
+
// Shared by both branches, so declared once: a clause targets a field and
|
|
30
|
+
// compares it the same way whichever side supplies the value.
|
|
31
|
+
const filterPath = s.string(describeIn('The document field this clause compares. It must be a field the transactions of the chosen workflow actually carry — a field of their form, or one of the properties the document itself exposes.', FILTER_EDITOR, 'path'));
|
|
32
|
+
const filterOp = optional(s.enumeration(describeIn("How the field is compared. Leave it null for the default, which is 'eq'. 'in' and 'nin' read the fixed value as a comma-separated list, and 'exists' ignores the comparison entirely and only tests whether the field is present, so pair it with a boolean value.", FILTER_EDITOR, 'op'), [...DOCUMENT_PICKER_FILTER_OPERATORS]));
|
|
33
|
+
/**
|
|
34
|
+
* The fixed branch.
|
|
35
|
+
*
|
|
36
|
+
* Its value node is a union and so has no description slot of its own. The
|
|
37
|
+
* editor's guidance is composed onto the string branch, the only one the comma
|
|
38
|
+
* rule for `in` / `nin` can apply to; repeating it on the numeric and boolean
|
|
39
|
+
* branches would put the same sentence in one prompt three times for no gain,
|
|
40
|
+
* because the choice being made there is the type, not the format.
|
|
41
|
+
*/
|
|
42
|
+
const fixedValueFilter = s.object(describeIn('Compares the field against a value written into the form definition itself. Use this for a restriction that is always true of this picker, such as only ever offering approved transactions.', FILTER_EDITOR, 'valueSource'), {
|
|
43
|
+
path: filterPath,
|
|
44
|
+
op: filterOp,
|
|
45
|
+
valueSource: s.literal(FIXED_SOURCE),
|
|
46
|
+
value: s.anyOf([
|
|
47
|
+
s.string(describeIn('The fixed value, as text. Write it as the field is stored, not as it is labelled on screen.', FILTER_EDITOR, 'value')),
|
|
48
|
+
s.number('The fixed value, as a number. Set valueType to number to match.'),
|
|
49
|
+
s.boolean('The fixed value, as a boolean. Set valueType to boolean to match.'),
|
|
50
|
+
]),
|
|
51
|
+
valueType: optional(s.enumeration(describeIn("How the fixed value is read before it is compared. Leave it null for the default, which is 'string'.", FILTER_EDITOR, 'valueType'), [...DOCUMENT_PICKER_FILTER_VALUE_TYPES])),
|
|
52
|
+
});
|
|
53
|
+
/** The input-bound branch. */
|
|
54
|
+
const inputValueFilter = s.object(describeIn('Compares the field against whatever the user has entered in another field of this form, read when the picker is opened. Use this to tie the picker to a choice the user makes earlier in the same form.', FILTER_EDITOR, 'valueSource'), {
|
|
55
|
+
path: filterPath,
|
|
56
|
+
op: filterOp,
|
|
57
|
+
valueSource: s.literal(INPUT_SOURCE),
|
|
58
|
+
// Named for the interface member it becomes, but carrying a
|
|
59
|
+
// formControlName: the model knows the fields it just wrote by name and has
|
|
60
|
+
// no way to know the ids the runtime keys them by. The application performs
|
|
61
|
+
// that lookup — see the draft note in this file's header.
|
|
62
|
+
fromInputId: s.string(describeIn('The formControlName of the field on this form to take the value from. It must be a field that actually exists in the form being authored, and one the user reaches before this picker. The application resolves the name to that field’s input id.', FILTER_EDITOR, 'fromInputId')),
|
|
63
|
+
omitWhenEmpty: s.boolean(describeIn('Whether to drop this clause while that field is still empty. Prefer true: false filters on the empty value literally, so the user opens the picker, is shown nothing, and has no way to tell that the cause is a field further up the form.', FILTER_EDITOR, 'omitWhenEmpty')),
|
|
64
|
+
});
|
|
65
|
+
/**
|
|
66
|
+
* One preset filter, as a union over the two value sources.
|
|
67
|
+
*
|
|
68
|
+
* The exclusivity is real rather than conventional: the runtime uses one source
|
|
69
|
+
* and ignores the other, so a clause carrying both is not a richer clause — it
|
|
70
|
+
* is a clause half of whose configuration silently does nothing. Modelling it
|
|
71
|
+
* as a union makes the model choose the source BEFORE it is offered the members
|
|
72
|
+
* that source uses, instead of handing it a flat record of six optional keys
|
|
73
|
+
* and leaving it to infer which combinations mean anything.
|
|
74
|
+
*/
|
|
75
|
+
const presetFilter = s.anyOf([fixedValueFilter, inputValueFilter]);
|
|
76
|
+
/**
|
|
77
|
+
* Builds the `workflowPickerConfig` schema over the workflows the caller has.
|
|
78
|
+
*
|
|
79
|
+
* @param options the workflows the model may choose between. Empty — the
|
|
80
|
+
* default — yields `null` rather than a schema whose only required field
|
|
81
|
+
* would have to be invented.
|
|
82
|
+
* @returns the schema, or `null` when there is no workflow to point it at.
|
|
83
|
+
*/
|
|
84
|
+
export function createWorkflowPickerSchema(options = {}) {
|
|
85
|
+
const workflows = options.workflows ?? [];
|
|
86
|
+
if (!workflows.length)
|
|
87
|
+
return null;
|
|
88
|
+
// Rendered into prose so the choice is made on the workflow's meaning rather
|
|
89
|
+
// than on the shape of its id, which carries none.
|
|
90
|
+
const candidates = workflows.map((w) => `${w.id} (${w.name})`).join('; ');
|
|
91
|
+
return s.object('Which workflow a document picker browses, and how its list of transactions is narrowed before the user sees it. Set this only on a control that picks an existing transaction.', {
|
|
92
|
+
workflowId: s.enumeration(describe(`Whose transactions this picker lists. Everything else here is scoped by it. Choose by meaning, not by position: ${candidates}.`, 'workflowPickerConfig', 'workflowId'), workflows.map((w) => w.id)),
|
|
93
|
+
stepFilter: optional(s.object('Restricts the picker to transactions currently sitting on particular workflow steps.', {
|
|
94
|
+
stepIds: s.array(describe('Workflow step ids the picker is limited to. These ids belong to the workflow and are not listed here, so leave the whole step filter null unless the request names real ones — an id that does not exist narrows the picker to nothing, and nothing says so. An empty list means the same as no filter: every step is eligible, and the user chooses one while browsing.', 'workflowPickerConfig', 'stepFilter', 'stepIds'), s.string('A workflow step id, exactly as the workflow defines it.')),
|
|
95
|
+
})),
|
|
96
|
+
presetFilters: s.array(describe('Filters applied before the user searches, each one narrowing which transactions can be picked at all. Leave this empty unless a restriction was actually asked for: every clause here is one the user can neither see nor undo.', 'workflowPickerConfig', 'presetFilters'), presetFilter),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Members of a generated `workflowPickerConfig` the application must resolve.
|
|
101
|
+
*
|
|
102
|
+
* Only one, and it is not a Joi requirement — `presetFilters[].id` is optional
|
|
103
|
+
* there, so an unexpanded filter still validates. It is listed because it would
|
|
104
|
+
* still be WRONG: `fromInputId` is keyed by input id, and a draft carries a
|
|
105
|
+
* formControlName in that slot. A filter left unresolved passes validation and
|
|
106
|
+
* then matches nothing, which is the failure mode this whole layer is most
|
|
107
|
+
* concerned with.
|
|
108
|
+
*/
|
|
109
|
+
export const WORKFLOW_PICKER_DRAFT_COMPLETION = [
|
|
110
|
+
'workflowPickerConfig.presetFilters[].fromInputId (resolved from formControlName)',
|
|
111
|
+
];
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { s } from '@hashbrownai/core';
|
|
2
|
+
import { DataSources } from '../../interfaces/formInput/APIDataFetchingConfigurationInterface.js';
|
|
3
|
+
import type { FormInputBasicOptionInterface } from '../../interfaces/formInput/FormInputBasicOptionInterface.js';
|
|
4
|
+
/**
|
|
5
|
+
* The `matOptions` member: where a control's value and its options come from.
|
|
6
|
+
*
|
|
7
|
+
* ## Why this is a factory, and why it produces a draft
|
|
8
|
+
*
|
|
9
|
+
* `APIDataFetchingConfigurationInterface` requires `_id`, `name`,
|
|
10
|
+
* `httpEndPoint`, `httpMethod`, `postFormData` and `backEndConfig`. A model
|
|
11
|
+
* asked for that will happily produce `httpEndPoint:
|
|
12
|
+
* 'https://api.example.com/departments'` and a plausible `_id`, and the result
|
|
13
|
+
* is a form that looks correct and is entirely broken — it points at an
|
|
14
|
+
* endpoint that does not exist. Of everything in these schemas, that is the
|
|
15
|
+
* failure most likely to reach production unnoticed, because nothing about the
|
|
16
|
+
* output looks wrong.
|
|
17
|
+
*
|
|
18
|
+
* The form builder never asks a human to do this either. The editor row for
|
|
19
|
+
* this property is `apiEndpointConfig`, backed by `postmanCollectionConfig` and
|
|
20
|
+
* `httpGetDataFunction` — the author PICKS an endpoint from a collection
|
|
21
|
+
* supplied at runtime. So the model picks too: {@link createMatOptionsSchema}
|
|
22
|
+
* takes the endpoints and workflows available in the caller's context and emits
|
|
23
|
+
* an enumeration over their ids. A source that has no candidates is not offered
|
|
24
|
+
* at all, which is why the branches are conditional rather than always present.
|
|
25
|
+
*
|
|
26
|
+
* The consequence is that this schema produces a {@link MatOptionsDraft}, not a
|
|
27
|
+
* `MatDataOptionsInterface`: an api draft carries the id of an endpoint, and
|
|
28
|
+
* the application expands it into the stored configuration before the form is
|
|
29
|
+
* validated. That expansion is the caller's job and there is no way around it —
|
|
30
|
+
* the full configuration is simply not knowable from a prompt.
|
|
31
|
+
*
|
|
32
|
+
* ## Deliberate narrowing
|
|
33
|
+
*
|
|
34
|
+
* `options` is declared as `Array<FormInputBasicOptionInterface |
|
|
35
|
+
* DynamicObjectInterface>`. The second branch is an index signature, which
|
|
36
|
+
* Skillet cannot express and a model has no basis for inventing, so only the
|
|
37
|
+
* `{ label, value }` shape is offered.
|
|
38
|
+
*
|
|
39
|
+
* A `mongoPipeline` source carries `pipeline: any[]` — a raw aggregation
|
|
40
|
+
* pipeline. Selecting the workflow is offered; authoring the pipeline is not.
|
|
41
|
+
*/
|
|
42
|
+
/** An endpoint the caller has available for the model to choose between. */
|
|
43
|
+
export interface AvailableApiEndpoint {
|
|
44
|
+
/** The persisted `_id` of the endpoint configuration. */
|
|
45
|
+
id: string;
|
|
46
|
+
/** Human name, shown to the model so it can choose on meaning, not on id. */
|
|
47
|
+
name: string;
|
|
48
|
+
/** What the endpoint returns, if known. Sharpens the choice considerably. */
|
|
49
|
+
description?: string;
|
|
50
|
+
}
|
|
51
|
+
/** A workflow whose data pipeline the caller has available. */
|
|
52
|
+
export interface AvailableWorkflow {
|
|
53
|
+
id: string;
|
|
54
|
+
name: string;
|
|
55
|
+
}
|
|
56
|
+
export interface MatOptionsSchemaOptions {
|
|
57
|
+
/**
|
|
58
|
+
* Endpoints the model may select for an `api` source. Omit, or pass an empty
|
|
59
|
+
* list, and the `api` branch is not offered — which is the safe default,
|
|
60
|
+
* because the alternative is a hallucinated URL.
|
|
61
|
+
*/
|
|
62
|
+
apiEndpoints?: readonly AvailableApiEndpoint[];
|
|
63
|
+
/** Workflows the model may select for a `mongoPipeline` source. */
|
|
64
|
+
workflows?: readonly AvailableWorkflow[];
|
|
65
|
+
}
|
|
66
|
+
export interface LocalSourceDraft {
|
|
67
|
+
source: DataSources.Local;
|
|
68
|
+
inputSourceId: string;
|
|
69
|
+
allowManualValueEntry: boolean;
|
|
70
|
+
}
|
|
71
|
+
export interface CustomOptionsSourceDraft {
|
|
72
|
+
source: DataSources.CustomOptions;
|
|
73
|
+
}
|
|
74
|
+
export interface ApiSourceDraft {
|
|
75
|
+
source: DataSources.Api;
|
|
76
|
+
endpointId: string;
|
|
77
|
+
allowManualValueEntry: boolean;
|
|
78
|
+
}
|
|
79
|
+
export interface MongoSourceDraft {
|
|
80
|
+
source: DataSources.MongoDb;
|
|
81
|
+
workflowId: string;
|
|
82
|
+
}
|
|
83
|
+
export type ValueSourceDraft = LocalSourceDraft | CustomOptionsSourceDraft | ApiSourceDraft | MongoSourceDraft;
|
|
84
|
+
export interface MatOptionsDraft {
|
|
85
|
+
optionType: 'user' | null;
|
|
86
|
+
options: FormInputBasicOptionInterface[];
|
|
87
|
+
fetch: {
|
|
88
|
+
value: ValueSourceDraft | null;
|
|
89
|
+
options: ValueSourceDraft | null;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Builds the `matOptions` schema for the sources available to the caller.
|
|
94
|
+
*
|
|
95
|
+
* @param options endpoints and workflows the model may choose between. Both
|
|
96
|
+
* default to empty, which offers only the sources a model can author outright
|
|
97
|
+
* and cannot get factually wrong.
|
|
98
|
+
*/
|
|
99
|
+
export declare function createMatOptionsSchema(options?: MatOptionsSchemaOptions): s.ObjectType<{
|
|
100
|
+
optionType: import("@hashbrownai/core/src/schema/base").SchemaForUnion<"user" | null>;
|
|
101
|
+
options: s.ArrayType<s.ObjectType<{
|
|
102
|
+
label: s.StringType;
|
|
103
|
+
value: s.StringType;
|
|
104
|
+
}>>;
|
|
105
|
+
fetch: s.ObjectType<{
|
|
106
|
+
value: import("@hashbrownai/core/src/schema/base").SchemaForUnion<{
|
|
107
|
+
source: DataSources.Local;
|
|
108
|
+
inputSourceId: string;
|
|
109
|
+
allowManualValueEntry: boolean;
|
|
110
|
+
} | {
|
|
111
|
+
source: DataSources.CustomOptions;
|
|
112
|
+
} | {
|
|
113
|
+
source: DataSources.Api;
|
|
114
|
+
endpointId: string;
|
|
115
|
+
allowManualValueEntry: boolean;
|
|
116
|
+
} | {
|
|
117
|
+
source: DataSources.MongoDb;
|
|
118
|
+
workflowId: string;
|
|
119
|
+
} | null>;
|
|
120
|
+
options: import("@hashbrownai/core/src/schema/base").SchemaForUnion<{
|
|
121
|
+
source: DataSources.Local;
|
|
122
|
+
inputSourceId: string;
|
|
123
|
+
allowManualValueEntry: boolean;
|
|
124
|
+
} | {
|
|
125
|
+
source: DataSources.CustomOptions;
|
|
126
|
+
} | {
|
|
127
|
+
source: DataSources.Api;
|
|
128
|
+
endpointId: string;
|
|
129
|
+
allowManualValueEntry: boolean;
|
|
130
|
+
} | {
|
|
131
|
+
source: DataSources.MongoDb;
|
|
132
|
+
workflowId: string;
|
|
133
|
+
} | null>;
|
|
134
|
+
}>;
|
|
135
|
+
}>;
|
|
136
|
+
/**
|
|
137
|
+
* The default `matOptions` schema: no external endpoints or workflows, so it
|
|
138
|
+
* offers only the sources a model can author correctly from a prompt alone.
|
|
139
|
+
*
|
|
140
|
+
* Callers holding a real endpoint collection should build their own with
|
|
141
|
+
* {@link createMatOptionsSchema} rather than reaching for this one.
|
|
142
|
+
*/
|
|
143
|
+
export declare const matOptionsSchema: s.ObjectType<{
|
|
144
|
+
optionType: import("@hashbrownai/core/src/schema/base").SchemaForUnion<"user" | null>;
|
|
145
|
+
options: s.ArrayType<s.ObjectType<{
|
|
146
|
+
label: s.StringType;
|
|
147
|
+
value: s.StringType;
|
|
148
|
+
}>>;
|
|
149
|
+
fetch: s.ObjectType<{
|
|
150
|
+
value: import("@hashbrownai/core/src/schema/base").SchemaForUnion<{
|
|
151
|
+
source: DataSources.Local;
|
|
152
|
+
inputSourceId: string;
|
|
153
|
+
allowManualValueEntry: boolean;
|
|
154
|
+
} | {
|
|
155
|
+
source: DataSources.CustomOptions;
|
|
156
|
+
} | {
|
|
157
|
+
source: DataSources.Api;
|
|
158
|
+
endpointId: string;
|
|
159
|
+
allowManualValueEntry: boolean;
|
|
160
|
+
} | {
|
|
161
|
+
source: DataSources.MongoDb;
|
|
162
|
+
workflowId: string;
|
|
163
|
+
} | null>;
|
|
164
|
+
options: import("@hashbrownai/core/src/schema/base").SchemaForUnion<{
|
|
165
|
+
source: DataSources.Local;
|
|
166
|
+
inputSourceId: string;
|
|
167
|
+
allowManualValueEntry: boolean;
|
|
168
|
+
} | {
|
|
169
|
+
source: DataSources.CustomOptions;
|
|
170
|
+
} | {
|
|
171
|
+
source: DataSources.Api;
|
|
172
|
+
endpointId: string;
|
|
173
|
+
allowManualValueEntry: boolean;
|
|
174
|
+
} | {
|
|
175
|
+
source: DataSources.MongoDb;
|
|
176
|
+
workflowId: string;
|
|
177
|
+
} | null>;
|
|
178
|
+
}>;
|
|
179
|
+
}>;
|
|
180
|
+
/**
|
|
181
|
+
* Members of a generated `matOptions` the application must expand.
|
|
182
|
+
*
|
|
183
|
+
* `endpointId` and `workflowId` are draft-only keys that do not exist on
|
|
184
|
+
* `MatDataOptionsInterface` at all — Joi rejects unknown keys, so a draft that
|
|
185
|
+
* reaches the validator unexpanded fails outright rather than degrading.
|
|
186
|
+
*/
|
|
187
|
+
export declare const MAT_OPTIONS_DRAFT_COMPLETION: readonly ["matOptions.fetch.value|options.endpointId (expanded to the stored APIDataFetchingConfiguration)", "matOptions.fetch.value|options.workflowId (expanded to the stored pipeline configuration)", "matOptions.fetch.value|options.inputSourceId (resolved from formControlName to an input id)"];
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { s } from '@hashbrownai/core';
|
|
2
|
+
import { DataSources } from '../../interfaces/formInput/APIDataFetchingConfigurationInterface.js';
|
|
3
|
+
import { describe } from '../internal/editor-guidance.js';
|
|
4
|
+
// --- schema -----------------------------------------------------------------
|
|
5
|
+
const optionSchema = s.object('A selectable option.', {
|
|
6
|
+
label: s.string('The text shown to the user. It may change freely.'),
|
|
7
|
+
value: s.string("The value stored when this option is chosen: a single word with no spaces ('urgent', 'capex'), kept stable once the form is live. A decision gate compares the stored value, not the label, and splits its expression on spaces."),
|
|
8
|
+
});
|
|
9
|
+
/** Renders the endpoint list into prose so the model chooses on meaning. */
|
|
10
|
+
function describeCandidates(lead, candidates) {
|
|
11
|
+
const lines = candidates.map((c) => c.description ? `${c.id} (${c.name}: ${c.description})` : `${c.id} (${c.name})`);
|
|
12
|
+
return `${lead} Choose by meaning, not by position: ${lines.join('; ')}.`;
|
|
13
|
+
}
|
|
14
|
+
function sourceBranches(slot, options) {
|
|
15
|
+
const localLead = slot === 'value'
|
|
16
|
+
? 'Takes its value from another field on this form.'
|
|
17
|
+
: 'Takes its options from another field on this form.';
|
|
18
|
+
const endpoints = options.apiEndpoints ?? [];
|
|
19
|
+
const workflows = options.workflows ?? [];
|
|
20
|
+
// Built by conditional spread rather than by pushing onto an array: a `push`
|
|
21
|
+
// would have to widen every branch to the shape of the first two, which
|
|
22
|
+
// erases the per-branch typing that `s.Infer` needs to reconstruct the union.
|
|
23
|
+
return [
|
|
24
|
+
s.object(describe(localLead, 'matOptions', 'fetch', slot, 'source'), {
|
|
25
|
+
source: s.literal(DataSources.Local),
|
|
26
|
+
inputSourceId: s.string('The formControlName of the field on this form to read from. It must be a field that actually exists in the form being authored.'),
|
|
27
|
+
allowManualValueEntry: s.boolean('Only meaningful on the value slot of a field whose dataType is object or array (an mSCOA, a multiple-input table). An inherited value of that shape can arrive partly filled in — only some mSCOA segments, rows missing a column — and this decides whether the user may finish it. True leaves the field editable: whenever the inherited value changes, only what actually changed is applied over it and whatever the user added is kept. False locks the field. Choose false unless the field is object- or array-shaped AND the source field is expected to supply only part of it. It has no effect on a scalar field or on an options slot, where the inherited value always wins.'),
|
|
28
|
+
}),
|
|
29
|
+
s.object('Uses a fixed list of options authored into the form itself.', {
|
|
30
|
+
source: s.literal(DataSources.CustomOptions),
|
|
31
|
+
}),
|
|
32
|
+
...(endpoints.length
|
|
33
|
+
? [
|
|
34
|
+
s.object('Fetches from a configured API endpoint.', {
|
|
35
|
+
source: s.literal(DataSources.Api),
|
|
36
|
+
endpointId: s.enumeration(describeCandidates('Which configured endpoint to call.', endpoints), endpoints.map((e) => e.id)),
|
|
37
|
+
allowManualValueEntry: s.boolean('Whether the user may supply what the endpoint does not. On a scalar field: true lets a value they type stand whenever the endpoint contributes nothing (no response, an empty one, a failed request), and a non-empty response always wins over what they typed. On a field whose dataType is object or array (an mSCOA, a multiple-input table): true leaves the field editable so they can finish a response that arrives incomplete — rows missing a column, only some mSCOA segments — and each new response is applied over the live value as a delta, so what the endpoint sends updates and what the user added is kept. False locks the field, which is the right choice whenever the endpoint answers completely.'),
|
|
38
|
+
}),
|
|
39
|
+
]
|
|
40
|
+
: []),
|
|
41
|
+
...(workflows.length
|
|
42
|
+
? [
|
|
43
|
+
s.object('Fetches through a workflow data pipeline.', {
|
|
44
|
+
source: s.literal(DataSources.MongoDb),
|
|
45
|
+
workflowId: s.enumeration(describeCandidates('Which workflow supplies the data.', workflows), workflows.map((w) => w.id)),
|
|
46
|
+
}),
|
|
47
|
+
]
|
|
48
|
+
: []),
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Builds the `matOptions` schema for the sources available to the caller.
|
|
53
|
+
*
|
|
54
|
+
* @param options endpoints and workflows the model may choose between. Both
|
|
55
|
+
* default to empty, which offers only the sources a model can author outright
|
|
56
|
+
* and cannot get factually wrong.
|
|
57
|
+
*/
|
|
58
|
+
export function createMatOptionsSchema(options = {}) {
|
|
59
|
+
const valueSource = s.anyOf(sourceBranches('value', options));
|
|
60
|
+
const optionsSource = s.anyOf(sourceBranches('options', options));
|
|
61
|
+
return s.object('Where this control gets its value and the options it offers. Only set this on a control that actually reads from somewhere; a plain text input does not.', {
|
|
62
|
+
optionType: s.anyOf([
|
|
63
|
+
s.literal('user'),
|
|
64
|
+
s.nullish(),
|
|
65
|
+
]),
|
|
66
|
+
options: s.array(describe('The fixed options offered by this control, for a short list fixed by policy. Fill this in only when the options source is a custom list; leave it empty otherwise. Never retype a list a system already owns — departments, suppliers, users, cost centres — as custom options; it goes stale.', 'matOptions', 'options'), optionSchema),
|
|
67
|
+
fetch: s.object('Where the value and the options are read from.', {
|
|
68
|
+
value: s.anyOf([valueSource, s.nullish()]),
|
|
69
|
+
options: s.anyOf([optionsSource, s.nullish()]),
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The default `matOptions` schema: no external endpoints or workflows, so it
|
|
75
|
+
* offers only the sources a model can author correctly from a prompt alone.
|
|
76
|
+
*
|
|
77
|
+
* Callers holding a real endpoint collection should build their own with
|
|
78
|
+
* {@link createMatOptionsSchema} rather than reaching for this one.
|
|
79
|
+
*/
|
|
80
|
+
export const matOptionsSchema = createMatOptionsSchema();
|
|
81
|
+
/**
|
|
82
|
+
* Members of a generated `matOptions` the application must expand.
|
|
83
|
+
*
|
|
84
|
+
* `endpointId` and `workflowId` are draft-only keys that do not exist on
|
|
85
|
+
* `MatDataOptionsInterface` at all — Joi rejects unknown keys, so a draft that
|
|
86
|
+
* reaches the validator unexpanded fails outright rather than degrading.
|
|
87
|
+
*/
|
|
88
|
+
export const MAT_OPTIONS_DRAFT_COMPLETION = [
|
|
89
|
+
'matOptions.fetch.value|options.endpointId (expanded to the stored APIDataFetchingConfiguration)',
|
|
90
|
+
'matOptions.fetch.value|options.workflowId (expanded to the stored pipeline configuration)',
|
|
91
|
+
'matOptions.fetch.value|options.inputSourceId (resolved from formControlName to an input id)',
|
|
92
|
+
];
|