@haiiro/docuflow 0.0.1 → 0.0.3
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/{.twenty/output/manifest.json → manifest.json} +9 -9
- package/package.json +15 -3
- package/{.twenty/output/src → src}/front-components/docflow-documents.front-component.mjs +24 -24
- package/src/front-components/docflow-documents.front-component.tsx +21 -6
- package/{.twenty/output/src → src}/front-components/docflow-processes.front-component.mjs +18 -18
- package/{.twenty/output/src → src}/front-components/docflow-tasks.front-component.mjs +18 -18
- package/{.twenty/output/src → src}/front-components/process-steps.front-component.mjs +19 -19
- package/src/front-components/process-steps.front-component.tsx +6 -1
- package/{.twenty/output/src → src}/front-components/task-form.front-component.mjs +24 -24
- package/{.twenty/output/src → src}/logic-functions/launch-process.logic-function.mjs +14 -0
- package/src/logic-functions/launch-process.logic-function.ts +5 -0
- package/{.twenty/output/src → src}/logic-functions/save-launch-fields.logic-function.mjs +14 -0
- package/src/logic-functions/save-launch-fields.logic-function.ts +5 -0
- package/.gitignore +0 -43
- package/.nvmrc +0 -1
- package/.oxlintrc.json +0 -37
- package/.twenty/output/README.md +0 -128
- package/.twenty/output/package.json +0 -49
- package/.twenty/output/public/cover.svg +0 -15
- package/.twenty/output/public/logo.svg +0 -9
- package/.twenty/output/src/front-components/docflow-documents.front-component.tsx +0 -516
- package/.twenty/output/src/front-components/docflow-processes.front-component.tsx +0 -266
- package/.twenty/output/src/front-components/docflow-tasks.front-component.tsx +0 -303
- package/.twenty/output/src/front-components/process-steps.front-component.tsx +0 -286
- package/.twenty/output/src/front-components/task-form.front-component.tsx +0 -29
- package/.twenty/output/src/logic-functions/bitrix-app.logic-function.ts +0 -143
- package/.twenty/output/src/logic-functions/bitrix-event.logic-function.ts +0 -54
- package/.twenty/output/src/logic-functions/bitrix-install.logic-function.ts +0 -81
- package/.twenty/output/src/logic-functions/capture-file.logic-function.ts +0 -32
- package/.twenty/output/src/logic-functions/complete-task.logic-function.ts +0 -46
- package/.twenty/output/src/logic-functions/hand-out-form-step.logic-function.ts +0 -30
- package/.twenty/output/src/logic-functions/health-check.ts +0 -31
- package/.twenty/output/src/logic-functions/launch-parameters-step.logic-function.ts +0 -33
- package/.twenty/output/src/logic-functions/launch-process.logic-function.ts +0 -96
- package/.twenty/output/src/logic-functions/notify-bitrix-assignee.logic-function.ts +0 -85
- package/.twenty/output/src/logic-functions/on-attachment-created.logic-function.ts +0 -67
- package/.twenty/output/src/logic-functions/on-task-updated.logic-function.ts +0 -71
- package/.twenty/output/src/logic-functions/on-workflow-run-updated.logic-function.ts +0 -70
- package/.twenty/output/src/logic-functions/save-launch-fields.logic-function.ts +0 -73
- package/.twenty/output/src/logic-functions/save-step-settings.logic-function.ts +0 -90
- package/.twenty/output/src/logic-functions/save-task-form.logic-function.ts +0 -62
- package/.twenty/output/yarn.lock +0 -3292
- package/.yarn/install-state.gz +0 -0
- package/.yarnrc.yml +0 -1
- package/dist/tsconfig.spec.tsbuildinfo +0 -1
- package/src/__tests__/api-and-bitrix.test.ts +0 -89
- package/src/__tests__/form-fields.test.ts +0 -73
- package/src/__tests__/launch-parameters.test.ts +0 -75
- package/src/__tests__/step-settings.test.ts +0 -39
- package/src/__tests__/workflow-run.test.ts +0 -113
- package/src/application-config.ts +0 -58
- package/src/fields/company-documents.field.ts +0 -29
- package/src/fields/document-author.field.ts +0 -32
- package/src/fields/document-counterparty.field.ts +0 -32
- package/src/fields/document-folder-child-folders.field.ts +0 -23
- package/src/fields/document-folder-documents.field.ts +0 -24
- package/src/fields/document-folder-parent-folder.field.ts +0 -31
- package/src/fields/document-folder.field.ts +0 -32
- package/src/fields/document-responsible.field.ts +0 -32
- package/src/fields/member-authored-documents.field.ts +0 -29
- package/src/fields/member-responsible-documents.field.ts +0 -29
- package/src/fields/task-form-schema.field.ts +0 -21
- package/src/fields/task-form-values.field.ts +0 -21
- package/src/fields/task-instruction.field.ts +0 -21
- package/src/fields/task-section.field.ts +0 -36
- package/src/fields/task-workflow-run-id.field.ts +0 -21
- package/src/fields/task-workflow-step-id.field.ts +0 -21
- package/src/fields/task-workflow-thread-id.field.ts +0 -21
- package/src/fields/workflow-launch-fields.field.ts +0 -21
- package/src/fields/workflow-section.field.ts +0 -36
- package/src/front/api.ts +0 -56
- package/src/front/format.ts +0 -60
- package/src/front/hooks/use-async.ts +0 -54
- package/src/front/hooks/use-workspace-members.ts +0 -22
- package/src/front/types.ts +0 -48
- package/src/front/ui/components.tsx +0 -436
- package/src/front/ui/form-field-inputs.tsx +0 -187
- package/src/front/ui/form-fields-builder.tsx +0 -183
- package/src/front/ui/task-work-panel.tsx +0 -218
- package/src/navigation-menu-items/docflow.navigation-menu-item.ts +0 -15
- package/src/objects/document-folder.object.ts +0 -80
- package/src/objects/document.object.ts +0 -312
- package/src/page-layouts/docflow.page-layout.ts +0 -86
- package/src/page-layouts/task-docflow.page-layout-tab.ts +0 -36
- package/src/page-layouts/workflow-steps.page-layout-tab.ts +0 -37
- package/src/roles/default-role.ts +0 -21
- package/src/roles/docflow-member.role.ts +0 -23
- package/src/server/bitrix-page.ts +0 -52
- package/src/server/bitrix-portal.ts +0 -67
- package/src/server/document-capture.ts +0 -102
- package/src/server/http-response.ts +0 -18
- package/src/server/launch-step.ts +0 -166
- package/src/server/process-tasks.ts +0 -116
- package/src/server/records.ts +0 -132
- package/src/server/task-completion.ts +0 -100
- package/src/server/workflow-engine.ts +0 -105
- package/src/shared/api/core-graphql.util.ts +0 -98
- package/src/shared/api/object-summaries.ts +0 -79
- package/src/shared/api/rest-filter.util.ts +0 -37
- package/src/shared/api/rest-response.util.ts +0 -43
- package/src/shared/bitrix/bitrix-client.ts +0 -114
- package/src/shared/bitrix/bitrix.type.ts +0 -25
- package/src/shared/bitrix/bitrix.util.ts +0 -139
- package/src/shared/docflow.constants.ts +0 -31
- package/src/shared/documents/attachment-target.util.ts +0 -64
- package/src/shared/engine.ts +0 -11
- package/src/shared/form/form-field-builder.util.ts +0 -122
- package/src/shared/form/form-field.type.ts +0 -23
- package/src/shared/form/form-fields.util.ts +0 -138
- package/src/shared/form/step-settings.util.ts +0 -104
- package/src/shared/workflow/launch-step.util.ts +0 -56
- package/src/shared/workflow/run-target-record.util.ts +0 -70
- package/src/shared/workflow/workflow-run.type.ts +0 -35
- package/src/shared/workflow/workflow-run.util.ts +0 -174
- package/src/universal-identifiers.ts +0 -199
- package/src/views/all-document-folders.view.ts +0 -50
- package/src/views/all-documents.view.ts +0 -91
- package/src/views/documents-by-status.view.ts +0 -76
- package/tsconfig.json +0 -37
- package/tsconfig.spec.json +0 -9
- package/vitest.unit.config.ts +0 -14
- /package/{.twenty/output/front-component-shared-dependencies.mjs → front-component-shared-dependencies.mjs} +0 -0
- /package/{.twenty/output/src → src}/logic-functions/bitrix-app.logic-function.mjs +0 -0
- /package/{.twenty/output/src → src}/logic-functions/bitrix-event.logic-function.mjs +0 -0
- /package/{.twenty/output/src → src}/logic-functions/bitrix-install.logic-function.mjs +0 -0
- /package/{.twenty/output/src → src}/logic-functions/capture-file.logic-function.mjs +0 -0
- /package/{.twenty/output/src → src}/logic-functions/complete-task.logic-function.mjs +0 -0
- /package/{.twenty/output/src → src}/logic-functions/hand-out-form-step.logic-function.mjs +0 -0
- /package/{.twenty/output/src → src}/logic-functions/health-check.mjs +0 -0
- /package/{.twenty/output/src → src}/logic-functions/launch-parameters-step.logic-function.mjs +0 -0
- /package/{.twenty/output/src → src}/logic-functions/notify-bitrix-assignee.logic-function.mjs +0 -0
- /package/{.twenty/output/src → src}/logic-functions/on-attachment-created.logic-function.mjs +0 -0
- /package/{.twenty/output/src → src}/logic-functions/on-task-updated.logic-function.mjs +0 -0
- /package/{.twenty/output/src → src}/logic-functions/on-workflow-run-updated.logic-function.mjs +0 -0
- /package/{.twenty/output/src → src}/logic-functions/save-step-settings.logic-function.mjs +0 -0
- /package/{.twenty/output/src → src}/logic-functions/save-task-form.logic-function.mjs +0 -0
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ANSWER_TOOL_CALL_MUTATION,
|
|
3
|
-
callCoreGraphql,
|
|
4
|
-
RUN_WORKFLOW_VERSION_MUTATION,
|
|
5
|
-
} from 'src/shared/api/core-graphql.util';
|
|
6
|
-
import { type FormValues } from 'src/shared/form/form-field.type';
|
|
7
|
-
import {
|
|
8
|
-
buildFormStepResponse,
|
|
9
|
-
findMissingRequiredFields,
|
|
10
|
-
getFormFieldRole,
|
|
11
|
-
} from 'src/shared/form/form-fields.util';
|
|
12
|
-
import { type WorkflowRunState } from 'src/shared/workflow/workflow-run.type';
|
|
13
|
-
import {
|
|
14
|
-
getFormStepAssignment,
|
|
15
|
-
getFormStepFields,
|
|
16
|
-
isFormStep,
|
|
17
|
-
resolveFormStepFields,
|
|
18
|
-
} from 'src/shared/workflow/workflow-run.util';
|
|
19
|
-
|
|
20
|
-
// The two calls the app makes to the workflow engine. Both run as the person
|
|
21
|
-
// behind the current run: Twenty lets only a person with the "Workflows"
|
|
22
|
-
// permission start a version or answer a step of a running process.
|
|
23
|
-
export const runWorkflowVersion = async ({
|
|
24
|
-
workflowVersionId,
|
|
25
|
-
workflowRunId,
|
|
26
|
-
payload,
|
|
27
|
-
}: {
|
|
28
|
-
workflowVersionId: string;
|
|
29
|
-
workflowRunId: string;
|
|
30
|
-
payload: Record<string, unknown>;
|
|
31
|
-
}): Promise<string> => {
|
|
32
|
-
const data = await callCoreGraphql<{
|
|
33
|
-
runWorkflowVersion: { workflowRunId: string };
|
|
34
|
-
}>({
|
|
35
|
-
query: RUN_WORKFLOW_VERSION_MUTATION,
|
|
36
|
-
variables: { input: { workflowVersionId, workflowRunId, payload } },
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
return data.runWorkflowVersion.workflowRunId;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export type AnswerFormStepResult =
|
|
43
|
-
| { status: 'ANSWERED' }
|
|
44
|
-
| { status: 'NOT_WAITING' }
|
|
45
|
-
| { status: 'MISSING_REQUIRED'; labels: string[] };
|
|
46
|
-
|
|
47
|
-
export const answerFormStep = async ({
|
|
48
|
-
state,
|
|
49
|
-
stepId,
|
|
50
|
-
threadId,
|
|
51
|
-
values,
|
|
52
|
-
}: {
|
|
53
|
-
state: WorkflowRunState | null | undefined;
|
|
54
|
-
stepId: string;
|
|
55
|
-
threadId: string;
|
|
56
|
-
values: FormValues;
|
|
57
|
-
}): Promise<AnswerFormStepResult> => {
|
|
58
|
-
const step = state?.flow?.steps?.find((candidate) => candidate.id === stepId);
|
|
59
|
-
const stepInfo = state?.stepInfos?.[stepId];
|
|
60
|
-
|
|
61
|
-
if (
|
|
62
|
-
step === undefined ||
|
|
63
|
-
!isFormStep(step) ||
|
|
64
|
-
stepInfo?.status !== 'PENDING' ||
|
|
65
|
-
stepInfo.threadId !== threadId
|
|
66
|
-
) {
|
|
67
|
-
return { status: 'NOT_WAITING' };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const fields = resolveFormStepFields(
|
|
71
|
-
getFormStepFields(step),
|
|
72
|
-
state?.stepInfos,
|
|
73
|
-
);
|
|
74
|
-
const missingFields = findMissingRequiredFields({ fields, values });
|
|
75
|
-
|
|
76
|
-
if (missingFields.length > 0) {
|
|
77
|
-
return {
|
|
78
|
-
status: 'MISSING_REQUIRED',
|
|
79
|
-
labels: missingFields.map(({ label }) => label),
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// The assignee goes out as { id }: the engine then puts the whole member
|
|
84
|
-
// into the step output, as for a record picked in the form.
|
|
85
|
-
const { assigneeWorkspaceMemberId } = getFormStepAssignment({
|
|
86
|
-
fields,
|
|
87
|
-
now: new Date(),
|
|
88
|
-
});
|
|
89
|
-
const response = buildFormStepResponse({
|
|
90
|
-
fields: fields.map((field) =>
|
|
91
|
-
getFormFieldRole(field) === 'ASSIGNEE' &&
|
|
92
|
-
assigneeWorkspaceMemberId !== undefined
|
|
93
|
-
? { ...field, value: { id: assigneeWorkspaceMemberId } }
|
|
94
|
-
: field,
|
|
95
|
-
),
|
|
96
|
-
values,
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
await callCoreGraphql({
|
|
100
|
-
query: ANSWER_TOOL_CALL_MUTATION,
|
|
101
|
-
variables: { input: { threadId, toolCallId: stepId, response } },
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
return { status: 'ANSWERED' };
|
|
105
|
-
};
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
// Mutations of the workflow engine (run a version, answer a form step) live
|
|
2
|
-
// in the core GraphQL schema, outside the generated record client, so they
|
|
3
|
-
// are sent as plain requests with the token Twenty gives the app.
|
|
4
|
-
type CoreGraphqlResponse<TData> = {
|
|
5
|
-
data?: TData;
|
|
6
|
-
errors?: { message?: string; extensions?: { userFriendlyMessage?: unknown } }[];
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
export class CoreGraphqlError extends Error {
|
|
10
|
-
constructor(
|
|
11
|
-
message: string,
|
|
12
|
-
readonly userMessage: string | undefined,
|
|
13
|
-
) {
|
|
14
|
-
super(message);
|
|
15
|
-
this.name = 'CoreGraphqlError';
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const readUserMessage = (value: unknown): string | undefined => {
|
|
20
|
-
if (typeof value === 'string') {
|
|
21
|
-
return value;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (typeof value === 'object' && value !== null && 'message' in value) {
|
|
25
|
-
const message = (value as { message: unknown }).message;
|
|
26
|
-
|
|
27
|
-
return typeof message === 'string' ? message : undefined;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return undefined;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export const callCoreGraphql = async <TData>({
|
|
34
|
-
query,
|
|
35
|
-
variables,
|
|
36
|
-
}: {
|
|
37
|
-
query: string;
|
|
38
|
-
variables: Record<string, unknown>;
|
|
39
|
-
}): Promise<TData> => {
|
|
40
|
-
const apiUrl = process.env.TWENTY_API_URL;
|
|
41
|
-
const token = process.env.TWENTY_APP_ACCESS_TOKEN;
|
|
42
|
-
|
|
43
|
-
if (apiUrl === undefined || token === undefined) {
|
|
44
|
-
throw new CoreGraphqlError(
|
|
45
|
-
'TWENTY_API_URL or TWENTY_APP_ACCESS_TOKEN is not set',
|
|
46
|
-
undefined,
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const response = await fetch(`${apiUrl.replace(/\/$/, '')}/graphql`, {
|
|
51
|
-
method: 'POST',
|
|
52
|
-
headers: {
|
|
53
|
-
'content-type': 'application/json',
|
|
54
|
-
authorization: `Bearer ${token}`,
|
|
55
|
-
},
|
|
56
|
-
body: JSON.stringify({ query, variables }),
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
const body = (await response.json()) as CoreGraphqlResponse<TData>;
|
|
60
|
-
const [firstError] = body.errors ?? [];
|
|
61
|
-
|
|
62
|
-
if (firstError !== undefined || body.data === undefined) {
|
|
63
|
-
throw new CoreGraphqlError(
|
|
64
|
-
firstError?.message ?? `GraphQL request failed with ${response.status}`,
|
|
65
|
-
readUserMessage(firstError?.extensions?.userFriendlyMessage),
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return body.data;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
export const RUN_WORKFLOW_VERSION_MUTATION = `mutation RunWorkflowVersion($input: RunWorkflowVersionInput!) {
|
|
73
|
-
runWorkflowVersion(input: $input) { workflowRunId }
|
|
74
|
-
}`;
|
|
75
|
-
|
|
76
|
-
export const ANSWER_TOOL_CALL_MUTATION = `mutation AnswerToolCall($input: AnswerToolCallInput!) {
|
|
77
|
-
answerToolCall(input: $input) { streamId }
|
|
78
|
-
}`;
|
|
79
|
-
|
|
80
|
-
export const UPDATE_WORKFLOW_VERSION_STEP_MUTATION = `mutation UpdateWorkflowVersionStep($input: UpdateWorkflowVersionStepInput!) {
|
|
81
|
-
updateWorkflowVersionStep(input: $input) { id }
|
|
82
|
-
}`;
|
|
83
|
-
|
|
84
|
-
export const CREATE_WORKFLOW_VERSION_STEP_MUTATION = `mutation CreateWorkflowVersionStep($input: CreateWorkflowVersionStepInput!) {
|
|
85
|
-
createWorkflowVersionStep(input: $input) { __typename }
|
|
86
|
-
}`;
|
|
87
|
-
|
|
88
|
-
export const DELETE_WORKFLOW_VERSION_STEP_MUTATION = `mutation DeleteWorkflowVersionStep($input: DeleteWorkflowVersionStepInput!) {
|
|
89
|
-
deleteWorkflowVersionStep(input: $input) { __typename }
|
|
90
|
-
}`;
|
|
91
|
-
|
|
92
|
-
export const UPDATE_WORKFLOW_VERSION_TRIGGER_MUTATION = `mutation UpdateWorkflowVersionTrigger($input: UpdateWorkflowVersionTriggerInput!) {
|
|
93
|
-
updateWorkflowVersionTrigger(input: $input) { __typename }
|
|
94
|
-
}`;
|
|
95
|
-
|
|
96
|
-
export const CREATE_DRAFT_FROM_WORKFLOW_VERSION_MUTATION = `mutation CreateDraftFromWorkflowVersion($input: CreateDraftFromWorkflowVersionInput!) {
|
|
97
|
-
createDraftFromWorkflowVersion(input: $input) { id }
|
|
98
|
-
}`;
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
|
2
|
-
|
|
3
|
-
export type ObjectSummary = {
|
|
4
|
-
nameSingular: string;
|
|
5
|
-
namePlural: string;
|
|
6
|
-
labelSingular: string;
|
|
7
|
-
labelFieldName?: string;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
const OBJECTS_PAGE_SIZE = 100;
|
|
11
|
-
|
|
12
|
-
// Names and label fields of the workspace objects, read once per run. The
|
|
13
|
-
// metadata API cannot filter by name, so it is paged through.
|
|
14
|
-
export const getObjectSummaries = async (): Promise<Map<string, ObjectSummary>> => {
|
|
15
|
-
const client = new MetadataApiClient();
|
|
16
|
-
const summaries = new Map<string, ObjectSummary>();
|
|
17
|
-
let after: string | undefined;
|
|
18
|
-
|
|
19
|
-
do {
|
|
20
|
-
const { objects } = await client.query({
|
|
21
|
-
objects: {
|
|
22
|
-
__args: {
|
|
23
|
-
paging: { first: OBJECTS_PAGE_SIZE, ...(after !== undefined ? { after } : {}) },
|
|
24
|
-
filter: {},
|
|
25
|
-
},
|
|
26
|
-
pageInfo: { hasNextPage: true, endCursor: true },
|
|
27
|
-
edges: {
|
|
28
|
-
node: {
|
|
29
|
-
nameSingular: true,
|
|
30
|
-
namePlural: true,
|
|
31
|
-
labelSingular: true,
|
|
32
|
-
labelIdentifierFieldMetadataId: true,
|
|
33
|
-
fieldsList: { id: true, name: true },
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
for (const { node } of objects.edges) {
|
|
40
|
-
summaries.set(node.nameSingular, {
|
|
41
|
-
nameSingular: node.nameSingular,
|
|
42
|
-
namePlural: node.namePlural,
|
|
43
|
-
labelSingular: node.labelSingular,
|
|
44
|
-
labelFieldName: node.fieldsList?.find(
|
|
45
|
-
(field) => field.id === node.labelIdentifierFieldMetadataId,
|
|
46
|
-
)?.name,
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
after =
|
|
51
|
-
objects.pageInfo.hasNextPage === true
|
|
52
|
-
? (objects.pageInfo.endCursor ?? undefined)
|
|
53
|
-
: undefined;
|
|
54
|
-
} while (after !== undefined);
|
|
55
|
-
|
|
56
|
-
return summaries;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
export type SelectOption = { value: string; label: string };
|
|
60
|
-
|
|
61
|
-
// A SELECT field of a form takes its options from a field of an object
|
|
62
|
-
// (settings.selectedFieldId), like the standard form step does.
|
|
63
|
-
export const getFieldOptions = async (fieldId: string): Promise<SelectOption[]> => {
|
|
64
|
-
const { fields } = await new MetadataApiClient().query({
|
|
65
|
-
fields: {
|
|
66
|
-
__args: { filter: { id: { eq: fieldId } }, paging: { first: 1 } },
|
|
67
|
-
edges: { node: { options: true } },
|
|
68
|
-
},
|
|
69
|
-
});
|
|
70
|
-
const options = fields.edges[0]?.node.options;
|
|
71
|
-
|
|
72
|
-
return Array.isArray(options)
|
|
73
|
-
? options.flatMap((option: unknown) => {
|
|
74
|
-
const { value, label } = (option ?? {}) as Record<string, unknown>;
|
|
75
|
-
|
|
76
|
-
return typeof value === 'string' && typeof label === 'string' ? [{ value, label }] : [];
|
|
77
|
-
})
|
|
78
|
-
: [];
|
|
79
|
-
};
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
// Filters of the Twenty REST API: `and(field[eq]:'value',or(...))`. The
|
|
2
|
-
// parser counts brackets and quotes without escaping, so text a person types
|
|
3
|
-
// loses those characters before it becomes part of a filter.
|
|
4
|
-
const UNSAFE_FILTER_CHARACTERS = /[()[\]'",]/g;
|
|
5
|
-
|
|
6
|
-
export const sanitizeFilterText = (text: string): string =>
|
|
7
|
-
text.replace(UNSAFE_FILTER_CHARACTERS, ' ').trim();
|
|
8
|
-
|
|
9
|
-
const quote = (value: string): string => `'${sanitizeFilterText(value)}'`;
|
|
10
|
-
|
|
11
|
-
export const restEq = (field: string, value: string): string =>
|
|
12
|
-
`${field}[eq]:${quote(value)}`;
|
|
13
|
-
|
|
14
|
-
export const restNeq = (field: string, value: string): string =>
|
|
15
|
-
`${field}[neq]:${quote(value)}`;
|
|
16
|
-
|
|
17
|
-
export const restIsNull = (field: string): string => `${field}[is]:NULL`;
|
|
18
|
-
|
|
19
|
-
export const restLt = (field: string, value: string): string =>
|
|
20
|
-
`${field}[lt]:${quote(value)}`;
|
|
21
|
-
|
|
22
|
-
export const restIn = (field: string, values: string[]): string =>
|
|
23
|
-
`${field}[in]:[${values.map((value) => `"${sanitizeFilterText(value)}"`).join(',')}]`;
|
|
24
|
-
|
|
25
|
-
export const restIlike = (field: string, text: string): string =>
|
|
26
|
-
`${field}[ilike]:'%${sanitizeFilterText(text)}%'`;
|
|
27
|
-
|
|
28
|
-
export const restAnd = (...conditions: (string | undefined)[]): string | undefined => {
|
|
29
|
-
const present = conditions.filter(
|
|
30
|
-
(condition): condition is string => condition !== undefined,
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
return present.length === 0 ? undefined : `and(${present.join(',')})`;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export const restOr = (...conditions: string[]): string =>
|
|
37
|
-
`or(${conditions.join(',')})`;
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
export type RestPageInfo = {
|
|
2
|
-
hasNextPage?: boolean;
|
|
3
|
-
endCursor?: string | null;
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
export type RestList<TRecord> = {
|
|
7
|
-
records: TRecord[];
|
|
8
|
-
pageInfo: RestPageInfo;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
type RestEnvelope = {
|
|
12
|
-
data?: Record<string, unknown>;
|
|
13
|
-
pageInfo?: RestPageInfo;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
// Every REST answer carries its records under one key of `data`
|
|
17
|
-
// (`tasks`, `createTask`, `updateTask`...): the shape is read once here.
|
|
18
|
-
const readDataValue = (response: unknown): unknown => {
|
|
19
|
-
const data = (response as RestEnvelope | null)?.data;
|
|
20
|
-
|
|
21
|
-
if (typeof data !== 'object' || data === null) {
|
|
22
|
-
return undefined;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return Object.values(data)[0];
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export const readRestList = <TRecord>(response: unknown): RestList<TRecord> => {
|
|
29
|
-
const value = readDataValue(response);
|
|
30
|
-
|
|
31
|
-
return {
|
|
32
|
-
records: Array.isArray(value) ? (value as TRecord[]) : [],
|
|
33
|
-
pageInfo: (response as RestEnvelope | null)?.pageInfo ?? {},
|
|
34
|
-
};
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export const readRestRecord = <TRecord>(response: unknown): TRecord | undefined => {
|
|
38
|
-
const value = readDataValue(response);
|
|
39
|
-
|
|
40
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
41
|
-
? (value as TRecord)
|
|
42
|
-
: undefined;
|
|
43
|
-
};
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { type BitrixGrant, type BitrixPortal } from 'src/shared/bitrix/bitrix.type';
|
|
2
|
-
import { BITRIX_OAUTH_TOKEN_URL, parseBitrixGrant } from 'src/shared/bitrix/bitrix.util';
|
|
3
|
-
|
|
4
|
-
const TOKEN_REFRESH_MARGIN_MS = 60_000;
|
|
5
|
-
|
|
6
|
-
export class BitrixError extends Error {
|
|
7
|
-
constructor(message: string) {
|
|
8
|
-
super(message);
|
|
9
|
-
this.name = 'BitrixError';
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const getClientCredentials = (): { clientId: string; clientSecret: string } => {
|
|
14
|
-
const clientId = process.env.BITRIX_CLIENT_ID;
|
|
15
|
-
const clientSecret = process.env.BITRIX_CLIENT_SECRET;
|
|
16
|
-
|
|
17
|
-
if (!clientId || !clientSecret) {
|
|
18
|
-
throw new BitrixError('BITRIX_CLIENT_ID and BITRIX_CLIENT_SECRET are not set');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return { clientId, clientSecret };
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export const isBitrixConfigured = (): boolean =>
|
|
25
|
-
Boolean(process.env.BITRIX_CLIENT_ID) && Boolean(process.env.BITRIX_CLIENT_SECRET);
|
|
26
|
-
|
|
27
|
-
// The one proof a portal request has to pass: its refresh token is exchanged
|
|
28
|
-
// with the app's client secret, and the portal id, REST address and tokens
|
|
29
|
-
// are taken from the answer of oauth.bitrix.info, not from the request.
|
|
30
|
-
export const exchangeBitrixRefreshToken = async (
|
|
31
|
-
refreshToken: string,
|
|
32
|
-
): Promise<BitrixGrant> => {
|
|
33
|
-
const { clientId, clientSecret } = getClientCredentials();
|
|
34
|
-
const url = new URL(BITRIX_OAUTH_TOKEN_URL);
|
|
35
|
-
|
|
36
|
-
url.searchParams.set('grant_type', 'refresh_token');
|
|
37
|
-
url.searchParams.set('client_id', clientId);
|
|
38
|
-
url.searchParams.set('client_secret', clientSecret);
|
|
39
|
-
url.searchParams.set('refresh_token', refreshToken);
|
|
40
|
-
|
|
41
|
-
let response: Response;
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
response = await fetch(url, { redirect: 'error' });
|
|
45
|
-
} catch {
|
|
46
|
-
// The URL carries the client secret, so the original error is not kept.
|
|
47
|
-
throw new BitrixError('oauth.bitrix.info is unreachable');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const grant = parseBitrixGrant(await response.json().catch(() => null), Date.now());
|
|
51
|
-
|
|
52
|
-
if (!response.ok || grant === undefined) {
|
|
53
|
-
throw new BitrixError(`Bitrix24 did not accept the refresh token (HTTP ${response.status})`);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return grant;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
export const withFreshBitrixToken = async (
|
|
60
|
-
portal: BitrixPortal,
|
|
61
|
-
): Promise<{ portal: BitrixPortal; isRefreshed: boolean }> => {
|
|
62
|
-
if (portal.expiresAt - TOKEN_REFRESH_MARGIN_MS > Date.now()) {
|
|
63
|
-
return { portal, isRefreshed: false };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const grant = await exchangeBitrixRefreshToken(portal.refreshToken);
|
|
67
|
-
|
|
68
|
-
if (grant.memberId !== portal.memberId) {
|
|
69
|
-
throw new BitrixError('The refreshed token belongs to another portal');
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return {
|
|
73
|
-
portal: {
|
|
74
|
-
...portal,
|
|
75
|
-
accessToken: grant.accessToken,
|
|
76
|
-
refreshToken: grant.refreshToken,
|
|
77
|
-
expiresAt: grant.expiresAt,
|
|
78
|
-
restEndpoint: grant.restEndpoint,
|
|
79
|
-
},
|
|
80
|
-
isRefreshed: true,
|
|
81
|
-
};
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
export const callBitrix = async <TResult>({
|
|
85
|
-
restEndpoint,
|
|
86
|
-
accessToken,
|
|
87
|
-
method,
|
|
88
|
-
params,
|
|
89
|
-
}: {
|
|
90
|
-
restEndpoint: string;
|
|
91
|
-
accessToken: string;
|
|
92
|
-
method: string;
|
|
93
|
-
params: Record<string, unknown>;
|
|
94
|
-
}): Promise<TResult> => {
|
|
95
|
-
const response = await fetch(`${restEndpoint}${method}.json`, {
|
|
96
|
-
method: 'POST',
|
|
97
|
-
headers: { 'content-type': 'application/json' },
|
|
98
|
-
body: JSON.stringify({ ...params, auth: accessToken }),
|
|
99
|
-
redirect: 'error',
|
|
100
|
-
});
|
|
101
|
-
const body = (await response.json().catch(() => null)) as {
|
|
102
|
-
result?: TResult;
|
|
103
|
-
error?: string;
|
|
104
|
-
error_description?: string;
|
|
105
|
-
} | null;
|
|
106
|
-
|
|
107
|
-
if (!response.ok || body === null || body.error !== undefined) {
|
|
108
|
-
throw new BitrixError(
|
|
109
|
-
`Bitrix24 ${method} failed: ${body?.error ?? response.status} ${body?.error_description ?? ''}`.trim(),
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return body.result as TResult;
|
|
114
|
-
};
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
// A Bitrix24 portal linked to this workspace. Kept in the app's key-value
|
|
2
|
-
// store, never in records: it holds tokens.
|
|
3
|
-
export type BitrixPortal = {
|
|
4
|
-
memberId: string;
|
|
5
|
-
portalDomain: string;
|
|
6
|
-
restEndpoint: string;
|
|
7
|
-
accessToken: string;
|
|
8
|
-
refreshToken: string;
|
|
9
|
-
expiresAt: number;
|
|
10
|
-
// Sent by the portal with every event; proves an event comes from it.
|
|
11
|
-
applicationToken?: string;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export type BitrixGrant = Omit<BitrixPortal, 'applicationToken'> & {
|
|
15
|
-
bitrixUserId?: string;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export type BitrixRequest = {
|
|
19
|
-
authId?: string;
|
|
20
|
-
refreshId?: string;
|
|
21
|
-
memberId?: string;
|
|
22
|
-
event?: string;
|
|
23
|
-
applicationToken?: string;
|
|
24
|
-
domain?: string;
|
|
25
|
-
};
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import { type BitrixGrant, type BitrixRequest } from 'src/shared/bitrix/bitrix.type';
|
|
2
|
-
|
|
3
|
-
export const BITRIX_OAUTH_TOKEN_URL = 'https://oauth.bitrix.info/oauth/token/';
|
|
4
|
-
export const BITRIX_PORTAL_KV_KEY = 'bitrix:portal';
|
|
5
|
-
|
|
6
|
-
const DEFAULT_EXPIRES_IN_SECONDS = 3600;
|
|
7
|
-
|
|
8
|
-
const readString = (value: unknown): string | undefined =>
|
|
9
|
-
typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
10
|
-
|
|
11
|
-
// The portal posts form fields (AUTH_ID, REFRESH_ID, member_id; for events
|
|
12
|
-
// event=..., auth[application_token]=...). Nothing here is trusted until the
|
|
13
|
-
// refresh token has been exchanged, or the event token compared.
|
|
14
|
-
export const parseBitrixRequest = ({
|
|
15
|
-
rawBody,
|
|
16
|
-
body,
|
|
17
|
-
query,
|
|
18
|
-
}: {
|
|
19
|
-
rawBody?: string;
|
|
20
|
-
body?: unknown;
|
|
21
|
-
query?: Record<string, string | undefined>;
|
|
22
|
-
}): BitrixRequest => {
|
|
23
|
-
const fields = new Map<string, string>();
|
|
24
|
-
|
|
25
|
-
if (typeof rawBody === 'string' && rawBody.length > 0 && !rawBody.startsWith('{')) {
|
|
26
|
-
new URLSearchParams(rawBody).forEach((value, key) => fields.set(key, value));
|
|
27
|
-
} else if (typeof body === 'object' && body !== null) {
|
|
28
|
-
for (const [key, value] of Object.entries(body as Record<string, unknown>)) {
|
|
29
|
-
if (typeof value === 'string') {
|
|
30
|
-
fields.set(key, value);
|
|
31
|
-
} else if (typeof value === 'object' && value !== null) {
|
|
32
|
-
for (const [nestedKey, nestedValue] of Object.entries(
|
|
33
|
-
value as Record<string, unknown>,
|
|
34
|
-
)) {
|
|
35
|
-
if (typeof nestedValue === 'string') {
|
|
36
|
-
fields.set(`${key}[${nestedKey}]`, nestedValue);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return {
|
|
44
|
-
authId: readString(fields.get('AUTH_ID')) ?? readString(fields.get('auth[access_token]')),
|
|
45
|
-
refreshId:
|
|
46
|
-
readString(fields.get('REFRESH_ID')) ?? readString(fields.get('auth[refresh_token]')),
|
|
47
|
-
memberId: readString(fields.get('member_id')) ?? readString(fields.get('auth[member_id]')),
|
|
48
|
-
event: readString(fields.get('event')),
|
|
49
|
-
applicationToken: readString(fields.get('auth[application_token]')),
|
|
50
|
-
domain: readString(query?.DOMAIN) ?? readString(fields.get('auth[domain]')),
|
|
51
|
-
};
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
// Only an https portal REST address of the form https://<host>/rest/ is
|
|
55
|
-
// accepted from the token answer.
|
|
56
|
-
export const parseBitrixRestEndpoint = (
|
|
57
|
-
value: unknown,
|
|
58
|
-
): { restEndpoint: string; portalDomain: string } | undefined => {
|
|
59
|
-
const text = readString(value);
|
|
60
|
-
|
|
61
|
-
if (text === undefined) {
|
|
62
|
-
return undefined;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
let url: URL;
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
url = new URL(text);
|
|
69
|
-
} catch {
|
|
70
|
-
return undefined;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (
|
|
74
|
-
url.protocol !== 'https:' ||
|
|
75
|
-
url.username !== '' ||
|
|
76
|
-
url.password !== '' ||
|
|
77
|
-
url.search !== '' ||
|
|
78
|
-
url.hash !== '' ||
|
|
79
|
-
url.port !== '' ||
|
|
80
|
-
url.pathname.replace(/\/+$/, '') !== '/rest'
|
|
81
|
-
) {
|
|
82
|
-
return undefined;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return { restEndpoint: `https://${url.hostname}/rest/`, portalDomain: url.hostname };
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
export const parseBitrixGrant = (
|
|
89
|
-
data: unknown,
|
|
90
|
-
now: number,
|
|
91
|
-
): BitrixGrant | undefined => {
|
|
92
|
-
if (typeof data !== 'object' || data === null) {
|
|
93
|
-
return undefined;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const record = data as Record<string, unknown>;
|
|
97
|
-
const endpoint = parseBitrixRestEndpoint(record.client_endpoint);
|
|
98
|
-
const memberId = readString(record.member_id);
|
|
99
|
-
const accessToken = readString(record.access_token);
|
|
100
|
-
const refreshToken = readString(record.refresh_token);
|
|
101
|
-
|
|
102
|
-
if (
|
|
103
|
-
endpoint === undefined ||
|
|
104
|
-
memberId === undefined ||
|
|
105
|
-
accessToken === undefined ||
|
|
106
|
-
refreshToken === undefined
|
|
107
|
-
) {
|
|
108
|
-
return undefined;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const expiresInSeconds = Number(record.expires_in);
|
|
112
|
-
const userId = record.user_id;
|
|
113
|
-
|
|
114
|
-
return {
|
|
115
|
-
memberId,
|
|
116
|
-
restEndpoint: endpoint.restEndpoint,
|
|
117
|
-
portalDomain: endpoint.portalDomain,
|
|
118
|
-
accessToken,
|
|
119
|
-
refreshToken,
|
|
120
|
-
expiresAt:
|
|
121
|
-
now +
|
|
122
|
-
(Number.isFinite(expiresInSeconds) && expiresInSeconds > 0
|
|
123
|
-
? expiresInSeconds
|
|
124
|
-
: DEFAULT_EXPIRES_IN_SECONDS) *
|
|
125
|
-
1000,
|
|
126
|
-
bitrixUserId:
|
|
127
|
-
typeof userId === 'number' ? String(userId) : readString(userId),
|
|
128
|
-
};
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
// HTML is built from text the app controls, but names and titles come from
|
|
132
|
-
// records, so everything shown is escaped.
|
|
133
|
-
export const escapeHtml = (text: string): string =>
|
|
134
|
-
text
|
|
135
|
-
.replace(/&/g, '&')
|
|
136
|
-
.replace(/</g, '<')
|
|
137
|
-
.replace(/>/g, '>')
|
|
138
|
-
.replace(/"/g, '"')
|
|
139
|
-
.replace(/'/g, ''');
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
// Values shared by the app's server functions and front components. The
|
|
2
|
-
// NorDiv fork server uses the same values (smb-docflow, smb-forms), so a
|
|
3
|
-
// workspace can move between the fork engine and the app engine.
|
|
4
|
-
export const DOCFLOW_SECTION = 'DOCFLOW';
|
|
5
|
-
export const CRM_SECTION = 'CRM';
|
|
6
|
-
|
|
7
|
-
// Key of the start parameters in the payload of a manual run; the payload is
|
|
8
|
-
// kept as the run's trigger output, so the parameters stay in the run.
|
|
9
|
-
export const DOCFLOW_LAUNCH_PARAMETERS_KEY = 'smbDocflowParameters';
|
|
10
|
-
|
|
11
|
-
// Input of the "Launch with parameters" step: the whole object of parameters,
|
|
12
|
-
// so the step returns them as its output and later steps read them as
|
|
13
|
-
// {{<step>.<field>}}.
|
|
14
|
-
export const LAUNCH_PARAMETERS_STEP_INPUT = `{{trigger.${DOCFLOW_LAUNCH_PARAMETERS_KEY}}}`;
|
|
15
|
-
export const LAUNCH_PARAMETERS_STEP_NAME = 'Запуск с параметрами';
|
|
16
|
-
|
|
17
|
-
export const TASK_DONE_STATUS = 'DONE';
|
|
18
|
-
export const TASK_TODO_STATUS = 'TODO';
|
|
19
|
-
|
|
20
|
-
// Keys a form field keeps in its free-form `settings`: the workflow FORM step
|
|
21
|
-
// schema drops unknown keys everywhere else.
|
|
22
|
-
export const FORM_FIELD_REQUIRED_SETTING = 'smbIsRequired';
|
|
23
|
-
export const FORM_FIELD_ROLE_SETTING = 'smbRole';
|
|
24
|
-
export const FORM_FIELD_DUE_IN_DAYS_SETTING = 'smbDueInDays';
|
|
25
|
-
|
|
26
|
-
export const TRIGGER_STEP_ID = 'trigger';
|
|
27
|
-
|
|
28
|
-
export const DOCUMENT_OBJECT_NAME = 'smbDocument';
|
|
29
|
-
export const DOCUMENT_FOLDER_OBJECT_NAME = 'smbDocumentFolder';
|
|
30
|
-
|
|
31
|
-
export const PAGE_SIZE = 50;
|