openxiangda-cli 2.0.0-alpha.85 → 2.0.0-alpha.89
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/package.json +4 -4
- package/template/AGENTS.md +4 -4
- package/template/README.md +4 -4
- package/template/apps/server/package.json +1 -1
- package/template/apps/server/src/app.module.ts +14 -1
- package/template/apps/server/test/smoke.test.ts +3 -1
- package/template/apps/web/data-api-live.e2e.html +15 -0
- package/template/apps/web/e2e/data-api-live-fixture.tsx +141 -0
- package/template/apps/web/e2e/data-api-live.spec.ts +138 -0
- package/template/apps/web/e2e/field-protocol-fixture.tsx +280 -0
- package/template/apps/web/e2e/field-protocol.spec.ts +100 -0
- package/template/apps/web/e2e/resources.spec.ts +258 -20
- package/template/apps/web/field-protocol.e2e.html +12 -0
- package/template/apps/web/package.json +1 -1
- package/template/apps/web/playwright.config.ts +3 -1
- package/template/apps/web/scripts/check.mjs +8 -5
- package/template/apps/web/scripts/verify-build.mjs +1 -1
- package/template/apps/web/src/AuthoritativeSelector.tsx +165 -132
- package/template/apps/web/src/FilePreviewPage.tsx +30 -9
- package/template/apps/web/src/RoleSubjectSelect.tsx +128 -0
- package/template/apps/web/src/Shell.tsx +93 -6
- package/template/apps/web/src/components/platform-fields/AddressField.tsx +338 -0
- package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +25 -6
- package/template/apps/web/src/components/platform-fields/CascadeField.tsx +49 -0
- package/template/apps/web/src/components/platform-fields/DateTimeField.tsx +148 -0
- package/template/apps/web/src/components/platform-fields/JsonField.tsx +77 -0
- package/template/apps/web/src/components/platform-fields/LocationField.tsx +164 -0
- package/template/apps/web/src/components/platform-fields/PlatformDirectoryPicker.tsx +19 -44
- package/template/apps/web/src/components/platform-fields/ResourceReferenceField.tsx +67 -0
- package/template/apps/web/src/components/platform-fields/RichTextField.tsx +196 -0
- package/template/apps/web/src/components/platform-fields/SignatureField.tsx +315 -0
- package/template/apps/web/src/components/platform-fields/SubtableField.tsx +553 -0
- package/template/apps/web/src/components/platform-fields/address-value.ts +80 -0
- package/template/apps/web/src/components/platform-fields/cascade-value.ts +66 -0
- package/template/apps/web/src/components/platform-fields/directory-value.ts +53 -0
- package/template/apps/web/src/components/platform-fields/field-form-codec.ts +98 -0
- package/template/apps/web/src/components/platform-fields/location-value.ts +87 -0
- package/template/apps/web/src/components/platform-fields/resource-query.ts +131 -0
- package/template/apps/web/src/components/platform-fields/rich-text-value.ts +59 -0
- package/template/apps/web/src/components/platform-fields/subtable-value.ts +187 -0
- package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +194 -108
- package/template/apps/web/src/components/resource/ResourceBatchActions.tsx +8 -1
- package/template/apps/web/src/components/resource/SurfaceFields.tsx +385 -79
- package/template/apps/web/src/components/resource/generated-resource-definition.ts +18 -0
- package/template/apps/web/src/components/resource/resource-import.ts +4 -4
- package/template/apps/web/src/data-provider.ts +59 -34
- package/template/apps/web/src/platform-client.ts +341 -136
- package/template/apps/web/src/runtime.tsx +156 -17
- package/template/apps/web/src/styles.css +264 -0
- package/template/apps/web/test/address-field.test.ts +66 -0
- package/template/apps/web/test/cascade-value.test.ts +68 -0
- package/template/apps/web/test/contracts.test.ts +29 -5
- package/template/apps/web/test/directory-value.test.ts +55 -0
- package/template/apps/web/test/field-bounds.test.ts +17 -0
- package/template/apps/web/test/field-form-codec.test.ts +85 -0
- package/template/apps/web/test/location-field.test.ts +83 -0
- package/template/apps/web/test/resource-import.test.ts +8 -7
- package/template/apps/web/test/resource-query.test.ts +144 -0
- package/template/apps/web/test/rich-json-field.test.ts +53 -0
- package/template/apps/web/test/signature-serial-field.test.ts +40 -0
- package/template/apps/web/test/subtable-value.test.ts +144 -0
- package/template/openxiangda.config.ts +1 -1
- package/template/package.json +3 -3
- package/template/packages/contracts/src/generated.ts +95 -0
- package/template/scripts/verify-template-budget.mjs +11 -4
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import type { DataResourceSurface } from 'openxiangda-contracts/browser';
|
|
4
|
+
import {
|
|
5
|
+
buildSubtableOperations,
|
|
6
|
+
type SubtableDraftRow,
|
|
7
|
+
} from '../src/components/platform-fields/subtable-value';
|
|
8
|
+
|
|
9
|
+
const childSurface: DataResourceSurface = {
|
|
10
|
+
fields: {
|
|
11
|
+
parent_id: field('uuid'),
|
|
12
|
+
display_order: field('number.integer'),
|
|
13
|
+
description: field('text.short'),
|
|
14
|
+
private_note: {
|
|
15
|
+
...field('text.long'),
|
|
16
|
+
createCapabilities: [],
|
|
17
|
+
updateCapabilities: [],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
test('plans parent references, updates, deletes and deterministic reorder', () => {
|
|
23
|
+
const rows: SubtableDraftRow[] = [
|
|
24
|
+
{
|
|
25
|
+
key: 'new',
|
|
26
|
+
state: 'created',
|
|
27
|
+
data: { description: 'New', private_note: 'forbidden' },
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
key: 'existing',
|
|
31
|
+
state: 'persisted',
|
|
32
|
+
id: 'child-1',
|
|
33
|
+
revision: 3,
|
|
34
|
+
originalOrder: 0,
|
|
35
|
+
originalData: { description: 'Old' },
|
|
36
|
+
data: { description: 'Changed' },
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
key: 'deleted',
|
|
40
|
+
state: 'deleted',
|
|
41
|
+
id: 'child-2',
|
|
42
|
+
revision: 2,
|
|
43
|
+
data: {},
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
const operations = buildSubtableOperations({
|
|
47
|
+
rows,
|
|
48
|
+
childResourceCode: 'children',
|
|
49
|
+
childSurface,
|
|
50
|
+
foreignKey: 'parent_id',
|
|
51
|
+
orderField: 'display_order',
|
|
52
|
+
parent: { operation: 'create', operationIndex: 0 },
|
|
53
|
+
canWrite: (_code, field, operation) =>
|
|
54
|
+
(operation === 'create'
|
|
55
|
+
? field.createCapabilities
|
|
56
|
+
: field.updateCapabilities
|
|
57
|
+
).length > 0,
|
|
58
|
+
canDelete: true,
|
|
59
|
+
});
|
|
60
|
+
assert.deepEqual(operations, [
|
|
61
|
+
{
|
|
62
|
+
operation: 'delete',
|
|
63
|
+
resourceCode: 'children',
|
|
64
|
+
id: 'child-2',
|
|
65
|
+
expectedRevision: 2,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
operation: 'create',
|
|
69
|
+
resourceCode: 'children',
|
|
70
|
+
data: {
|
|
71
|
+
description: 'New',
|
|
72
|
+
parent_id: { operationIndex: 0, field: 'id' },
|
|
73
|
+
display_order: 0,
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
operation: 'update',
|
|
78
|
+
resourceCode: 'children',
|
|
79
|
+
id: 'child-1',
|
|
80
|
+
expectedRevision: 3,
|
|
81
|
+
data: { description: 'Changed', display_order: 1 },
|
|
82
|
+
},
|
|
83
|
+
]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('omits unchanged persisted rows and enforces the transaction-derived row bound', () => {
|
|
87
|
+
const unchanged: SubtableDraftRow = {
|
|
88
|
+
key: 'existing',
|
|
89
|
+
state: 'persisted',
|
|
90
|
+
id: 'child-1',
|
|
91
|
+
revision: 1,
|
|
92
|
+
originalOrder: 0,
|
|
93
|
+
originalData: { description: 'Same' },
|
|
94
|
+
data: { description: 'Same' },
|
|
95
|
+
};
|
|
96
|
+
assert.deepEqual(
|
|
97
|
+
buildSubtableOperations({
|
|
98
|
+
rows: [unchanged],
|
|
99
|
+
childResourceCode: 'children',
|
|
100
|
+
childSurface,
|
|
101
|
+
foreignKey: 'parent_id',
|
|
102
|
+
orderField: 'display_order',
|
|
103
|
+
parent: { operation: 'update', id: 'parent-1' },
|
|
104
|
+
canWrite: () => true,
|
|
105
|
+
canDelete: true,
|
|
106
|
+
}),
|
|
107
|
+
[]
|
|
108
|
+
);
|
|
109
|
+
assert.throws(
|
|
110
|
+
() =>
|
|
111
|
+
buildSubtableOperations({
|
|
112
|
+
rows: Array.from({ length: 50 }, (_, index) => ({
|
|
113
|
+
key: String(index),
|
|
114
|
+
state: 'created' as const,
|
|
115
|
+
data: { description: String(index) },
|
|
116
|
+
})),
|
|
117
|
+
childResourceCode: 'children',
|
|
118
|
+
childSurface,
|
|
119
|
+
foreignKey: 'parent_id',
|
|
120
|
+
orderField: 'display_order',
|
|
121
|
+
maxRows: 49,
|
|
122
|
+
parent: { operation: 'update', id: 'parent-1' },
|
|
123
|
+
canWrite: () => true,
|
|
124
|
+
canDelete: true,
|
|
125
|
+
}),
|
|
126
|
+
/OPENXIANGDA_SUBTABLE_MAX_ROWS_EXCEEDED/
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
function field(type: string) {
|
|
131
|
+
return {
|
|
132
|
+
label: type,
|
|
133
|
+
type,
|
|
134
|
+
widget:
|
|
135
|
+
type === 'uuid'
|
|
136
|
+
? 'readonly'
|
|
137
|
+
: type === 'number.integer'
|
|
138
|
+
? 'number'
|
|
139
|
+
: 'text',
|
|
140
|
+
readCapabilities: ['read'],
|
|
141
|
+
createCapabilities: ['create'],
|
|
142
|
+
updateCapabilities: ['update'],
|
|
143
|
+
} as any;
|
|
144
|
+
}
|
|
@@ -25,6 +25,6 @@ export default defineOpenXiangdaApp({
|
|
|
25
25
|
authorizationTransitions: [],
|
|
26
26
|
},
|
|
27
27
|
data: { resources: [] },
|
|
28
|
-
events: { subscriptions: [], timers: [] },
|
|
28
|
+
events: { schemas: [], subscriptions: [], timers: [], dateTriggers: [] },
|
|
29
29
|
workflows: { definitions: [], bindings: [], activations: [], providers: [], editableParameters: [] },
|
|
30
30
|
});
|
package/template/package.json
CHANGED
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
"build": "pnpm --recursive build"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"openxiangda-cli": "2.0.0-alpha.
|
|
26
|
-
"openxiangda-contracts": "2.0.0-alpha.
|
|
27
|
-
"openxiangda-devkit-core": "2.0.0-alpha.
|
|
25
|
+
"openxiangda-cli": "2.0.0-alpha.89",
|
|
26
|
+
"openxiangda-contracts": "2.0.0-alpha.43",
|
|
27
|
+
"openxiangda-devkit-core": "2.0.0-alpha.56",
|
|
28
28
|
"typescript": "5.9.3"
|
|
29
29
|
}
|
|
30
30
|
}
|
|
@@ -17,6 +17,13 @@ export const appOperations = {} as const;
|
|
|
17
17
|
export const appRoutes = {} as const;
|
|
18
18
|
export const eventTypes = [] as const;
|
|
19
19
|
export const eventSubscriptionCodes = [] as const;
|
|
20
|
+
export const eventHandlerManifest = {
|
|
21
|
+
"schemaVersion": "openxiangda.event-handler-manifest/v2",
|
|
22
|
+
"appCode": "openxiangda-application",
|
|
23
|
+
"handlers": []
|
|
24
|
+
} as const;
|
|
25
|
+
export const eventHandlers = {} as const;
|
|
26
|
+
export const eventSchemas = [] as const;
|
|
20
27
|
export const workflowCodes = [] as const;
|
|
21
28
|
|
|
22
29
|
export type ResourceCode = (typeof resourceCodes)[number];
|
|
@@ -25,4 +32,92 @@ export type AppOperation = (typeof appOperations)[keyof typeof appOperations];
|
|
|
25
32
|
export type AppRoute = (typeof appRoutes)[keyof typeof appRoutes];
|
|
26
33
|
export type EventType = (typeof eventTypes)[number];
|
|
27
34
|
export type EventSubscriptionCode = (typeof eventSubscriptionCodes)[number];
|
|
35
|
+
export type EventTypesForSubscription<TCode extends EventSubscriptionCode> = never;
|
|
28
36
|
export type WorkflowCode = (typeof workflowCodes)[number];
|
|
37
|
+
|
|
38
|
+
export interface OpenXiangdaCloudEvent<TType extends EventType = EventType, TData extends Record<string, unknown> = Record<string, unknown>> {
|
|
39
|
+
specversion: "1.0";
|
|
40
|
+
id: string;
|
|
41
|
+
source: string;
|
|
42
|
+
type: TType;
|
|
43
|
+
subject?: string;
|
|
44
|
+
time: string;
|
|
45
|
+
datacontenttype: "application/json";
|
|
46
|
+
dataschema?: string;
|
|
47
|
+
data: TData;
|
|
48
|
+
tenantid: string;
|
|
49
|
+
appcode: typeof appCode;
|
|
50
|
+
environment: string;
|
|
51
|
+
traceid?: string;
|
|
52
|
+
schemaversion: string;
|
|
53
|
+
}
|
|
54
|
+
export type EventHandlerInput<TCode extends EventSubscriptionCode> = OpenXiangdaCloudEvent<EventTypesForSubscription<TCode>>;
|
|
55
|
+
|
|
56
|
+
export interface LabeledValue {
|
|
57
|
+
label: string;
|
|
58
|
+
value: string;
|
|
59
|
+
description?: string;
|
|
60
|
+
color?: string;
|
|
61
|
+
}
|
|
62
|
+
export interface ResourceReferenceValue extends LabeledValue {
|
|
63
|
+
resourceCode: string;
|
|
64
|
+
snapshot?: Record<string, unknown>;
|
|
65
|
+
}
|
|
66
|
+
export interface DepartmentReferenceValue extends LabeledValue {
|
|
67
|
+
fullPath?: string;
|
|
68
|
+
path?: LabeledValue[];
|
|
69
|
+
parent?: LabeledValue;
|
|
70
|
+
}
|
|
71
|
+
export interface UserReferenceValue extends LabeledValue {
|
|
72
|
+
avatarUrl?: string;
|
|
73
|
+
employeeNo?: string;
|
|
74
|
+
title?: string;
|
|
75
|
+
mobile?: string;
|
|
76
|
+
email?: string;
|
|
77
|
+
departments?: DepartmentReferenceValue[];
|
|
78
|
+
}
|
|
79
|
+
export interface DateRangeValue { start: string; end: string; }
|
|
80
|
+
export type CascadePathValue = LabeledValue[];
|
|
81
|
+
export interface DataFileRef {
|
|
82
|
+
schemaVersion: 'openxiangda.data-file-ref/v2';
|
|
83
|
+
id: string;
|
|
84
|
+
name: string;
|
|
85
|
+
size: number;
|
|
86
|
+
contentType: string;
|
|
87
|
+
}
|
|
88
|
+
export interface DataImageRef extends DataFileRef {
|
|
89
|
+
width: number;
|
|
90
|
+
height: number;
|
|
91
|
+
thumbnailUrl: string;
|
|
92
|
+
previewUrl: string;
|
|
93
|
+
}
|
|
94
|
+
export interface StableSignatureValue {
|
|
95
|
+
file: Omit<DataFileRef, 'schemaVersion'>;
|
|
96
|
+
signer?: UserReferenceValue;
|
|
97
|
+
signedAt: string;
|
|
98
|
+
points?: Array<{ x: number; y: number; t: number }>;
|
|
99
|
+
hash: string;
|
|
100
|
+
}
|
|
101
|
+
export interface StableAddressValue {
|
|
102
|
+
country?: LabeledValue;
|
|
103
|
+
province?: LabeledValue;
|
|
104
|
+
city?: LabeledValue;
|
|
105
|
+
district?: LabeledValue;
|
|
106
|
+
street?: LabeledValue;
|
|
107
|
+
detail?: string;
|
|
108
|
+
fullAddress?: string;
|
|
109
|
+
}
|
|
110
|
+
export interface StableLocationSnapshot {
|
|
111
|
+
address?: string;
|
|
112
|
+
name?: string;
|
|
113
|
+
province?: string;
|
|
114
|
+
city?: string;
|
|
115
|
+
district?: string;
|
|
116
|
+
accuracy?: number;
|
|
117
|
+
capturedAt?: string;
|
|
118
|
+
}
|
|
119
|
+
export type StableLocationValue = StableLocationSnapshot & {
|
|
120
|
+
source: 'browser' | 'dingTalk';
|
|
121
|
+
longitude: number;
|
|
122
|
+
latitude: number;
|
|
123
|
+
};
|
|
@@ -9,14 +9,18 @@ function visit(directory) {
|
|
|
9
9
|
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
10
10
|
const path = join(directory, entry.name);
|
|
11
11
|
const name = relative(root, path).replaceAll('\\', '/');
|
|
12
|
-
if (
|
|
12
|
+
if (
|
|
13
|
+
generated.test(name) ||
|
|
14
|
+
name === 'pnpm-lock.yaml' ||
|
|
15
|
+
name === 'packages/contracts/src/generated.ts'
|
|
16
|
+
) continue;
|
|
13
17
|
if (entry.isDirectory()) visit(path);
|
|
14
18
|
else if (entry.isFile() && sourceExtensions.test(name)) files.push(path);
|
|
15
19
|
}
|
|
16
20
|
}
|
|
17
21
|
visit(root);
|
|
18
22
|
const bytes = files.reduce((total, file) => total + statSync(file).size, 0);
|
|
19
|
-
if (files.length >
|
|
23
|
+
if (files.length > 80) throw new Error(`TEMPLATE_SOURCE_FILE_BUDGET_EXCEEDED:${files.length}>80`);
|
|
20
24
|
if (bytes > 500_000) throw new Error(`TEMPLATE_SOURCE_BYTE_BUDGET_EXCEEDED:${bytes}>500000`);
|
|
21
25
|
const productionFiles = files.filter(file => {
|
|
22
26
|
const name = relative(root, file).replaceAll('\\', '/');
|
|
@@ -33,8 +37,11 @@ const productionFiles = files.filter(file => {
|
|
|
33
37
|
const source = productionFiles.map(file => readFileSync(file, 'utf8')).join('\n');
|
|
34
38
|
const agents = readFileSync(resolve(root, 'AGENTS.md'), 'utf8');
|
|
35
39
|
if (!agents.includes('pnpm openxiangda')) throw new Error('TEMPLATE_AGENT_PINNED_CLI_MISSING');
|
|
36
|
-
if (
|
|
37
|
-
|
|
40
|
+
if (
|
|
41
|
+
!agents.includes("union of that user's application roles") ||
|
|
42
|
+
!agents.includes('opaque authorization context')
|
|
43
|
+
) {
|
|
44
|
+
throw new Error('TEMPLATE_AGENT_ROLE_UNION_BOUNDARY_MISSING');
|
|
38
45
|
}
|
|
39
46
|
for (const marker of ['@umijs/', 'openxiangda-' + 'admin', 'openxiangda-' + 'user', 'instrument_query', 'instrument_save', 'function.invoke', 'departmentScopeKey', 'instrumentScopeKey', 'adminUserIds', 'college-' + 'am', 'user-' + 'wang', 'dept-' + 'am-test']) {
|
|
40
47
|
if (source.includes(marker)) throw new Error(`TEMPLATE_FORBIDDEN_MARKER:${marker}`);
|