openyida 2026.8.25 → 2026.8.26
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 +6 -2
- package/bin/yida.js +3 -0
- package/lib/aggregate-table/aggregate-table.js +37 -14
- package/lib/aggregate-table/contract.js +234 -0
- package/lib/app/canvas-compile.js +169 -0
- package/lib/app/create-form/api-path.js +11 -2
- package/lib/app/create-form.js +109 -339
- package/lib/app/create-page.js +25 -13
- package/lib/app/form-detail-style.js +3 -28
- package/lib/app/import-app.js +13 -31
- package/lib/app/publish.js +18 -74
- package/lib/connector/api.js +144 -36
- package/lib/connector/connector-create.js +4 -4
- package/lib/connector/connector-delete.js +7 -10
- package/lib/connector/connector-test.js +3 -2
- package/lib/connector/contract.js +98 -0
- package/lib/core/command-manifest.js +10 -1
- package/lib/core/locales/en.js +23 -24
- package/lib/core/locales/zh.js +23 -22
- package/lib/core/query-data.js +111 -12
- package/lib/core/utils.js +41 -0
- package/lib/core/yida-client.js +19 -0
- package/lib/integration/integration-create.js +12 -0
- package/lib/integration/integration-update.js +113 -0
- package/lib/report/append.js +18 -1
- package/lib/report/contract.js +158 -0
- package/lib/report/http.js +28 -5
- package/lib/report/index.js +23 -1
- package/package.json +2 -1
- package/scripts/i18n-baseline.json +45 -23
- package/scripts/validate-i18n.js +45 -15
- package/yida-skills/references/yida-api.md +0 -29
- package/yida-skills/skills/yida-app/references/common-issues.md +2 -0
- package/yida-skills/skills/yida-app/workflow/step-7-page-code.md +1 -0
- package/yida-skills/skills/yida-app/workflow/step-8-publish-navigation.md +10 -8
- package/yida-skills/skills/yida-app/workflow/step-9-output-finish.md +8 -6
- package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +14 -0
- package/yida-skills/skills/yida-canvas-custom-page/references/data-bridge-guide.md +2 -0
- package/yida-skills/skills/yida-canvas-custom-page/references/navigation-and-entry-guide.md +7 -5
- package/yida-skills/skills/yida-canvas-data-binding/SKILL.md +1 -0
- package/yida-skills/skills/yida-connector/SKILL.md +6 -6
- package/yida-skills/skills/yida-form-detail/references/injection-guide.md +0 -22
- package/yida-skills/skills/yida-integration/SKILL.md +23 -5
- package/yida-skills/skills/yida-publish-page/SKILL.md +3 -3
package/README.md
CHANGED
|
@@ -349,6 +349,9 @@ openyida connector list
|
|
|
349
349
|
openyida integration create APP_XXX FORM_XXX "Sync customer data"
|
|
350
350
|
openyida integration create APP_XXX FORM_XXX "Approval result notify" \
|
|
351
351
|
--events processFinish --approval-actions agree,disagree --receivers 123456
|
|
352
|
+
# Capability probe only; currently fails closed before auth/spec reads/remote writes
|
|
353
|
+
openyida integration update APP_XXX FORM_XXX LPROC_XXX \
|
|
354
|
+
--spec .cache/openyida/integration/desired-spec.json
|
|
352
355
|
openyida create-report APP_XXX "Sales Dashboard" .cache/openyida/reports/charts.json
|
|
353
356
|
openyida append-chart APP_XXX REPORT_XXX .cache/openyida/reports/chart.json
|
|
354
357
|
```
|
|
@@ -461,7 +464,7 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
|
|
|
461
464
|
| `openyida connector list` | List HTTP connectors |
|
|
462
465
|
| `openyida connector create "name" "domain" ...` | Create a connector |
|
|
463
466
|
| `openyida connector detail <id>` | View connector details |
|
|
464
|
-
| `openyida connector delete <id
|
|
467
|
+
| `openyida connector delete <id> [--force]` | Show manual deletion guidance (CLI does not delete) |
|
|
465
468
|
| `openyida connector add-action --operations <file> --connector-id <id>` | Add an action |
|
|
466
469
|
| `openyida connector list-actions <id>` | List actions |
|
|
467
470
|
| `openyida connector delete-action <id> <operation-id>` | Delete an action |
|
|
@@ -477,6 +480,7 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
|
|
|
477
480
|
| Command | Description |
|
|
478
481
|
|---------|-------------|
|
|
479
482
|
| `openyida integration create <appType> ... [--spec file.json]` | Create integration automation flow |
|
|
483
|
+
| `openyida integration update <appType> <formUuid> <processCode> --spec <desired-spec.json> [--publish]` | Probe integration update capability (currently blocked without full readback) |
|
|
480
484
|
| `openyida integration list <appType> [--form-uuid <uuid>] [--status y\|n] [--json]` | List integration automation flows |
|
|
481
485
|
| `openyida integration enable <appType> <formUuid> <processCode>` | Enable integration automation flow |
|
|
482
486
|
| `openyida integration disable <appType> <formUuid> <processCode>` | Disable integration automation flow |
|
|
@@ -537,7 +541,7 @@ Form field definitions can include `alias` or `componentAlias` to populate Yida
|
|
|
537
541
|
|
|
538
542
|
#### Workflow, Reports, and Integrations
|
|
539
543
|
|
|
540
|
-
`openyida integration create` supports form events (`insert`, `update`, `delete`, `comment`) and approval events (`processFinish`, `activityTask`; aliases: `approval`, `approvalNode`). Approval events require `--approval-actions agree,disagree,terminated`; `activityTask` also requires `--approval-node-ids <nodeId,...>`.
|
|
544
|
+
`openyida integration create` supports form events (`insert`, `update`, `delete`, `comment`) and approval events (`processFinish`, `activityTask`; aliases: `approval`, `approvalNode`). Approval events require `--approval-actions agree,disagree,terminated`; `activityTask` also requires `--approval-node-ids <nodeId,...>`. Passing `--process-code` selects a full graph replacement and now requires explicit `--replace`; it is not a safe update. `integration update` currently performs capability detection only: because full platform `processJson` + `viewJson` readback is unproven, it writes a redacted local probe artifact and returns `PLATFORM_PROBE_REQUIRED` before authentication, spec reading, or remote writes. It does not edit a flow.
|
|
541
545
|
|
|
542
546
|
## Agent Skills
|
|
543
547
|
|
package/bin/yida.js
CHANGED
|
@@ -1173,6 +1173,9 @@ async function main() {
|
|
|
1173
1173
|
if (subCommand === 'create') {
|
|
1174
1174
|
const { run: runIntegration } = require('../lib/integration/integration-create');
|
|
1175
1175
|
await runIntegration(subArgs);
|
|
1176
|
+
} else if (subCommand === 'update') {
|
|
1177
|
+
const { run: runIntegrationUpdate } = require('../lib/integration/integration-update');
|
|
1178
|
+
await runIntegrationUpdate(subArgs);
|
|
1176
1179
|
} else if (subCommand === 'list') {
|
|
1177
1180
|
const { runList } = require('../lib/integration/integration-list');
|
|
1178
1181
|
await runList(subArgs);
|
|
@@ -8,16 +8,13 @@ const { t } = require('../core/i18n');
|
|
|
8
8
|
const { buildYidaTitleI18n, normalizeYidaLocale, resolveContentLocale } = require('../core/yida-i18n');
|
|
9
9
|
const { parseOpenOption, withBrowserHandoff } = require('../core/browser-handoff');
|
|
10
10
|
const { fetchFormPageList, resolveLocalizedText } = require('../app/form-navigation');
|
|
11
|
+
const {
|
|
12
|
+
DESIGN_KEYS,
|
|
13
|
+
assertAggregateDesignConfig,
|
|
14
|
+
assertAggregateDesignReadback,
|
|
15
|
+
} = require('./contract');
|
|
11
16
|
|
|
12
17
|
const FORM_TYPE_VIRTUAL_VIEW = 'virtualView';
|
|
13
|
-
const DESIGN_KEYS = [
|
|
14
|
-
'relationForms',
|
|
15
|
-
'relationships',
|
|
16
|
-
'aggregatedFields',
|
|
17
|
-
'auxFields',
|
|
18
|
-
'formulaFields',
|
|
19
|
-
'validators',
|
|
20
|
-
];
|
|
21
18
|
|
|
22
19
|
function hasHelpFlag(args) {
|
|
23
20
|
return (args || []).includes('--help') || (args || []).includes('-h');
|
|
@@ -133,7 +130,7 @@ async function checkVirtualViewFeature(authRef, appType) {
|
|
|
133
130
|
}
|
|
134
131
|
|
|
135
132
|
async function createEmptyVirtualView(authRef, appType, title) {
|
|
136
|
-
return createYidaClient({ authRef }).
|
|
133
|
+
return createYidaClient({ authRef }).postFormOnce(
|
|
137
134
|
`/dingtalk/web/${appType}/query/formdesign/saveFormSchemaInfo.json`,
|
|
138
135
|
buildCreateEmptyPostData(title)
|
|
139
136
|
);
|
|
@@ -157,7 +154,11 @@ async function postVirtualViewDesign(authRef, appType, formUuid, designInfo, act
|
|
|
157
154
|
throw new Error(`Unknown aggregate table action: ${action}`);
|
|
158
155
|
}
|
|
159
156
|
|
|
160
|
-
|
|
157
|
+
const client = createYidaClient({ authRef });
|
|
158
|
+
const post = action === 'save' || action === 'publish'
|
|
159
|
+
? client.postFormOnce.bind(client)
|
|
160
|
+
: client.postForm.bind(client);
|
|
161
|
+
return post(
|
|
161
162
|
`/alibaba/web/${appType}/query/virtualview/${endpoint}`,
|
|
162
163
|
buildDesignPostData(formUuid, designInfo, gmtModified)
|
|
163
164
|
);
|
|
@@ -318,6 +319,11 @@ async function runCreateEmpty(args) {
|
|
|
318
319
|
const createResult = await createEmptyVirtualView(authRef, appType, name);
|
|
319
320
|
const created = assertSuccess(createResult, t('aggregate_table.create_empty'));
|
|
320
321
|
const formUuid = (created && created.formUuid) || created;
|
|
322
|
+
if ((typeof formUuid !== 'string' && typeof formUuid !== 'number') || String(formUuid).length === 0) {
|
|
323
|
+
const error = new Error('Aggregate table creation returned no recoverable identity.');
|
|
324
|
+
error.code = 'AGGREGATE_CREATE_IDENTITY_MISSING';
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
321
327
|
const designUrl = buildDesignUrl(authRef.baseUrl, appType, formUuid, { fromNew: true });
|
|
322
328
|
const workbenchUrl = buildWorkbenchUrl(authRef.baseUrl, appType, formUuid);
|
|
323
329
|
|
|
@@ -401,6 +407,7 @@ async function runPreview(args) {
|
|
|
401
407
|
|
|
402
408
|
const authRef = await createAuthRef();
|
|
403
409
|
const designInfo = normalizeDesignConfig(readJsonInput(input), formUuid);
|
|
410
|
+
assertAggregateDesignConfig(designInfo, { mode: 'draft' });
|
|
404
411
|
const result = await postVirtualViewDesign(authRef, appType, formUuid, designInfo, 'preview');
|
|
405
412
|
const rows = assertSuccess(result, t('aggregate_table.preview'));
|
|
406
413
|
const rowCount = Array.isArray(rows) ? rows.length : 0;
|
|
@@ -439,13 +446,28 @@ async function runSaveOrPublish(args, action) {
|
|
|
439
446
|
}
|
|
440
447
|
|
|
441
448
|
const authRef = await createAuthRef();
|
|
442
|
-
const { designInfo, gmtModified } = await loadDesignForMutation(authRef, appType, formUuid, input, action);
|
|
443
|
-
|
|
444
|
-
throw new Error(t('aggregate_table.publish_requires_source'));
|
|
445
|
-
}
|
|
449
|
+
const { designInfo, currentConfig, gmtModified } = await loadDesignForMutation(authRef, appType, formUuid, input, action);
|
|
450
|
+
assertAggregateDesignConfig(designInfo, { mode: isPublish ? 'publish' : 'draft' });
|
|
446
451
|
|
|
447
452
|
const result = await postVirtualViewDesign(authRef, appType, formUuid, designInfo, action, gmtModified);
|
|
448
453
|
const content = assertSuccess(result, isPublish ? t('aggregate_table.publish') : t('aggregate_table.save'));
|
|
454
|
+
const readbackResult = await getVirtualViewConfig(authRef, appType, formUuid);
|
|
455
|
+
const readback = assertSuccess(readbackResult, t('aggregate_table.inspect'));
|
|
456
|
+
assertAggregateDesignReadback(designInfo, readback);
|
|
457
|
+
const responseRevision = content && content.gmtModified;
|
|
458
|
+
const readbackRevision = readback && readback.gmtModified;
|
|
459
|
+
if ((responseRevision === undefined || responseRevision === null) &&
|
|
460
|
+
(readbackRevision === undefined || readbackRevision === null)) {
|
|
461
|
+
const error = new Error('AGGREGATE_WRITE_REVISION_MISSING');
|
|
462
|
+
error.code = 'AGGREGATE_WRITE_REVISION_MISSING';
|
|
463
|
+
throw error;
|
|
464
|
+
}
|
|
465
|
+
if ((responseRevision === undefined || responseRevision === null) &&
|
|
466
|
+
currentConfig && String(readbackRevision) === String(currentConfig.gmtModified)) {
|
|
467
|
+
const error = new Error('AGGREGATE_WRITE_REVISION_UNCHANGED');
|
|
468
|
+
error.code = 'AGGREGATE_WRITE_REVISION_UNCHANGED';
|
|
469
|
+
throw error;
|
|
470
|
+
}
|
|
449
471
|
const designUrl = buildDesignUrl(authRef.baseUrl, appType, formUuid);
|
|
450
472
|
const workbenchUrl = buildWorkbenchUrl(authRef.baseUrl, appType, formUuid);
|
|
451
473
|
|
|
@@ -465,6 +487,7 @@ async function runSaveOrPublish(args, action) {
|
|
|
465
487
|
formUuid,
|
|
466
488
|
gmtModified: content && content.gmtModified,
|
|
467
489
|
response: content,
|
|
490
|
+
readbackVerified: true,
|
|
468
491
|
designUrl,
|
|
469
492
|
workbenchUrl,
|
|
470
493
|
}, designUrl, { stage: `aggregate_table_${action}_success`, title: formUuid }, flags.openMode), null, 2));
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { isDeepStrictEqual } = require('util');
|
|
4
|
+
|
|
5
|
+
const DESIGN_KEYS = [
|
|
6
|
+
'relationForms',
|
|
7
|
+
'relationships',
|
|
8
|
+
'aggregatedFields',
|
|
9
|
+
'auxFields',
|
|
10
|
+
'formulaFields',
|
|
11
|
+
'validators',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
// Mirrors the designer defaults read from configLimit. Only limits that map to
|
|
15
|
+
// persisted design arrays are enforced here.
|
|
16
|
+
const DEFAULT_LIMITS = Object.freeze({
|
|
17
|
+
relationForms: 10,
|
|
18
|
+
relationships: 10,
|
|
19
|
+
formulaFields: 10,
|
|
20
|
+
auxFields: 10,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function addError(errors, code, path) {
|
|
24
|
+
errors.push({ code, path });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isNonEmptyValue(value) {
|
|
28
|
+
return value !== null && value !== undefined && String(value).length > 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function validateArrayShapes(config, errors) {
|
|
32
|
+
for (const key of DESIGN_KEYS) {
|
|
33
|
+
if (!Array.isArray(config && config[key])) {
|
|
34
|
+
addError(errors, 'AGGREGATE_DESIGN_ARRAY_REQUIRED', key);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function validateLimits(config, limits, errors) {
|
|
40
|
+
const limitCodes = {
|
|
41
|
+
relationForms: 'AGGREGATE_RELATION_FORMS_LIMIT',
|
|
42
|
+
relationships: 'AGGREGATE_RELATIONSHIPS_LIMIT',
|
|
43
|
+
formulaFields: 'AGGREGATE_FORMULA_FIELDS_LIMIT',
|
|
44
|
+
auxFields: 'AGGREGATE_AUX_FIELDS_LIMIT',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
for (const [key, limit] of Object.entries(limits)) {
|
|
48
|
+
if (Array.isArray(config[key]) && config[key].length > limit) {
|
|
49
|
+
addError(errors, limitCodes[key] || 'AGGREGATE_DESIGN_LIMIT', key);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function validateEntries(config, errors) {
|
|
55
|
+
if (Array.isArray(config.relationForms)) {
|
|
56
|
+
config.relationForms.forEach((item, index) => {
|
|
57
|
+
if (!item || !isNonEmptyValue(item.formUuid)) {
|
|
58
|
+
addError(errors, 'AGGREGATE_RELATION_FORM_INVALID', `relationForms[${index}]`);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (Array.isArray(config.relationships)) {
|
|
64
|
+
config.relationships.forEach((item, index) => {
|
|
65
|
+
const relationshipInfos = item && item.relationshipInfos;
|
|
66
|
+
if (!Array.isArray(relationshipInfos) || relationshipInfos.length === 0 ||
|
|
67
|
+
relationshipInfos.some((entry) => entry === null || entry === undefined)) {
|
|
68
|
+
addError(errors, 'AGGREGATE_RELATIONSHIP_INVALID', `relationships[${index}]`);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (Array.isArray(config.aggregatedFields)) {
|
|
74
|
+
config.aggregatedFields.forEach((item, index) => {
|
|
75
|
+
if (!item || !isNonEmptyValue(item.name)) {
|
|
76
|
+
addError(errors, 'AGGREGATE_COLUMN_INVALID', `aggregatedFields[${index}]`);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (Array.isArray(config.formulaFields)) {
|
|
82
|
+
config.formulaFields.forEach((item, index) => {
|
|
83
|
+
if (!item || !isNonEmptyValue(item.formula)) {
|
|
84
|
+
addError(errors, 'AGGREGATE_FORMULA_FIELD_INVALID', `formulaFields[${index}]`);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (Array.isArray(config.validators)) {
|
|
90
|
+
config.validators.forEach((item, index) => {
|
|
91
|
+
if (!item || !isNonEmptyValue(item.formula)) {
|
|
92
|
+
addError(errors, 'AGGREGATE_VALIDATOR_INVALID', `validators[${index}]`);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function validatePublishCompleteness(config, errors) {
|
|
99
|
+
const required = [
|
|
100
|
+
['relationForms', 'AGGREGATE_RELATION_FORMS_REQUIRED'],
|
|
101
|
+
['relationships', 'AGGREGATE_RELATIONSHIPS_REQUIRED'],
|
|
102
|
+
['aggregatedFields', 'AGGREGATE_COLUMNS_REQUIRED'],
|
|
103
|
+
['formulaFields', 'AGGREGATE_FORMULA_FIELDS_REQUIRED'],
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
for (const [key, code] of required) {
|
|
107
|
+
if (!Array.isArray(config[key]) || config[key].length === 0) {
|
|
108
|
+
addError(errors, code, key);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function validateAggregateDesignConfig(config, options = {}) {
|
|
114
|
+
const mode = options.mode || 'draft';
|
|
115
|
+
if (mode !== 'draft' && mode !== 'publish') {
|
|
116
|
+
throw new Error(`Unsupported aggregate contract mode: ${mode}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const safeConfig = config && typeof config === 'object' && !Array.isArray(config)
|
|
120
|
+
? config
|
|
121
|
+
: {};
|
|
122
|
+
const limits = { ...DEFAULT_LIMITS, ...(options.limits || {}) };
|
|
123
|
+
const errors = [];
|
|
124
|
+
|
|
125
|
+
validateArrayShapes(safeConfig, errors);
|
|
126
|
+
validateLimits(safeConfig, limits, errors);
|
|
127
|
+
validateEntries(safeConfig, errors);
|
|
128
|
+
if (mode === 'publish') {
|
|
129
|
+
validatePublishCompleteness(safeConfig, errors);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { valid: errors.length === 0, errors };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function assertAggregateDesignConfig(config, options = {}) {
|
|
136
|
+
const result = validateAggregateDesignConfig(config, options);
|
|
137
|
+
if (result.valid) {
|
|
138
|
+
return config;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const error = new Error(result.errors.map((item) => item.code).join(', '));
|
|
142
|
+
error.name = 'AggregateDesignContractError';
|
|
143
|
+
error.code = 'AGGREGATE_DESIGN_CONTRACT_INVALID';
|
|
144
|
+
error.details = result.errors;
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function projectAggregateDesignConfig(config) {
|
|
149
|
+
const safeConfig = config && typeof config === 'object' ? config : {};
|
|
150
|
+
const projected = {};
|
|
151
|
+
for (const key of DESIGN_KEYS) {
|
|
152
|
+
projected[key] = Array.isArray(safeConfig[key]) ? safeConfig[key] : [];
|
|
153
|
+
}
|
|
154
|
+
return projected;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function isI18nValue(value) {
|
|
158
|
+
return value && typeof value === 'object' && !Array.isArray(value) && (
|
|
159
|
+
value.type === 'i18n' ||
|
|
160
|
+
Object.prototype.hasOwnProperty.call(value, 'zh_CN') ||
|
|
161
|
+
Object.prototype.hasOwnProperty.call(value, 'en_US') ||
|
|
162
|
+
Object.prototype.hasOwnProperty.call(value, 'pureEn_US')
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function canonicalizeI18nValue(value) {
|
|
167
|
+
const zhCN = value.zh_CN || value.value || '';
|
|
168
|
+
const enUS = value.en_US || value.pureEn_US || zhCN;
|
|
169
|
+
return {
|
|
170
|
+
type: value.type || 'i18n',
|
|
171
|
+
zh_CN: String(zhCN),
|
|
172
|
+
en_US: String(enUS),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function isEmptyFilter(value) {
|
|
177
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
const rules = value.rules;
|
|
181
|
+
return (rules === undefined || rules === null || (Array.isArray(rules) && rules.length === 0)) &&
|
|
182
|
+
!value.logicOperator;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function canonicalizeAggregateValue(value, key = '') {
|
|
186
|
+
if (isI18nValue(value)) {
|
|
187
|
+
return canonicalizeI18nValue(value);
|
|
188
|
+
}
|
|
189
|
+
if (key === 'filter' && isEmptyFilter(value)) {
|
|
190
|
+
return {};
|
|
191
|
+
}
|
|
192
|
+
if (Array.isArray(value)) {
|
|
193
|
+
return value.map((item) => canonicalizeAggregateValue(item));
|
|
194
|
+
}
|
|
195
|
+
if (!value || typeof value !== 'object') {
|
|
196
|
+
if (key === 'precision' && value !== undefined && value !== null && value !== '') {
|
|
197
|
+
return String(value);
|
|
198
|
+
}
|
|
199
|
+
return value;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const result = {};
|
|
203
|
+
for (const childKey of Object.keys(value)) {
|
|
204
|
+
const childValue = value[childKey];
|
|
205
|
+
if ((childKey === 'filterState' && (childValue === null || childValue === undefined)) ||
|
|
206
|
+
(childKey === 'parentId' && (childValue === '' || childValue === null || childValue === undefined))) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
result[childKey] = canonicalizeAggregateValue(childValue, childKey);
|
|
210
|
+
}
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function assertAggregateDesignReadback(expected, actual) {
|
|
215
|
+
const expectedProjection = canonicalizeAggregateValue(projectAggregateDesignConfig(expected));
|
|
216
|
+
const actualProjection = canonicalizeAggregateValue(projectAggregateDesignConfig(actual));
|
|
217
|
+
if (isDeepStrictEqual(expectedProjection, actualProjection)) {
|
|
218
|
+
return actualProjection;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const error = new Error('AGGREGATE_DESIGN_READBACK_MISMATCH');
|
|
222
|
+
error.name = 'AggregateDesignReadbackError';
|
|
223
|
+
error.code = 'AGGREGATE_DESIGN_READBACK_MISMATCH';
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
module.exports = {
|
|
228
|
+
DEFAULT_LIMITS,
|
|
229
|
+
DESIGN_KEYS,
|
|
230
|
+
assertAggregateDesignConfig,
|
|
231
|
+
assertAggregateDesignReadback,
|
|
232
|
+
projectAggregateDesignConfig,
|
|
233
|
+
validateAggregateDesignConfig,
|
|
234
|
+
};
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
const Babel = require('@babel/standalone');
|
|
32
|
+
const globals = require('globals');
|
|
32
33
|
const {
|
|
33
34
|
assertNoEmojiInArtifactName,
|
|
34
35
|
assertNoEmojiInText,
|
|
@@ -46,6 +47,7 @@ const {
|
|
|
46
47
|
findFormDetailLinkIssues,
|
|
47
48
|
formatFormDetailLinkMessage,
|
|
48
49
|
} = require('./form-detail-link-guard');
|
|
50
|
+
const { t } = require('../core/i18n');
|
|
49
51
|
|
|
50
52
|
/**
|
|
51
53
|
* 依赖 → window 别名白名单,用于把 import 改写到运行时全局对象。
|
|
@@ -132,6 +134,15 @@ const BARE_DEPENDENCY_GLOBALS = Object.freeze({
|
|
|
132
134
|
},
|
|
133
135
|
});
|
|
134
136
|
|
|
137
|
+
// 以标准 ECMAScript + Browser 环境定义为完整白名单;Canvas 装配器额外
|
|
138
|
+
// 注入 iframeWindow / parentWindow。Node 专属全局不属于页面运行时,不能放行。
|
|
139
|
+
const CANVAS_RUNTIME_GLOBAL_IDENTIFIERS = new Set([
|
|
140
|
+
...Object.keys(globals.builtin),
|
|
141
|
+
...Object.keys(globals.browser),
|
|
142
|
+
'iframeWindow',
|
|
143
|
+
'parentWindow',
|
|
144
|
+
]);
|
|
145
|
+
|
|
135
146
|
const LUCIDE_REACT_PACKAGE = 'lucide-react';
|
|
136
147
|
const LUCIDE_DYNAMIC_ICON_ALIAS = 'DynamicIcon';
|
|
137
148
|
const LUCIDE_MODULE_ALIAS = 'LucideReact';
|
|
@@ -267,6 +278,159 @@ function assertNoBareDependencyGlobals(source, options = {}) {
|
|
|
267
278
|
});
|
|
268
279
|
}
|
|
269
280
|
|
|
281
|
+
function findUnboundIdentifierIssues(source, options = {}) {
|
|
282
|
+
const issuesByName = new Map();
|
|
283
|
+
const allowedIdentifiers = new Set(CANVAS_RUNTIME_GLOBAL_IDENTIFIERS);
|
|
284
|
+
(options.implicitIdentifiers || []).forEach((name) => allowedIdentifiers.add(name));
|
|
285
|
+
|
|
286
|
+
const guardPlugin = () => ({
|
|
287
|
+
name: 'yida-canvas-unbound-identifier-guard',
|
|
288
|
+
visitor: {
|
|
289
|
+
ReferencedIdentifier(path) {
|
|
290
|
+
const name = path.node.name;
|
|
291
|
+
if (allowedIdentifiers.has(name) || path.scope.hasBinding(name) || issuesByName.has(name)) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
issuesByName.set(name, {
|
|
295
|
+
name,
|
|
296
|
+
line: path.node.loc && path.node.loc.start ? path.node.loc.start.line : 0,
|
|
297
|
+
column: path.node.loc && path.node.loc.start ? path.node.loc.start.column : 0,
|
|
298
|
+
});
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
Babel.transform(source, {
|
|
304
|
+
filename: 'canvas.tsx',
|
|
305
|
+
presets: [
|
|
306
|
+
['typescript', { allExtensions: true, isTSX: true, allowDeclareFields: true }],
|
|
307
|
+
['react', { runtime: 'classic' }],
|
|
308
|
+
],
|
|
309
|
+
plugins: [guardPlugin],
|
|
310
|
+
sourceType: 'module',
|
|
311
|
+
compact: false,
|
|
312
|
+
babelrc: false,
|
|
313
|
+
configFile: false,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
return Array.from(issuesByName.values());
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function formatUnboundIdentifierMessage(issues) {
|
|
320
|
+
const summary = issues
|
|
321
|
+
.map(issue => `${issue.name} (${issue.line}:${issue.column + 1})`)
|
|
322
|
+
.join(', ');
|
|
323
|
+
return t('publish.canvas_unbound_identifiers', summary);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function assertNoUnboundIdentifiers(source, options = {}) {
|
|
327
|
+
const issues = findUnboundIdentifierIssues(source, options);
|
|
328
|
+
if (issues.length === 0) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
throw new CliError(formatUnboundIdentifierMessage(issues), {
|
|
332
|
+
code: 'OPENYIDA_CANVAS_UNBOUND_IDENTIFIER',
|
|
333
|
+
details: {
|
|
334
|
+
stage: 'canvas_compile',
|
|
335
|
+
sourcePath: options.sourcePath || '',
|
|
336
|
+
issues,
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function findUnavailableCanvasInstanceApiIssues(source) {
|
|
342
|
+
const issues = [];
|
|
343
|
+
const seen = new Set();
|
|
344
|
+
|
|
345
|
+
function addIssue(path, api) {
|
|
346
|
+
if (seen.has(api)) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
seen.add(api);
|
|
350
|
+
issues.push({
|
|
351
|
+
api,
|
|
352
|
+
line: path.node.loc && path.node.loc.start ? path.node.loc.start.line : 0,
|
|
353
|
+
column: path.node.loc && path.node.loc.start ? path.node.loc.start.column : 0,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const guardPlugin = ({ types: types }) => ({
|
|
358
|
+
name: 'yida-canvas-instance-api-guard',
|
|
359
|
+
visitor: {
|
|
360
|
+
MemberExpression(path) {
|
|
361
|
+
const object = path.node.object;
|
|
362
|
+
const property = path.node.property;
|
|
363
|
+
const propertyName = path.node.computed && types.isStringLiteral(property)
|
|
364
|
+
? property.value
|
|
365
|
+
: (types.isIdentifier(property) ? property.name : '');
|
|
366
|
+
|
|
367
|
+
if (types.isMemberExpression(object)) {
|
|
368
|
+
const owner = object.object;
|
|
369
|
+
const ownerProperty = object.property;
|
|
370
|
+
const ownerPropertyName = object.computed && types.isStringLiteral(ownerProperty)
|
|
371
|
+
? ownerProperty.value
|
|
372
|
+
: (types.isIdentifier(ownerProperty) ? ownerProperty.name : '');
|
|
373
|
+
if (
|
|
374
|
+
types.isIdentifier(owner, { name: 'props' }) &&
|
|
375
|
+
ownerPropertyName === 'utils' &&
|
|
376
|
+
propertyName === 'getDataList'
|
|
377
|
+
) {
|
|
378
|
+
addIssue(path, 'props.utils.getDataList');
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (
|
|
382
|
+
types.isThisExpression(owner) &&
|
|
383
|
+
ownerPropertyName === 'utils' &&
|
|
384
|
+
propertyName === 'yida'
|
|
385
|
+
) {
|
|
386
|
+
addIssue(path, 'this.utils.yida');
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (!types.isThisExpression(object)) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (propertyName === 'dataSourceMap') {
|
|
394
|
+
addIssue(path, 'this.dataSourceMap');
|
|
395
|
+
} else if (propertyName === '$') {
|
|
396
|
+
addIssue(path, 'this.$');
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
Babel.transform(source, {
|
|
403
|
+
filename: 'canvas.tsx',
|
|
404
|
+
presets: [
|
|
405
|
+
['typescript', { allExtensions: true, isTSX: true, allowDeclareFields: true }],
|
|
406
|
+
['react', { runtime: 'classic' }],
|
|
407
|
+
],
|
|
408
|
+
plugins: [guardPlugin],
|
|
409
|
+
sourceType: 'module',
|
|
410
|
+
compact: false,
|
|
411
|
+
babelrc: false,
|
|
412
|
+
configFile: false,
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
return issues;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function assertNoUnavailableCanvasInstanceApis(source, options = {}) {
|
|
419
|
+
const issues = findUnavailableCanvasInstanceApiIssues(source);
|
|
420
|
+
if (issues.length === 0) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const summary = issues.map(issue => issue.api).join(', ');
|
|
424
|
+
throw new CliError(t('publish.canvas_instance_api_unavailable', summary), {
|
|
425
|
+
code: 'OPENYIDA_CANVAS_INSTANCE_API_UNAVAILABLE',
|
|
426
|
+
details: {
|
|
427
|
+
stage: 'canvas_compile',
|
|
428
|
+
sourcePath: options.sourcePath || '',
|
|
429
|
+
issues,
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
270
434
|
/**
|
|
271
435
|
* 从源码里正则抽取裸包名(过滤相对/绝对路径),去重排序。
|
|
272
436
|
* 对齐画布运行时的依赖抽取行为。
|
|
@@ -617,6 +781,7 @@ function compileCanvasLocal(source, options = {}) {
|
|
|
617
781
|
},
|
|
618
782
|
});
|
|
619
783
|
}
|
|
784
|
+
assertNoUnavailableCanvasInstanceApis(source, options);
|
|
620
785
|
assertNoBareDependencyGlobals(source, options);
|
|
621
786
|
assertNoManualDependencyGlobals(source, options);
|
|
622
787
|
|
|
@@ -643,6 +808,10 @@ function compileCanvasLocal(source, options = {}) {
|
|
|
643
808
|
// 'react' 进入依赖清单(运行时据此注入 React UMD)。
|
|
644
809
|
const usesJsxRuntime = /\bReact\.createElement\b|\bReact\.Fragment\b/.test(intermediate);
|
|
645
810
|
const hasReactBinding = /\b(var|let|const)\s+React\b/.test(intermediate) || /\bimport\s+React\b/.test(intermediate);
|
|
811
|
+
assertNoUnboundIdentifiers(source, {
|
|
812
|
+
sourcePath: options.sourcePath,
|
|
813
|
+
implicitIdentifiers: usesJsxRuntime ? ['React'] : [],
|
|
814
|
+
});
|
|
646
815
|
if (usesJsxRuntime && !hasReactBinding) {
|
|
647
816
|
intermediate = "import React from 'react';\n" + intermediate;
|
|
648
817
|
if (!importedModules.includes('react')) {
|
|
@@ -10,14 +10,23 @@
|
|
|
10
10
|
* @param {object} [options] 可选项
|
|
11
11
|
* @param {string} [options.prefix] 额外的路径前缀(会以 / 拼接)
|
|
12
12
|
* @param {string} [options.namespace] 命名空间,默认 dingtalk
|
|
13
|
+
* @param {'formdesign'|'formnav'} [options.queryModule] 查询模块,默认 formdesign
|
|
13
14
|
* @param {boolean} [options.addTimestamp] 是否追加 _stamp 时间戳查询参数
|
|
14
15
|
* @returns {string} 拼接完成的 API 请求路径
|
|
15
16
|
*/
|
|
16
17
|
function buildApiPath(appType, apiName, options = {}) {
|
|
17
|
-
const {
|
|
18
|
+
const {
|
|
19
|
+
prefix = '',
|
|
20
|
+
namespace = 'dingtalk',
|
|
21
|
+
queryModule = 'formdesign',
|
|
22
|
+
addTimestamp = false,
|
|
23
|
+
} = options;
|
|
24
|
+
if (queryModule !== 'formdesign' && queryModule !== 'formnav') {
|
|
25
|
+
throw new Error(`Unsupported query module: ${queryModule}`);
|
|
26
|
+
}
|
|
18
27
|
const prefixPath = prefix ? `/${prefix}` : '';
|
|
19
28
|
const timestamp = addTimestamp ? `?_stamp=${Date.now()}` : '';
|
|
20
|
-
return `/${namespace}/web/${appType}${prefixPath}/query
|
|
29
|
+
return `/${namespace}/web/${appType}${prefixPath}/query/${queryModule}/${apiName}.json${timestamp}`;
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
module.exports = {
|